mirror of
https://github.com/bitwarden/server
synced 2026-01-02 16:43:25 +00:00
Merge branch 'main' into jmccannon/ac/pm-27131-auto-confirm-req
This commit is contained in:
@@ -473,6 +473,7 @@ public class OrganizationsController : Controller
|
||||
organization.UseOrganizationDomains = model.UseOrganizationDomains;
|
||||
organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies;
|
||||
organization.UseAutomaticUserConfirmation = model.UseAutomaticUserConfirmation;
|
||||
organization.UsePhishingBlocker = model.UsePhishingBlocker;
|
||||
|
||||
//secrets
|
||||
organization.SmSeats = model.SmSeats;
|
||||
|
||||
@@ -107,6 +107,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts;
|
||||
UseOrganizationDomains = org.UseOrganizationDomains;
|
||||
UseAutomaticUserConfirmation = org.UseAutomaticUserConfirmation;
|
||||
UsePhishingBlocker = org.UsePhishingBlocker;
|
||||
|
||||
_plans = plans;
|
||||
}
|
||||
@@ -160,6 +161,8 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
public new bool UseSecretsManager { get; set; }
|
||||
[Display(Name = "Risk Insights")]
|
||||
public new bool UseRiskInsights { get; set; }
|
||||
[Display(Name = "Phishing Blocker")]
|
||||
public new bool UsePhishingBlocker { get; set; }
|
||||
[Display(Name = "Admin Sponsored Families")]
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
[Display(Name = "Self Host")]
|
||||
@@ -327,6 +330,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
existingOrganization.SmServiceAccounts = SmServiceAccounts;
|
||||
existingOrganization.MaxAutoscaleSmServiceAccounts = MaxAutoscaleSmServiceAccounts;
|
||||
existingOrganization.UseOrganizationDomains = UseOrganizationDomains;
|
||||
existingOrganization.UsePhishingBlocker = UsePhishingBlocker;
|
||||
return existingOrganization;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ public class OrganizationViewModel
|
||||
public int OccupiedSmSeatsCount { get; set; }
|
||||
public bool UseSecretsManager => Organization.UseSecretsManager;
|
||||
public bool UseRiskInsights => Organization.UseRiskInsights;
|
||||
public bool UsePhishingBlocker => Organization.UsePhishingBlocker;
|
||||
public IEnumerable<OrganizationUserUserDetails> OwnersDetails { get; set; }
|
||||
public IEnumerable<OrganizationUserUserDetails> AdminsDetails { get; set; }
|
||||
}
|
||||
|
||||
@@ -156,6 +156,10 @@
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseAdminSponsoredFamilies" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UseAdminSponsoredFamilies"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UsePhishingBlocker" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UsePhishingBlocker"></label>
|
||||
</div>
|
||||
@if(FeatureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
|
||||
{
|
||||
<div class="form-check">
|
||||
|
||||
@@ -483,43 +483,10 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
}
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
[HttpPut("{id}/reset-password")]
|
||||
[Authorize<ManageAccountRecoveryRequirement>]
|
||||
public async Task<IResult> PutResetPassword(Guid orgId, Guid id, [FromBody] OrganizationUserResetPasswordRequestModel model)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountRecoveryCommand))
|
||||
{
|
||||
// TODO: remove legacy implementation after feature flag is enabled.
|
||||
return await PutResetPasswordNew(orgId, id, model);
|
||||
}
|
||||
|
||||
// Get the users role, since provider users aren't a member of the organization we use the owner check
|
||||
var orgUserType = await _currentContext.OrganizationOwner(orgId)
|
||||
? OrganizationUserType.Owner
|
||||
: _currentContext.Organizations?.FirstOrDefault(o => o.Id == orgId)?.Type;
|
||||
if (orgUserType == null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
var result = await _userService.AdminResetPasswordAsync(orgUserType.Value, orgId, id, model.NewMasterPasswordHash, model.Key);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
|
||||
await Task.Delay(2000);
|
||||
return TypedResults.BadRequest(ModelState);
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
// TODO: make sure the route and authorize attributes are maintained when the legacy implementation is removed.
|
||||
private async Task<IResult> PutResetPasswordNew(Guid orgId, Guid id, [FromBody] OrganizationUserResetPasswordRequestModel model)
|
||||
{
|
||||
var targetOrganizationUser = await _organizationUserRepository.GetByIdAsync(id);
|
||||
if (targetOrganizationUser == null || targetOrganizationUser.OrganizationId != orgId)
|
||||
|
||||
@@ -42,7 +42,6 @@ public class PoliciesController : Controller
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ISavePolicyCommand _savePolicyCommand;
|
||||
private readonly IVNextSavePolicyCommand _vNextSavePolicyCommand;
|
||||
|
||||
@@ -55,7 +54,6 @@ public class PoliciesController : Controller
|
||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IFeatureService featureService,
|
||||
ISavePolicyCommand savePolicyCommand,
|
||||
IVNextSavePolicyCommand vNextSavePolicyCommand)
|
||||
{
|
||||
@@ -69,7 +67,6 @@ public class PoliciesController : Controller
|
||||
_organizationRepository = organizationRepository;
|
||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
|
||||
_featureService = featureService;
|
||||
_savePolicyCommand = savePolicyCommand;
|
||||
_vNextSavePolicyCommand = vNextSavePolicyCommand;
|
||||
}
|
||||
@@ -221,9 +218,7 @@ public class PoliciesController : Controller
|
||||
{
|
||||
var savePolicyRequest = await model.ToSavePolicyModelAsync(orgId, type, _currentContext);
|
||||
|
||||
var policy = _featureService.IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor) ?
|
||||
await _vNextSavePolicyCommand.SaveAsync(savePolicyRequest) :
|
||||
await _savePolicyCommand.VNextSaveAsync(savePolicyRequest);
|
||||
var policy = await _vNextSavePolicyCommand.SaveAsync(savePolicyRequest);
|
||||
|
||||
return new PolicyResponseModel(policy);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ public abstract class BaseProfileOrganizationResponseModel : ResponseModel
|
||||
UseAdminSponsoredFamilies = organizationDetails.UseAdminSponsoredFamilies;
|
||||
UseAutomaticUserConfirmation = organizationDetails.UseAutomaticUserConfirmation;
|
||||
UseSecretsManager = organizationDetails.UseSecretsManager;
|
||||
UsePhishingBlocker = organizationDetails.UsePhishingBlocker;
|
||||
UsePasswordManager = organizationDetails.UsePasswordManager;
|
||||
SelfHost = organizationDetails.SelfHost;
|
||||
Seats = organizationDetails.Seats;
|
||||
@@ -99,6 +100,7 @@ public abstract class BaseProfileOrganizationResponseModel : ResponseModel
|
||||
public bool UseOrganizationDomains { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public bool UseAutomaticUserConfirmation { get; set; }
|
||||
public bool UsePhishingBlocker { get; set; }
|
||||
public bool SelfHost { get; set; }
|
||||
public int? Seats { get; set; }
|
||||
public short? MaxCollections { get; set; }
|
||||
|
||||
@@ -71,6 +71,7 @@ public class OrganizationResponseModel : ResponseModel
|
||||
UseOrganizationDomains = organization.UseOrganizationDomains;
|
||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||
UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation;
|
||||
UsePhishingBlocker = organization.UsePhishingBlocker;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
@@ -120,6 +121,7 @@ public class OrganizationResponseModel : ResponseModel
|
||||
public bool UseOrganizationDomains { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public bool UseAutomaticUserConfirmation { get; set; }
|
||||
public bool UsePhishingBlocker { get; set; }
|
||||
}
|
||||
|
||||
public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
|
||||
|
||||
@@ -5,15 +5,10 @@ using System.Net;
|
||||
using Bit.Api.AdminConsole.Public.Models.Request;
|
||||
using Bit.Api.AdminConsole.Public.Models.Response;
|
||||
using Bit.Api.Models.Public.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -24,25 +19,16 @@ namespace Bit.Api.AdminConsole.Public.Controllers;
|
||||
public class PoliciesController : Controller
|
||||
{
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ISavePolicyCommand _savePolicyCommand;
|
||||
private readonly IVNextSavePolicyCommand _vNextSavePolicyCommand;
|
||||
|
||||
public PoliciesController(
|
||||
IPolicyRepository policyRepository,
|
||||
IPolicyService policyService,
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
ISavePolicyCommand savePolicyCommand,
|
||||
IVNextSavePolicyCommand vNextSavePolicyCommand)
|
||||
{
|
||||
_policyRepository = policyRepository;
|
||||
_policyService = policyService;
|
||||
_currentContext = currentContext;
|
||||
_featureService = featureService;
|
||||
_savePolicyCommand = savePolicyCommand;
|
||||
_vNextSavePolicyCommand = vNextSavePolicyCommand;
|
||||
}
|
||||
|
||||
@@ -97,17 +83,8 @@ public class PoliciesController : Controller
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> Put(PolicyType type, [FromBody] PolicyUpdateRequestModel model)
|
||||
{
|
||||
Policy policy;
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor))
|
||||
{
|
||||
var savePolicyModel = model.ToSavePolicyModel(_currentContext.OrganizationId!.Value, type);
|
||||
policy = await _vNextSavePolicyCommand.SaveAsync(savePolicyModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
var policyUpdate = model.ToPolicyUpdate(_currentContext.OrganizationId!.Value, type);
|
||||
policy = await _savePolicyCommand.SaveAsync(policyUpdate);
|
||||
}
|
||||
var savePolicyModel = model.ToSavePolicyModel(_currentContext.OrganizationId!.Value, type);
|
||||
var policy = await _vNextSavePolicyCommand.SaveAsync(savePolicyModel);
|
||||
|
||||
var response = new PolicyResponseModel(policy);
|
||||
return new JsonResult(response);
|
||||
|
||||
@@ -66,7 +66,10 @@ public class HibpController : Controller
|
||||
}
|
||||
else if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
return new NotFoundResult();
|
||||
/* 12/1/2025 - Per the HIBP API, If the domain does not have any email addresses in any breaches,
|
||||
an HTTP 404 response will be returned. API also specifies that "404 Not found is the account could
|
||||
not be found and has therefore not been pwned". Per REST semantics we will return 200 OK with empty array. */
|
||||
return Content("[]", "application/json");
|
||||
}
|
||||
else if (response.StatusCode == HttpStatusCode.TooManyRequests && retry)
|
||||
{
|
||||
|
||||
@@ -80,7 +80,7 @@ public class AccountsKeyManagementController : Controller
|
||||
[HttpPost("key-management/regenerate-keys")]
|
||||
public async Task RegenerateKeysAsync([FromBody] KeyRegenerationRequestModel request)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration))
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration) && !_featureService.IsEnabled(FeatureFlagKeys.DataRecoveryTool))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.KeyManagement.Models.Api.Request;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
|
||||
|
||||
337
src/Api/SecretsManager/Controllers/SecretVersionsController.cs
Normal file
337
src/Api/SecretsManager/Controllers/SecretVersionsController.cs
Normal file
@@ -0,0 +1,337 @@
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.SecretsManager.Models.Request;
|
||||
using Bit.Api.SecretsManager.Models.Response;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.SecretsManager.Controllers;
|
||||
|
||||
[Authorize("secrets")]
|
||||
public class SecretVersionsController : Controller
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ISecretVersionRepository _secretVersionRepository;
|
||||
private readonly ISecretRepository _secretRepository;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
|
||||
public SecretVersionsController(
|
||||
ICurrentContext currentContext,
|
||||
ISecretVersionRepository secretVersionRepository,
|
||||
ISecretRepository secretRepository,
|
||||
IUserService userService,
|
||||
IOrganizationUserRepository organizationUserRepository)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_secretVersionRepository = secretVersionRepository;
|
||||
_secretRepository = secretRepository;
|
||||
_userService = userService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
}
|
||||
|
||||
[HttpGet("secrets/{secretId}/versions")]
|
||||
public async Task<ListResponseModel<SecretVersionResponseModel>> GetVersionsBySecretIdAsync([FromRoute] Guid secretId)
|
||||
{
|
||||
var secret = await _secretRepository.GetByIdAsync(secretId);
|
||||
if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// For service accounts and organization API, skip user-level access checks
|
||||
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount ||
|
||||
_currentContext.IdentityClientType == IdentityClientType.Organization)
|
||||
{
|
||||
// Already verified Secrets Manager access above
|
||||
var versionList = await _secretVersionRepository.GetManyBySecretIdAsync(secretId);
|
||||
var responseList = versionList.Select(v => new SecretVersionResponseModel(v));
|
||||
return new ListResponseModel<SecretVersionResponseModel>(responseList);
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);
|
||||
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);
|
||||
|
||||
var access = await _secretRepository.AccessToSecretAsync(secretId, userId.Value, accessClient);
|
||||
if (!access.Read)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var versions = await _secretVersionRepository.GetManyBySecretIdAsync(secretId);
|
||||
var responses = versions.Select(v => new SecretVersionResponseModel(v));
|
||||
|
||||
return new ListResponseModel<SecretVersionResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpGet("secret-versions/{id}")]
|
||||
public async Task<SecretVersionResponseModel> GetByIdAsync([FromRoute] Guid id)
|
||||
{
|
||||
var secretVersion = await _secretVersionRepository.GetByIdAsync(id);
|
||||
if (secretVersion == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var secret = await _secretRepository.GetByIdAsync(secretVersion.SecretId);
|
||||
if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// For service accounts and organization API, skip user-level access checks
|
||||
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount ||
|
||||
_currentContext.IdentityClientType == IdentityClientType.Organization)
|
||||
{
|
||||
// Already verified Secrets Manager access above
|
||||
return new SecretVersionResponseModel(secretVersion);
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);
|
||||
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);
|
||||
|
||||
var access = await _secretRepository.AccessToSecretAsync(secretVersion.SecretId, userId.Value, accessClient);
|
||||
if (!access.Read)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new SecretVersionResponseModel(secretVersion);
|
||||
}
|
||||
|
||||
[HttpPost("secret-versions/get-by-ids")]
|
||||
public async Task<ListResponseModel<SecretVersionResponseModel>> GetManyByIdsAsync([FromBody] List<Guid> ids)
|
||||
{
|
||||
if (!ids.Any())
|
||||
{
|
||||
throw new BadRequestException("No version IDs provided.");
|
||||
}
|
||||
|
||||
// Get all versions
|
||||
var versions = (await _secretVersionRepository.GetManyByIdsAsync(ids)).ToList();
|
||||
if (!versions.Any())
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Get all associated secrets and check permissions
|
||||
var secretIds = versions.Select(v => v.SecretId).Distinct().ToList();
|
||||
var secrets = (await _secretRepository.GetManyByIds(secretIds)).ToList();
|
||||
|
||||
if (!secrets.Any())
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Ensure all secrets belong to the same organization
|
||||
var organizationId = secrets.First().OrganizationId;
|
||||
if (secrets.Any(s => s.OrganizationId != organizationId) ||
|
||||
!_currentContext.AccessSecretsManager(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// For service accounts and organization API, skip user-level access checks
|
||||
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount ||
|
||||
_currentContext.IdentityClientType == IdentityClientType.Organization)
|
||||
{
|
||||
// Already verified Secrets Manager access and organization ownership above
|
||||
var serviceAccountResponses = versions.Select(v => new SecretVersionResponseModel(v));
|
||||
return new ListResponseModel<SecretVersionResponseModel>(serviceAccountResponses);
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var isAdmin = await _currentContext.OrganizationAdmin(organizationId);
|
||||
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, isAdmin);
|
||||
|
||||
// Verify read access to all associated secrets
|
||||
var accessResults = await _secretRepository.AccessToSecretsAsync(secretIds, userId.Value, accessClient);
|
||||
if (accessResults.Values.Any(access => !access.Read))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var responses = versions.Select(v => new SecretVersionResponseModel(v));
|
||||
return new ListResponseModel<SecretVersionResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpPut("secrets/{secretId}/versions/restore")]
|
||||
public async Task<SecretResponseModel> RestoreVersionAsync([FromRoute] Guid secretId, [FromBody] RestoreSecretVersionRequestModel request)
|
||||
{
|
||||
if (!(_currentContext.IdentityClientType == IdentityClientType.User || _currentContext.IdentityClientType == IdentityClientType.ServiceAccount))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var secret = await _secretRepository.GetByIdAsync(secretId);
|
||||
if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Get the version first to validate it belongs to this secret
|
||||
var version = await _secretVersionRepository.GetByIdAsync(request.VersionId);
|
||||
if (version == null || version.SecretId != secretId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Store the current value before restoration
|
||||
var currentValue = secret.Value;
|
||||
|
||||
// For service accounts and organization API, skip user-level access checks
|
||||
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount)
|
||||
{
|
||||
// Save current value as a version before restoring
|
||||
if (currentValue != version.Value)
|
||||
{
|
||||
var editorUserId = _userService.GetProperUserId(User);
|
||||
if (editorUserId.HasValue)
|
||||
{
|
||||
var currentVersionSnapshot = new Core.SecretsManager.Entities.SecretVersion
|
||||
{
|
||||
SecretId = secretId,
|
||||
Value = currentValue!,
|
||||
VersionDate = DateTime.UtcNow,
|
||||
EditorServiceAccountId = editorUserId.Value
|
||||
};
|
||||
|
||||
await _secretVersionRepository.CreateAsync(currentVersionSnapshot);
|
||||
}
|
||||
}
|
||||
|
||||
// Already verified Secrets Manager access above
|
||||
secret.Value = version.Value;
|
||||
secret.RevisionDate = DateTime.UtcNow;
|
||||
var updatedSec = await _secretRepository.UpdateAsync(secret);
|
||||
return new SecretResponseModel(updatedSec, true, true);
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);
|
||||
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);
|
||||
|
||||
var access = await _secretRepository.AccessToSecretAsync(secretId, userId.Value, accessClient);
|
||||
if (!access.Write)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Save current value as a version before restoring
|
||||
if (currentValue != version.Value)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(secret.OrganizationId, userId.Value);
|
||||
if (orgUser == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var currentVersionSnapshot = new Core.SecretsManager.Entities.SecretVersion
|
||||
{
|
||||
SecretId = secretId,
|
||||
Value = currentValue!,
|
||||
VersionDate = DateTime.UtcNow,
|
||||
EditorOrganizationUserId = orgUser.Id
|
||||
};
|
||||
|
||||
await _secretVersionRepository.CreateAsync(currentVersionSnapshot);
|
||||
}
|
||||
|
||||
// Update the secret with the version's value
|
||||
secret.Value = version.Value;
|
||||
secret.RevisionDate = DateTime.UtcNow;
|
||||
|
||||
var updatedSecret = await _secretRepository.UpdateAsync(secret);
|
||||
|
||||
return new SecretResponseModel(updatedSecret, true, true);
|
||||
}
|
||||
|
||||
[HttpPost("secret-versions/delete")]
|
||||
public async Task<IActionResult> BulkDeleteAsync([FromBody] List<Guid> ids)
|
||||
{
|
||||
if (!ids.Any())
|
||||
{
|
||||
throw new BadRequestException("No version IDs provided.");
|
||||
}
|
||||
|
||||
var secretVersions = (await _secretVersionRepository.GetManyByIdsAsync(ids)).ToList();
|
||||
if (secretVersions.Count != ids.Count)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Ensure all versions belong to secrets in the same organization
|
||||
var secretIds = secretVersions.Select(v => v.SecretId).Distinct().ToList();
|
||||
var secrets = await _secretRepository.GetManyByIds(secretIds);
|
||||
var secretsList = secrets.ToList();
|
||||
|
||||
if (!secretsList.Any())
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var organizationId = secretsList.First().OrganizationId;
|
||||
if (secretsList.Any(s => s.OrganizationId != organizationId) ||
|
||||
!_currentContext.AccessSecretsManager(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// For service accounts and organization API, skip user-level access checks
|
||||
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount ||
|
||||
_currentContext.IdentityClientType == IdentityClientType.Organization)
|
||||
{
|
||||
// Already verified Secrets Manager access and organization ownership above
|
||||
await _secretVersionRepository.DeleteManyByIdAsync(ids);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
|
||||
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);
|
||||
|
||||
// Verify write access to all associated secrets
|
||||
var accessResults = await _secretRepository.AccessToSecretsAsync(secretIds, userId.Value, accessClient);
|
||||
if (accessResults.Values.Any(access => !access.Write))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _secretVersionRepository.DeleteManyByIdAsync(ids);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
@@ -29,6 +30,7 @@ public class SecretsController : Controller
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly ISecretRepository _secretRepository;
|
||||
private readonly ISecretVersionRepository _secretVersionRepository;
|
||||
private readonly ICreateSecretCommand _createSecretCommand;
|
||||
private readonly IUpdateSecretCommand _updateSecretCommand;
|
||||
private readonly IDeleteSecretCommand _deleteSecretCommand;
|
||||
@@ -38,11 +40,13 @@ public class SecretsController : Controller
|
||||
private readonly IUserService _userService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
|
||||
public SecretsController(
|
||||
ICurrentContext currentContext,
|
||||
IProjectRepository projectRepository,
|
||||
ISecretRepository secretRepository,
|
||||
ISecretVersionRepository secretVersionRepository,
|
||||
ICreateSecretCommand createSecretCommand,
|
||||
IUpdateSecretCommand updateSecretCommand,
|
||||
IDeleteSecretCommand deleteSecretCommand,
|
||||
@@ -51,11 +55,13 @@ public class SecretsController : Controller
|
||||
ISecretAccessPoliciesUpdatesQuery secretAccessPoliciesUpdatesQuery,
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IAuthorizationService authorizationService)
|
||||
IAuthorizationService authorizationService,
|
||||
IOrganizationUserRepository organizationUserRepository)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_projectRepository = projectRepository;
|
||||
_secretRepository = secretRepository;
|
||||
_secretVersionRepository = secretVersionRepository;
|
||||
_createSecretCommand = createSecretCommand;
|
||||
_updateSecretCommand = updateSecretCommand;
|
||||
_deleteSecretCommand = deleteSecretCommand;
|
||||
@@ -65,6 +71,7 @@ public class SecretsController : Controller
|
||||
_userService = userService;
|
||||
_eventService = eventService;
|
||||
_authorizationService = authorizationService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
|
||||
}
|
||||
|
||||
@@ -190,6 +197,44 @@ public class SecretsController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
// Create a version record if the value changed
|
||||
if (updateRequest.ValueChanged)
|
||||
{
|
||||
// Store the old value before updating
|
||||
var oldValue = secret.Value;
|
||||
var userId = _userService.GetProperUserId(User)!.Value;
|
||||
Guid? editorServiceAccountId = null;
|
||||
Guid? editorOrganizationUserId = null;
|
||||
|
||||
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount)
|
||||
{
|
||||
editorServiceAccountId = userId;
|
||||
}
|
||||
else if (_currentContext.IdentityClientType == IdentityClientType.User)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(secret.OrganizationId, userId);
|
||||
if (orgUser != null)
|
||||
{
|
||||
editorOrganizationUserId = orgUser.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
var secretVersion = new SecretVersion
|
||||
{
|
||||
SecretId = id,
|
||||
Value = oldValue,
|
||||
VersionDate = DateTime.UtcNow,
|
||||
EditorServiceAccountId = editorServiceAccountId,
|
||||
EditorOrganizationUserId = editorOrganizationUserId
|
||||
};
|
||||
|
||||
await _secretVersionRepository.CreateAsync(secretVersion);
|
||||
}
|
||||
|
||||
var result = await _updateSecretCommand.UpdateAsync(updatedSecret, accessPoliciesUpdates);
|
||||
await LogSecretEventAsync(secret, EventType.Secret_Edited);
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.SecretsManager.Models.Request;
|
||||
|
||||
public class RestoreSecretVersionRequestModel
|
||||
{
|
||||
[Required]
|
||||
public Guid VersionId { get; set; }
|
||||
}
|
||||
@@ -28,6 +28,8 @@ public class SecretUpdateRequestModel : IValidatableObject
|
||||
|
||||
public SecretAccessPoliciesRequestsModel AccessPoliciesRequests { get; set; }
|
||||
|
||||
public bool ValueChanged { get; set; } = false;
|
||||
|
||||
public Secret ToSecret(Secret secret)
|
||||
{
|
||||
secret.Key = Key;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
|
||||
namespace Bit.Api.SecretsManager.Models.Response;
|
||||
|
||||
public class SecretVersionResponseModel : ResponseModel
|
||||
{
|
||||
private const string _objectName = "secretVersion";
|
||||
|
||||
public Guid Id { get; set; }
|
||||
public Guid SecretId { get; set; }
|
||||
public string Value { get; set; } = string.Empty;
|
||||
public DateTime VersionDate { get; set; }
|
||||
public Guid? EditorServiceAccountId { get; set; }
|
||||
public Guid? EditorOrganizationUserId { get; set; }
|
||||
|
||||
public SecretVersionResponseModel() : base(_objectName) { }
|
||||
|
||||
public SecretVersionResponseModel(SecretVersion secretVersion) : base(_objectName)
|
||||
{
|
||||
Id = secretVersion.Id;
|
||||
SecretId = secretVersion.SecretId;
|
||||
Value = secretVersion.Value;
|
||||
VersionDate = secretVersion.VersionDate;
|
||||
EditorServiceAccountId = secretVersion.EditorServiceAccountId;
|
||||
EditorOrganizationUserId = secretVersion.EditorOrganizationUserId;
|
||||
}
|
||||
}
|
||||
@@ -216,7 +216,7 @@ public class Startup
|
||||
config.Conventions.Add(new PublicApiControllersModelConvention());
|
||||
});
|
||||
|
||||
services.AddSwagger(globalSettings, Environment);
|
||||
services.AddSwaggerGen(globalSettings, Environment);
|
||||
Jobs.JobsHostedService.AddJobsServices(services, globalSettings.SelfHosted);
|
||||
services.AddHostedService<Jobs.JobsHostedService>();
|
||||
|
||||
@@ -292,17 +292,59 @@ public class Startup
|
||||
});
|
||||
|
||||
// Add Swagger
|
||||
// Note that the swagger.json generation is configured in the call to AddSwaggerGen above.
|
||||
if (Environment.IsDevelopment() || globalSettings.SelfHosted)
|
||||
{
|
||||
// adds the middleware to serve the swagger.json while the server is running
|
||||
app.UseSwagger(config =>
|
||||
{
|
||||
config.RouteTemplate = "specs/{documentName}/swagger.json";
|
||||
|
||||
// Remove all Bitwarden cloud servers and only register the local server
|
||||
config.PreSerializeFilters.Add((swaggerDoc, httpReq) =>
|
||||
swaggerDoc.Servers = new List<OpenApiServer>
|
||||
{
|
||||
swaggerDoc.Servers.Clear();
|
||||
swaggerDoc.Servers.Add(new OpenApiServer
|
||||
{
|
||||
new OpenApiServer { Url = globalSettings.BaseServiceUri.Api }
|
||||
Url = globalSettings.BaseServiceUri.Api,
|
||||
});
|
||||
|
||||
swaggerDoc.Components.SecuritySchemes.Clear();
|
||||
swaggerDoc.Components.SecuritySchemes.Add("oauth2-client-credentials", new OpenApiSecurityScheme
|
||||
{
|
||||
Type = SecuritySchemeType.OAuth2,
|
||||
Flows = new OpenApiOAuthFlows
|
||||
{
|
||||
ClientCredentials = new OpenApiOAuthFlow
|
||||
{
|
||||
TokenUrl = new Uri($"{globalSettings.BaseServiceUri.Identity}/connect/token"),
|
||||
Scopes = new Dictionary<string, string>
|
||||
{
|
||||
{ ApiScopes.ApiOrganization, "Organization APIs" }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
swaggerDoc.SecurityRequirements.Clear();
|
||||
swaggerDoc.SecurityRequirements.Add(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = "oauth2-client-credentials"
|
||||
}
|
||||
},
|
||||
[ApiScopes.ApiOrganization]
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// adds the middleware to display the web UI
|
||||
app.UseSwaggerUI(config =>
|
||||
{
|
||||
config.DocumentTitle = "Bitwarden API Documentation";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Bit.Api.AdminConsole.Authorization;
|
||||
using Bit.Api.Tools.Authorization;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.PhishingDomainFeatures;
|
||||
using Bit.Core.PhishingDomainFeatures.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
@@ -10,6 +9,7 @@ using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Authorization.SecurityTasks;
|
||||
using Bit.SharedWeb.Health;
|
||||
using Bit.SharedWeb.Swagger;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
@@ -17,7 +17,10 @@ namespace Bit.Api.Utilities;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static void AddSwagger(this IServiceCollection services, GlobalSettings globalSettings, IWebHostEnvironment environment)
|
||||
/// <summary>
|
||||
/// Configures the generation of swagger.json OpenAPI spec.
|
||||
/// </summary>
|
||||
public static void AddSwaggerGen(this IServiceCollection services, GlobalSettings globalSettings, IWebHostEnvironment environment)
|
||||
{
|
||||
services.AddSwaggerGen(config =>
|
||||
{
|
||||
@@ -36,6 +39,8 @@ public static class ServiceCollectionExtensions
|
||||
organizations tools for managing members, collections, groups, event logs, and policies.
|
||||
If you are looking for the Vault Management API, refer instead to
|
||||
[this document](https://bitwarden.com/help/vault-management-api/).
|
||||
|
||||
**Note:** your authorization must match the server you have selected.
|
||||
""",
|
||||
License = new OpenApiLicense
|
||||
{
|
||||
@@ -46,36 +51,20 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
config.SwaggerDoc("internal", new OpenApiInfo { Title = "Bitwarden Internal API", Version = "latest" });
|
||||
|
||||
config.AddSecurityDefinition("oauth2-client-credentials", new OpenApiSecurityScheme
|
||||
{
|
||||
Type = SecuritySchemeType.OAuth2,
|
||||
Flows = new OpenApiOAuthFlows
|
||||
{
|
||||
ClientCredentials = new OpenApiOAuthFlow
|
||||
{
|
||||
TokenUrl = new Uri($"{globalSettings.BaseServiceUri.Identity}/connect/token"),
|
||||
Scopes = new Dictionary<string, string>
|
||||
{
|
||||
{ ApiScopes.ApiOrganization, "Organization APIs" },
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
// Configure Bitwarden cloud US and EU servers. These will appear in the swagger.json build artifact
|
||||
// used for our help center. These are overwritten with the local server when running in self-hosted
|
||||
// or dev mode (see Api Startup.cs).
|
||||
config.AddSwaggerServerWithSecurity(
|
||||
serverId: "US_server",
|
||||
serverUrl: "https://api.bitwarden.com",
|
||||
identityTokenUrl: "https://identity.bitwarden.com/connect/token",
|
||||
serverDescription: "US server");
|
||||
|
||||
config.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = "oauth2-client-credentials"
|
||||
},
|
||||
},
|
||||
new[] { ApiScopes.ApiOrganization }
|
||||
}
|
||||
});
|
||||
config.AddSwaggerServerWithSecurity(
|
||||
serverId: "EU_server",
|
||||
serverUrl: "https://api.bitwarden.eu",
|
||||
identityTokenUrl: "https://identity.bitwarden.eu/connect/token",
|
||||
serverDescription: "EU server");
|
||||
|
||||
config.DescribeAllParametersInCamelCase();
|
||||
// config.UseReferencedDefinitionsForEnums();
|
||||
|
||||
@@ -757,11 +757,6 @@ public class CiphersController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
if (cipher.ArchivedDate.HasValue)
|
||||
{
|
||||
throw new BadRequestException("Cannot move an archived item to an organization.");
|
||||
}
|
||||
|
||||
ValidateClientVersionForFido2CredentialSupport(cipher);
|
||||
|
||||
var original = cipher.Clone();
|
||||
@@ -1271,11 +1266,6 @@ public class CiphersController : Controller
|
||||
_logger.LogError("Cipher was not encrypted for the current user. CipherId: {CipherId}, CurrentUser: {CurrentUserId}, EncryptedFor: {EncryptedFor}", cipher.Id, userId, cipher.EncryptedFor);
|
||||
throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
|
||||
}
|
||||
|
||||
if (cipher.ArchivedDate.HasValue)
|
||||
{
|
||||
throw new BadRequestException("Cannot move archived items to an organization.");
|
||||
}
|
||||
}
|
||||
|
||||
var shareCiphers = new List<(CipherDetails, DateTime?)>();
|
||||
@@ -1288,11 +1278,6 @@ public class CiphersController : Controller
|
||||
|
||||
ValidateClientVersionForFido2CredentialSupport(existingCipher);
|
||||
|
||||
if (existingCipher.ArchivedDate.HasValue)
|
||||
{
|
||||
throw new BadRequestException("Cannot move archived items to an organization.");
|
||||
}
|
||||
|
||||
shareCiphers.Add((cipher.ToCipherDetails(existingCipher), cipher.LastKnownRevisionDate));
|
||||
}
|
||||
|
||||
|
||||
@@ -116,4 +116,10 @@ public interface IStripeFacade
|
||||
TestClockGetOptions testClockGetOptions = null,
|
||||
RequestOptions requestOptions = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<Coupon> GetCoupon(
|
||||
string couponId,
|
||||
CouponGetOptions couponGetOptions = null,
|
||||
RequestOptions requestOptions = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ public class StripeFacade : IStripeFacade
|
||||
private readonly DiscountService _discountService = new();
|
||||
private readonly SetupIntentService _setupIntentService = new();
|
||||
private readonly TestClockService _testClockService = new();
|
||||
private readonly CouponService _couponService = new();
|
||||
|
||||
public async Task<Charge> GetCharge(
|
||||
string chargeId,
|
||||
@@ -143,4 +144,11 @@ public class StripeFacade : IStripeFacade
|
||||
RequestOptions requestOptions = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
_testClockService.GetAsync(testClockId, testClockGetOptions, requestOptions, cancellationToken);
|
||||
|
||||
public Task<Coupon> GetCoupon(
|
||||
string couponId,
|
||||
CouponGetOptions couponGetOptions = null,
|
||||
RequestOptions requestOptions = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
_couponService.GetAsync(couponId, couponGetOptions, requestOptions, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using System.Globalization;
|
||||
using Bit.Billing.Constants;
|
||||
using Bit.Billing.Constants;
|
||||
using Bit.Billing.Jobs;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
@@ -134,11 +132,6 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
||||
}
|
||||
case StripeSubscriptionStatus.Active when providerId.HasValue:
|
||||
{
|
||||
var providerPortalTakeover = _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover);
|
||||
if (!providerPortalTakeover)
|
||||
{
|
||||
break;
|
||||
}
|
||||
var provider = await _providerRepository.GetByIdAsync(providerId.Value);
|
||||
if (provider != null)
|
||||
{
|
||||
@@ -321,13 +314,6 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
||||
Event parsedEvent,
|
||||
Subscription currentSubscription)
|
||||
{
|
||||
var providerPortalTakeover = _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover);
|
||||
|
||||
if (!providerPortalTakeover)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||
if (provider == null)
|
||||
{
|
||||
@@ -343,22 +329,17 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
||||
{
|
||||
var previousSubscription = parsedEvent.Data.PreviousAttributes.ToObject<Subscription>() as Subscription;
|
||||
|
||||
var updateIsSubscriptionGoingUnpaid = previousSubscription is
|
||||
{
|
||||
Status:
|
||||
if (previousSubscription is
|
||||
{
|
||||
Status:
|
||||
StripeSubscriptionStatus.Trialing or
|
||||
StripeSubscriptionStatus.Active or
|
||||
StripeSubscriptionStatus.PastDue
|
||||
} && currentSubscription is
|
||||
{
|
||||
Status: StripeSubscriptionStatus.Unpaid,
|
||||
LatestInvoice.BillingReason: "subscription_cycle" or "subscription_create"
|
||||
};
|
||||
|
||||
var updateIsManualSuspensionViaMetadata = CheckForManualSuspensionViaMetadata(
|
||||
previousSubscription, currentSubscription);
|
||||
|
||||
if (updateIsSubscriptionGoingUnpaid || updateIsManualSuspensionViaMetadata)
|
||||
} && currentSubscription is
|
||||
{
|
||||
Status: StripeSubscriptionStatus.Unpaid,
|
||||
LatestInvoice.BillingReason: "subscription_cycle" or "subscription_create"
|
||||
})
|
||||
{
|
||||
if (currentSubscription.TestClock != null)
|
||||
{
|
||||
@@ -369,14 +350,6 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
||||
|
||||
var subscriptionUpdateOptions = new SubscriptionUpdateOptions { CancelAt = now.AddDays(7) };
|
||||
|
||||
if (updateIsManualSuspensionViaMetadata)
|
||||
{
|
||||
subscriptionUpdateOptions.Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["suspended_provider_via_webhook_at"] = DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)
|
||||
};
|
||||
}
|
||||
|
||||
await _stripeFacade.UpdateSubscription(currentSubscription.Id, subscriptionUpdateOptions);
|
||||
}
|
||||
}
|
||||
@@ -399,37 +372,4 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool CheckForManualSuspensionViaMetadata(
|
||||
Subscription? previousSubscription,
|
||||
Subscription currentSubscription)
|
||||
{
|
||||
/*
|
||||
* When metadata on a subscription is updated, we'll receive an event that has:
|
||||
* Previous Metadata: { newlyAddedKey: null }
|
||||
* Current Metadata: { newlyAddedKey: newlyAddedValue }
|
||||
*
|
||||
* As such, our check for a manual suspension must ensure that the 'previous_attributes' does contain the
|
||||
* 'metadata' property, but also that the "suspend_provider" key in that metadata is set to null.
|
||||
*
|
||||
* If we don't do this and instead do a null coalescing check on 'previous_attributes?.metadata?.TryGetValue',
|
||||
* we'll end up marking an event where 'previous_attributes.metadata' = null (which could be any subscription update
|
||||
* that does not update the metadata) the same as a manual suspension.
|
||||
*/
|
||||
const string key = "suspend_provider";
|
||||
|
||||
if (previousSubscription is not { Metadata: not null } ||
|
||||
!previousSubscription.Metadata.TryGetValue(key, out var previousValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (previousValue == null)
|
||||
{
|
||||
return !string.IsNullOrEmpty(
|
||||
currentSubscription.Metadata.TryGetValue(key, out var currentValue) ? currentValue : null);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Payment.Queries;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Mail.Billing.Renewal.Families2019Renewal;
|
||||
using Bit.Core.Models.Mail.Billing.Renewal.Families2020Renewal;
|
||||
using Bit.Core.Models.Mail.Billing.Renewal.Premium;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
using Bit.Core.Platform.Mail.Mailer;
|
||||
using Bit.Core.Repositories;
|
||||
@@ -284,7 +286,7 @@ public class UpcomingInvoiceHandler(
|
||||
{
|
||||
await organizationRepository.ReplaceAsync(organization);
|
||||
await stripeFacade.UpdateSubscription(subscription.Id, options);
|
||||
await SendFamiliesRenewalEmailAsync(organization, familiesPlan);
|
||||
await SendFamiliesRenewalEmailAsync(organization, familiesPlan, plan);
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
@@ -546,7 +548,18 @@ public class UpcomingInvoiceHandler(
|
||||
|
||||
private async Task SendFamiliesRenewalEmailAsync(
|
||||
Organization organization,
|
||||
Plan familiesPlan)
|
||||
Plan familiesPlan,
|
||||
Plan planBeforeAlignment)
|
||||
{
|
||||
await (planBeforeAlignment switch
|
||||
{
|
||||
{ Type: PlanType.FamiliesAnnually2025 } => SendFamilies2020RenewalEmailAsync(organization, familiesPlan),
|
||||
{ Type: PlanType.FamiliesAnnually2019 } => SendFamilies2019RenewalEmailAsync(organization, familiesPlan),
|
||||
_ => throw new InvalidOperationException("Unsupported families plan in SendFamiliesRenewalEmailAsync().")
|
||||
});
|
||||
}
|
||||
|
||||
private async Task SendFamilies2020RenewalEmailAsync(Organization organization, Plan familiesPlan)
|
||||
{
|
||||
var email = new Families2020RenewalMail
|
||||
{
|
||||
@@ -560,18 +573,61 @@ public class UpcomingInvoiceHandler(
|
||||
await mailer.SendEmail(email);
|
||||
}
|
||||
|
||||
private async Task SendFamilies2019RenewalEmailAsync(Organization organization, Plan familiesPlan)
|
||||
{
|
||||
var coupon = await stripeFacade.GetCoupon(CouponIDs.Milestone3SubscriptionDiscount);
|
||||
if (coupon == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Coupon for sending families 2019 email id:{CouponIDs.Milestone3SubscriptionDiscount} not found");
|
||||
}
|
||||
|
||||
if (coupon.PercentOff == null)
|
||||
{
|
||||
throw new InvalidOperationException($"coupon.PercentOff for sending families 2019 email id:{CouponIDs.Milestone3SubscriptionDiscount} is null");
|
||||
}
|
||||
|
||||
var discountedAnnualRenewalPrice = familiesPlan.PasswordManager.BasePrice * (100 - coupon.PercentOff.Value) / 100;
|
||||
|
||||
var email = new Families2019RenewalMail
|
||||
{
|
||||
ToEmails = [organization.BillingEmail],
|
||||
View = new Families2019RenewalMailView
|
||||
{
|
||||
BaseMonthlyRenewalPrice = (familiesPlan.PasswordManager.BasePrice / 12).ToString("C", new CultureInfo("en-US")),
|
||||
BaseAnnualRenewalPrice = familiesPlan.PasswordManager.BasePrice.ToString("C", new CultureInfo("en-US")),
|
||||
DiscountAmount = $"{coupon.PercentOff}%",
|
||||
DiscountedAnnualRenewalPrice = discountedAnnualRenewalPrice.ToString("C", new CultureInfo("en-US"))
|
||||
}
|
||||
};
|
||||
|
||||
await mailer.SendEmail(email);
|
||||
}
|
||||
|
||||
private async Task SendPremiumRenewalEmailAsync(
|
||||
User user,
|
||||
PremiumPlan premiumPlan)
|
||||
{
|
||||
/* TODO: Replace with proper premium renewal email template once finalized.
|
||||
Using Families2020RenewalMail as a temporary stop-gap. */
|
||||
var email = new Families2020RenewalMail
|
||||
var coupon = await stripeFacade.GetCoupon(CouponIDs.Milestone2SubscriptionDiscount);
|
||||
if (coupon == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Coupon for sending premium renewal email id:{CouponIDs.Milestone2SubscriptionDiscount} not found");
|
||||
}
|
||||
|
||||
if (coupon.PercentOff == null)
|
||||
{
|
||||
throw new InvalidOperationException($"coupon.PercentOff for sending premium renewal email id:{CouponIDs.Milestone2SubscriptionDiscount} is null");
|
||||
}
|
||||
|
||||
var discountedAnnualRenewalPrice = premiumPlan.Seat.Price * (100 - coupon.PercentOff.Value) / 100;
|
||||
|
||||
var email = new PremiumRenewalMail
|
||||
{
|
||||
ToEmails = [user.Email],
|
||||
View = new Families2020RenewalMailView
|
||||
View = new PremiumRenewalMailView
|
||||
{
|
||||
MonthlyRenewalPrice = (premiumPlan.Seat.Price / 12).ToString("C", new CultureInfo("en-US"))
|
||||
BaseMonthlyRenewalPrice = (premiumPlan.Seat.Price / 12).ToString("C", new CultureInfo("en-US")),
|
||||
DiscountAmount = $"{coupon.PercentOff}%",
|
||||
DiscountedMonthlyRenewalPrice = (discountedAnnualRenewalPrice / 12).ToString("C", new CultureInfo("en-US"))
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -134,6 +134,11 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable
|
||||
/// </summary>
|
||||
public bool UseAutomaticUserConfirmation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set to true, the organization has phishing protection enabled.
|
||||
/// </summary>
|
||||
public bool UsePhishingBlocker { get; set; }
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
if (Id == default(Guid))
|
||||
@@ -334,5 +339,6 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable
|
||||
UseOrganizationDomains = license.UseOrganizationDomains;
|
||||
UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies;
|
||||
UseAutomaticUserConfirmation = license.UseAutomaticUserConfirmation;
|
||||
UsePhishingBlocker = license.UsePhishingBlocker;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,4 +53,5 @@ public interface IProfileOrganizationDetails
|
||||
bool UseAdminSponsoredFamilies { get; set; }
|
||||
bool UseOrganizationDomains { get; set; }
|
||||
bool UseAutomaticUserConfirmation { get; set; }
|
||||
bool UsePhishingBlocker { get; set; }
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ public class OrganizationAbility
|
||||
UseOrganizationDomains = organization.UseOrganizationDomains;
|
||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||
UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation;
|
||||
UsePhishingBlocker = organization.UsePhishingBlocker;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
@@ -51,4 +52,5 @@ public class OrganizationAbility
|
||||
public bool UseOrganizationDomains { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public bool UseAutomaticUserConfirmation { get; set; }
|
||||
public bool UsePhishingBlocker { get; set; }
|
||||
}
|
||||
|
||||
@@ -65,4 +65,5 @@ public class OrganizationUserOrganizationDetails : IProfileOrganizationDetails
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public bool? IsAdminInitiated { get; set; }
|
||||
public bool UseAutomaticUserConfirmation { get; set; }
|
||||
public bool UsePhishingBlocker { get; set; }
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ public class SelfHostedOrganizationDetails : Organization
|
||||
Status = Status,
|
||||
UseRiskInsights = UseRiskInsights,
|
||||
UseAdminSponsoredFamilies = UseAdminSponsoredFamilies,
|
||||
UsePhishingBlocker = UsePhishingBlocker,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,4 +56,5 @@ public class ProviderUserOrganizationDetails : IProfileOrganizationDetails
|
||||
public string? SsoExternalId { get; set; }
|
||||
public string? Permissions { get; set; }
|
||||
public string? ResetPasswordKey { get; set; }
|
||||
public bool UsePhishingBlocker { get; set; }
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
@@ -25,8 +24,6 @@ public class VerifyOrganizationDomainCommand(
|
||||
IEventService eventService,
|
||||
IGlobalSettings globalSettings,
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
ISavePolicyCommand savePolicyCommand,
|
||||
IVNextSavePolicyCommand vNextSavePolicyCommand,
|
||||
IMailService mailService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@@ -144,15 +141,8 @@ public class VerifyOrganizationDomainCommand(
|
||||
PerformedBy = actingUser
|
||||
};
|
||||
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor))
|
||||
{
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, actingUser);
|
||||
await vNextSavePolicyCommand.SaveAsync(savePolicyModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
await savePolicyCommand.SaveAsync(policyUpdate);
|
||||
}
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, actingUser);
|
||||
await vNextSavePolicyCommand.SaveAsync(savePolicyModel);
|
||||
}
|
||||
|
||||
private async Task SendVerifiedDomainUserEmailAsync(OrganizationDomain domain)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Automatic User Confirmation
|
||||
|
||||
Owned by: admin-console
|
||||
|
||||
Automatic confirmation requests are server driven events that are sent to the admin's client where via a background service the confirmation will occur. The basic model
|
||||
for the workflow is as follows:
|
||||
|
||||
- The Api server sends an invite email to a user.
|
||||
- The user accepts the invite request, which is sent back to the Api server
|
||||
- The Api server sends a push-notification with the OrganizationId and UserId to a client admin session.
|
||||
- The Client performs the key exchange in the background and POSTs the ConfirmRequest back to the Api server
|
||||
- The Api server runs the OrgUser_Confirm sproc to confirm the user in the DB
|
||||
|
||||
This Feature has the following security measures in place in order to achieve our security goals:
|
||||
|
||||
- The single organization exemption for admins/owners is removed for this policy.
|
||||
- This is enforced by preventing enabling the policy and organization plan feature if there are non-compliant users
|
||||
- Emergency access is removed for all organization users
|
||||
- Automatic confirmation will only apply to the User role (You cannot auto confirm admins/owners to an organization)
|
||||
- The organization has no members with the Provider user type.
|
||||
- This will also prevent the policy and organization plan feature from being enabled
|
||||
- This will prevent sending organization invites to provider users
|
||||
@@ -38,6 +38,7 @@ public static class PolicyServiceCollectionExtensions
|
||||
services.AddScoped<IPolicyValidator, MaximumVaultTimeoutPolicyValidator>();
|
||||
services.AddScoped<IPolicyValidator, UriMatchDefaultPolicyValidator>();
|
||||
services.AddScoped<IPolicyValidator, FreeFamiliesForEnterprisePolicyValidator>();
|
||||
services.AddScoped<IPolicyValidator, BlockClaimedDomainAccountCreationPolicyValidator>();
|
||||
services.AddScoped<IPolicyValidator, AutomaticUserConfirmationPolicyEventHandler>();
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ public static class OrganizationFactory
|
||||
UseAdminSponsoredFamilies =
|
||||
claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseAdminSponsoredFamilies),
|
||||
UseAutomaticUserConfirmation = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseAutomaticUserConfirmation),
|
||||
UsePhishingBlocker = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UsePhishingBlocker),
|
||||
};
|
||||
|
||||
public static Organization Create(
|
||||
@@ -111,6 +112,7 @@ public static class OrganizationFactory
|
||||
UseRiskInsights = license.UseRiskInsights,
|
||||
UseOrganizationDomains = license.UseOrganizationDomains,
|
||||
UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies,
|
||||
UseAutomaticUserConfirmation = license.UseAutomaticUserConfirmation
|
||||
UseAutomaticUserConfirmation = license.UseAutomaticUserConfirmation,
|
||||
UsePhishingBlocker = license.UsePhishingBlocker,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
@@ -26,8 +25,6 @@ public class SsoConfigService : ISsoConfigService
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ISavePolicyCommand _savePolicyCommand;
|
||||
private readonly IVNextSavePolicyCommand _vNextSavePolicyCommand;
|
||||
|
||||
public SsoConfigService(
|
||||
@@ -36,8 +33,6 @@ public class SsoConfigService : ISsoConfigService
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IEventService eventService,
|
||||
IFeatureService featureService,
|
||||
ISavePolicyCommand savePolicyCommand,
|
||||
IVNextSavePolicyCommand vNextSavePolicyCommand)
|
||||
{
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
@@ -45,8 +40,6 @@ public class SsoConfigService : ISsoConfigService
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_eventService = eventService;
|
||||
_featureService = featureService;
|
||||
_savePolicyCommand = savePolicyCommand;
|
||||
_vNextSavePolicyCommand = vNextSavePolicyCommand;
|
||||
}
|
||||
|
||||
@@ -97,19 +90,10 @@ public class SsoConfigService : ISsoConfigService
|
||||
Enabled = true
|
||||
};
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor))
|
||||
{
|
||||
var performedBy = new SystemUser(EventSystemUser.Unknown);
|
||||
await _vNextSavePolicyCommand.SaveAsync(new SavePolicyModel(singleOrgPolicy, performedBy));
|
||||
await _vNextSavePolicyCommand.SaveAsync(new SavePolicyModel(resetPasswordPolicy, performedBy));
|
||||
await _vNextSavePolicyCommand.SaveAsync(new SavePolicyModel(requireSsoPolicy, performedBy));
|
||||
}
|
||||
else
|
||||
{
|
||||
await _savePolicyCommand.SaveAsync(singleOrgPolicy);
|
||||
await _savePolicyCommand.SaveAsync(resetPasswordPolicy);
|
||||
await _savePolicyCommand.SaveAsync(requireSsoPolicy);
|
||||
}
|
||||
var performedBy = new SystemUser(EventSystemUser.Unknown);
|
||||
await _vNextSavePolicyCommand.SaveAsync(new SavePolicyModel(singleOrgPolicy, performedBy));
|
||||
await _vNextSavePolicyCommand.SaveAsync(new SavePolicyModel(resetPasswordPolicy, performedBy));
|
||||
await _vNextSavePolicyCommand.SaveAsync(new SavePolicyModel(requireSsoPolicy, performedBy));
|
||||
}
|
||||
|
||||
await LogEventsAsync(config, oldConfig);
|
||||
|
||||
@@ -44,6 +44,7 @@ public static class OrganizationLicenseConstants
|
||||
public const string UseAdminSponsoredFamilies = nameof(UseAdminSponsoredFamilies);
|
||||
public const string UseOrganizationDomains = nameof(UseOrganizationDomains);
|
||||
public const string UseAutomaticUserConfirmation = nameof(UseAutomaticUserConfirmation);
|
||||
public const string UsePhishingBlocker = nameof(UsePhishingBlocker);
|
||||
}
|
||||
|
||||
public static class UserLicenseConstants
|
||||
|
||||
@@ -57,6 +57,7 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory<Organizati
|
||||
new(nameof(OrganizationLicenseConstants.UseAdminSponsoredFamilies), entity.UseAdminSponsoredFamilies.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseOrganizationDomains), entity.UseOrganizationDomains.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseAutomaticUserConfirmation), entity.UseAutomaticUserConfirmation.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UsePhishingBlocker), entity.UsePhishingBlocker.ToString()),
|
||||
};
|
||||
|
||||
if (entity.Name is not null)
|
||||
|
||||
@@ -143,6 +143,7 @@ public class OrganizationLicense : ILicense
|
||||
public int? SmSeats { get; set; }
|
||||
public int? SmServiceAccounts { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
public bool UsePhishingBlocker { get; set; }
|
||||
|
||||
// Deprecated. Left for backwards compatibility with old license versions.
|
||||
public bool LimitCollectionCreationDeletion { get; set; } = true;
|
||||
@@ -228,7 +229,8 @@ public class OrganizationLicense : ILicense
|
||||
!p.Name.Equals(nameof(UseRiskInsights)) &&
|
||||
!p.Name.Equals(nameof(UseAdminSponsoredFamilies)) &&
|
||||
!p.Name.Equals(nameof(UseOrganizationDomains)) &&
|
||||
!p.Name.Equals(nameof(UseAutomaticUserConfirmation)))
|
||||
!p.Name.Equals(nameof(UseAutomaticUserConfirmation)) &&
|
||||
!p.Name.Equals(nameof(UsePhishingBlocker)))
|
||||
.OrderBy(p => p.Name)
|
||||
.Select(p => $"{p.Name}:{Core.Utilities.CoreHelpers.FormatLicenseSignatureValue(p.GetValue(this, null))}")
|
||||
.Aggregate((c, n) => $"{c}|{n}");
|
||||
|
||||
@@ -140,9 +140,7 @@ public static class FeatureFlagKeys
|
||||
public const string CreateDefaultLocation = "pm-19467-create-default-location";
|
||||
public const string AutomaticConfirmUsers = "pm-19934-auto-confirm-organization-users";
|
||||
public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache";
|
||||
public const string AccountRecoveryCommand = "pm-25581-prevent-provider-account-recovery";
|
||||
public const string BlockClaimedDomainAccountCreation = "pm-28297-block-uninvited-claimed-domain-registration";
|
||||
public const string PolicyValidatorsRefactor = "pm-26423-refactor-policy-side-effects";
|
||||
public const string IncreaseBulkReinviteLimitForCloud = "pm-28251-increase-bulk-reinvite-limit-for-cloud";
|
||||
|
||||
/* Architecture */
|
||||
@@ -188,7 +186,6 @@ public static class FeatureFlagKeys
|
||||
|
||||
/* Billing Team */
|
||||
public const string TrialPayment = "PM-8163-trial-payment";
|
||||
public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover";
|
||||
public const string PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings";
|
||||
public const string PM25379_UseNewOrganizationMetadataStructure = "pm-25379-use-new-organization-metadata-structure";
|
||||
public const string PM24996ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog";
|
||||
@@ -215,6 +212,7 @@ public static class FeatureFlagKeys
|
||||
public const string NoLogoutOnKdfChange = "pm-23995-no-logout-on-kdf-change";
|
||||
public const string DisableType0Decryption = "pm-25174-disable-type-0-decryption";
|
||||
public const string ConsolidatedSessionTimeoutComponent = "pm-26056-consolidated-session-timeout-component";
|
||||
public const string DataRecoveryTool = "pm-28813-data-recovery-tool";
|
||||
|
||||
/* Mobile Team */
|
||||
public const string AndroidImportLoginsFlow = "import-logins-flow";
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
using Bit.Core.KeyManagement.Models.Api.Request;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
namespace Bit.Core.KeyManagement.Models.Api.Request;
|
||||
|
||||
public class AccountKeysRequestModel
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
namespace Bit.Core.KeyManagement.Models.Api.Request;
|
||||
|
||||
public class PublicKeyEncryptionKeyPairRequestModel
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
namespace Bit.Core.KeyManagement.Models.Api.Request;
|
||||
|
||||
public class SignatureKeyPairRequestModel
|
||||
{
|
||||
@@ -0,0 +1,42 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-include path="../../../components/head.mjml"/>
|
||||
</mj-head>
|
||||
|
||||
<!-- Blue Header Section-->
|
||||
<mj-body css-class="border-fix">
|
||||
<mj-wrapper css-class="border-fix" padding="20px 20px 0px 20px">
|
||||
<mj-bw-simple-hero />
|
||||
</mj-wrapper>
|
||||
|
||||
<!-- Main Content Section -->
|
||||
<mj-wrapper padding="0px 20px 0px 20px">
|
||||
<mj-section background-color="#fff" padding="15px 10px 10px 10px">
|
||||
<mj-column>
|
||||
<mj-text font-size="16px" line-height="24px" padding="10px 15px 15px 15px">
|
||||
Your Bitwarden Families subscription renews in 15 days. The price is updating to {{BaseMonthlyRenewalPrice}}/month, billed annually
|
||||
at {{BaseAnnualRenewalPrice}} + tax.
|
||||
</mj-text>
|
||||
<mj-text font-size="16px" line-height="24px" padding="10px 15px 15px 15px">
|
||||
As a long time Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this renewal.
|
||||
This renewal will now be billed annually at {{DiscountedAnnualRenewalPrice}} + tax.
|
||||
</mj-text>
|
||||
<mj-text font-size="16px" line-height="24px" padding="10px 15px">
|
||||
Questions? Contact
|
||||
<a href="mailto:support@bitwarden.com" class="link">support@bitwarden.com</a>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section background-color="#fff" padding="0 20px 20px 20px">
|
||||
</mj-section>
|
||||
</mj-wrapper>
|
||||
|
||||
<!-- Learn More Section -->
|
||||
<mj-wrapper padding="0px 20px 10px 20px">
|
||||
<mj-bw-learn-more-footer/>
|
||||
</mj-wrapper>
|
||||
|
||||
<!-- Footer -->
|
||||
<mj-include path="../../../components/footer.mjml"/>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -0,0 +1,41 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-include path="../../../components/head.mjml"/>
|
||||
</mj-head>
|
||||
|
||||
<!-- Blue Header Section-->
|
||||
<mj-body css-class="border-fix">
|
||||
<mj-wrapper css-class="border-fix" padding="20px 20px 0px 20px">
|
||||
<mj-bw-simple-hero />
|
||||
</mj-wrapper>
|
||||
|
||||
<!-- Main Content Section -->
|
||||
<mj-wrapper padding="0px 20px 0px 20px">
|
||||
<mj-section background-color="#fff" padding="15px 10px 10px 10px">
|
||||
<mj-column>
|
||||
<mj-text font-size="16px" line-height="24px" padding="10px 15px 15px 15px">
|
||||
Your Bitwarden Premium subscription renews in 15 days. The price is updating to {{BaseMonthlyRenewalPrice}}/month, billed annually.
|
||||
</mj-text>
|
||||
<mj-text font-size="16px" line-height="24px" padding="10px 15px 15px 15px">
|
||||
As an existing Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this renewal.
|
||||
This renewal now will be {{DiscountedMonthlyRenewalPrice}}/month, billed annually.
|
||||
</mj-text>
|
||||
<mj-text font-size="16px" line-height="24px" padding="10px 15px">
|
||||
Questions? Contact
|
||||
<a href="mailto:support@bitwarden.com" class="link">support@bitwarden.com</a>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section background-color="#fff" padding="0 20px 20px 20px">
|
||||
</mj-section>
|
||||
</mj-wrapper>
|
||||
|
||||
<!-- Learn More Section -->
|
||||
<mj-wrapper padding="0px 20px 10px 20px">
|
||||
<mj-bw-learn-more-footer/>
|
||||
</mj-wrapper>
|
||||
|
||||
<!-- Footer -->
|
||||
<mj-include path="../../../components/footer.mjml"/>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -0,0 +1,16 @@
|
||||
using Bit.Core.Platform.Mail.Mailer;
|
||||
|
||||
namespace Bit.Core.Models.Mail.Billing.Renewal.Families2019Renewal;
|
||||
|
||||
public class Families2019RenewalMailView : BaseMailView
|
||||
{
|
||||
public required string BaseMonthlyRenewalPrice { get; set; }
|
||||
public required string BaseAnnualRenewalPrice { get; set; }
|
||||
public required string DiscountedAnnualRenewalPrice { get; set; }
|
||||
public required string DiscountAmount { get; set; }
|
||||
}
|
||||
|
||||
public class Families2019RenewalMail : BaseMail<Families2019RenewalMailView>
|
||||
{
|
||||
public override string Subject { get => "Your Bitwarden Families renewal is updating"; }
|
||||
}
|
||||
@@ -0,0 +1,584 @@
|
||||
<!doctype html>
|
||||
<html lang="und" dir="auto" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<title></title>
|
||||
<!--[if !mso]><!-->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a { padding:0; }
|
||||
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
|
||||
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
|
||||
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
|
||||
p { display:block;margin:13px 0; }
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
|
||||
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 { width:100% !important; max-width: 100%; }
|
||||
.mj-column-per-70 { width:70% !important; max-width: 70%; }
|
||||
.mj-column-per-30 { width:30% !important; max-width: 30%; }
|
||||
}
|
||||
</style>
|
||||
<style media="screen and (min-width:480px)">
|
||||
.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }
|
||||
.moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }
|
||||
.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
<style type="text/css">
|
||||
|
||||
@media only screen and (max-width:480px) {
|
||||
.mj-bw-learn-more-footer-responsive-img {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width:479px) {
|
||||
table.mj-full-width-mobile { width: 100% !important; }
|
||||
td.mj-full-width-mobile { width: auto !important; }
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style type="text/css">
|
||||
.border-fix > table {
|
||||
border-collapse: separate !important;
|
||||
}
|
||||
.border-fix > table > tbody > tr > td {
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body style="word-spacing:normal;background-color:#e6e9ef;">
|
||||
|
||||
|
||||
<div class="border-fix" style="background-color:#e6e9ef;" lang="und" dir="auto">
|
||||
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="border-fix-outlook" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div class="border-fix" style="margin:0px auto;max-width:660px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 20px 0px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><![endif]-->
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#175ddc" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;border-radius:4px 4px 0px 0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:580px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 5px;word-break:break-word;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:150px;">
|
||||
|
||||
<img alt src="https://bitwarden.com/images/logo-horizontal-white.png" style="border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;" width="150" height="30">
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
<!-- Main Content Section -->
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="margin:0px auto;max-width:660px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px 20px 0px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:15px 10px 10px 10px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 15px 15px 15px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;">Your Bitwarden Families subscription renews in 15 days. The price is updating to {{BaseMonthlyRenewalPrice}}/month, billed annually
|
||||
at {{BaseAnnualRenewalPrice}} + tax.</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 15px 15px 15px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;">As a long time Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this renewal.
|
||||
This renewal will now be billed annually at {{DiscountedAnnualRenewalPrice}} + tax.</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 15px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;">Questions? Contact
|
||||
<a href="mailto:support@bitwarden.com" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;">support@bitwarden.com</a></div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0 20px 20px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
<!-- Learn More Section -->
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="margin:0px auto;max-width:660px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px 20px 10px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#f6f6f6" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="background:#f6f6f6;background-color:#f6f6f6;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#f6f6f6;background-color:#f6f6f6;width:100%;border-radius:0px 0px 4px 4px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:5px 10px 10px 10px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:420px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-70 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;"><p style="font-size: 18px; line-height: 28px; font-weight: bold;">
|
||||
Learn more about Bitwarden
|
||||
</p>
|
||||
Find user guides, product documentation, and videos on the
|
||||
<a href="https://bitwarden.com/help/" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;"> Bitwarden Help Center</a>.</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td><td class="" style="vertical-align:top;width:180px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-30 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="center" class="mj-bw-learn-more-footer-responsive-img" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:94px;">
|
||||
|
||||
<img alt src="https://assets.bitwarden.com/email/v1/spot-community.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;" width="94" height="auto">
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
<!-- Footer -->
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="margin:0px auto;max-width:660px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:660px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:0;word-break:break-word;">
|
||||
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" ><tr><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://x.com/bitwarden" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-x.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://www.reddit.com/r/Bitwarden/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-reddit.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://community.bitwarden.com/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-discourse.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://github.com/bitwarden" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-github.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-youtube.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://www.linkedin.com/company/bitwarden1/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-linkedin.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://www.facebook.com/bitwarden/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-facebook.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;"><p style="margin-bottom: 5px">
|
||||
© 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa
|
||||
Barbara, CA, USA
|
||||
</p>
|
||||
<p style="margin-top: 5px">
|
||||
Always confirm you are on a trusted Bitwarden domain before logging
|
||||
in:<br>
|
||||
<a href="https://bitwarden.com/">bitwarden.com</a> |
|
||||
<a href="https://bitwarden.com/help/emails-from-bitwarden/">Learn why we include this</a>
|
||||
</p></div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
Your Bitwarden Families subscription renews in 15 days. The price is updating to {{BaseMonthlyRenewalPrice}}/month, billed annually
|
||||
at {{BaseAnnualRenewalPrice}} + tax.
|
||||
|
||||
As a long time Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this renewal.
|
||||
This renewal will now be billed annually at {{DiscountedAnnualRenewalPrice}} + tax.
|
||||
|
||||
Questions? Contact support@bitwarden.com
|
||||
@@ -0,0 +1,15 @@
|
||||
using Bit.Core.Platform.Mail.Mailer;
|
||||
|
||||
namespace Bit.Core.Models.Mail.Billing.Renewal.Premium;
|
||||
|
||||
public class PremiumRenewalMailView : BaseMailView
|
||||
{
|
||||
public required string BaseMonthlyRenewalPrice { get; set; }
|
||||
public required string DiscountedMonthlyRenewalPrice { get; set; }
|
||||
public required string DiscountAmount { get; set; }
|
||||
}
|
||||
|
||||
public class PremiumRenewalMail : BaseMail<PremiumRenewalMailView>
|
||||
{
|
||||
public override string Subject { get => "Your Bitwarden Premium renewal is updating"; }
|
||||
}
|
||||
@@ -0,0 +1,583 @@
|
||||
<!doctype html>
|
||||
<html lang="und" dir="auto" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<title></title>
|
||||
<!--[if !mso]><!-->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a { padding:0; }
|
||||
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
|
||||
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
|
||||
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
|
||||
p { display:block;margin:13px 0; }
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
|
||||
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 { width:100% !important; max-width: 100%; }
|
||||
.mj-column-per-70 { width:70% !important; max-width: 70%; }
|
||||
.mj-column-per-30 { width:30% !important; max-width: 30%; }
|
||||
}
|
||||
</style>
|
||||
<style media="screen and (min-width:480px)">
|
||||
.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }
|
||||
.moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }
|
||||
.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
<style type="text/css">
|
||||
|
||||
@media only screen and (max-width:480px) {
|
||||
.mj-bw-learn-more-footer-responsive-img {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width:479px) {
|
||||
table.mj-full-width-mobile { width: 100% !important; }
|
||||
td.mj-full-width-mobile { width: auto !important; }
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style type="text/css">
|
||||
.border-fix > table {
|
||||
border-collapse: separate !important;
|
||||
}
|
||||
.border-fix > table > tbody > tr > td {
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body style="word-spacing:normal;background-color:#e6e9ef;">
|
||||
|
||||
|
||||
<div class="border-fix" style="background-color:#e6e9ef;" lang="und" dir="auto">
|
||||
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="border-fix-outlook" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div class="border-fix" style="margin:0px auto;max-width:660px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 20px 0px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><![endif]-->
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#175ddc" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;border-radius:4px 4px 0px 0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:580px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 5px;word-break:break-word;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:150px;">
|
||||
|
||||
<img alt src="https://bitwarden.com/images/logo-horizontal-white.png" style="border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;" width="150" height="30">
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
<!-- Main Content Section -->
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="margin:0px auto;max-width:660px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px 20px 0px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:15px 10px 10px 10px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 15px 15px 15px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;">Your Bitwarden Premium subscription renews in 15 days. The price is updating to {{BaseMonthlyRenewalPrice}}/month, billed annually.</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 15px 15px 15px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;">As an existing Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this renewal.
|
||||
This renewal now will be {{DiscountedMonthlyRenewalPrice}}/month, billed annually.</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 15px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;">Questions? Contact
|
||||
<a href="mailto:support@bitwarden.com" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;">support@bitwarden.com</a></div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0 20px 20px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
<!-- Learn More Section -->
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="margin:0px auto;max-width:660px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px 20px 10px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#f6f6f6" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="background:#f6f6f6;background-color:#f6f6f6;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#f6f6f6;background-color:#f6f6f6;width:100%;border-radius:0px 0px 4px 4px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:5px 10px 10px 10px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:420px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-70 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;"><p style="font-size: 18px; line-height: 28px; font-weight: bold;">
|
||||
Learn more about Bitwarden
|
||||
</p>
|
||||
Find user guides, product documentation, and videos on the
|
||||
<a href="https://bitwarden.com/help/" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;"> Bitwarden Help Center</a>.</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td><td class="" style="vertical-align:top;width:180px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-30 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="center" class="mj-bw-learn-more-footer-responsive-img" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:94px;">
|
||||
|
||||
<img alt src="https://assets.bitwarden.com/email/v1/spot-community.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;" width="94" height="auto">
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
<!-- Footer -->
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="margin:0px auto;max-width:660px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:660px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:0;word-break:break-word;">
|
||||
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" ><tr><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://x.com/bitwarden" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-x.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://www.reddit.com/r/Bitwarden/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-reddit.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://community.bitwarden.com/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-discourse.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://github.com/bitwarden" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-github.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-youtube.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://www.linkedin.com/company/bitwarden1/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-linkedin.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://www.facebook.com/bitwarden/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-facebook.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;"><p style="margin-bottom: 5px">
|
||||
© 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa
|
||||
Barbara, CA, USA
|
||||
</p>
|
||||
<p style="margin-top: 5px">
|
||||
Always confirm you are on a trusted Bitwarden domain before logging
|
||||
in:<br>
|
||||
<a href="https://bitwarden.com/">bitwarden.com</a> |
|
||||
<a href="https://bitwarden.com/help/emails-from-bitwarden/">Learn why we include this</a>
|
||||
</p></div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
Your Bitwarden Premium subscription renews in 15 days. The price is updating to {{BaseMonthlyRenewalPrice}}/month, billed annually.
|
||||
|
||||
As an existing Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this renewal.
|
||||
This renewal now will be {{DiscountedMonthlyRenewalPrice}}/month, billed annually.
|
||||
|
||||
Questions? Contact support@bitwarden.com
|
||||
@@ -0,0 +1,12 @@
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
|
||||
namespace Bit.Core.SecretsManager.Repositories;
|
||||
|
||||
public interface ISecretVersionRepository
|
||||
{
|
||||
Task<SecretVersion?> GetByIdAsync(Guid id);
|
||||
Task<IEnumerable<SecretVersion>> GetManyBySecretIdAsync(Guid secretId);
|
||||
Task<IEnumerable<SecretVersion>> GetManyByIdsAsync(IEnumerable<Guid> ids);
|
||||
Task<SecretVersion> CreateAsync(SecretVersion secretVersion);
|
||||
Task DeleteManyByIdAsync(IEnumerable<Guid> ids);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
|
||||
namespace Bit.Core.SecretsManager.Repositories.Noop;
|
||||
|
||||
public class NoopSecretVersionRepository : ISecretVersionRepository
|
||||
{
|
||||
public Task<SecretVersion?> GetByIdAsync(Guid id)
|
||||
{
|
||||
return Task.FromResult(null as SecretVersion);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<SecretVersion>> GetManyBySecretIdAsync(Guid secretId)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<SecretVersion>());
|
||||
}
|
||||
|
||||
public Task<SecretVersion> CreateAsync(SecretVersion secretVersion)
|
||||
{
|
||||
return Task.FromResult(secretVersion);
|
||||
}
|
||||
|
||||
public Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IEnumerable<SecretVersion>> GetManyByIdsAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<SecretVersion>());
|
||||
}
|
||||
}
|
||||
@@ -483,7 +483,7 @@ public class GlobalSettings : IGlobalSettings
|
||||
public string CertificatePassword { get; set; }
|
||||
public string RedisConnectionString { get; set; }
|
||||
public string CosmosConnectionString { get; set; }
|
||||
public string LicenseKey { get; set; } = "eyJhbGciOiJQUzI1NiIsImtpZCI6IklkZW50aXR5U2VydmVyTGljZW5zZWtleS83Y2VhZGJiNzgxMzA0NjllODgwNjg5MTAyNTQxNGYxNiIsInR5cCI6ImxpY2Vuc2Urand0In0.eyJpc3MiOiJodHRwczovL2R1ZW5kZXNvZnR3YXJlLmNvbSIsImF1ZCI6IklkZW50aXR5U2VydmVyIiwiaWF0IjoxNzM0NTY2NDAwLCJleHAiOjE3NjQ5NzkyMDAsImNvbXBhbnlfbmFtZSI6IkJpdHdhcmRlbiBJbmMuIiwiY29udGFjdF9pbmZvIjoiY29udGFjdEBkdWVuZGVzb2Z0d2FyZS5jb20iLCJlZGl0aW9uIjoiU3RhcnRlciIsImlkIjoiNjg3OCIsImZlYXR1cmUiOlsiaXN2IiwidW5saW1pdGVkX2NsaWVudHMiXSwicHJvZHVjdCI6IkJpdHdhcmRlbiJ9.TYc88W_t2t0F2AJV3rdyKwGyQKrKFriSAzm1tWFNHNR9QizfC-8bliGdT4Wgeie-ynCXs9wWaF-sKC5emg--qS7oe2iIt67Qd88WS53AwgTvAddQRA4NhGB1R7VM8GAikLieSos-DzzwLYRgjZdmcsprItYGSJuY73r-7-F97ta915majBytVxGF966tT9zF1aYk0bA8FS6DcDYkr5f7Nsy8daS_uIUAgNa_agKXtmQPqKujqtUb6rgWEpSp4OcQcG-8Dpd5jHqoIjouGvY-5LTgk5WmLxi_m-1QISjxUJrUm-UGao3_VwV5KFGqYrz8csdTl-HS40ihWcsWnrV0ug";
|
||||
public string LicenseKey { get; set; } = "eyJhbGciOiJQUzI1NiIsImtpZCI6IklkZW50aXR5U2VydmVyTGljZW5zZUtleS83Y2VhZGJiNzgxMzA0NjllODgwNjg5MTAyNTQxNGYxNiIsInR5cCI6ImxpY2Vuc2Urand0In0.eyJpc3MiOiJodHRwczovL2R1ZW5kZXNvZnR3YXJlLmNvbSIsImF1ZCI6IklkZW50aXR5U2VydmVyIiwiaWF0IjoxNzY1MDY1NjAwLCJleHAiOjE3OTY1MTUyMDAsImNvbXBhbnlfbmFtZSI6IkJpdHdhcmRlbiBJbmMuIiwiY29udGFjdF9pbmZvIjoiY29udGFjdEBkdWVuZGVzb2Z0d2FyZS5jb20iLCJlZGl0aW9uIjoiU3RhcnRlciIsImlkIjoiOTUxNSIsImZlYXR1cmUiOlsiaXN2IiwidW5saW1pdGVkX2NsaWVudHMiXSwiY2xpZW50X2xpbWl0IjowfQ.rWUsq-XBKNwPG7BRKG-vShXHuyHLHJCh0sEWdWT4Rkz4ArIPOAepEp9wNya-hxFKkBTFlPaQ5IKk4wDTvkQkuq1qaI_v6kSCdaP9fvXp0rmh4KcFEffVLB-wAOK2S2Cld5DzdyCoskUUfwNQP7xuLsz2Ydxe_whSRIdv8bsMbvTC3Kl8PYZPZ4MxqW8rSZ_mEuCpSe5-Q40sB7aiu_7YmWLJaKrfBTIqYH-XuzQj36Aemoei0efcntej-gvxovy-5SiSEsGuRZj41rjEZYOuj5KgHihJViO1VDHK6CNtlu2Ks8bkv6G2hO-TkF16Y28ywEG_beLEf_s5dzhbDBDbvA";
|
||||
/// <summary>
|
||||
/// Sliding lifetime of a refresh token in seconds.
|
||||
///
|
||||
|
||||
@@ -990,11 +990,6 @@ public class CipherService : ICipherService
|
||||
throw new BadRequestException("One or more ciphers do not belong to you.");
|
||||
}
|
||||
|
||||
if (cipher.ArchivedDate.HasValue)
|
||||
{
|
||||
throw new BadRequestException("Cipher cannot be shared with organization because it is archived.");
|
||||
}
|
||||
|
||||
var attachments = cipher.GetAttachments();
|
||||
var hasAttachments = attachments?.Any() ?? false;
|
||||
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
@@ -161,16 +161,16 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
}
|
||||
|
||||
// Key connector data should have already been set in the decryption options
|
||||
// for backwards compatibility we set them this way too. We can eventually get rid of this
|
||||
// when all clients don't read them from the existing locations.
|
||||
// for backwards compatibility we set them this way too. We can eventually get rid of this once we clean up
|
||||
// ResetMasterPassword
|
||||
if (!context.Result.CustomResponse.TryGetValue("UserDecryptionOptions", out var userDecryptionOptionsObj) ||
|
||||
userDecryptionOptionsObj is not UserDecryptionOptions userDecryptionOptions)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (userDecryptionOptions is { KeyConnectorOption: { } })
|
||||
{
|
||||
context.Result.CustomResponse["KeyConnectorUrl"] = userDecryptionOptions.KeyConnectorOption.KeyConnectorUrl;
|
||||
context.Result.CustomResponse["ResetMasterPassword"] = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -113,7 +113,8 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
|
||||
UseRiskInsights = e.UseRiskInsights,
|
||||
UseOrganizationDomains = e.UseOrganizationDomains,
|
||||
UseAdminSponsoredFamilies = e.UseAdminSponsoredFamilies,
|
||||
UseAutomaticUserConfirmation = e.UseAutomaticUserConfirmation
|
||||
UseAutomaticUserConfirmation = e.UseAutomaticUserConfirmation,
|
||||
UsePhishingBlocker = e.UsePhishingBlocker
|
||||
}).ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,8 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery<OrganizationU
|
||||
LimitItemDeletion = o.LimitItemDeletion,
|
||||
IsAdminInitiated = os.IsAdminInitiated,
|
||||
UseOrganizationDomains = o.UseOrganizationDomains,
|
||||
UseAutomaticUserConfirmation = o.UseAutomaticUserConfirmation
|
||||
UseAutomaticUserConfirmation = o.UseAutomaticUserConfirmation,
|
||||
UsePhishingBlocker = o.UsePhishingBlocker
|
||||
};
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ public class ProviderUserOrganizationDetailsViewQuery : IQuery<ProviderUserOrgan
|
||||
UseAutomaticUserConfirmation = x.o.UseAutomaticUserConfirmation,
|
||||
SsoEnabled = x.ss.Enabled,
|
||||
SsoConfig = x.ss.Data,
|
||||
UsePhishingBlocker = x.o.UsePhishingBlocker
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,9 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using StackExchange.Redis;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using ZiggyCreatures.Caching.Fusion;
|
||||
using NoopRepos = Bit.Core.Repositories.Noop;
|
||||
using Role = Bit.Core.Entities.Role;
|
||||
@@ -342,6 +344,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<IProviderService, NoopProviderService>();
|
||||
services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>();
|
||||
services.AddScoped<ISecretRepository, NoopSecretRepository>();
|
||||
services.AddScoped<ISecretVersionRepository, NoopSecretVersionRepository>();
|
||||
services.AddScoped<IProjectRepository, NoopProjectRepository>();
|
||||
}
|
||||
|
||||
@@ -1067,4 +1070,61 @@ public static class ServiceCollectionExtensions
|
||||
CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Password) &&
|
||||
CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.EventExchangeName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a server with its corresponding OAuth2 client credentials security definition and requirement.
|
||||
/// </summary>
|
||||
/// <param name="config">The SwaggerGen configuration</param>
|
||||
/// <param name="serverId">Unique identifier for this server (e.g., "us-server", "eu-server")</param>
|
||||
/// <param name="serverUrl">The API server URL</param>
|
||||
/// <param name="identityTokenUrl">The identity server token URL</param>
|
||||
/// <param name="serverDescription">Human-readable description for the server</param>
|
||||
public static void AddSwaggerServerWithSecurity(
|
||||
this SwaggerGenOptions config,
|
||||
string serverId,
|
||||
string serverUrl,
|
||||
string identityTokenUrl,
|
||||
string serverDescription)
|
||||
{
|
||||
// Add server
|
||||
config.AddServer(new OpenApiServer
|
||||
{
|
||||
Url = serverUrl,
|
||||
Description = serverDescription
|
||||
});
|
||||
|
||||
// Add security definition
|
||||
config.AddSecurityDefinition(serverId, new OpenApiSecurityScheme
|
||||
{
|
||||
Type = SecuritySchemeType.OAuth2,
|
||||
Description = $"**Use this option if you've selected the {serverDescription}**",
|
||||
Flows = new OpenApiOAuthFlows
|
||||
{
|
||||
ClientCredentials = new OpenApiOAuthFlow
|
||||
{
|
||||
TokenUrl = new Uri(identityTokenUrl),
|
||||
Scopes = new Dictionary<string, string>
|
||||
{
|
||||
{ ApiScopes.ApiOrganization, $"Organization APIs ({serverDescription})" },
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Add security requirement
|
||||
config.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = serverId
|
||||
},
|
||||
},
|
||||
[ApiScopes.ApiOrganization]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,8 @@ CREATE PROCEDURE [dbo].[Organization_Create]
|
||||
@UseOrganizationDomains BIT = 0,
|
||||
@UseAdminSponsoredFamilies BIT = 0,
|
||||
@SyncSeats BIT = 0,
|
||||
@UseAutomaticUserConfirmation BIT = 0
|
||||
@UseAutomaticUserConfirmation BIT = 0,
|
||||
@UsePhishingBlocker BIT = 0
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@@ -126,7 +127,8 @@ BEGIN
|
||||
[UseOrganizationDomains],
|
||||
[UseAdminSponsoredFamilies],
|
||||
[SyncSeats],
|
||||
[UseAutomaticUserConfirmation]
|
||||
[UseAutomaticUserConfirmation],
|
||||
[UsePhishingBlocker]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@@ -190,6 +192,7 @@ BEGIN
|
||||
@UseOrganizationDomains,
|
||||
@UseAdminSponsoredFamilies,
|
||||
@SyncSeats,
|
||||
@UseAutomaticUserConfirmation
|
||||
@UseAutomaticUserConfirmation,
|
||||
@UsePhishingBlocker
|
||||
);
|
||||
END
|
||||
|
||||
@@ -28,7 +28,8 @@ BEGIN
|
||||
[LimitItemDeletion],
|
||||
[UseOrganizationDomains],
|
||||
[UseAdminSponsoredFamilies],
|
||||
[UseAutomaticUserConfirmation]
|
||||
[UseAutomaticUserConfirmation],
|
||||
[UsePhishingBlocker]
|
||||
FROM
|
||||
[dbo].[Organization]
|
||||
END
|
||||
|
||||
@@ -59,7 +59,8 @@ CREATE PROCEDURE [dbo].[Organization_Update]
|
||||
@UseOrganizationDomains BIT = 0,
|
||||
@UseAdminSponsoredFamilies BIT = 0,
|
||||
@SyncSeats BIT = 0,
|
||||
@UseAutomaticUserConfirmation BIT = 0
|
||||
@UseAutomaticUserConfirmation BIT = 0,
|
||||
@UsePhishingBlocker BIT = 0
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@@ -126,7 +127,8 @@ BEGIN
|
||||
[UseOrganizationDomains] = @UseOrganizationDomains,
|
||||
[UseAdminSponsoredFamilies] = @UseAdminSponsoredFamilies,
|
||||
[SyncSeats] = @SyncSeats,
|
||||
[UseAutomaticUserConfirmation] = @UseAutomaticUserConfirmation
|
||||
[UseAutomaticUserConfirmation] = @UseAutomaticUserConfirmation,
|
||||
[UsePhishingBlocker] = @UsePhishingBlocker
|
||||
WHERE
|
||||
[Id] = @Id;
|
||||
END
|
||||
|
||||
@@ -61,6 +61,7 @@ CREATE TABLE [dbo].[Organization] (
|
||||
[SyncSeats] BIT NOT NULL CONSTRAINT [DF_Organization_SyncSeats] DEFAULT (0),
|
||||
[UseAutomaticUserConfirmation] BIT NOT NULL CONSTRAINT [DF_Organization_UseAutomaticUserConfirmation] DEFAULT (0),
|
||||
[MaxStorageGbIncreased] SMALLINT NULL,
|
||||
[UsePhishingBlocker] BIT NOT NULL CONSTRAINT [DF_Organization_UsePhishingBlocker] DEFAULT (0),
|
||||
CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC)
|
||||
);
|
||||
|
||||
|
||||
@@ -55,7 +55,8 @@ SELECT
|
||||
O.[UseAdminSponsoredFamilies],
|
||||
O.[UseOrganizationDomains],
|
||||
OS.[IsAdminInitiated],
|
||||
O.[UseAutomaticUserConfirmation]
|
||||
O.[UseAutomaticUserConfirmation],
|
||||
O.[UsePhishingBlocker]
|
||||
FROM
|
||||
[dbo].[OrganizationUser] OU
|
||||
LEFT JOIN
|
||||
|
||||
@@ -61,6 +61,7 @@ SELECT
|
||||
[UseOrganizationDomains],
|
||||
[UseAdminSponsoredFamilies],
|
||||
[SyncSeats],
|
||||
[UseAutomaticUserConfirmation]
|
||||
[UseAutomaticUserConfirmation],
|
||||
[UsePhishingBlocker]
|
||||
FROM
|
||||
[dbo].[Organization]
|
||||
|
||||
@@ -44,7 +44,8 @@ SELECT
|
||||
O.[UseOrganizationDomains],
|
||||
O.[UseAutomaticUserConfirmation],
|
||||
SS.[Enabled] SsoEnabled,
|
||||
SS.[Data] SsoConfig
|
||||
SS.[Data] SsoConfig,
|
||||
O.[UsePhishingBlocker]
|
||||
FROM
|
||||
[dbo].[ProviderUser] PU
|
||||
INNER JOIN
|
||||
|
||||
Reference in New Issue
Block a user