mirror of
https://github.com/bitwarden/server.git
synced 2025-04-11 08:08:14 -05:00
Merge branch 'main' into jmccannon/ac/pm-16811-scim-invite-optimization
# Conflicts: # src/Core/Models/Commands/CommandResult.cs
This commit is contained in:
commit
813333e9bb
@ -1,5 +1,3 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
bitwarden_server:
|
||||
image: mcr.microsoft.com/devcontainers/dotnet:8.0
|
||||
@ -13,7 +11,8 @@ services:
|
||||
platform: linux/amd64
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
../../dev/.env
|
||||
- path: ../../dev/.env
|
||||
required: false
|
||||
environment:
|
||||
ACCEPT_EULA: "Y"
|
||||
MSSQL_PID: Developer
|
||||
|
@ -51,4 +51,10 @@ Proceed? [y/N] " response
|
||||
}
|
||||
|
||||
# main
|
||||
one_time_setup
|
||||
if [[ -z "${CODESPACES}" ]]; then
|
||||
one_time_setup
|
||||
else
|
||||
# Ignore interactive elements when running in codespaces since they are not supported there
|
||||
# TODO Write codespaces specific instructions and link here
|
||||
echo "Running in codespaces, follow instructions here: https://contributing.bitwarden.com/getting-started/server/guide/ to continue the setup"
|
||||
fi
|
||||
|
@ -1,5 +1,3 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
bitwarden_storage:
|
||||
image: mcr.microsoft.com/azure-storage/azurite:latest
|
||||
|
@ -89,4 +89,10 @@ install_stripe_cli() {
|
||||
}
|
||||
|
||||
# main
|
||||
one_time_setup
|
||||
if [[ -z "${CODESPACES}" ]]; then
|
||||
one_time_setup
|
||||
else
|
||||
# Ignore interactive elements when running in codespaces since they are not supported there
|
||||
# TODO Write codespaces specific instructions and link here
|
||||
echo "Running in codespaces, follow instructions here: https://contributing.bitwarden.com/getting-started/server/guide/ to continue the setup"
|
||||
fi
|
23
.github/workflows/test-database.yml
vendored
23
.github/workflows/test-database.yml
vendored
@ -32,28 +32,9 @@ on:
|
||||
- "src/**/Entities/**/*.cs" # Database entity definitions
|
||||
|
||||
jobs:
|
||||
check-test-secrets:
|
||||
name: Check for test secrets
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
available: ${{ steps.check-test-secrets.outputs.available }}
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Check
|
||||
id: check-test-secrets
|
||||
run: |
|
||||
if [ "${{ secrets.CODECOV_TOKEN }}" != '' ]; then
|
||||
echo "available=true" >> $GITHUB_OUTPUT;
|
||||
else
|
||||
echo "available=false" >> $GITHUB_OUTPUT;
|
||||
fi
|
||||
|
||||
test:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-22.04
|
||||
needs: check-test-secrets
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@ -166,8 +147,8 @@ jobs:
|
||||
run: 'docker logs $(docker ps --quiet --filter "name=mssql")'
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1
|
||||
if: ${{ needs.check-test-secrets.outputs.available == 'true' && !cancelled() }}
|
||||
uses: dorny/test-reporter@6e6a65b7a0bd2c9197df7d0ae36ac5cee784230c # v2.0.0
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
||||
with:
|
||||
name: Test Results
|
||||
path: "**/*-test-results.trx"
|
||||
|
23
.github/workflows/test.yml
vendored
23
.github/workflows/test.yml
vendored
@ -13,29 +13,10 @@ env:
|
||||
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
|
||||
|
||||
jobs:
|
||||
check-test-secrets:
|
||||
name: Check for test secrets
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
available: ${{ steps.check-test-secrets.outputs.available }}
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Check
|
||||
id: check-test-secrets
|
||||
run: |
|
||||
if [ "${{ secrets.CODECOV_TOKEN }}" != '' ]; then
|
||||
echo "available=true" >> $GITHUB_OUTPUT;
|
||||
else
|
||||
echo "available=false" >> $GITHUB_OUTPUT;
|
||||
fi
|
||||
|
||||
testing:
|
||||
name: Run tests
|
||||
if: ${{ startsWith(github.head_ref, 'version_bump_') == false }}
|
||||
runs-on: ubuntu-22.04
|
||||
needs: check-test-secrets
|
||||
permissions:
|
||||
checks: write
|
||||
contents: read
|
||||
@ -68,8 +49,8 @@ jobs:
|
||||
run: dotnet test ./bitwarden_license/test --configuration Debug --logger "trx;LogFileName=bw-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1
|
||||
if: ${{ needs.check-test-secrets.outputs.available == 'true' && !cancelled() }}
|
||||
uses: dorny/test-reporter@6e6a65b7a0bd2c9197df7d0ae36ac5cee784230c # v2.0.0
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
||||
with:
|
||||
name: Test Results
|
||||
path: "**/*-test-results.trx"
|
||||
|
@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<Version>2025.2.4</Version>
|
||||
<Version>2025.3.0</Version>
|
||||
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
@ -628,6 +628,19 @@ public class ProviderBillingService(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdatePaymentMethod(
|
||||
Provider provider,
|
||||
TokenizedPaymentSource tokenizedPaymentSource,
|
||||
TaxInformation taxInformation)
|
||||
{
|
||||
await Task.WhenAll(
|
||||
subscriberService.UpdatePaymentSource(provider, tokenizedPaymentSource),
|
||||
subscriberService.UpdateTaxInformation(provider, taxInformation));
|
||||
|
||||
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
|
||||
new SubscriptionUpdateOptions { CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically });
|
||||
}
|
||||
|
||||
public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command)
|
||||
{
|
||||
if (command.Configuration.Any(x => x.SeatsMinimum < 0))
|
||||
|
@ -11,6 +11,7 @@ using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
@ -42,6 +43,7 @@ public class ProvidersController : Controller
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IProviderPlanRepository _providerPlanRepository;
|
||||
private readonly IProviderBillingService _providerBillingService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly string _stripeUrl;
|
||||
private readonly string _braintreeMerchantUrl;
|
||||
private readonly string _braintreeMerchantId;
|
||||
@ -60,7 +62,8 @@ public class ProvidersController : Controller
|
||||
IFeatureService featureService,
|
||||
IProviderPlanRepository providerPlanRepository,
|
||||
IProviderBillingService providerBillingService,
|
||||
IWebHostEnvironment webHostEnvironment)
|
||||
IWebHostEnvironment webHostEnvironment,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationService = organizationService;
|
||||
@ -75,6 +78,7 @@ public class ProvidersController : Controller
|
||||
_featureService = featureService;
|
||||
_providerPlanRepository = providerPlanRepository;
|
||||
_providerBillingService = providerBillingService;
|
||||
_pricingClient = pricingClient;
|
||||
_stripeUrl = webHostEnvironment.GetStripeUrl();
|
||||
_braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl();
|
||||
_braintreeMerchantId = globalSettings.Braintree.MerchantId;
|
||||
@ -415,7 +419,9 @@ public class ProvidersController : Controller
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
return View(new OrganizationEditModel(provider));
|
||||
var plans = await _pricingClient.ListPlans();
|
||||
|
||||
return View(new OrganizationEditModel(provider, plans));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
|
@ -22,13 +22,14 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
|
||||
public OrganizationEditModel() { }
|
||||
|
||||
public OrganizationEditModel(Provider provider)
|
||||
public OrganizationEditModel(Provider provider, List<Plan> plans)
|
||||
{
|
||||
Provider = provider;
|
||||
BillingEmail = provider.Type == ProviderType.Reseller ? provider.BillingEmail : string.Empty;
|
||||
PlanType = Core.Billing.Enums.PlanType.TeamsMonthly;
|
||||
Plan = Core.Billing.Enums.PlanType.TeamsMonthly.GetDisplayAttribute()?.GetName();
|
||||
LicenseKey = RandomLicenseKey;
|
||||
_plans = plans;
|
||||
}
|
||||
|
||||
public OrganizationEditModel(
|
||||
|
@ -23,6 +23,7 @@ public class PendingOrganizationAuthRequestResponseModel : ResponseModel
|
||||
RequestDeviceType = authRequest.RequestDeviceType.GetType().GetMember(authRequest.RequestDeviceType.ToString())
|
||||
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName();
|
||||
RequestIpAddress = authRequest.RequestIpAddress;
|
||||
RequestCountryName = authRequest.RequestCountryName;
|
||||
CreationDate = authRequest.CreationDate;
|
||||
}
|
||||
|
||||
@ -34,5 +35,6 @@ public class PendingOrganizationAuthRequestResponseModel : ResponseModel
|
||||
public string RequestDeviceIdentifier { get; set; }
|
||||
public string RequestDeviceType { get; set; }
|
||||
public string RequestIpAddress { get; set; }
|
||||
public string RequestCountryName { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
}
|
||||
|
@ -4,11 +4,9 @@ using Bit.Api.Auth.Models.Request;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
using Bit.Api.KeyManagement.Validators;
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Request.Accounts;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
@ -19,23 +17,15 @@ using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.Models.Api.Response;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -47,20 +37,15 @@ namespace Bit.Api.Auth.Controllers;
|
||||
[Authorize("Application")]
|
||||
public class AccountsController : Controller
|
||||
{
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
|
||||
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
||||
private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ISubscriberService _subscriberService;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
|
||||
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
|
||||
private readonly IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> _folderValidator;
|
||||
@ -75,20 +60,15 @@ public class AccountsController : Controller
|
||||
|
||||
|
||||
public AccountsController(
|
||||
GlobalSettings globalSettings,
|
||||
IOrganizationService organizationService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IPaymentService paymentService,
|
||||
IUserService userService,
|
||||
IPolicyService policyService,
|
||||
ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand,
|
||||
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
|
||||
IRotateUserKeyCommand rotateUserKeyCommand,
|
||||
IFeatureService featureService,
|
||||
ISubscriberService subscriberService,
|
||||
IReferenceEventService referenceEventService,
|
||||
ICurrentContext currentContext,
|
||||
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
|
||||
IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator,
|
||||
IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> sendValidator,
|
||||
@ -99,20 +79,15 @@ public class AccountsController : Controller
|
||||
IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> webAuthnKeyValidator
|
||||
)
|
||||
{
|
||||
_globalSettings = globalSettings;
|
||||
_organizationService = organizationService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_paymentService = paymentService;
|
||||
_userService = userService;
|
||||
_policyService = policyService;
|
||||
_setInitialMasterPasswordCommand = setInitialMasterPasswordCommand;
|
||||
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
|
||||
_rotateUserKeyCommand = rotateUserKeyCommand;
|
||||
_featureService = featureService;
|
||||
_subscriberService = subscriberService;
|
||||
_referenceEventService = referenceEventService;
|
||||
_currentContext = currentContext;
|
||||
_cipherValidator = cipherValidator;
|
||||
_folderValidator = folderValidator;
|
||||
_sendValidator = sendValidator;
|
||||
@ -638,212 +613,6 @@ public class AccountsController : Controller
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
[HttpPost("premium")]
|
||||
public async Task<PaymentResponseModel> PostPremium(PremiumRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var valid = model.Validate(_globalSettings);
|
||||
UserLicense license = null;
|
||||
if (valid && _globalSettings.SelfHosted)
|
||||
{
|
||||
license = await ApiHelpers.ReadJsonFileFromBody<UserLicense>(HttpContext, model.License);
|
||||
}
|
||||
|
||||
if (!valid && !_globalSettings.SelfHosted && string.IsNullOrWhiteSpace(model.Country))
|
||||
{
|
||||
throw new BadRequestException("Country is required.");
|
||||
}
|
||||
|
||||
if (!valid || (_globalSettings.SelfHosted && license == null))
|
||||
{
|
||||
throw new BadRequestException("Invalid license.");
|
||||
}
|
||||
|
||||
var result = await _userService.SignUpPremiumAsync(user, model.PaymentToken,
|
||||
model.PaymentMethodType.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license,
|
||||
new TaxInfo
|
||||
{
|
||||
BillingAddressCountry = model.Country,
|
||||
BillingAddressPostalCode = model.PostalCode
|
||||
});
|
||||
|
||||
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
||||
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
||||
var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id);
|
||||
|
||||
var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsManagingActiveUser);
|
||||
return new PaymentResponseModel
|
||||
{
|
||||
UserProfile = profile,
|
||||
PaymentIntentClientSecret = result.Item2,
|
||||
Success = result.Item1
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("subscription")]
|
||||
public async Task<SubscriptionResponseModel> GetSubscription()
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
if (!_globalSettings.SelfHosted && user.Gateway != null)
|
||||
{
|
||||
var subscriptionInfo = await _paymentService.GetSubscriptionAsync(user);
|
||||
var license = await _userService.GenerateLicenseAsync(user, subscriptionInfo);
|
||||
return new SubscriptionResponseModel(user, subscriptionInfo, license);
|
||||
}
|
||||
else if (!_globalSettings.SelfHosted)
|
||||
{
|
||||
var license = await _userService.GenerateLicenseAsync(user);
|
||||
return new SubscriptionResponseModel(user, license);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new SubscriptionResponseModel(user);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("payment")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PostPayment([FromBody] PaymentRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
await _userService.ReplacePaymentMethodAsync(user, model.PaymentToken, model.PaymentMethodType.Value,
|
||||
new TaxInfo
|
||||
{
|
||||
BillingAddressLine1 = model.Line1,
|
||||
BillingAddressLine2 = model.Line2,
|
||||
BillingAddressCity = model.City,
|
||||
BillingAddressState = model.State,
|
||||
BillingAddressCountry = model.Country,
|
||||
BillingAddressPostalCode = model.PostalCode,
|
||||
TaxIdNumber = model.TaxId
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("storage")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<PaymentResponseModel> PostStorage([FromBody] StorageRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var result = await _userService.AdjustStorageAsync(user, model.StorageGbAdjustment.Value);
|
||||
return new PaymentResponseModel
|
||||
{
|
||||
Success = true,
|
||||
PaymentIntentClientSecret = result
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("license")]
|
||||
[SelfHosted(SelfHostedOnly = true)]
|
||||
public async Task PostLicense(LicenseRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var license = await ApiHelpers.ReadJsonFileFromBody<UserLicense>(HttpContext, model.License);
|
||||
if (license == null)
|
||||
{
|
||||
throw new BadRequestException("Invalid license");
|
||||
}
|
||||
|
||||
await _userService.UpdateLicenseAsync(user, license);
|
||||
}
|
||||
|
||||
[HttpPost("cancel")]
|
||||
public async Task PostCancel([FromBody] SubscriptionCancellationRequestModel request)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
await _subscriberService.CancelSubscription(user,
|
||||
new OffboardingSurveyResponse
|
||||
{
|
||||
UserId = user.Id,
|
||||
Reason = request.Reason,
|
||||
Feedback = request.Feedback
|
||||
},
|
||||
user.IsExpired());
|
||||
|
||||
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(
|
||||
ReferenceEventType.CancelSubscription,
|
||||
user,
|
||||
_currentContext)
|
||||
{
|
||||
EndOfPeriod = user.IsExpired()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("reinstate-premium")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PostReinstate()
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
await _userService.ReinstatePremiumAsync(user);
|
||||
}
|
||||
|
||||
[HttpGet("tax")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<TaxInfoResponseModel> GetTaxInfo()
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var taxInfo = await _paymentService.GetTaxInfoAsync(user);
|
||||
return new TaxInfoResponseModel(taxInfo);
|
||||
}
|
||||
|
||||
[HttpPut("tax")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PutTaxInfo([FromBody] TaxInfoUpdateRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var taxInfo = new TaxInfo
|
||||
{
|
||||
BillingAddressPostalCode = model.PostalCode,
|
||||
BillingAddressCountry = model.Country,
|
||||
};
|
||||
await _paymentService.SaveTaxInfoAsync(user, taxInfo);
|
||||
}
|
||||
|
||||
[HttpDelete("sso/{organizationId}")]
|
||||
public async Task DeleteSsoUser(string organizationId)
|
||||
{
|
||||
|
@ -167,7 +167,7 @@ public class EmergencyAccessController : Controller
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var viewResult = await _emergencyAccessService.ViewAsync(id, user);
|
||||
return new EmergencyAccessViewResponseModel(_globalSettings, viewResult.EmergencyAccess, viewResult.Ciphers);
|
||||
return new EmergencyAccessViewResponseModel(_globalSettings, viewResult.EmergencyAccess, viewResult.Ciphers, user);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/{cipherId}/attachment/{attachmentId}")]
|
||||
|
@ -23,6 +23,7 @@ public class AuthRequestResponseModel : ResponseModel
|
||||
RequestDeviceType = authRequest.RequestDeviceType.GetType().GetMember(authRequest.RequestDeviceType.ToString())
|
||||
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName();
|
||||
RequestIpAddress = authRequest.RequestIpAddress;
|
||||
RequestCountryName = authRequest.RequestCountryName;
|
||||
Key = authRequest.Key;
|
||||
MasterPasswordHash = authRequest.MasterPasswordHash;
|
||||
CreationDate = authRequest.CreationDate;
|
||||
@ -37,6 +38,7 @@ public class AuthRequestResponseModel : ResponseModel
|
||||
public DeviceType RequestDeviceTypeValue { get; set; }
|
||||
public string RequestDeviceType { get; set; }
|
||||
public string RequestIpAddress { get; set; }
|
||||
public string RequestCountryName { get; set; }
|
||||
public string Key { get; set; }
|
||||
public string MasterPasswordHash { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
|
@ -116,11 +116,17 @@ public class EmergencyAccessViewResponseModel : ResponseModel
|
||||
public EmergencyAccessViewResponseModel(
|
||||
IGlobalSettings globalSettings,
|
||||
EmergencyAccess emergencyAccess,
|
||||
IEnumerable<CipherDetails> ciphers)
|
||||
IEnumerable<CipherDetails> ciphers,
|
||||
User user)
|
||||
: base("emergencyAccessView")
|
||||
{
|
||||
KeyEncrypted = emergencyAccess.KeyEncrypted;
|
||||
Ciphers = ciphers.Select(c => new CipherResponseModel(c, globalSettings));
|
||||
Ciphers = ciphers.Select(cipher =>
|
||||
new CipherResponseModel(
|
||||
cipher,
|
||||
user,
|
||||
organizationAbilities: null, // Emergency access only retrieves personal ciphers so organizationAbilities is not needed
|
||||
globalSettings));
|
||||
}
|
||||
|
||||
public string KeyEncrypted { get; set; }
|
||||
|
237
src/Api/Billing/Controllers/AccountsController.cs
Normal file
237
src/Api/Billing/Controllers/AccountsController.cs
Normal file
@ -0,0 +1,237 @@
|
||||
#nullable enable
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Request.Accounts;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Billing.Controllers;
|
||||
|
||||
[Route("accounts")]
|
||||
[Authorize("Application")]
|
||||
public class AccountsController(
|
||||
IUserService userService) : Controller
|
||||
{
|
||||
[HttpPost("premium")]
|
||||
public async Task<PaymentResponseModel> PostPremiumAsync(
|
||||
PremiumRequestModel model,
|
||||
[FromServices] GlobalSettings globalSettings)
|
||||
{
|
||||
var user = await userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var valid = model.Validate(globalSettings);
|
||||
UserLicense? license = null;
|
||||
if (valid && globalSettings.SelfHosted)
|
||||
{
|
||||
license = await ApiHelpers.ReadJsonFileFromBody<UserLicense>(HttpContext, model.License);
|
||||
}
|
||||
|
||||
if (!valid && !globalSettings.SelfHosted && string.IsNullOrWhiteSpace(model.Country))
|
||||
{
|
||||
throw new BadRequestException("Country is required.");
|
||||
}
|
||||
|
||||
if (!valid || (globalSettings.SelfHosted && license == null))
|
||||
{
|
||||
throw new BadRequestException("Invalid license.");
|
||||
}
|
||||
|
||||
var result = await userService.SignUpPremiumAsync(user, model.PaymentToken,
|
||||
model.PaymentMethodType!.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license,
|
||||
new TaxInfo { BillingAddressCountry = model.Country, BillingAddressPostalCode = model.PostalCode });
|
||||
|
||||
var userTwoFactorEnabled = await userService.TwoFactorIsEnabledAsync(user);
|
||||
var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user);
|
||||
var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id);
|
||||
|
||||
var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled,
|
||||
userHasPremiumFromOrganization, organizationIdsManagingActiveUser);
|
||||
return new PaymentResponseModel
|
||||
{
|
||||
UserProfile = profile,
|
||||
PaymentIntentClientSecret = result.Item2,
|
||||
Success = result.Item1
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("subscription")]
|
||||
public async Task<SubscriptionResponseModel> GetSubscriptionAsync(
|
||||
[FromServices] GlobalSettings globalSettings,
|
||||
[FromServices] IPaymentService paymentService)
|
||||
{
|
||||
var user = await userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
if (!globalSettings.SelfHosted && user.Gateway != null)
|
||||
{
|
||||
var subscriptionInfo = await paymentService.GetSubscriptionAsync(user);
|
||||
var license = await userService.GenerateLicenseAsync(user, subscriptionInfo);
|
||||
return new SubscriptionResponseModel(user, subscriptionInfo, license);
|
||||
}
|
||||
else if (!globalSettings.SelfHosted)
|
||||
{
|
||||
var license = await userService.GenerateLicenseAsync(user);
|
||||
return new SubscriptionResponseModel(user, license);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new SubscriptionResponseModel(user);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("payment")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PostPaymentAsync([FromBody] PaymentRequestModel model)
|
||||
{
|
||||
var user = await userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
await userService.ReplacePaymentMethodAsync(user, model.PaymentToken, model.PaymentMethodType!.Value,
|
||||
new TaxInfo
|
||||
{
|
||||
BillingAddressLine1 = model.Line1,
|
||||
BillingAddressLine2 = model.Line2,
|
||||
BillingAddressCity = model.City,
|
||||
BillingAddressState = model.State,
|
||||
BillingAddressCountry = model.Country,
|
||||
BillingAddressPostalCode = model.PostalCode,
|
||||
TaxIdNumber = model.TaxId
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("storage")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<PaymentResponseModel> PostStorageAsync([FromBody] StorageRequestModel model)
|
||||
{
|
||||
var user = await userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var result = await userService.AdjustStorageAsync(user, model.StorageGbAdjustment!.Value);
|
||||
return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result };
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpPost("license")]
|
||||
[SelfHosted(SelfHostedOnly = true)]
|
||||
public async Task PostLicenseAsync(LicenseRequestModel model)
|
||||
{
|
||||
var user = await userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var license = await ApiHelpers.ReadJsonFileFromBody<UserLicense>(HttpContext, model.License);
|
||||
if (license == null)
|
||||
{
|
||||
throw new BadRequestException("Invalid license");
|
||||
}
|
||||
|
||||
await userService.UpdateLicenseAsync(user, license);
|
||||
}
|
||||
|
||||
[HttpPost("cancel")]
|
||||
public async Task PostCancelAsync(
|
||||
[FromBody] SubscriptionCancellationRequestModel request,
|
||||
[FromServices] ICurrentContext currentContext,
|
||||
[FromServices] IReferenceEventService referenceEventService,
|
||||
[FromServices] ISubscriberService subscriberService)
|
||||
{
|
||||
var user = await userService.GetUserByPrincipalAsync(User);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
await subscriberService.CancelSubscription(user,
|
||||
new OffboardingSurveyResponse { UserId = user.Id, Reason = request.Reason, Feedback = request.Feedback },
|
||||
user.IsExpired());
|
||||
|
||||
await referenceEventService.RaiseEventAsync(new ReferenceEvent(
|
||||
ReferenceEventType.CancelSubscription,
|
||||
user,
|
||||
currentContext)
|
||||
{ EndOfPeriod = user.IsExpired() });
|
||||
}
|
||||
|
||||
[HttpPost("reinstate-premium")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PostReinstateAsync()
|
||||
{
|
||||
var user = await userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
await userService.ReinstatePremiumAsync(user);
|
||||
}
|
||||
|
||||
[HttpGet("tax")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<TaxInfoResponseModel> GetTaxInfoAsync(
|
||||
[FromServices] IPaymentService paymentService)
|
||||
{
|
||||
var user = await userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var taxInfo = await paymentService.GetTaxInfoAsync(user);
|
||||
return new TaxInfoResponseModel(taxInfo);
|
||||
}
|
||||
|
||||
[HttpPut("tax")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PutTaxInfoAsync(
|
||||
[FromBody] TaxInfoUpdateRequestModel model,
|
||||
[FromServices] IPaymentService paymentService)
|
||||
{
|
||||
var user = await userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var taxInfo = new TaxInfo
|
||||
{
|
||||
BillingAddressPostalCode = model.PostalCode,
|
||||
BillingAddressCountry = model.Country,
|
||||
};
|
||||
await paymentService.SaveTaxInfoAsync(user, taxInfo);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<Guid>> GetOrganizationIdsManagingUserAsync(Guid userId)
|
||||
{
|
||||
var organizationManagingUser = await userService.GetOrganizationsManagingUserAsync(userId);
|
||||
return organizationManagingUser.Select(o => o.Id);
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
using Bit.Api.Billing.Models.Requests;
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
@ -20,6 +21,7 @@ namespace Bit.Api.Billing.Controllers;
|
||||
[Authorize("Application")]
|
||||
public class ProviderBillingController(
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
ILogger<BaseProviderController> logger,
|
||||
IPricingClient pricingClient,
|
||||
IProviderBillingService providerBillingService,
|
||||
@ -71,6 +73,65 @@ public class ProviderBillingController(
|
||||
"text/csv");
|
||||
}
|
||||
|
||||
[HttpPut("payment-method")]
|
||||
public async Task<IResult> UpdatePaymentMethodAsync(
|
||||
[FromRoute] Guid providerId,
|
||||
[FromBody] UpdatePaymentMethodRequestBody requestBody)
|
||||
{
|
||||
var allowProviderPaymentMethod = featureService.IsEnabled(FeatureFlagKeys.PM18794_ProviderPaymentMethod);
|
||||
|
||||
if (!allowProviderPaymentMethod)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);
|
||||
|
||||
if (provider == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var tokenizedPaymentSource = requestBody.PaymentSource.ToDomain();
|
||||
var taxInformation = requestBody.TaxInformation.ToDomain();
|
||||
|
||||
await providerBillingService.UpdatePaymentMethod(
|
||||
provider,
|
||||
tokenizedPaymentSource,
|
||||
taxInformation);
|
||||
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
[HttpPost("payment-method/verify-bank-account")]
|
||||
public async Task<IResult> VerifyBankAccountAsync(
|
||||
[FromRoute] Guid providerId,
|
||||
[FromBody] VerifyBankAccountRequestBody requestBody)
|
||||
{
|
||||
var allowProviderPaymentMethod = featureService.IsEnabled(FeatureFlagKeys.PM18794_ProviderPaymentMethod);
|
||||
|
||||
if (!allowProviderPaymentMethod)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);
|
||||
|
||||
if (provider == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (requestBody.DescriptorCode.Length != 6 || !requestBody.DescriptorCode.StartsWith("SM"))
|
||||
{
|
||||
return Error.BadRequest("Statement descriptor should be a 6-character value that starts with 'SM'");
|
||||
}
|
||||
|
||||
await subscriberService.VerifyBankAccount(provider, requestBody.DescriptorCode);
|
||||
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
[HttpGet("subscription")]
|
||||
public async Task<IResult> GetSubscriptionAsync([FromRoute] Guid providerId)
|
||||
{
|
||||
@ -102,12 +163,32 @@ public class ProviderBillingController(
|
||||
|
||||
var subscriptionSuspension = await GetSubscriptionSuspensionAsync(stripeAdapter, subscription);
|
||||
|
||||
var paymentSource = await subscriberService.GetPaymentSource(provider);
|
||||
|
||||
var response = ProviderSubscriptionResponse.From(
|
||||
subscription,
|
||||
configuredProviderPlans,
|
||||
taxInformation,
|
||||
subscriptionSuspension,
|
||||
provider);
|
||||
provider,
|
||||
paymentSource);
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet("tax-information")]
|
||||
public async Task<IResult> GetTaxInformationAsync([FromRoute] Guid providerId)
|
||||
{
|
||||
var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);
|
||||
|
||||
if (provider == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var taxInformation = await subscriberService.GetTaxInformation(provider);
|
||||
|
||||
var response = TaxInformationResponse.From(taxInformation);
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
@ -16,7 +16,8 @@ public record ProviderSubscriptionResponse(
|
||||
TaxInformation TaxInformation,
|
||||
DateTime? CancelAt,
|
||||
SubscriptionSuspension Suspension,
|
||||
ProviderType ProviderType)
|
||||
ProviderType ProviderType,
|
||||
PaymentSource PaymentSource)
|
||||
{
|
||||
private const string _annualCadence = "Annual";
|
||||
private const string _monthlyCadence = "Monthly";
|
||||
@ -26,7 +27,8 @@ public record ProviderSubscriptionResponse(
|
||||
ICollection<ConfiguredProviderPlan> providerPlans,
|
||||
TaxInformation taxInformation,
|
||||
SubscriptionSuspension subscriptionSuspension,
|
||||
Provider provider)
|
||||
Provider provider,
|
||||
PaymentSource paymentSource)
|
||||
{
|
||||
var providerPlanResponses = providerPlans
|
||||
.Select(providerPlan =>
|
||||
@ -57,7 +59,8 @@ public record ProviderSubscriptionResponse(
|
||||
taxInformation,
|
||||
subscription.CancelAt,
|
||||
subscriptionSuspension,
|
||||
provider.Type);
|
||||
provider.Type,
|
||||
paymentSource);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,14 +79,16 @@ public class CiphersController : Controller
|
||||
[HttpGet("{id}")]
|
||||
public async Task<CipherResponseModel> Get(Guid id)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var cipher = await GetByIdAsync(id, userId);
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var cipher = await GetByIdAsync(id, user.Id);
|
||||
if (cipher == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new CipherResponseModel(cipher, _globalSettings);
|
||||
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
|
||||
return new CipherResponseModel(cipher, user, organizationAbilities, _globalSettings);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/admin")]
|
||||
@ -109,32 +111,37 @@ public class CiphersController : Controller
|
||||
[HttpGet("{id}/details")]
|
||||
public async Task<CipherDetailsResponseModel> GetDetails(Guid id)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var cipher = await GetByIdAsync(id, userId);
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var cipher = await GetByIdAsync(id, user.Id);
|
||||
if (cipher == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id);
|
||||
return new CipherDetailsResponseModel(cipher, _globalSettings, collectionCiphers);
|
||||
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id);
|
||||
return new CipherDetailsResponseModel(cipher, user, organizationAbilities, _globalSettings, collectionCiphers);
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<CipherDetailsResponseModel>> Get()
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var hasOrgs = _currentContext.Organizations?.Any() ?? false;
|
||||
// TODO: Use hasOrgs proper for cipher listing here?
|
||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: true || hasOrgs);
|
||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, withOrganizations: true || hasOrgs);
|
||||
Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict = null;
|
||||
if (hasOrgs)
|
||||
{
|
||||
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdAsync(userId);
|
||||
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdAsync(user.Id);
|
||||
collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);
|
||||
}
|
||||
|
||||
var responses = ciphers.Select(c => new CipherDetailsResponseModel(c, _globalSettings,
|
||||
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
var responses = ciphers.Select(cipher => new CipherDetailsResponseModel(
|
||||
cipher,
|
||||
user,
|
||||
organizationAbilities,
|
||||
_globalSettings,
|
||||
collectionCiphersGroupDict)).ToList();
|
||||
return new ListResponseModel<CipherDetailsResponseModel>(responses);
|
||||
}
|
||||
@ -142,30 +149,38 @@ public class CiphersController : Controller
|
||||
[HttpPost("")]
|
||||
public async Task<CipherResponseModel> Post([FromBody] CipherRequestModel model)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var cipher = model.ToCipherDetails(userId);
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var cipher = model.ToCipherDetails(user.Id);
|
||||
if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _cipherService.SaveDetailsAsync(cipher, userId, model.LastKnownRevisionDate, null, cipher.OrganizationId.HasValue);
|
||||
var response = new CipherResponseModel(cipher, _globalSettings);
|
||||
await _cipherService.SaveDetailsAsync(cipher, user.Id, model.LastKnownRevisionDate, null, cipher.OrganizationId.HasValue);
|
||||
var response = new CipherResponseModel(
|
||||
cipher,
|
||||
user,
|
||||
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
|
||||
_globalSettings);
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("create")]
|
||||
public async Task<CipherResponseModel> PostCreate([FromBody] CipherCreateRequestModel model)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var cipher = model.Cipher.ToCipherDetails(userId);
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var cipher = model.Cipher.ToCipherDetails(user.Id);
|
||||
if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _cipherService.SaveDetailsAsync(cipher, userId, model.Cipher.LastKnownRevisionDate, model.CollectionIds, cipher.OrganizationId.HasValue);
|
||||
var response = new CipherResponseModel(cipher, _globalSettings);
|
||||
await _cipherService.SaveDetailsAsync(cipher, user.Id, model.Cipher.LastKnownRevisionDate, model.CollectionIds, cipher.OrganizationId.HasValue);
|
||||
var response = new CipherResponseModel(
|
||||
cipher,
|
||||
user,
|
||||
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
|
||||
_globalSettings);
|
||||
return response;
|
||||
}
|
||||
|
||||
@ -191,8 +206,8 @@ public class CiphersController : Controller
|
||||
[HttpPost("{id}")]
|
||||
public async Task<CipherResponseModel> Put(Guid id, [FromBody] CipherRequestModel model)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var cipher = await GetByIdAsync(id, userId);
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var cipher = await GetByIdAsync(id, user.Id);
|
||||
if (cipher == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
@ -200,7 +215,7 @@ public class CiphersController : Controller
|
||||
|
||||
ValidateClientVersionForFido2CredentialSupport(cipher);
|
||||
|
||||
var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id)).Select(c => c.CollectionId).ToList();
|
||||
var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id)).Select(c => c.CollectionId).ToList();
|
||||
var modelOrgId = string.IsNullOrWhiteSpace(model.OrganizationId) ?
|
||||
(Guid?)null : new Guid(model.OrganizationId);
|
||||
if (cipher.OrganizationId != modelOrgId)
|
||||
@ -209,9 +224,13 @@ public class CiphersController : Controller
|
||||
"then try again.");
|
||||
}
|
||||
|
||||
await _cipherService.SaveDetailsAsync(model.ToCipherDetails(cipher), userId, model.LastKnownRevisionDate, collectionIds);
|
||||
await _cipherService.SaveDetailsAsync(model.ToCipherDetails(cipher), user.Id, model.LastKnownRevisionDate, collectionIds);
|
||||
|
||||
var response = new CipherResponseModel(cipher, _globalSettings);
|
||||
var response = new CipherResponseModel(
|
||||
cipher,
|
||||
user,
|
||||
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
|
||||
_globalSettings);
|
||||
return response;
|
||||
}
|
||||
|
||||
@ -278,7 +297,14 @@ public class CiphersController : Controller
|
||||
}));
|
||||
}
|
||||
|
||||
var responses = ciphers.Select(c => new CipherDetailsResponseModel(c, _globalSettings));
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
var responses = ciphers.Select(cipher =>
|
||||
new CipherDetailsResponseModel(
|
||||
cipher,
|
||||
user,
|
||||
organizationAbilities,
|
||||
_globalSettings));
|
||||
|
||||
return new ListResponseModel<CipherDetailsResponseModel>(responses);
|
||||
}
|
||||
@ -572,12 +598,16 @@ public class CiphersController : Controller
|
||||
[HttpPost("{id}/partial")]
|
||||
public async Task<CipherResponseModel> PutPartial(Guid id, [FromBody] CipherPartialRequestModel model)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var folderId = string.IsNullOrWhiteSpace(model.FolderId) ? null : (Guid?)new Guid(model.FolderId);
|
||||
await _cipherRepository.UpdatePartialAsync(id, userId, folderId, model.Favorite);
|
||||
await _cipherRepository.UpdatePartialAsync(id, user.Id, folderId, model.Favorite);
|
||||
|
||||
var cipher = await GetByIdAsync(id, userId);
|
||||
var response = new CipherResponseModel(cipher, _globalSettings);
|
||||
var cipher = await GetByIdAsync(id, user.Id);
|
||||
var response = new CipherResponseModel(
|
||||
cipher,
|
||||
user,
|
||||
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
|
||||
_globalSettings);
|
||||
return response;
|
||||
}
|
||||
|
||||
@ -585,9 +615,9 @@ public class CiphersController : Controller
|
||||
[HttpPost("{id}/share")]
|
||||
public async Task<CipherResponseModel> PutShare(Guid id, [FromBody] CipherShareRequestModel model)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var cipher = await _cipherRepository.GetByIdAsync(id);
|
||||
if (cipher == null || cipher.UserId != userId ||
|
||||
if (cipher == null || cipher.UserId != user.Id ||
|
||||
!await _currentContext.OrganizationUser(new Guid(model.Cipher.OrganizationId)))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
@ -597,10 +627,14 @@ public class CiphersController : Controller
|
||||
|
||||
var original = cipher.Clone();
|
||||
await _cipherService.ShareAsync(original, model.Cipher.ToCipher(cipher), new Guid(model.Cipher.OrganizationId),
|
||||
model.CollectionIds.Select(c => new Guid(c)), userId, model.Cipher.LastKnownRevisionDate);
|
||||
model.CollectionIds.Select(c => new Guid(c)), user.Id, model.Cipher.LastKnownRevisionDate);
|
||||
|
||||
var sharedCipher = await GetByIdAsync(id, userId);
|
||||
var response = new CipherResponseModel(sharedCipher, _globalSettings);
|
||||
var sharedCipher = await GetByIdAsync(id, user.Id);
|
||||
var response = new CipherResponseModel(
|
||||
sharedCipher,
|
||||
user,
|
||||
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
|
||||
_globalSettings);
|
||||
return response;
|
||||
}
|
||||
|
||||
@ -608,8 +642,8 @@ public class CiphersController : Controller
|
||||
[HttpPost("{id}/collections")]
|
||||
public async Task<CipherDetailsResponseModel> PutCollections(Guid id, [FromBody] CipherCollectionsRequestModel model)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var cipher = await GetByIdAsync(id, userId);
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var cipher = await GetByIdAsync(id, user.Id);
|
||||
if (cipher == null || !cipher.OrganizationId.HasValue ||
|
||||
!await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
|
||||
{
|
||||
@ -617,20 +651,25 @@ public class CiphersController : Controller
|
||||
}
|
||||
|
||||
await _cipherService.SaveCollectionsAsync(cipher,
|
||||
model.CollectionIds.Select(c => new Guid(c)), userId, false);
|
||||
model.CollectionIds.Select(c => new Guid(c)), user.Id, false);
|
||||
|
||||
var updatedCipher = await GetByIdAsync(id, userId);
|
||||
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id);
|
||||
var updatedCipher = await GetByIdAsync(id, user.Id);
|
||||
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id);
|
||||
|
||||
return new CipherDetailsResponseModel(updatedCipher, _globalSettings, collectionCiphers);
|
||||
return new CipherDetailsResponseModel(
|
||||
updatedCipher,
|
||||
user,
|
||||
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
|
||||
_globalSettings,
|
||||
collectionCiphers);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/collections_v2")]
|
||||
[HttpPost("{id}/collections_v2")]
|
||||
public async Task<OptionalCipherDetailsResponseModel> PutCollections_vNext(Guid id, [FromBody] CipherCollectionsRequestModel model)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var cipher = await GetByIdAsync(id, userId);
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var cipher = await GetByIdAsync(id, user.Id);
|
||||
if (cipher == null || !cipher.OrganizationId.HasValue ||
|
||||
!await _currentContext.OrganizationUser(cipher.OrganizationId.Value) || !cipher.ViewPassword)
|
||||
{
|
||||
@ -638,10 +677,10 @@ public class CiphersController : Controller
|
||||
}
|
||||
|
||||
await _cipherService.SaveCollectionsAsync(cipher,
|
||||
model.CollectionIds.Select(c => new Guid(c)), userId, false);
|
||||
model.CollectionIds.Select(c => new Guid(c)), user.Id, false);
|
||||
|
||||
var updatedCipher = await GetByIdAsync(id, userId);
|
||||
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id);
|
||||
var updatedCipher = await GetByIdAsync(id, user.Id);
|
||||
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id);
|
||||
// If a user removes the last Can Manage access of a cipher, the "updatedCipher" will return null
|
||||
// We will be returning an "Unavailable" property so the client knows the user can no longer access this
|
||||
var response = new OptionalCipherDetailsResponseModel()
|
||||
@ -649,7 +688,12 @@ public class CiphersController : Controller
|
||||
Unavailable = updatedCipher is null,
|
||||
Cipher = updatedCipher is null
|
||||
? null
|
||||
: new CipherDetailsResponseModel(updatedCipher, _globalSettings, collectionCiphers)
|
||||
: new CipherDetailsResponseModel(
|
||||
updatedCipher,
|
||||
user,
|
||||
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
|
||||
_globalSettings,
|
||||
collectionCiphers)
|
||||
};
|
||||
return response;
|
||||
}
|
||||
@ -839,15 +883,19 @@ public class CiphersController : Controller
|
||||
[HttpPut("{id}/restore")]
|
||||
public async Task<CipherResponseModel> PutRestore(Guid id)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var cipher = await GetByIdAsync(id, userId);
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var cipher = await GetByIdAsync(id, user.Id);
|
||||
if (cipher == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _cipherService.RestoreAsync(cipher, userId);
|
||||
return new CipherResponseModel(cipher, _globalSettings);
|
||||
await _cipherService.RestoreAsync(cipher, user.Id);
|
||||
return new CipherResponseModel(
|
||||
cipher,
|
||||
user,
|
||||
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
|
||||
_globalSettings);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/restore-admin")]
|
||||
@ -996,10 +1044,10 @@ public class CiphersController : Controller
|
||||
[HttpPost("{id}/attachment/v2")]
|
||||
public async Task<AttachmentUploadDataResponseModel> PostAttachment(Guid id, [FromBody] AttachmentRequestModel request)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var cipher = request.AdminRequest ?
|
||||
await _cipherRepository.GetOrganizationDetailsByIdAsync(id) :
|
||||
await GetByIdAsync(id, userId);
|
||||
await GetByIdAsync(id, user.Id);
|
||||
|
||||
if (cipher == null || (request.AdminRequest && (!cipher.OrganizationId.HasValue ||
|
||||
!await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))))
|
||||
@ -1013,13 +1061,17 @@ public class CiphersController : Controller
|
||||
}
|
||||
|
||||
var (attachmentId, uploadUrl) = await _cipherService.CreateAttachmentForDelayedUploadAsync(cipher,
|
||||
request.Key, request.FileName, request.FileSize, request.AdminRequest, userId);
|
||||
request.Key, request.FileName, request.FileSize, request.AdminRequest, user.Id);
|
||||
return new AttachmentUploadDataResponseModel
|
||||
{
|
||||
AttachmentId = attachmentId,
|
||||
Url = uploadUrl,
|
||||
FileUploadType = _attachmentStorageService.FileUploadType,
|
||||
CipherResponse = request.AdminRequest ? null : new CipherResponseModel((CipherDetails)cipher, _globalSettings),
|
||||
CipherResponse = request.AdminRequest ? null : new CipherResponseModel(
|
||||
(CipherDetails)cipher,
|
||||
user,
|
||||
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
|
||||
_globalSettings),
|
||||
CipherMiniResponse = request.AdminRequest ? new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp) : null,
|
||||
};
|
||||
}
|
||||
@ -1077,8 +1129,8 @@ public class CiphersController : Controller
|
||||
{
|
||||
ValidateAttachment();
|
||||
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var cipher = await GetByIdAsync(id, userId);
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var cipher = await GetByIdAsync(id, user.Id);
|
||||
if (cipher == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
@ -1087,10 +1139,14 @@ public class CiphersController : Controller
|
||||
await Request.GetFileAsync(async (stream, fileName, key) =>
|
||||
{
|
||||
await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, key,
|
||||
Request.ContentLength.GetValueOrDefault(0), userId);
|
||||
Request.ContentLength.GetValueOrDefault(0), user.Id);
|
||||
});
|
||||
|
||||
return new CipherResponseModel(cipher, _globalSettings);
|
||||
return new CipherResponseModel(
|
||||
cipher,
|
||||
user,
|
||||
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
|
||||
_globalSettings);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/attachment-admin")]
|
||||
|
@ -36,6 +36,7 @@ public class SyncController : Controller
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion);
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
|
||||
public SyncController(
|
||||
IUserService userService,
|
||||
@ -49,7 +50,8 @@ public class SyncController : Controller
|
||||
ISendRepository sendRepository,
|
||||
GlobalSettings globalSettings,
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService)
|
||||
IFeatureService featureService,
|
||||
IApplicationCacheService applicationCacheService)
|
||||
{
|
||||
_userService = userService;
|
||||
_folderRepository = folderRepository;
|
||||
@ -63,6 +65,7 @@ public class SyncController : Controller
|
||||
_globalSettings = globalSettings;
|
||||
_currentContext = currentContext;
|
||||
_featureService = featureService;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
@ -104,7 +107,9 @@ public class SyncController : Controller
|
||||
var organizationManagingActiveUser = await _userService.GetOrganizationsManagingUserAsync(user.Id);
|
||||
var organizationIdsManagingActiveUser = organizationManagingActiveUser.Select(o => o.Id);
|
||||
|
||||
var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization,
|
||||
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
|
||||
var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities,
|
||||
organizationIdsManagingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,
|
||||
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
|
||||
return response;
|
||||
|
@ -0,0 +1,27 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Vault.Authorization.Permissions;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
|
||||
namespace Bit.Api.Vault.Models.Response;
|
||||
|
||||
public record CipherPermissionsResponseModel
|
||||
{
|
||||
public bool Delete { get; init; }
|
||||
public bool Restore { get; init; }
|
||||
|
||||
public CipherPermissionsResponseModel(
|
||||
User user,
|
||||
CipherDetails cipherDetails,
|
||||
IDictionary<Guid, OrganizationAbility> organizationAbilities)
|
||||
{
|
||||
OrganizationAbility organizationAbility = null;
|
||||
if (cipherDetails.OrganizationId.HasValue && !organizationAbilities.TryGetValue(cipherDetails.OrganizationId.Value, out organizationAbility))
|
||||
{
|
||||
throw new Exception("OrganizationAbility not found for organization cipher.");
|
||||
}
|
||||
|
||||
Delete = NormalCipherPermissions.CanDelete(user, cipherDetails, organizationAbility);
|
||||
Restore = NormalCipherPermissions.CanRestore(user, cipherDetails, organizationAbility);
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Enums;
|
||||
@ -96,26 +97,37 @@ public class CipherMiniResponseModel : ResponseModel
|
||||
|
||||
public class CipherResponseModel : CipherMiniResponseModel
|
||||
{
|
||||
public CipherResponseModel(CipherDetails cipher, IGlobalSettings globalSettings, string obj = "cipher")
|
||||
public CipherResponseModel(
|
||||
CipherDetails cipher,
|
||||
User user,
|
||||
IDictionary<Guid, OrganizationAbility> organizationAbilities,
|
||||
IGlobalSettings globalSettings,
|
||||
string obj = "cipher")
|
||||
: base(cipher, globalSettings, cipher.OrganizationUseTotp, obj)
|
||||
{
|
||||
FolderId = cipher.FolderId;
|
||||
Favorite = cipher.Favorite;
|
||||
Edit = cipher.Edit;
|
||||
ViewPassword = cipher.ViewPassword;
|
||||
Permissions = new CipherPermissionsResponseModel(user, cipher, organizationAbilities);
|
||||
}
|
||||
|
||||
public Guid? FolderId { get; set; }
|
||||
public bool Favorite { get; set; }
|
||||
public bool Edit { get; set; }
|
||||
public bool ViewPassword { get; set; }
|
||||
public CipherPermissionsResponseModel Permissions { get; set; }
|
||||
}
|
||||
|
||||
public class CipherDetailsResponseModel : CipherResponseModel
|
||||
{
|
||||
public CipherDetailsResponseModel(CipherDetails cipher, GlobalSettings globalSettings,
|
||||
public CipherDetailsResponseModel(
|
||||
CipherDetails cipher,
|
||||
User user,
|
||||
IDictionary<Guid, OrganizationAbility> organizationAbilities,
|
||||
GlobalSettings globalSettings,
|
||||
IDictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphers, string obj = "cipherDetails")
|
||||
: base(cipher, globalSettings, obj)
|
||||
: base(cipher, user, organizationAbilities, globalSettings, obj)
|
||||
{
|
||||
if (collectionCiphers?.ContainsKey(cipher.Id) ?? false)
|
||||
{
|
||||
@ -127,15 +139,24 @@ public class CipherDetailsResponseModel : CipherResponseModel
|
||||
}
|
||||
}
|
||||
|
||||
public CipherDetailsResponseModel(CipherDetails cipher, GlobalSettings globalSettings,
|
||||
public CipherDetailsResponseModel(
|
||||
CipherDetails cipher,
|
||||
User user,
|
||||
IDictionary<Guid, OrganizationAbility> organizationAbilities,
|
||||
GlobalSettings globalSettings,
|
||||
IEnumerable<CollectionCipher> collectionCiphers, string obj = "cipherDetails")
|
||||
: base(cipher, globalSettings, obj)
|
||||
: base(cipher, user, organizationAbilities, globalSettings, obj)
|
||||
{
|
||||
CollectionIds = collectionCiphers?.Select(c => c.CollectionId) ?? new List<Guid>();
|
||||
}
|
||||
|
||||
public CipherDetailsResponseModel(CipherDetailsWithCollections cipher, GlobalSettings globalSettings, string obj = "cipherDetails")
|
||||
: base(cipher, globalSettings, obj)
|
||||
public CipherDetailsResponseModel(
|
||||
CipherDetailsWithCollections cipher,
|
||||
User user,
|
||||
IDictionary<Guid, OrganizationAbility> organizationAbilities,
|
||||
GlobalSettings globalSettings,
|
||||
string obj = "cipherDetails")
|
||||
: base(cipher, user, organizationAbilities, globalSettings, obj)
|
||||
{
|
||||
CollectionIds = cipher.CollectionIds ?? new List<Guid>();
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Entities;
|
||||
@ -21,6 +22,7 @@ public class SyncResponseModel : ResponseModel
|
||||
User user,
|
||||
bool userTwoFactorEnabled,
|
||||
bool userHasPremiumFromOrganization,
|
||||
IDictionary<Guid, OrganizationAbility> organizationAbilities,
|
||||
IEnumerable<Guid> organizationIdsManagingUser,
|
||||
IEnumerable<OrganizationUserOrganizationDetails> organizationUserDetails,
|
||||
IEnumerable<ProviderUserProviderDetails> providerUserDetails,
|
||||
@ -37,7 +39,13 @@ public class SyncResponseModel : ResponseModel
|
||||
Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
|
||||
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsManagingUser);
|
||||
Folders = folders.Select(f => new FolderResponseModel(f));
|
||||
Ciphers = ciphers.Select(c => new CipherDetailsResponseModel(c, globalSettings, collectionCiphersDict));
|
||||
Ciphers = ciphers.Select(cipher =>
|
||||
new CipherDetailsResponseModel(
|
||||
cipher,
|
||||
user,
|
||||
organizationAbilities,
|
||||
globalSettings,
|
||||
collectionCiphersDict));
|
||||
Collections = collections?.Select(
|
||||
c => new CollectionDetailsResponseModel(c)) ?? new List<CollectionDetailsResponseModel>();
|
||||
Domains = excludeDomains ? null : new DomainsResponseModel(user, false);
|
||||
|
3
src/Core/AdminConsole/Errors/Error.cs
Normal file
3
src/Core/AdminConsole/Errors/Error.cs
Normal file
@ -0,0 +1,3 @@
|
||||
namespace Bit.Core.AdminConsole.Errors;
|
||||
|
||||
public record Error<T>(string Message, T ErroredValue);
|
11
src/Core/AdminConsole/Errors/InsufficientPermissionsError.cs
Normal file
11
src/Core/AdminConsole/Errors/InsufficientPermissionsError.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Bit.Core.AdminConsole.Errors;
|
||||
|
||||
public record InsufficientPermissionsError<T>(string Message, T ErroredValue) : Error<T>(Message, ErroredValue)
|
||||
{
|
||||
public const string Code = "Insufficient Permissions";
|
||||
|
||||
public InsufficientPermissionsError(T ErroredValue) : this(Code, ErroredValue)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
11
src/Core/AdminConsole/Errors/RecordNotFoundError.cs
Normal file
11
src/Core/AdminConsole/Errors/RecordNotFoundError.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Bit.Core.AdminConsole.Errors;
|
||||
|
||||
public record RecordNotFoundError<T>(string Message, T ErroredValue) : Error<T>(Message, ErroredValue)
|
||||
{
|
||||
public const string Code = "Record Not Found";
|
||||
|
||||
public RecordNotFoundError(T ErroredValue) : this(Code, ErroredValue)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
@ -8,21 +8,25 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||
|
||||
public class PolicyRequirementQuery(
|
||||
IPolicyRepository policyRepository,
|
||||
IEnumerable<RequirementFactory<IPolicyRequirement>> factories)
|
||||
IEnumerable<IPolicyRequirementFactory<IPolicyRequirement>> factories)
|
||||
: IPolicyRequirementQuery
|
||||
{
|
||||
public async Task<T> GetAsync<T>(Guid userId) where T : IPolicyRequirement
|
||||
{
|
||||
var factory = factories.OfType<RequirementFactory<T>>().SingleOrDefault();
|
||||
var factory = factories.OfType<IPolicyRequirementFactory<T>>().SingleOrDefault();
|
||||
if (factory is null)
|
||||
{
|
||||
throw new NotImplementedException("No Policy Requirement found for " + typeof(T));
|
||||
throw new NotImplementedException("No Requirement Factory found for " + typeof(T));
|
||||
}
|
||||
|
||||
return factory(await GetPolicyDetails(userId));
|
||||
var policyDetails = await GetPolicyDetails(userId);
|
||||
var filteredPolicies = policyDetails
|
||||
.Where(p => p.PolicyType == factory.PolicyType)
|
||||
.Where(factory.Enforce);
|
||||
var requirement = factory.Create(filteredPolicies);
|
||||
return requirement;
|
||||
}
|
||||
|
||||
private Task<IEnumerable<PolicyDetails>> GetPolicyDetails(Guid userId) =>
|
||||
policyRepository.GetPolicyDetailsByUserId(userId);
|
||||
private Task<IEnumerable<PolicyDetails>> GetPolicyDetails(Guid userId)
|
||||
=> policyRepository.GetPolicyDetailsByUserId(userId);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,44 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
/// <summary>
|
||||
/// A simple base implementation of <see cref="IPolicyRequirementFactory{T}"/> which will be suitable for most policies.
|
||||
/// It provides sensible defaults to help teams to implement their own Policy Requirements.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public abstract class BasePolicyRequirementFactory<T> : IPolicyRequirementFactory<T> where T : IPolicyRequirement
|
||||
{
|
||||
/// <summary>
|
||||
/// User roles that are exempt from policy enforcement.
|
||||
/// Owners and Admins are exempt by default but this may be overridden.
|
||||
/// </summary>
|
||||
protected virtual IEnumerable<OrganizationUserType> ExemptRoles { get; } =
|
||||
[OrganizationUserType.Owner, OrganizationUserType.Admin];
|
||||
|
||||
/// <summary>
|
||||
/// User statuses that are exempt from policy enforcement.
|
||||
/// Invited and Revoked users are exempt by default, which is appropriate in the majority of cases.
|
||||
/// </summary>
|
||||
protected virtual IEnumerable<OrganizationUserStatusType> ExemptStatuses { get; } =
|
||||
[OrganizationUserStatusType.Invited, OrganizationUserStatusType.Revoked];
|
||||
|
||||
/// <summary>
|
||||
/// Whether a Provider User for the organization is exempt from policy enforcement.
|
||||
/// Provider Users are exempt by default, which is appropriate in the majority of cases.
|
||||
/// </summary>
|
||||
protected virtual bool ExemptProviders { get; } = true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract PolicyType PolicyType { get; }
|
||||
|
||||
public bool Enforce(PolicyDetails policyDetails)
|
||||
=> !policyDetails.HasRole(ExemptRoles) &&
|
||||
!policyDetails.HasStatus(ExemptStatuses) &&
|
||||
(!policyDetails.IsProvider || !ExemptProviders);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract T Create(IEnumerable<PolicyDetails> policyDetails);
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
/// <summary>
|
||||
/// Policy requirements for the Disable Send policy.
|
||||
/// </summary>
|
||||
public class DisableSendPolicyRequirement : IPolicyRequirement
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates whether Send is disabled for the user. If true, the user should not be able to create or edit Sends.
|
||||
/// They may still delete existing Sends.
|
||||
/// </summary>
|
||||
public bool DisableSend { get; init; }
|
||||
}
|
||||
|
||||
public class DisableSendPolicyRequirementFactory : BasePolicyRequirementFactory<DisableSendPolicyRequirement>
|
||||
{
|
||||
public override PolicyType PolicyType => PolicyType.DisableSend;
|
||||
|
||||
public override DisableSendPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
|
||||
{
|
||||
var result = new DisableSendPolicyRequirement { DisableSend = policyDetails.Any() };
|
||||
return result;
|
||||
}
|
||||
}
|
@ -1,24 +1,11 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the business requirements of how one or more enterprise policies will be enforced against a user.
|
||||
/// The implementation of this interface will depend on how the policies are enforced in the relevant domain.
|
||||
/// An object that represents how a <see cref="PolicyType"/> will be enforced against a user.
|
||||
/// This acts as a bridge between the <see cref="Policy"/> entity saved to the database and the domain that the policy
|
||||
/// affects. You may represent the impact of the policy in any way that makes sense for the domain.
|
||||
/// </summary>
|
||||
public interface IPolicyRequirement;
|
||||
|
||||
/// <summary>
|
||||
/// A factory function that takes a sequence of <see cref="PolicyDetails"/> and transforms them into a single
|
||||
/// <see cref="IPolicyRequirement"/> for consumption by the relevant domain. This will receive *all* policy types
|
||||
/// that may be enforced against a user; when implementing this delegate, you must filter out irrelevant policy types
|
||||
/// as well as policies that should not be enforced against a user (e.g. due to the user's role or status).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See <see cref="PolicyRequirementHelpers"/> for extension methods to handle common requirements when implementing
|
||||
/// this delegate.
|
||||
/// </remarks>
|
||||
public delegate T RequirementFactory<out T>(IEnumerable<PolicyDetails> policyDetails)
|
||||
where T : IPolicyRequirement;
|
||||
|
@ -0,0 +1,39 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
/// <summary>
|
||||
/// An interface that defines how to create a single <see cref="IPolicyRequirement"/> from a sequence of
|
||||
/// <see cref="PolicyDetails"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The <see cref="IPolicyRequirement"/> that the factory produces.</typeparam>
|
||||
/// <remarks>
|
||||
/// See <see cref="BasePolicyRequirementFactory{T}"/> for a simple base implementation suitable for most policies.
|
||||
/// </remarks>
|
||||
public interface IPolicyRequirementFactory<out T> where T : IPolicyRequirement
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="PolicyType"/> that the requirement relates to.
|
||||
/// </summary>
|
||||
PolicyType PolicyType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A predicate that determines whether a policy should be enforced against the user.
|
||||
/// </summary>
|
||||
/// <remarks>Use this to exempt users based on their role, status or other attributes.</remarks>
|
||||
/// <param name="policyDetails">Policy details for the defined PolicyType.</param>
|
||||
/// <returns>True if the policy should be enforced against the user, false otherwise.</returns>
|
||||
bool Enforce(PolicyDetails policyDetails);
|
||||
|
||||
/// <summary>
|
||||
/// A reducer method that creates a single <see cref="IPolicyRequirement"/> from a set of PolicyDetails.
|
||||
/// </summary>
|
||||
/// <param name="policyDetails">
|
||||
/// PolicyDetails for the specified PolicyType, after they have been filtered by the Enforce predicate. That is,
|
||||
/// this is the final interface to be called.
|
||||
/// </param>
|
||||
T Create(IEnumerable<PolicyDetails> policyDetails);
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
@ -7,35 +6,16 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements
|
||||
public static class PolicyRequirementHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Filters the PolicyDetails by PolicyType. This is generally required to only get the PolicyDetails that your
|
||||
/// IPolicyRequirement relates to.
|
||||
/// Returns true if the <see cref="PolicyDetails"/> is for one of the specified roles, false otherwise.
|
||||
/// </summary>
|
||||
public static IEnumerable<PolicyDetails> GetPolicyType(
|
||||
this IEnumerable<PolicyDetails> policyDetails,
|
||||
PolicyType type)
|
||||
=> policyDetails.Where(x => x.PolicyType == type);
|
||||
|
||||
/// <summary>
|
||||
/// Filters the PolicyDetails to remove the specified user roles. This can be used to exempt
|
||||
/// owners and admins from policy enforcement.
|
||||
/// </summary>
|
||||
public static IEnumerable<PolicyDetails> ExemptRoles(
|
||||
this IEnumerable<PolicyDetails> policyDetails,
|
||||
public static bool HasRole(
|
||||
this PolicyDetails policyDetails,
|
||||
IEnumerable<OrganizationUserType> roles)
|
||||
=> policyDetails.Where(x => !roles.Contains(x.OrganizationUserType));
|
||||
=> roles.Contains(policyDetails.OrganizationUserType);
|
||||
|
||||
/// <summary>
|
||||
/// Filters the PolicyDetails to remove organization users who are also provider users for the organization.
|
||||
/// This can be used to exempt provider users from policy enforcement.
|
||||
/// Returns true if the <see cref="PolicyDetails"/> relates to one of the specified statuses, false otherwise.
|
||||
/// </summary>
|
||||
public static IEnumerable<PolicyDetails> ExemptProviders(this IEnumerable<PolicyDetails> policyDetails)
|
||||
=> policyDetails.Where(x => !x.IsProvider);
|
||||
|
||||
/// <summary>
|
||||
/// Filters the PolicyDetails to remove the specified organization user statuses. For example, this can be used
|
||||
/// to exempt users in the invited and revoked statuses from policy enforcement.
|
||||
/// </summary>
|
||||
public static IEnumerable<PolicyDetails> ExemptStatus(
|
||||
this IEnumerable<PolicyDetails> policyDetails, IEnumerable<OrganizationUserStatusType> status)
|
||||
=> policyDetails.Where(x => !status.Contains(x.OrganizationUserStatus));
|
||||
public static bool HasStatus(this PolicyDetails policyDetails, IEnumerable<OrganizationUserStatusType> status)
|
||||
=> status.Contains(policyDetails.OrganizationUserStatus);
|
||||
}
|
||||
|
@ -0,0 +1,34 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
/// <summary>
|
||||
/// Policy requirements for the Send Options policy.
|
||||
/// </summary>
|
||||
public class SendOptionsPolicyRequirement : IPolicyRequirement
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates whether the user is prohibited from hiding their email from the recipient of a Send.
|
||||
/// </summary>
|
||||
public bool DisableHideEmail { get; init; }
|
||||
}
|
||||
|
||||
public class SendOptionsPolicyRequirementFactory : BasePolicyRequirementFactory<SendOptionsPolicyRequirement>
|
||||
{
|
||||
public override PolicyType PolicyType => PolicyType.SendOptions;
|
||||
|
||||
public override SendOptionsPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
|
||||
{
|
||||
var result = policyDetails
|
||||
.Select(p => p.GetDataModel<SendOptionsPolicyData>())
|
||||
.Aggregate(
|
||||
new SendOptionsPolicyRequirement(),
|
||||
(result, data) => new SendOptionsPolicyRequirement
|
||||
{
|
||||
DisableHideEmail = result.DisableHideEmail || data.DisableHideEmail
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
/// <summary>
|
||||
/// Policy requirements for the Disable Send and Send Options policies.
|
||||
/// </summary>
|
||||
public class SendPolicyRequirement : IPolicyRequirement
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates whether Send is disabled for the user. If true, the user should not be able to create or edit Sends.
|
||||
/// They may still delete existing Sends.
|
||||
/// </summary>
|
||||
public bool DisableSend { get; init; }
|
||||
/// <summary>
|
||||
/// Indicates whether the user is prohibited from hiding their email from the recipient of a Send.
|
||||
/// </summary>
|
||||
public bool DisableHideEmail { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new SendPolicyRequirement.
|
||||
/// </summary>
|
||||
/// <param name="policyDetails">All PolicyDetails relating to the user.</param>
|
||||
/// <remarks>
|
||||
/// This is a <see cref="RequirementFactory{T}"/> for the SendPolicyRequirement.
|
||||
/// </remarks>
|
||||
public static SendPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
|
||||
{
|
||||
var filteredPolicies = policyDetails
|
||||
.ExemptRoles([OrganizationUserType.Owner, OrganizationUserType.Admin])
|
||||
.ExemptStatus([OrganizationUserStatusType.Invited, OrganizationUserStatusType.Revoked])
|
||||
.ExemptProviders()
|
||||
.ToList();
|
||||
|
||||
var result = filteredPolicies
|
||||
.GetPolicyType(PolicyType.SendOptions)
|
||||
.Select(p => p.GetDataModel<SendOptionsPolicyData>())
|
||||
.Aggregate(
|
||||
new SendPolicyRequirement
|
||||
{
|
||||
// Set Disable Send requirement in the initial seed
|
||||
DisableSend = filteredPolicies.GetPolicyType(PolicyType.DisableSend).Any()
|
||||
},
|
||||
(result, data) => new SendPolicyRequirement
|
||||
{
|
||||
DisableSend = result.DisableSend,
|
||||
DisableHideEmail = result.DisableHideEmail || data.DisableHideEmail
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -31,32 +31,7 @@ public static class PolicyServiceCollectionExtensions
|
||||
|
||||
private static void AddPolicyRequirements(this IServiceCollection services)
|
||||
{
|
||||
// Register policy requirement factories here
|
||||
services.AddPolicyRequirement(SendPolicyRequirement.Create);
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, DisableSendPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, SendOptionsPolicyRequirementFactory>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to register simple policy requirements where its factory method implements CreateRequirement.
|
||||
/// This MUST be used rather than calling AddScoped directly, because it will ensure the factory method has
|
||||
/// the correct type to be injected and then identified by <see cref="PolicyRequirementQuery"/> at runtime.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The specific PolicyRequirement being registered.</typeparam>
|
||||
private static void AddPolicyRequirement<T>(this IServiceCollection serviceCollection, RequirementFactory<T> factory)
|
||||
where T : class, IPolicyRequirement
|
||||
=> serviceCollection.AddPolicyRequirement(_ => factory);
|
||||
|
||||
/// <summary>
|
||||
/// Used to register policy requirements where you need to access additional dependencies (usually to return a
|
||||
/// curried factory method).
|
||||
/// This MUST be used rather than calling AddScoped directly, because it will ensure the factory method has
|
||||
/// the correct type to be injected and then identified by <see cref="PolicyRequirementQuery"/> at runtime.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">
|
||||
/// A callback that takes IServiceProvider and returns a RequirementFactory for
|
||||
/// your policy requirement.
|
||||
/// </typeparam>
|
||||
private static void AddPolicyRequirement<T>(this IServiceCollection serviceCollection,
|
||||
Func<IServiceProvider, RequirementFactory<T>> factory)
|
||||
where T : class, IPolicyRequirement
|
||||
=> serviceCollection.AddScoped<RequirementFactory<IPolicyRequirement>>(factory);
|
||||
}
|
||||
|
6
src/Core/AdminConsole/Shared/Validation/IValidator.cs
Normal file
6
src/Core/AdminConsole/Shared/Validation/IValidator.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.AdminConsole.Shared.Validation;
|
||||
|
||||
public interface IValidator<T>
|
||||
{
|
||||
public Task<ValidationResult<T>> ValidateAsync(T value);
|
||||
}
|
15
src/Core/AdminConsole/Shared/Validation/ValidationResult.cs
Normal file
15
src/Core/AdminConsole/Shared/Validation/ValidationResult.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using Bit.Core.AdminConsole.Errors;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Shared.Validation;
|
||||
|
||||
public abstract record ValidationResult<T>;
|
||||
|
||||
public record Valid<T> : ValidationResult<T>
|
||||
{
|
||||
public T Value { get; init; }
|
||||
}
|
||||
|
||||
public record Invalid<T> : ValidationResult<T>
|
||||
{
|
||||
public IEnumerable<Error<T>> Errors { get; init; }
|
||||
}
|
@ -16,6 +16,12 @@ public class AuthRequest : ITableObject<Guid>
|
||||
public DeviceType RequestDeviceType { get; set; }
|
||||
[MaxLength(50)]
|
||||
public string RequestIpAddress { get; set; }
|
||||
/// <summary>
|
||||
/// This country name is populated through a header value fetched from the ISO-3166 country code.
|
||||
/// It will always be the English short form of the country name. The length should never be over 200 characters.
|
||||
/// </summary>
|
||||
[MaxLength(200)]
|
||||
public string RequestCountryName { get; set; }
|
||||
public Guid? ResponseDeviceId { get; set; }
|
||||
[MaxLength(25)]
|
||||
public string AccessCode { get; set; }
|
||||
|
@ -164,6 +164,7 @@ public class AuthRequestService : IAuthRequestService
|
||||
RequestDeviceIdentifier = model.DeviceIdentifier,
|
||||
RequestDeviceType = _currentContext.DeviceType.Value,
|
||||
RequestIpAddress = _currentContext.IpAddress,
|
||||
RequestCountryName = _currentContext.CountryName,
|
||||
AccessCode = model.AccessCode,
|
||||
PublicKey = model.PublicKey,
|
||||
UserId = user.Id,
|
||||
@ -176,12 +177,7 @@ public class AuthRequestService : IAuthRequestService
|
||||
|
||||
public async Task<AuthRequest> UpdateAuthRequestAsync(Guid authRequestId, Guid currentUserId, AuthRequestUpdateRequestModel model)
|
||||
{
|
||||
var authRequest = await _authRequestRepository.GetByIdAsync(authRequestId);
|
||||
|
||||
if (authRequest == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
var authRequest = await _authRequestRepository.GetByIdAsync(authRequestId) ?? throw new NotFoundException();
|
||||
|
||||
// Once Approval/Disapproval has been set, this AuthRequest should not be updated again.
|
||||
if (authRequest.Approved is not null)
|
||||
|
@ -95,5 +95,16 @@ public interface IProviderBillingService
|
||||
Task<Subscription> SetupSubscription(
|
||||
Provider provider);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the <paramref name="provider"/>'s payment source and tax information and then sets their subscription's collection_method to be "charge_automatically".
|
||||
/// </summary>
|
||||
/// <param name="provider">The <paramref name="provider"/> to update the payment source and tax information for.</param>
|
||||
/// <param name="tokenizedPaymentSource">The tokenized payment source (ex. Credit Card) to attach to the <paramref name="provider"/>.</param>
|
||||
/// <param name="taxInformation">The <paramref name="provider"/>'s updated tax information.</param>
|
||||
Task UpdatePaymentMethod(
|
||||
Provider provider,
|
||||
TokenizedPaymentSource tokenizedPaymentSource,
|
||||
TaxInformation taxInformation);
|
||||
|
||||
Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command);
|
||||
}
|
||||
|
@ -115,6 +115,16 @@ public static class FeatureFlagKeys
|
||||
public const string ItemShare = "item-share";
|
||||
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
|
||||
public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications";
|
||||
public const string DesktopSendUIRefresh = "desktop-send-ui-refresh";
|
||||
public const string ExportAttachments = "export-attachments";
|
||||
|
||||
/* Vault Team */
|
||||
public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form";
|
||||
public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss";
|
||||
public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss";
|
||||
public const string VaultBulkManagementAction = "vault-bulk-management-action";
|
||||
public const string RestrictProviderAccess = "restrict-provider-access";
|
||||
public const string SecurityTasks = "security-tasks";
|
||||
|
||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
|
||||
@ -122,10 +132,7 @@ public static class FeatureFlagKeys
|
||||
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
||||
public const string EmailVerification = "email-verification";
|
||||
public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays";
|
||||
public const string ExtensionRefresh = "extension-refresh";
|
||||
public const string RestrictProviderAccess = "restrict-provider-access";
|
||||
public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service";
|
||||
public const string VaultBulkManagementAction = "vault-bulk-management-action";
|
||||
public const string InlineMenuFieldQualification = "inline-menu-field-qualification";
|
||||
public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements";
|
||||
public const string DeviceTrustLogging = "pm-8285-device-trust-logging";
|
||||
@ -150,13 +157,8 @@ public static class FeatureFlagKeys
|
||||
public const string RemoveServerVersionHeader = "remove-server-version-header";
|
||||
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
||||
public const string NewDeviceVerification = "new-device-verification";
|
||||
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 MacOsNativeCredentialSync = "macos-native-credential-sync";
|
||||
public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form";
|
||||
public const string InlineMenuTotp = "inline-menu-totp";
|
||||
public const string SelfHostLicenseRefactor = "pm-11516-self-host-license-refactor";
|
||||
public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration";
|
||||
public const string AppReviewPrompt = "app-review-prompt";
|
||||
public const string ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs";
|
||||
@ -174,6 +176,8 @@ public static class FeatureFlagKeys
|
||||
public const string WebPush = "web-push";
|
||||
public const string AndroidImportLoginsFlow = "import-logins-flow";
|
||||
public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features";
|
||||
public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method";
|
||||
public const string PM3553_MobileSimpleLoginSelfHostAlias = "simple-login-self-host-alias";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
{
|
||||
|
@ -30,6 +30,7 @@ public class CurrentContext : ICurrentContext
|
||||
public virtual string DeviceIdentifier { get; set; }
|
||||
public virtual DeviceType? DeviceType { get; set; }
|
||||
public virtual string IpAddress { get; set; }
|
||||
public virtual string CountryName { get; set; }
|
||||
public virtual List<CurrentContextOrganization> Organizations { get; set; }
|
||||
public virtual List<CurrentContextProvider> Providers { get; set; }
|
||||
public virtual Guid? InstallationId { get; set; }
|
||||
@ -104,6 +105,12 @@ public class CurrentContext : ICurrentContext
|
||||
{
|
||||
ClientVersionIsPrerelease = clientVersionIsPrerelease == "1";
|
||||
}
|
||||
|
||||
if (httpContext.Request.Headers.TryGetValue("country-name", out var countryName))
|
||||
{
|
||||
CountryName = countryName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async virtual Task BuildAsync(ClaimsPrincipal user, GlobalSettings globalSettings)
|
||||
|
@ -20,6 +20,7 @@ public interface ICurrentContext
|
||||
string DeviceIdentifier { get; set; }
|
||||
DeviceType? DeviceType { get; set; }
|
||||
string IpAddress { get; set; }
|
||||
string CountryName { get; set; }
|
||||
List<CurrentContextOrganization> Organizations { get; set; }
|
||||
Guid? InstallationId { get; set; }
|
||||
Guid? OrganizationId { get; set; }
|
||||
|
@ -1,5 +1,7 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Errors;
|
||||
|
||||
namespace Bit.Core.Models.Commands;
|
||||
|
||||
public class CommandResult(IEnumerable<string> errors)
|
||||
@ -28,7 +30,7 @@ public class Success : CommandResult
|
||||
{
|
||||
}
|
||||
|
||||
public abstract class CommandResult<T> { }
|
||||
public abstract class CommandResult<T>;
|
||||
|
||||
public class Success<T>(T value) : CommandResult<T>
|
||||
{
|
||||
@ -43,3 +45,15 @@ public class Failure<T>(IEnumerable<string> errorMessages) : CommandResult<T>
|
||||
|
||||
public Failure(string error) : this([error]) { }
|
||||
}
|
||||
|
||||
public class Partial<T> : CommandResult<T>
|
||||
{
|
||||
public T[] Successes { get; set; } = [];
|
||||
public Error<T>[] Failures { get; set; } = [];
|
||||
|
||||
public Partial(IEnumerable<T> successfulItems, IEnumerable<Error<T>> failedItems)
|
||||
{
|
||||
Successes = successfulItems.ToArray();
|
||||
Failures = failedItems.ToArray();
|
||||
}
|
||||
}
|
||||
|
@ -42,10 +42,7 @@ public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuer
|
||||
|
||||
var subscriptionInfo = await GetSubscriptionAsync(organization);
|
||||
var license = new OrganizationLicense(organization, subscriptionInfo, installationId, _licensingService, version);
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.SelfHostLicenseRefactor))
|
||||
{
|
||||
license.Token = await _licensingService.CreateOrganizationTokenAsync(organization, installationId, subscriptionInfo);
|
||||
}
|
||||
license.Token = await _licensingService.CreateOrganizationTokenAsync(organization, installationId, subscriptionInfo);
|
||||
|
||||
return license;
|
||||
}
|
||||
|
@ -1218,10 +1218,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
? new UserLicense(user, _licenseService)
|
||||
: new UserLicense(user, subscriptionInfo, _licenseService);
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.SelfHostLicenseRefactor))
|
||||
{
|
||||
userLicense.Token = await _licenseService.CreateUserTokenAsync(user, subscriptionInfo);
|
||||
}
|
||||
userLicense.Token = await _licenseService.CreateUserTokenAsync(user, subscriptionInfo);
|
||||
|
||||
return userLicense;
|
||||
}
|
||||
|
@ -326,14 +326,14 @@ public class SendService : ISendService
|
||||
return;
|
||||
}
|
||||
|
||||
var sendPolicyRequirement = await _policyRequirementQuery.GetAsync<SendPolicyRequirement>(userId.Value);
|
||||
|
||||
if (sendPolicyRequirement.DisableSend)
|
||||
var disableSendRequirement = await _policyRequirementQuery.GetAsync<DisableSendPolicyRequirement>(userId.Value);
|
||||
if (disableSendRequirement.DisableSend)
|
||||
{
|
||||
throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send.");
|
||||
}
|
||||
|
||||
if (sendPolicyRequirement.DisableHideEmail && send.HideEmail.GetValueOrDefault())
|
||||
var sendOptionsRequirement = await _policyRequirementQuery.GetAsync<SendOptionsPolicyRequirement>(userId.Value);
|
||||
if (sendOptionsRequirement.DisableHideEmail && send.HideEmail.GetValueOrDefault())
|
||||
{
|
||||
throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send.");
|
||||
}
|
||||
|
@ -252,19 +252,19 @@ public class DeviceValidator(
|
||||
{
|
||||
case DeviceValidationResultType.InvalidUser:
|
||||
result.ErrorDescription = "Invalid user";
|
||||
customResponse.Add("ErrorModel", new ErrorResponseModel("invalid user"));
|
||||
customResponse.Add("ErrorModel", new ErrorResponseModel("Invalid user."));
|
||||
break;
|
||||
case DeviceValidationResultType.InvalidNewDeviceOtp:
|
||||
result.ErrorDescription = "Invalid New Device OTP";
|
||||
customResponse.Add("ErrorModel", new ErrorResponseModel("invalid new device otp"));
|
||||
customResponse.Add("ErrorModel", new ErrorResponseModel("Invalid new device OTP. Try again."));
|
||||
break;
|
||||
case DeviceValidationResultType.NewDeviceVerificationRequired:
|
||||
result.ErrorDescription = "New device verification required";
|
||||
customResponse.Add("ErrorModel", new ErrorResponseModel("new device verification required"));
|
||||
customResponse.Add("ErrorModel", new ErrorResponseModel("New device verification required."));
|
||||
break;
|
||||
case DeviceValidationResultType.NoDeviceInformationProvided:
|
||||
result.ErrorDescription = "No device information provided";
|
||||
customResponse.Add("ErrorModel", new ErrorResponseModel("no device information provided"));
|
||||
customResponse.Add("ErrorModel", new ErrorResponseModel("No device information provided."));
|
||||
break;
|
||||
}
|
||||
return (result, customResponse);
|
||||
|
@ -6,6 +6,7 @@
|
||||
@RequestDeviceIdentifier NVARCHAR(50),
|
||||
@RequestDeviceType TINYINT,
|
||||
@RequestIpAddress VARCHAR(50),
|
||||
@RequestCountryName NVARCHAR(200),
|
||||
@ResponseDeviceId UNIQUEIDENTIFIER,
|
||||
@AccessCode VARCHAR(25),
|
||||
@PublicKey VARCHAR(MAX),
|
||||
@ -20,7 +21,7 @@ BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
INSERT INTO [dbo].[AuthRequest]
|
||||
(
|
||||
(
|
||||
[Id],
|
||||
[UserId],
|
||||
[OrganizationId],
|
||||
@ -28,6 +29,7 @@ BEGIN
|
||||
[RequestDeviceIdentifier],
|
||||
[RequestDeviceType],
|
||||
[RequestIpAddress],
|
||||
[RequestCountryName],
|
||||
[ResponseDeviceId],
|
||||
[AccessCode],
|
||||
[PublicKey],
|
||||
@ -37,24 +39,25 @@ BEGIN
|
||||
[CreationDate],
|
||||
[ResponseDate],
|
||||
[AuthenticationDate]
|
||||
)
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@Id,
|
||||
@UserId,
|
||||
@OrganizationId,
|
||||
@Type,
|
||||
@RequestDeviceIdentifier,
|
||||
@RequestDeviceType,
|
||||
@RequestIpAddress,
|
||||
@ResponseDeviceId,
|
||||
@AccessCode,
|
||||
@PublicKey,
|
||||
@Key,
|
||||
@MasterPasswordHash,
|
||||
@Approved,
|
||||
@CreationDate,
|
||||
@ResponseDate,
|
||||
@AuthenticationDate
|
||||
(
|
||||
@Id,
|
||||
@UserId,
|
||||
@OrganizationId,
|
||||
@Type,
|
||||
@RequestDeviceIdentifier,
|
||||
@RequestDeviceType,
|
||||
@RequestIpAddress,
|
||||
@RequestCountryName,
|
||||
@ResponseDeviceId,
|
||||
@AccessCode,
|
||||
@PublicKey,
|
||||
@Key,
|
||||
@MasterPasswordHash,
|
||||
@Approved,
|
||||
@CreationDate,
|
||||
@ResponseDate,
|
||||
@AuthenticationDate
|
||||
)
|
||||
END
|
||||
END
|
||||
|
@ -2,10 +2,11 @@
|
||||
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@OrganizationId UNIQUEIDENTIFIER = NULL,
|
||||
@Type SMALLINT,
|
||||
@Type SMALLINT,
|
||||
@RequestDeviceIdentifier NVARCHAR(50),
|
||||
@RequestDeviceType SMALLINT,
|
||||
@RequestIpAddress VARCHAR(50),
|
||||
@RequestCountryName NVARCHAR(200),
|
||||
@ResponseDeviceId UNIQUEIDENTIFIER,
|
||||
@AccessCode VARCHAR(25),
|
||||
@PublicKey VARCHAR(MAX),
|
||||
@ -14,29 +15,30 @@
|
||||
@Approved BIT,
|
||||
@CreationDate DATETIME2 (7),
|
||||
@ResponseDate DATETIME2 (7),
|
||||
@AuthenticationDate DATETIME2 (7)
|
||||
@AuthenticationDate DATETIME2 (7)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE
|
||||
[dbo].[AuthRequest]
|
||||
SET
|
||||
[UserId] = @UserId,
|
||||
[Type] = @Type,
|
||||
[OrganizationId] = @OrganizationId,
|
||||
[RequestDeviceIdentifier] = @RequestDeviceIdentifier,
|
||||
[RequestDeviceType] = @RequestDeviceType,
|
||||
[RequestIpAddress] = @RequestIpAddress,
|
||||
[ResponseDeviceId] = @ResponseDeviceId,
|
||||
[AccessCode] = @AccessCode,
|
||||
[PublicKey] = @PublicKey,
|
||||
[Key] = @Key,
|
||||
[MasterPasswordHash] = @MasterPasswordHash,
|
||||
[Approved] = @Approved,
|
||||
[CreationDate] = @CreationDate,
|
||||
[ResponseDate] = @ResponseDate,
|
||||
[AuthenticationDate] = @AuthenticationDate
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
[dbo].[AuthRequest]
|
||||
SET
|
||||
[UserId] = @UserId,
|
||||
[Type] = @Type,
|
||||
[OrganizationId] = @OrganizationId,
|
||||
[RequestDeviceIdentifier] = @RequestDeviceIdentifier,
|
||||
[RequestDeviceType] = @RequestDeviceType,
|
||||
[RequestIpAddress] = @RequestIpAddress,
|
||||
[RequestCountryName] = @RequestCountryName,
|
||||
[ResponseDeviceId] = @ResponseDeviceId,
|
||||
[AccessCode] = @AccessCode,
|
||||
[PublicKey] = @PublicKey,
|
||||
[Key] = @Key,
|
||||
[MasterPasswordHash] = @MasterPasswordHash,
|
||||
[Approved] = @Approved,
|
||||
[CreationDate] = @CreationDate,
|
||||
[ResponseDate] = @ResponseDate,
|
||||
[AuthenticationDate] = @AuthenticationDate
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
||||
|
@ -10,6 +10,7 @@ BEGIN
|
||||
[RequestDeviceIdentifier] = ARI.[RequestDeviceIdentifier],
|
||||
[RequestDeviceType] = ARI.[RequestDeviceType],
|
||||
[RequestIpAddress] = ARI.[RequestIpAddress],
|
||||
[RequestCountryName] = ARI.[RequestCountryName],
|
||||
[ResponseDeviceId] = ARI.[ResponseDeviceId],
|
||||
[AccessCode] = ARI.[AccessCode],
|
||||
[PublicKey] = ARI.[PublicKey],
|
||||
@ -22,7 +23,7 @@ BEGIN
|
||||
[OrganizationId] = ARI.[OrganizationId]
|
||||
FROM
|
||||
[dbo].[AuthRequest] AR
|
||||
INNER JOIN
|
||||
INNER JOIN
|
||||
OPENJSON(@jsonData)
|
||||
WITH (
|
||||
Id UNIQUEIDENTIFIER '$.Id',
|
||||
@ -31,6 +32,7 @@ BEGIN
|
||||
RequestDeviceIdentifier NVARCHAR(50) '$.RequestDeviceIdentifier',
|
||||
RequestDeviceType SMALLINT '$.RequestDeviceType',
|
||||
RequestIpAddress VARCHAR(50) '$.RequestIpAddress',
|
||||
RequestCountryName NVARCHAR(200) '$.RequestCountryName',
|
||||
ResponseDeviceId UNIQUEIDENTIFIER '$.ResponseDeviceId',
|
||||
AccessCode VARCHAR(25) '$.AccessCode',
|
||||
PublicKey VARCHAR(MAX) '$.PublicKey',
|
||||
|
@ -15,11 +15,11 @@
|
||||
[ResponseDate] DATETIME2 (7) NULL,
|
||||
[AuthenticationDate] DATETIME2 (7) NULL,
|
||||
[OrganizationId] UNIQUEIDENTIFIER NULL,
|
||||
[RequestCountryName] NVARCHAR(200) NULL,
|
||||
CONSTRAINT [PK_AuthRequest] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||
CONSTRAINT [FK_AuthRequest_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]),
|
||||
CONSTRAINT [FK_AuthRequest_ResponseDevice] FOREIGN KEY ([ResponseDeviceId]) REFERENCES [dbo].[Device] ([Id]),
|
||||
CONSTRAINT [FK_AuthRequest_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id])
|
||||
);
|
||||
|
||||
|
||||
GO
|
||||
|
@ -15,16 +15,12 @@ using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@ -37,10 +33,8 @@ public class AccountsControllerTests : IDisposable
|
||||
{
|
||||
|
||||
private readonly AccountsController _sut;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IPolicyService _policyService;
|
||||
@ -48,9 +42,6 @@ public class AccountsControllerTests : IDisposable
|
||||
private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
|
||||
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ISubscriberService _subscriberService;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
|
||||
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
|
||||
private readonly IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> _folderValidator;
|
||||
@ -70,16 +61,11 @@ public class AccountsControllerTests : IDisposable
|
||||
_organizationService = Substitute.For<IOrganizationService>();
|
||||
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||
_providerUserRepository = Substitute.For<IProviderUserRepository>();
|
||||
_paymentService = Substitute.For<IPaymentService>();
|
||||
_globalSettings = new GlobalSettings();
|
||||
_policyService = Substitute.For<IPolicyService>();
|
||||
_setInitialMasterPasswordCommand = Substitute.For<ISetInitialMasterPasswordCommand>();
|
||||
_rotateUserKeyCommand = Substitute.For<IRotateUserKeyCommand>();
|
||||
_tdeOffboardingPasswordCommand = Substitute.For<ITdeOffboardingPasswordCommand>();
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_subscriberService = Substitute.For<ISubscriberService>();
|
||||
_referenceEventService = Substitute.For<IReferenceEventService>();
|
||||
_currentContext = Substitute.For<ICurrentContext>();
|
||||
_cipherValidator =
|
||||
Substitute.For<IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>>();
|
||||
_folderValidator =
|
||||
@ -93,20 +79,15 @@ public class AccountsControllerTests : IDisposable
|
||||
IReadOnlyList<OrganizationUser>>>();
|
||||
|
||||
_sut = new AccountsController(
|
||||
_globalSettings,
|
||||
_organizationService,
|
||||
_organizationUserRepository,
|
||||
_providerUserRepository,
|
||||
_paymentService,
|
||||
_userService,
|
||||
_policyService,
|
||||
_setInitialMasterPasswordCommand,
|
||||
_tdeOffboardingPasswordCommand,
|
||||
_rotateUserKeyCommand,
|
||||
_featureService,
|
||||
_subscriberService,
|
||||
_referenceEventService,
|
||||
_currentContext,
|
||||
_cipherValidator,
|
||||
_folderValidator,
|
||||
_sendValidator,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,23 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;
|
||||
|
||||
/// <summary>
|
||||
/// Intentionally simplified PolicyRequirement that just holds the input PolicyDetails for us to assert against.
|
||||
/// </summary>
|
||||
public class TestPolicyRequirement : IPolicyRequirement
|
||||
{
|
||||
public IEnumerable<PolicyDetails> Policies { get; init; } = [];
|
||||
}
|
||||
|
||||
public class TestPolicyRequirementFactory(Func<PolicyDetails, bool> enforce) : IPolicyRequirementFactory<TestPolicyRequirement>
|
||||
{
|
||||
public PolicyType PolicyType => PolicyType.SingleOrg;
|
||||
|
||||
public bool Enforce(PolicyDetails policyDetails) => enforce(policyDetails);
|
||||
|
||||
public TestPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
|
||||
=> new() { Policies = policyDetails };
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
@ -11,50 +11,72 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;
|
||||
[SutProviderCustomize]
|
||||
public class PolicyRequirementQueryTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests that the query correctly registers, retrieves and instantiates arbitrary IPolicyRequirements
|
||||
/// according to their provided CreateRequirement delegate.
|
||||
/// </summary>
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetAsync_Works(Guid userId, Guid organizationId)
|
||||
public async Task GetAsync_IgnoresOtherPolicyTypes(Guid userId)
|
||||
{
|
||||
var thisPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg };
|
||||
var otherPolicy = new PolicyDetails { PolicyType = PolicyType.RequireSso };
|
||||
var policyRepository = Substitute.For<IPolicyRepository>();
|
||||
var factories = new List<RequirementFactory<IPolicyRequirement>>
|
||||
{
|
||||
// In prod this cast is handled when the CreateRequirement delegate is registered in DI
|
||||
(RequirementFactory<TestPolicyRequirement>)TestPolicyRequirement.Create
|
||||
};
|
||||
policyRepository.GetPolicyDetailsByUserId(userId).Returns([otherPolicy, thisPolicy]);
|
||||
|
||||
var sut = new PolicyRequirementQuery(policyRepository, factories);
|
||||
policyRepository.GetPolicyDetailsByUserId(userId).Returns([
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = organizationId
|
||||
}
|
||||
]);
|
||||
var factory = new TestPolicyRequirementFactory(_ => true);
|
||||
var sut = new PolicyRequirementQuery(policyRepository, [factory]);
|
||||
|
||||
var requirement = await sut.GetAsync<TestPolicyRequirement>(userId);
|
||||
Assert.Equal(organizationId, requirement.OrganizationId);
|
||||
|
||||
Assert.Contains(thisPolicy, requirement.Policies);
|
||||
Assert.DoesNotContain(otherPolicy, requirement.Policies);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetAsync_ThrowsIfNoRequirementRegistered(Guid userId)
|
||||
public async Task GetAsync_CallsEnforceCallback(Guid userId)
|
||||
{
|
||||
// Arrange policies
|
||||
var policyRepository = Substitute.For<IPolicyRepository>();
|
||||
var thisPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg };
|
||||
var otherPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg };
|
||||
policyRepository.GetPolicyDetailsByUserId(userId).Returns([thisPolicy, otherPolicy]);
|
||||
|
||||
// Arrange a substitute Enforce function so that we can inspect the received calls
|
||||
var callback = Substitute.For<Func<PolicyDetails, bool>>();
|
||||
callback(Arg.Any<PolicyDetails>()).Returns(x => x.Arg<PolicyDetails>() == thisPolicy);
|
||||
|
||||
// Arrange the sut
|
||||
var factory = new TestPolicyRequirementFactory(callback);
|
||||
var sut = new PolicyRequirementQuery(policyRepository, [factory]);
|
||||
|
||||
// Act
|
||||
var requirement = await sut.GetAsync<TestPolicyRequirement>(userId);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(thisPolicy, requirement.Policies);
|
||||
Assert.DoesNotContain(otherPolicy, requirement.Policies);
|
||||
callback.Received()(Arg.Is(thisPolicy));
|
||||
callback.Received()(Arg.Is(otherPolicy));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetAsync_ThrowsIfNoFactoryRegistered(Guid userId)
|
||||
{
|
||||
var policyRepository = Substitute.For<IPolicyRepository>();
|
||||
var sut = new PolicyRequirementQuery(policyRepository, []);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<NotImplementedException>(()
|
||||
=> sut.GetAsync<TestPolicyRequirement>(userId));
|
||||
Assert.Contains("No Policy Requirement found", exception.Message);
|
||||
Assert.Contains("No Requirement Factory found", exception.Message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intentionally simplified PolicyRequirement that just holds the Policy.OrganizationId for us to assert against.
|
||||
/// </summary>
|
||||
private class TestPolicyRequirement : IPolicyRequirement
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetAsync_HandlesNoPolicies(Guid userId)
|
||||
{
|
||||
public Guid OrganizationId { get; init; }
|
||||
public static TestPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
|
||||
=> new() { OrganizationId = policyDetails.Single().OrganizationId };
|
||||
var policyRepository = Substitute.For<IPolicyRepository>();
|
||||
policyRepository.GetPolicyDetailsByUserId(userId).Returns([]);
|
||||
|
||||
var factory = new TestPolicyRequirementFactory(x => x.IsProvider);
|
||||
var sut = new PolicyRequirementQuery(policyRepository, [factory]);
|
||||
|
||||
var requirement = await sut.GetAsync<TestPolicyRequirement>(userId);
|
||||
|
||||
Assert.Empty(requirement.Policies);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,90 @@
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
public class BasePolicyRequirementFactoryTests
|
||||
{
|
||||
[Theory, AutoData]
|
||||
public void ExemptRoles_DoesNotEnforceAgainstThoseRoles(
|
||||
[PolicyDetails(PolicyType.SingleOrg, OrganizationUserType.Owner)] PolicyDetails ownerPolicy,
|
||||
[PolicyDetails(PolicyType.SingleOrg, OrganizationUserType.Admin)] PolicyDetails adminPolicy,
|
||||
[PolicyDetails(PolicyType.SingleOrg, OrganizationUserType.Custom)] PolicyDetails customPolicy,
|
||||
[PolicyDetails(PolicyType.SingleOrg)] PolicyDetails userPolicy)
|
||||
{
|
||||
var sut = new TestPolicyRequirementFactory(
|
||||
// These exempt roles are intentionally unusual to make sure we're properly testing the sut
|
||||
[OrganizationUserType.User, OrganizationUserType.Custom],
|
||||
[],
|
||||
false);
|
||||
|
||||
Assert.True(sut.Enforce(ownerPolicy));
|
||||
Assert.True(sut.Enforce(adminPolicy));
|
||||
Assert.False(sut.Enforce(customPolicy));
|
||||
Assert.False(sut.Enforce(userPolicy));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public void ExemptStatuses_DoesNotEnforceAgainstThoseStatuses(
|
||||
[PolicyDetails(PolicyType.SingleOrg, userStatus: OrganizationUserStatusType.Invited)] PolicyDetails invitedPolicy,
|
||||
[PolicyDetails(PolicyType.SingleOrg, userStatus: OrganizationUserStatusType.Accepted)] PolicyDetails acceptedPolicy,
|
||||
[PolicyDetails(PolicyType.SingleOrg, userStatus: OrganizationUserStatusType.Confirmed)] PolicyDetails confirmedPolicy,
|
||||
[PolicyDetails(PolicyType.SingleOrg, userStatus: OrganizationUserStatusType.Revoked)] PolicyDetails revokedPolicy)
|
||||
{
|
||||
var sut = new TestPolicyRequirementFactory(
|
||||
[],
|
||||
// These exempt statuses are intentionally unusual to make sure we're properly testing the sut
|
||||
[OrganizationUserStatusType.Confirmed, OrganizationUserStatusType.Accepted],
|
||||
false);
|
||||
|
||||
Assert.True(sut.Enforce(invitedPolicy));
|
||||
Assert.True(sut.Enforce(revokedPolicy));
|
||||
Assert.False(sut.Enforce(confirmedPolicy));
|
||||
Assert.False(sut.Enforce(acceptedPolicy));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public void ExemptProviders_DoesNotEnforceAgainstProviders(
|
||||
[PolicyDetails(PolicyType.SingleOrg, isProvider: true)] PolicyDetails policy)
|
||||
{
|
||||
var sut = new TestPolicyRequirementFactory(
|
||||
[],
|
||||
[],
|
||||
true);
|
||||
|
||||
Assert.False(sut.Enforce(policy));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public void NoExemptions_EnforcesAgainstAdminsAndProviders(
|
||||
[PolicyDetails(PolicyType.SingleOrg, OrganizationUserType.Owner, isProvider: true)] PolicyDetails policy)
|
||||
{
|
||||
var sut = new TestPolicyRequirementFactory(
|
||||
[],
|
||||
[],
|
||||
false);
|
||||
|
||||
Assert.True(sut.Enforce(policy));
|
||||
}
|
||||
|
||||
private class TestPolicyRequirementFactory(
|
||||
IEnumerable<OrganizationUserType> exemptRoles,
|
||||
IEnumerable<OrganizationUserStatusType> exemptStatuses,
|
||||
bool exemptProviders
|
||||
) : BasePolicyRequirementFactory<TestPolicyRequirement>
|
||||
{
|
||||
public override PolicyType PolicyType => PolicyType.SingleOrg;
|
||||
protected override IEnumerable<OrganizationUserType> ExemptRoles => exemptRoles;
|
||||
protected override IEnumerable<OrganizationUserStatusType> ExemptStatuses => exemptStatuses;
|
||||
|
||||
protected override bool ExemptProviders => exemptProviders;
|
||||
|
||||
public override TestPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
|
||||
=> new() { Policies = policyDetails };
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class DisableSendPolicyRequirementFactoryTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public void DisableSend_IsFalse_IfNoPolicies(SutProvider<DisableSendPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create([]);
|
||||
|
||||
Assert.False(actual.DisableSend);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void DisableSend_IsTrue_IfAnyDisableSendPolicies(
|
||||
[PolicyDetails(PolicyType.DisableSend)] PolicyDetails[] policies,
|
||||
SutProvider<DisableSendPolicyRequirementFactory> sutProvider
|
||||
)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create(policies);
|
||||
|
||||
Assert.True(actual.DisableSend);
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class SendOptionsPolicyRequirementFactoryTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public void DisableHideEmail_IsFalse_IfNoPolicies(SutProvider<SendOptionsPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create([]);
|
||||
|
||||
Assert.False(actual.DisableHideEmail);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void DisableHideEmail_IsFalse_IfNotConfigured(
|
||||
[PolicyDetails(PolicyType.SendOptions)] PolicyDetails[] policies,
|
||||
SutProvider<SendOptionsPolicyRequirementFactory> sutProvider
|
||||
)
|
||||
{
|
||||
policies[0].SetDataModel(new SendOptionsPolicyData { DisableHideEmail = false });
|
||||
policies[1].SetDataModel(new SendOptionsPolicyData { DisableHideEmail = false });
|
||||
|
||||
var actual = sutProvider.Sut.Create(policies);
|
||||
|
||||
Assert.False(actual.DisableHideEmail);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void DisableHideEmail_IsTrue_IfAnyConfigured(
|
||||
[PolicyDetails(PolicyType.SendOptions)] PolicyDetails[] policies,
|
||||
SutProvider<SendOptionsPolicyRequirementFactory> sutProvider
|
||||
)
|
||||
{
|
||||
policies[0].SetDataModel(new SendOptionsPolicyData { DisableHideEmail = true });
|
||||
policies[1].SetDataModel(new SendOptionsPolicyData { DisableHideEmail = false });
|
||||
|
||||
var actual = sutProvider.Sut.Create(policies);
|
||||
|
||||
Assert.True(actual.DisableHideEmail);
|
||||
}
|
||||
}
|
@ -1,138 +0,0 @@
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
public class SendPolicyRequirementTests
|
||||
{
|
||||
[Theory, AutoData]
|
||||
public void DisableSend_IsFalse_IfNoDisableSendPolicies(
|
||||
[PolicyDetails(PolicyType.RequireSso)] PolicyDetails otherPolicy1,
|
||||
[PolicyDetails(PolicyType.SendOptions)] PolicyDetails otherPolicy2)
|
||||
{
|
||||
EnableDisableHideEmail(otherPolicy2);
|
||||
|
||||
var actual = SendPolicyRequirement.Create([otherPolicy1, otherPolicy2]);
|
||||
|
||||
Assert.False(actual.DisableSend);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineAutoData(OrganizationUserType.Owner, false)]
|
||||
[InlineAutoData(OrganizationUserType.Admin, false)]
|
||||
[InlineAutoData(OrganizationUserType.User, true)]
|
||||
[InlineAutoData(OrganizationUserType.Custom, true)]
|
||||
public void DisableSend_TestRoles(
|
||||
OrganizationUserType userType,
|
||||
bool shouldBeEnforced,
|
||||
[PolicyDetails(PolicyType.DisableSend)] PolicyDetails policyDetails)
|
||||
{
|
||||
policyDetails.OrganizationUserType = userType;
|
||||
|
||||
var actual = SendPolicyRequirement.Create([policyDetails]);
|
||||
|
||||
Assert.Equal(shouldBeEnforced, actual.DisableSend);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public void DisableSend_Not_EnforcedAgainstProviders(
|
||||
[PolicyDetails(PolicyType.DisableSend, isProvider: true)] PolicyDetails policyDetails)
|
||||
{
|
||||
var actual = SendPolicyRequirement.Create([policyDetails]);
|
||||
|
||||
Assert.False(actual.DisableSend);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineAutoData(OrganizationUserStatusType.Confirmed, true)]
|
||||
[InlineAutoData(OrganizationUserStatusType.Accepted, true)]
|
||||
[InlineAutoData(OrganizationUserStatusType.Invited, false)]
|
||||
[InlineAutoData(OrganizationUserStatusType.Revoked, false)]
|
||||
public void DisableSend_TestStatuses(
|
||||
OrganizationUserStatusType userStatus,
|
||||
bool shouldBeEnforced,
|
||||
[PolicyDetails(PolicyType.DisableSend)] PolicyDetails policyDetails)
|
||||
{
|
||||
policyDetails.OrganizationUserStatus = userStatus;
|
||||
|
||||
var actual = SendPolicyRequirement.Create([policyDetails]);
|
||||
|
||||
Assert.Equal(shouldBeEnforced, actual.DisableSend);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public void DisableHideEmail_IsFalse_IfNoSendOptionsPolicies(
|
||||
[PolicyDetails(PolicyType.RequireSso)] PolicyDetails otherPolicy1,
|
||||
[PolicyDetails(PolicyType.DisableSend)] PolicyDetails otherPolicy2)
|
||||
{
|
||||
var actual = SendPolicyRequirement.Create([otherPolicy1, otherPolicy2]);
|
||||
|
||||
Assert.False(actual.DisableHideEmail);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineAutoData(OrganizationUserType.Owner, false)]
|
||||
[InlineAutoData(OrganizationUserType.Admin, false)]
|
||||
[InlineAutoData(OrganizationUserType.User, true)]
|
||||
[InlineAutoData(OrganizationUserType.Custom, true)]
|
||||
public void DisableHideEmail_TestRoles(
|
||||
OrganizationUserType userType,
|
||||
bool shouldBeEnforced,
|
||||
[PolicyDetails(PolicyType.SendOptions)] PolicyDetails policyDetails)
|
||||
{
|
||||
EnableDisableHideEmail(policyDetails);
|
||||
policyDetails.OrganizationUserType = userType;
|
||||
|
||||
var actual = SendPolicyRequirement.Create([policyDetails]);
|
||||
|
||||
Assert.Equal(shouldBeEnforced, actual.DisableHideEmail);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public void DisableHideEmail_Not_EnforcedAgainstProviders(
|
||||
[PolicyDetails(PolicyType.SendOptions, isProvider: true)] PolicyDetails policyDetails)
|
||||
{
|
||||
EnableDisableHideEmail(policyDetails);
|
||||
|
||||
var actual = SendPolicyRequirement.Create([policyDetails]);
|
||||
|
||||
Assert.False(actual.DisableHideEmail);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineAutoData(OrganizationUserStatusType.Confirmed, true)]
|
||||
[InlineAutoData(OrganizationUserStatusType.Accepted, true)]
|
||||
[InlineAutoData(OrganizationUserStatusType.Invited, false)]
|
||||
[InlineAutoData(OrganizationUserStatusType.Revoked, false)]
|
||||
public void DisableHideEmail_TestStatuses(
|
||||
OrganizationUserStatusType userStatus,
|
||||
bool shouldBeEnforced,
|
||||
[PolicyDetails(PolicyType.SendOptions)] PolicyDetails policyDetails)
|
||||
{
|
||||
EnableDisableHideEmail(policyDetails);
|
||||
policyDetails.OrganizationUserStatus = userStatus;
|
||||
|
||||
var actual = SendPolicyRequirement.Create([policyDetails]);
|
||||
|
||||
Assert.Equal(shouldBeEnforced, actual.DisableHideEmail);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public void DisableHideEmail_HandlesNullData(
|
||||
[PolicyDetails(PolicyType.SendOptions)] PolicyDetails policyDetails)
|
||||
{
|
||||
policyDetails.PolicyData = null;
|
||||
|
||||
var actual = SendPolicyRequirement.Create([policyDetails]);
|
||||
|
||||
Assert.False(actual.DisableHideEmail);
|
||||
}
|
||||
|
||||
private static void EnableDisableHideEmail(PolicyDetails policyDetails)
|
||||
=> policyDetails.SetDataModel(new SendOptionsPolicyData { DisableHideEmail = true });
|
||||
}
|
58
test/Core.Test/AdminConsole/Shared/IValidatorTests.cs
Normal file
58
test/Core.Test/AdminConsole/Shared/IValidatorTests.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using Bit.Core.AdminConsole.Errors;
|
||||
using Bit.Core.AdminConsole.Shared.Validation;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.Shared;
|
||||
|
||||
public class IValidatorTests
|
||||
{
|
||||
public class TestClass
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public record InvalidRequestError<T>(T ErroredValue) : Error<T>(Code, ErroredValue)
|
||||
{
|
||||
public const string Code = "InvalidRequest";
|
||||
}
|
||||
|
||||
public class TestClassValidator : IValidator<TestClass>
|
||||
{
|
||||
public Task<ValidationResult<TestClass>> ValidateAsync(TestClass value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value.Name))
|
||||
{
|
||||
return Task.FromResult<ValidationResult<TestClass>>(new Invalid<TestClass>
|
||||
{
|
||||
Errors = [new InvalidRequestError<TestClass>(value)]
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult<ValidationResult<TestClass>>(new Valid<TestClass> { Value = value });
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_WhenSomethingIsInvalid_ReturnsInvalidWithError()
|
||||
{
|
||||
var example = new TestClass();
|
||||
|
||||
var result = await new TestClassValidator().ValidateAsync(example);
|
||||
|
||||
Assert.IsType<Invalid<TestClass>>(result);
|
||||
var invalidResult = result as Invalid<TestClass>;
|
||||
Assert.Equal(InvalidRequestError<TestClass>.Code, invalidResult.Errors.First().Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_WhenIsValid_ReturnsValid()
|
||||
{
|
||||
var example = new TestClass { Name = "Valid" };
|
||||
|
||||
var result = await new TestClassValidator().ValidateAsync(example);
|
||||
|
||||
Assert.IsType<Valid<TestClass>>(result);
|
||||
var validResult = result as Valid<TestClass>;
|
||||
Assert.Equal(example.Name, validResult.Value.Name);
|
||||
}
|
||||
}
|
53
test/Core.Test/Models/Commands/CommandResultTests.cs
Normal file
53
test/Core.Test/Models/Commands/CommandResultTests.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using Bit.Core.AdminConsole.Errors;
|
||||
using Bit.Core.Models.Commands;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Models.Commands;
|
||||
|
||||
public class CommandResultTests
|
||||
{
|
||||
public class TestItem
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
public CommandResult<TestItem> BulkAction(IEnumerable<TestItem> items)
|
||||
{
|
||||
var itemList = items.ToList();
|
||||
var successfulItems = items.Where(x => x.Value == "SuccessfulRequest").ToArray();
|
||||
|
||||
var failedItems = itemList.Except(successfulItems).ToArray();
|
||||
|
||||
var notFound = failedItems.First(x => x.Value == "Failed due to not found");
|
||||
var invalidPermissions = failedItems.First(x => x.Value == "Failed due to invalid permissions");
|
||||
|
||||
var notFoundError = new RecordNotFoundError<TestItem>(notFound);
|
||||
var insufficientPermissionsError = new InsufficientPermissionsError<TestItem>(invalidPermissions);
|
||||
|
||||
return new Partial<TestItem>(successfulItems.ToArray(), [notFoundError, insufficientPermissionsError]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Partial_CommandResult_BulkRequestWithSuccessAndFailures(Guid successId1, Guid failureId1, Guid failureId2)
|
||||
{
|
||||
var listOfRecords = new List<TestItem>
|
||||
{
|
||||
new TestItem() { Id = successId1, Value = "SuccessfulRequest" },
|
||||
new TestItem() { Id = failureId1, Value = "Failed due to not found" },
|
||||
new TestItem() { Id = failureId2, Value = "Failed due to invalid permissions" }
|
||||
};
|
||||
|
||||
var result = BulkAction(listOfRecords);
|
||||
|
||||
Assert.IsType<Partial<TestItem>>(result);
|
||||
|
||||
var failures = (result as Partial<TestItem>).Failures.ToArray();
|
||||
var success = (result as Partial<TestItem>).Successes.First();
|
||||
|
||||
Assert.Equal(listOfRecords.First(), success);
|
||||
Assert.Equal(2, failures.Length);
|
||||
}
|
||||
}
|
@ -56,7 +56,6 @@ public class CloudGetOrganizationLicenseQueryTests
|
||||
sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).Returns(installation);
|
||||
sutProvider.GetDependency<IPaymentService>().GetSubscriptionAsync(organization).Returns(subInfo);
|
||||
sutProvider.GetDependency<ILicensingService>().SignLicense(Arg.Any<ILicense>()).Returns(licenseSignature);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.SelfHostLicenseRefactor).Returns(false);
|
||||
|
||||
var result = await sutProvider.Sut.GetLicenseAsync(organization, installationId);
|
||||
|
||||
@ -64,7 +63,7 @@ public class CloudGetOrganizationLicenseQueryTests
|
||||
Assert.Equal(organization.Id, result.Id);
|
||||
Assert.Equal(installationId, result.InstallationId);
|
||||
Assert.Equal(licenseSignature, result.SignatureBytes);
|
||||
Assert.Null(result.Token);
|
||||
Assert.Equal(string.Empty, result.Token);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -77,7 +76,6 @@ public class CloudGetOrganizationLicenseQueryTests
|
||||
sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).Returns(installation);
|
||||
sutProvider.GetDependency<IPaymentService>().GetSubscriptionAsync(organization).Returns(subInfo);
|
||||
sutProvider.GetDependency<ILicensingService>().SignLicense(Arg.Any<ILicense>()).Returns(licenseSignature);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.SelfHostLicenseRefactor).Returns(true);
|
||||
sutProvider.GetDependency<ILicensingService>()
|
||||
.CreateOrganizationTokenAsync(organization, installationId, subInfo)
|
||||
.Returns(token);
|
||||
|
@ -123,10 +123,12 @@ public class SendServiceTests
|
||||
|
||||
// Disable Send policy check - vNext
|
||||
private void SaveSendAsync_Setup_vNext(SutProvider<SendService> sutProvider, Send send,
|
||||
SendPolicyRequirement sendPolicyRequirement)
|
||||
DisableSendPolicyRequirement disableSendPolicyRequirement, SendOptionsPolicyRequirement sendOptionsPolicyRequirement)
|
||||
{
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<SendPolicyRequirement>(send.UserId!.Value)
|
||||
.Returns(sendPolicyRequirement);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<DisableSendPolicyRequirement>(send.UserId!.Value)
|
||||
.Returns(disableSendPolicyRequirement);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<SendOptionsPolicyRequirement>(send.UserId!.Value)
|
||||
.Returns(sendOptionsPolicyRequirement);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||
|
||||
// Should not be called in these tests
|
||||
@ -141,7 +143,7 @@ public class SendServiceTests
|
||||
SutProvider<SendService> sutProvider, [NewUserSendCustomize] Send send)
|
||||
{
|
||||
send.Type = sendType;
|
||||
SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement { DisableSend = true });
|
||||
SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement { DisableSend = true }, new SendOptionsPolicyRequirement());
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SaveSendAsync(send));
|
||||
Assert.Contains("Due to an Enterprise Policy, you are only able to delete an existing Send.",
|
||||
@ -155,7 +157,7 @@ public class SendServiceTests
|
||||
SutProvider<SendService> sutProvider, [NewUserSendCustomize] Send send)
|
||||
{
|
||||
send.Type = sendType;
|
||||
SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement());
|
||||
SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement());
|
||||
|
||||
await sutProvider.Sut.SaveSendAsync(send);
|
||||
|
||||
@ -171,7 +173,7 @@ public class SendServiceTests
|
||||
SutProvider<SendService> sutProvider, [NewUserSendCustomize] Send send)
|
||||
{
|
||||
send.Type = sendType;
|
||||
SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement { DisableHideEmail = true });
|
||||
SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement { DisableHideEmail = true });
|
||||
send.HideEmail = true;
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SaveSendAsync(send));
|
||||
@ -185,7 +187,7 @@ public class SendServiceTests
|
||||
SutProvider<SendService> sutProvider, [NewUserSendCustomize] Send send)
|
||||
{
|
||||
send.Type = sendType;
|
||||
SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement { DisableHideEmail = true });
|
||||
SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement { DisableHideEmail = true });
|
||||
send.HideEmail = false;
|
||||
|
||||
await sutProvider.Sut.SaveSendAsync(send);
|
||||
@ -200,7 +202,7 @@ public class SendServiceTests
|
||||
SutProvider<SendService> sutProvider, [NewUserSendCustomize] Send send)
|
||||
{
|
||||
send.Type = sendType;
|
||||
SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement());
|
||||
SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement());
|
||||
send.HideEmail = true;
|
||||
|
||||
await sutProvider.Sut.SaveSendAsync(send);
|
||||
|
@ -602,6 +602,78 @@ public class CipherServiceTests
|
||||
Assert.NotEqual(initialRevisionDate, cipher.RevisionDate);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RestoreAsync_WithAlreadyRestoredCipher_SkipsOperation(
|
||||
Guid restoringUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipher.DeletedDate = null;
|
||||
|
||||
await sutProvider.Sut.RestoreAsync(cipher, restoringUserId, true);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().UpsertAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushSyncCipherUpdateAsync(default, default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RestoreAsync_WithPersonalCipherBelongingToDifferentUser_ThrowsBadRequestException(
|
||||
Guid restoringUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipher.UserId = Guid.NewGuid();
|
||||
cipher.OrganizationId = null;
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreAsync(cipher, restoringUserId));
|
||||
|
||||
Assert.Contains("do not have permissions", exception.Message);
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().UpsertAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushSyncCipherUpdateAsync(default, default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task RestoreAsync_WithOrgCipherLackingEditPermission_ThrowsBadRequestException(
|
||||
Guid restoringUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(restoringUserId, cipher.Id)
|
||||
.Returns(false);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreAsync(cipher, restoringUserId));
|
||||
|
||||
Assert.Contains("do not have permissions", exception.Message);
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().UpsertAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushSyncCipherUpdateAsync(default, default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RestoreAsync_WithCipherDetailsType_RestoresCipherDetails(
|
||||
Guid restoringUserId, CipherDetails cipherDetails, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(restoringUserId, cipherDetails.Id)
|
||||
.Returns(true);
|
||||
|
||||
var initialRevisionDate = new DateTime(1970, 1, 1, 0, 0, 0);
|
||||
cipherDetails.DeletedDate = initialRevisionDate;
|
||||
cipherDetails.RevisionDate = initialRevisionDate;
|
||||
|
||||
await sutProvider.Sut.RestoreAsync(cipherDetails, restoringUserId);
|
||||
|
||||
Assert.Null(cipherDetails.DeletedDate);
|
||||
Assert.NotEqual(initialRevisionDate, cipherDetails.RevisionDate);
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpsertAsync(cipherDetails);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipherDetails, EventType.Cipher_Restored);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherUpdateAsync(cipherDetails, null);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RestoreManyAsync_UpdatesCiphers(ICollection<CipherDetails> ciphers,
|
||||
@ -725,6 +797,188 @@ public class CipherServiceTests
|
||||
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAsync_WithPersonalCipherOwner_DeletesCipher(
|
||||
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipher.UserId = deletingUserId;
|
||||
cipher.OrganizationId = null;
|
||||
|
||||
await sutProvider.Sut.DeleteAsync(cipher, deletingUserId);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).DeleteAsync(cipher);
|
||||
await sutProvider.GetDependency<IAttachmentStorageService>().Received(1).DeleteAttachmentsForCipherAsync(cipher.Id);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipher, EventType.Cipher_Deleted);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherDeleteAsync(cipher);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAsync_WithOrgCipherAndEditPermission_DeletesCipher(
|
||||
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(deletingUserId, cipher.Id)
|
||||
.Returns(true);
|
||||
|
||||
await sutProvider.Sut.DeleteAsync(cipher, deletingUserId);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).DeleteAsync(cipher);
|
||||
await sutProvider.GetDependency<IAttachmentStorageService>().Received(1).DeleteAttachmentsForCipherAsync(cipher.Id);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipher, EventType.Cipher_Deleted);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherDeleteAsync(cipher);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAsync_WithPersonalCipherBelongingToDifferentUser_ThrowsBadRequestException(
|
||||
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipher.UserId = Guid.NewGuid();
|
||||
cipher.OrganizationId = null;
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.DeleteAsync(cipher, deletingUserId));
|
||||
|
||||
Assert.Contains("do not have permissions", exception.Message);
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().DeleteAsync(default);
|
||||
await sutProvider.GetDependency<IAttachmentStorageService>().DidNotReceiveWithAnyArgs().DeleteAttachmentsForCipherAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushSyncCipherDeleteAsync(default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAsync_WithOrgCipherLackingEditPermission_ThrowsBadRequestException(
|
||||
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(deletingUserId, cipher.Id)
|
||||
.Returns(false);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.DeleteAsync(cipher, deletingUserId));
|
||||
|
||||
Assert.Contains("do not have permissions", exception.Message);
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().DeleteAsync(default);
|
||||
await sutProvider.GetDependency<IAttachmentStorageService>().DidNotReceiveWithAnyArgs().DeleteAttachmentsForCipherAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushSyncCipherDeleteAsync(default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteAsync_WithPersonalCipherOwner_SoftDeletesCipher(
|
||||
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipher.UserId = deletingUserId;
|
||||
cipher.OrganizationId = null;
|
||||
cipher.DeletedDate = null;
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(deletingUserId, cipher.Id)
|
||||
.Returns(true);
|
||||
|
||||
await sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId);
|
||||
|
||||
Assert.NotNull(cipher.DeletedDate);
|
||||
Assert.Equal(cipher.RevisionDate, cipher.DeletedDate);
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpsertAsync(cipher);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipher, EventType.Cipher_SoftDeleted);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherUpdateAsync(cipher, null);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteAsync_WithOrgCipherAndEditPermission_SoftDeletesCipher(
|
||||
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipher.DeletedDate = null;
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(deletingUserId, cipher.Id)
|
||||
.Returns(true);
|
||||
|
||||
await sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId);
|
||||
|
||||
Assert.NotNull(cipher.DeletedDate);
|
||||
Assert.Equal(cipher.DeletedDate, cipher.RevisionDate);
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpsertAsync(cipher);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipher, EventType.Cipher_SoftDeleted);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherUpdateAsync(cipher, null);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteAsync_WithPersonalCipherBelongingToDifferentUser_ThrowsBadRequestException(
|
||||
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipher.UserId = Guid.NewGuid();
|
||||
cipher.OrganizationId = null;
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(deletingUserId, cipher.Id)
|
||||
.Returns(false);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId));
|
||||
|
||||
Assert.Contains("do not have permissions", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteAsync_WithOrgCipherLackingEditPermission_ThrowsBadRequestException(
|
||||
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(deletingUserId, cipher.Id)
|
||||
.Returns(false);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId));
|
||||
|
||||
Assert.Contains("do not have permissions", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteAsync_WithCipherDetailsType_SoftDeletesCipherDetails(
|
||||
Guid deletingUserId, CipherDetails cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipher.DeletedDate = null;
|
||||
|
||||
await sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId, true);
|
||||
|
||||
Assert.NotNull(cipher.DeletedDate);
|
||||
Assert.Equal(cipher.DeletedDate, cipher.RevisionDate);
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpsertAsync(cipher);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipher, EventType.Cipher_SoftDeleted);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherUpdateAsync(cipher, null);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteAsync_WithAlreadySoftDeletedCipher_SkipsOperation(
|
||||
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(deletingUserId, cipher.Id)
|
||||
.Returns(true);
|
||||
cipher.DeletedDate = DateTime.UtcNow.AddDays(-1);
|
||||
|
||||
await sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceive().UpsertAsync(Arg.Any<Cipher>());
|
||||
await sutProvider.GetDependency<IEventService>().DidNotReceive().LogCipherEventAsync(Arg.Any<Cipher>(), Arg.Any<EventType>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceive().PushSyncCipherUpdateAsync(Arg.Any<Cipher>(), Arg.Any<IEnumerable<Guid>>());
|
||||
}
|
||||
|
||||
private async Task AssertNoActionsAsync(SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().GetManyOrganizationDetailsByOrganizationIdAsync(default);
|
||||
|
@ -172,7 +172,7 @@ public class DeviceValidatorTests
|
||||
|
||||
Assert.False(result);
|
||||
Assert.NotNull(context.CustomResponse["ErrorModel"]);
|
||||
var expectedErrorModel = new ErrorResponseModel("no device information provided");
|
||||
var expectedErrorModel = new ErrorResponseModel("No device information provided.");
|
||||
var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"];
|
||||
Assert.Equal(expectedErrorModel.Message, actualResponse.Message);
|
||||
}
|
||||
@ -418,7 +418,7 @@ public class DeviceValidatorTests
|
||||
Assert.False(result);
|
||||
Assert.NotNull(context.CustomResponse["ErrorModel"]);
|
||||
// PM-13340: The error message should be "invalid user" instead of "no device information provided"
|
||||
var expectedErrorMessage = "no device information provided";
|
||||
var expectedErrorMessage = "No device information provided.";
|
||||
var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"];
|
||||
Assert.Equal(expectedErrorMessage, actualResponse.Message);
|
||||
}
|
||||
@ -552,7 +552,7 @@ public class DeviceValidatorTests
|
||||
|
||||
Assert.False(result);
|
||||
Assert.NotNull(context.CustomResponse["ErrorModel"]);
|
||||
var expectedErrorMessage = "invalid new device otp";
|
||||
var expectedErrorMessage = "Invalid new device OTP. Try again.";
|
||||
var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"];
|
||||
Assert.Equal(expectedErrorMessage, actualResponse.Message);
|
||||
}
|
||||
@ -604,7 +604,7 @@ public class DeviceValidatorTests
|
||||
|
||||
Assert.False(result);
|
||||
Assert.NotNull(context.CustomResponse["ErrorModel"]);
|
||||
var expectedErrorMessage = "new device verification required";
|
||||
var expectedErrorMessage = "New device verification required.";
|
||||
var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"];
|
||||
Assert.Equal(expectedErrorMessage, actualResponse.Message);
|
||||
}
|
||||
|
168
util/Migrator/DbScripts/2025-02-27_00_AlterAuthRequest.sql
Normal file
168
util/Migrator/DbScripts/2025-02-27_00_AlterAuthRequest.sql
Normal file
@ -0,0 +1,168 @@
|
||||
ALTER TABLE
|
||||
[dbo].[AuthRequest]
|
||||
ADD
|
||||
[RequestCountryName] NVARCHAR(200) NULL;
|
||||
GO
|
||||
|
||||
EXECUTE sp_refreshview 'dbo.AuthRequestView'
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[AuthRequest_Create]
|
||||
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@OrganizationId UNIQUEIDENTIFIER = NULL,
|
||||
@Type TINYINT,
|
||||
@RequestDeviceIdentifier NVARCHAR(50),
|
||||
@RequestDeviceType TINYINT,
|
||||
@RequestIpAddress VARCHAR(50),
|
||||
@RequestCountryName NVARCHAR(200),
|
||||
@ResponseDeviceId UNIQUEIDENTIFIER,
|
||||
@AccessCode VARCHAR(25),
|
||||
@PublicKey VARCHAR(MAX),
|
||||
@Key VARCHAR(MAX),
|
||||
@MasterPasswordHash VARCHAR(MAX),
|
||||
@Approved BIT,
|
||||
@CreationDate DATETIME2(7),
|
||||
@ResponseDate DATETIME2(7),
|
||||
@AuthenticationDate DATETIME2(7)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
INSERT INTO [dbo].[AuthRequest]
|
||||
(
|
||||
[Id],
|
||||
[UserId],
|
||||
[OrganizationId],
|
||||
[Type],
|
||||
[RequestDeviceIdentifier],
|
||||
[RequestDeviceType],
|
||||
[RequestIpAddress],
|
||||
[RequestCountryName],
|
||||
[ResponseDeviceId],
|
||||
[AccessCode],
|
||||
[PublicKey],
|
||||
[Key],
|
||||
[MasterPasswordHash],
|
||||
[Approved],
|
||||
[CreationDate],
|
||||
[ResponseDate],
|
||||
[AuthenticationDate]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@Id,
|
||||
@UserId,
|
||||
@OrganizationId,
|
||||
@Type,
|
||||
@RequestDeviceIdentifier,
|
||||
@RequestDeviceType,
|
||||
@RequestIpAddress,
|
||||
@RequestCountryName,
|
||||
@ResponseDeviceId,
|
||||
@AccessCode,
|
||||
@PublicKey,
|
||||
@Key,
|
||||
@MasterPasswordHash,
|
||||
@Approved,
|
||||
@CreationDate,
|
||||
@ResponseDate,
|
||||
@AuthenticationDate
|
||||
)
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[AuthRequest_Update]
|
||||
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@OrganizationId UNIQUEIDENTIFIER = NULL,
|
||||
@Type SMALLINT,
|
||||
@RequestDeviceIdentifier NVARCHAR(50),
|
||||
@RequestDeviceType SMALLINT,
|
||||
@RequestIpAddress VARCHAR(50),
|
||||
@RequestCountryName NVARCHAR(200),
|
||||
@ResponseDeviceId UNIQUEIDENTIFIER,
|
||||
@AccessCode VARCHAR(25),
|
||||
@PublicKey VARCHAR(MAX),
|
||||
@Key VARCHAR(MAX),
|
||||
@MasterPasswordHash VARCHAR(MAX),
|
||||
@Approved BIT,
|
||||
@CreationDate DATETIME2 (7),
|
||||
@ResponseDate DATETIME2 (7),
|
||||
@AuthenticationDate DATETIME2 (7)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE
|
||||
[dbo].[AuthRequest]
|
||||
SET
|
||||
[UserId] = @UserId,
|
||||
[Type] = @Type,
|
||||
[OrganizationId] = @OrganizationId,
|
||||
[RequestDeviceIdentifier] = @RequestDeviceIdentifier,
|
||||
[RequestDeviceType] = @RequestDeviceType,
|
||||
[RequestIpAddress] = @RequestIpAddress,
|
||||
[RequestCountryName] = @RequestCountryName,
|
||||
[ResponseDeviceId] = @ResponseDeviceId,
|
||||
[AccessCode] = @AccessCode,
|
||||
[PublicKey] = @PublicKey,
|
||||
[Key] = @Key,
|
||||
[MasterPasswordHash] = @MasterPasswordHash,
|
||||
[Approved] = @Approved,
|
||||
[CreationDate] = @CreationDate,
|
||||
[ResponseDate] = @ResponseDate,
|
||||
[AuthenticationDate] = @AuthenticationDate
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE AuthRequest_UpdateMany
|
||||
@jsonData NVARCHAR(MAX)
|
||||
AS
|
||||
BEGIN
|
||||
UPDATE AR
|
||||
SET
|
||||
[Id] = ARI.[Id],
|
||||
[UserId] = ARI.[UserId],
|
||||
[Type] = ARI.[Type],
|
||||
[RequestDeviceIdentifier] = ARI.[RequestDeviceIdentifier],
|
||||
[RequestDeviceType] = ARI.[RequestDeviceType],
|
||||
[RequestIpAddress] = ARI.[RequestIpAddress],
|
||||
[RequestCountryName] = ARI.[RequestCountryName],
|
||||
[ResponseDeviceId] = ARI.[ResponseDeviceId],
|
||||
[AccessCode] = ARI.[AccessCode],
|
||||
[PublicKey] = ARI.[PublicKey],
|
||||
[Key] = ARI.[Key],
|
||||
[MasterPasswordHash] = ARI.[MasterPasswordHash],
|
||||
[Approved] = ARI.[Approved],
|
||||
[CreationDate] = ARI.[CreationDate],
|
||||
[ResponseDate] = ARI.[ResponseDate],
|
||||
[AuthenticationDate] = ARI.[AuthenticationDate],
|
||||
[OrganizationId] = ARI.[OrganizationId]
|
||||
FROM
|
||||
[dbo].[AuthRequest] AR
|
||||
INNER JOIN
|
||||
OPENJSON(@jsonData)
|
||||
WITH (
|
||||
Id UNIQUEIDENTIFIER '$.Id',
|
||||
UserId UNIQUEIDENTIFIER '$.UserId',
|
||||
Type SMALLINT '$.Type',
|
||||
RequestDeviceIdentifier NVARCHAR(50) '$.RequestDeviceIdentifier',
|
||||
RequestDeviceType SMALLINT '$.RequestDeviceType',
|
||||
RequestIpAddress VARCHAR(50) '$.RequestIpAddress',
|
||||
RequestCountryName NVARCHAR(200) '$.RequestCountryName',
|
||||
ResponseDeviceId UNIQUEIDENTIFIER '$.ResponseDeviceId',
|
||||
AccessCode VARCHAR(25) '$.AccessCode',
|
||||
PublicKey VARCHAR(MAX) '$.PublicKey',
|
||||
[Key] VARCHAR(MAX) '$.Key',
|
||||
MasterPasswordHash VARCHAR(MAX) '$.MasterPasswordHash',
|
||||
Approved BIT '$.Approved',
|
||||
CreationDate DATETIME2 '$.CreationDate',
|
||||
ResponseDate DATETIME2 '$.ResponseDate',
|
||||
AuthenticationDate DATETIME2 '$.AuthenticationDate',
|
||||
OrganizationId UNIQUEIDENTIFIER '$.OrganizationId'
|
||||
) ARI ON AR.Id = ARI.Id;
|
||||
END
|
||||
GO
|
3014
util/MySqlMigrations/Migrations/20250304221039_AlterAuthRequest.Designer.cs
generated
Normal file
3014
util/MySqlMigrations/Migrations/20250304221039_AlterAuthRequest.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.MySqlMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AlterAuthRequest : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "RequestCountryName",
|
||||
table: "AuthRequest",
|
||||
type: "varchar(200)",
|
||||
maxLength: 200,
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RequestCountryName",
|
||||
table: "AuthRequest");
|
||||
}
|
||||
}
|
@ -407,6 +407,10 @@ namespace Bit.MySqlMigrations.Migrations
|
||||
b.Property<DateTime?>("AuthenticationDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("RequestCountryName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<DateTime>("CreationDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
|
3020
util/PostgresMigrations/Migrations/20250304204625_AlterAuthRequestTable.Designer.cs
generated
Normal file
3020
util/PostgresMigrations/Migrations/20250304204625_AlterAuthRequestTable.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.PostgresMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AlterAuthRequestTable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "RequestCountryName",
|
||||
table: "AuthRequest",
|
||||
type: "character varying(200)",
|
||||
maxLength: 200,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RequestCountryName",
|
||||
table: "AuthRequest");
|
||||
}
|
||||
}
|
@ -410,6 +410,10 @@ namespace Bit.PostgresMigrations.Migrations
|
||||
b.Property<DateTime?>("AuthenticationDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("RequestCountryName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime>("CreationDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
services:
|
||||
mssql:
|
||||
image: bitwarden/mssql:{{{CoreVersion}}}
|
||||
image: ghcr.io/bitwarden/mssql:{{{CoreVersion}}}
|
||||
container_name: bitwarden-mssql
|
||||
restart: always
|
||||
stop_grace_period: 60s
|
||||
@ -33,7 +33,7 @@ services:
|
||||
- ../env/mssql.override.env
|
||||
|
||||
web:
|
||||
image: bitwarden/web:{{{WebVersion}}}
|
||||
image: ghcr.io/bitwarden/web:{{{WebVersion}}}
|
||||
container_name: bitwarden-web
|
||||
restart: always
|
||||
volumes:
|
||||
@ -43,7 +43,7 @@ services:
|
||||
- ../env/uid.env
|
||||
|
||||
attachments:
|
||||
image: bitwarden/attachments:{{{CoreVersion}}}
|
||||
image: ghcr.io/bitwarden/attachments:{{{CoreVersion}}}
|
||||
container_name: bitwarden-attachments
|
||||
restart: always
|
||||
volumes:
|
||||
@ -53,7 +53,7 @@ services:
|
||||
- ../env/uid.env
|
||||
|
||||
api:
|
||||
image: bitwarden/api:{{{CoreVersion}}}
|
||||
image: ghcr.io/bitwarden/api:{{{CoreVersion}}}
|
||||
container_name: bitwarden-api
|
||||
restart: always
|
||||
volumes:
|
||||
@ -69,7 +69,7 @@ services:
|
||||
- public
|
||||
|
||||
identity:
|
||||
image: bitwarden/identity:{{{CoreVersion}}}
|
||||
image: ghcr.io/bitwarden/identity:{{{CoreVersion}}}
|
||||
container_name: bitwarden-identity
|
||||
restart: always
|
||||
volumes:
|
||||
@ -86,7 +86,7 @@ services:
|
||||
- public
|
||||
|
||||
sso:
|
||||
image: bitwarden/sso:{{{CoreVersion}}}
|
||||
image: ghcr.io/bitwarden/sso:{{{CoreVersion}}}
|
||||
container_name: bitwarden-sso
|
||||
restart: always
|
||||
volumes:
|
||||
@ -103,7 +103,7 @@ services:
|
||||
- public
|
||||
|
||||
admin:
|
||||
image: bitwarden/admin:{{{CoreVersion}}}
|
||||
image: ghcr.io/bitwarden/admin:{{{CoreVersion}}}
|
||||
container_name: bitwarden-admin
|
||||
restart: always
|
||||
depends_on:
|
||||
@ -121,7 +121,7 @@ services:
|
||||
- public
|
||||
|
||||
icons:
|
||||
image: bitwarden/icons:{{{CoreVersion}}}
|
||||
image: ghcr.io/bitwarden/icons:{{{CoreVersion}}}
|
||||
container_name: bitwarden-icons
|
||||
restart: always
|
||||
volumes:
|
||||
@ -135,7 +135,7 @@ services:
|
||||
- public
|
||||
|
||||
notifications:
|
||||
image: bitwarden/notifications:{{{CoreVersion}}}
|
||||
image: ghcr.io/bitwarden/notifications:{{{CoreVersion}}}
|
||||
container_name: bitwarden-notifications
|
||||
restart: always
|
||||
volumes:
|
||||
@ -150,7 +150,7 @@ services:
|
||||
- public
|
||||
|
||||
events:
|
||||
image: bitwarden/events:{{{CoreVersion}}}
|
||||
image: ghcr.io/bitwarden/events:{{{CoreVersion}}}
|
||||
container_name: bitwarden-events
|
||||
restart: always
|
||||
volumes:
|
||||
@ -165,7 +165,7 @@ services:
|
||||
- public
|
||||
|
||||
nginx:
|
||||
image: bitwarden/nginx:{{{CoreVersion}}}
|
||||
image: ghcr.io/bitwarden/nginx:{{{CoreVersion}}}
|
||||
container_name: bitwarden-nginx
|
||||
restart: always
|
||||
depends_on:
|
||||
@ -195,7 +195,7 @@ services:
|
||||
|
||||
{{#if EnableKeyConnector}}
|
||||
key-connector:
|
||||
image: bitwarden/key-connector:{{{KeyConnectorVersion}}}
|
||||
image: ghcr.io/bitwarden/key-connector:{{{KeyConnectorVersion}}}
|
||||
container_name: bitwarden-key-connector
|
||||
restart: always
|
||||
volumes:
|
||||
@ -212,7 +212,7 @@ services:
|
||||
{{#if EnableScim}}
|
||||
|
||||
scim:
|
||||
image: bitwarden/scim:{{{CoreVersion}}}
|
||||
image: ghcr.io/bitwarden/scim:{{{CoreVersion}}}
|
||||
container_name: bitwarden-scim
|
||||
restart: always
|
||||
volumes:
|
||||
|
3003
util/SqliteMigrations/Migrations/20250304204635_AlterAuthRequestTable.Designer.cs
generated
Normal file
3003
util/SqliteMigrations/Migrations/20250304204635_AlterAuthRequestTable.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.SqliteMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AlterAuthRequestTable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "RequestCountryName",
|
||||
table: "AuthRequest",
|
||||
type: "TEXT",
|
||||
maxLength: 200,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RequestCountryName",
|
||||
table: "AuthRequest");
|
||||
}
|
||||
}
|
@ -402,6 +402,10 @@ namespace Bit.SqliteMigrations.Migrations
|
||||
b.Property<DateTime?>("AuthenticationDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RequestCountryName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreationDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user