1
0
mirror of https://github.com/bitwarden/server synced 2026-01-11 13:03:27 +00:00

Merge remote-tracking branch 'origin/master' into ac/ac-1638/disallow-secrets-manager-for-msp-managed-organizations

This commit is contained in:
Thomas Rittson
2023-10-04 14:29:38 +10:00
53 changed files with 7156 additions and 86 deletions

View File

@@ -19,6 +19,7 @@ using Bit.Core.Utilities;
using Bit.Core.Vault.Repositories;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Stripe;
namespace Bit.Admin.Controllers;
@@ -47,6 +48,7 @@ public class OrganizationsController : Controller
private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IStripeSyncService _stripeSyncService;
public OrganizationsController(
IOrganizationService organizationService,
@@ -70,7 +72,8 @@ public class OrganizationsController : Controller
ICurrentContext currentContext,
ISecretRepository secretRepository,
IProjectRepository projectRepository,
IServiceAccountRepository serviceAccountRepository)
IServiceAccountRepository serviceAccountRepository,
IStripeSyncService stripeSyncService)
{
_organizationService = organizationService;
_organizationRepository = organizationRepository;
@@ -94,6 +97,7 @@ public class OrganizationsController : Controller
_secretRepository = secretRepository;
_projectRepository = projectRepository;
_serviceAccountRepository = serviceAccountRepository;
_stripeSyncService = stripeSyncService;
}
[RequirePermission(Permission.Org_List_View)]
@@ -208,6 +212,16 @@ public class OrganizationsController : Controller
throw new BadRequestException("Plan does not support Secrets Manager");
}
try
{
await _stripeSyncService.UpdateCustomerEmailAddress(organization.GatewayCustomerId, organization.BillingEmail);
}
catch (StripeException stripeException)
{
_logger.LogError(stripeException, "Failed to update billing email address in Stripe for Organization with ID '{organizationId}'", organization.Id);
throw;
}
await _organizationRepository.ReplaceAsync(organization);
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.OrganizationEditedByAdmin, organization, _currentContext)
@@ -215,6 +229,7 @@ public class OrganizationsController : Controller
EventRaisedByUser = _userService.GetUserName(User),
SalesAssistedTrialStarted = model.SalesAssistedTrialStarted,
});
return RedirectToAction("Edit", new { id });
}

View File

@@ -277,7 +277,13 @@
}
else
{
<input type="email" class="form-control" asp-for="BillingEmail" readonly='@(!canEditBilling)'>
<input
type="text"
class="form-control"
asp-for="BillingEmail"
readonly='@(!canEditBilling)'
pattern="@(@"[^@\s]+@[^@\s]+\.[^@\s]+")"
title="Email address must be in the format 'address@domain.com'.">
}
</div>
</div>

View File

@@ -116,6 +116,7 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
{
Subscription = subscription.Subscription != null ? new BillingSubscription(subscription.Subscription) : null;
UpcomingInvoice = subscription.UpcomingInvoice != null ? new BillingSubscriptionUpcomingInvoice(subscription.UpcomingInvoice) : null;
Discount = subscription.Discount != null ? new BillingCustomerDiscount(subscription.Discount) : null;
Expiration = DateTime.UtcNow.AddYears(1); // Not used, so just give it a value.
if (hideSensitiveData)
@@ -146,6 +147,7 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
public string StorageName { get; set; }
public double? StorageGb { get; set; }
public BillingCustomerDiscount Discount { get; set; }
public BillingSubscription Subscription { get; set; }
public BillingSubscriptionUpcomingInvoice UpcomingInvoice { get; set; }

View File

@@ -14,6 +14,7 @@ public class SubscriptionResponseModel : ResponseModel
Subscription = subscription.Subscription != null ? new BillingSubscription(subscription.Subscription) : null;
UpcomingInvoice = subscription.UpcomingInvoice != null ?
new BillingSubscriptionUpcomingInvoice(subscription.UpcomingInvoice) : null;
Discount = subscription.Discount != null ? new BillingCustomerDiscount(subscription.Discount) : null;
StorageName = user.Storage.HasValue ? CoreHelpers.ReadableBytesSize(user.Storage.Value) : null;
StorageGb = user.Storage.HasValue ? Math.Round(user.Storage.Value / 1073741824D, 2) : 0; // 1 GB
MaxStorageGb = user.MaxStorageGb;
@@ -41,11 +42,24 @@ public class SubscriptionResponseModel : ResponseModel
public short? MaxStorageGb { get; set; }
public BillingSubscriptionUpcomingInvoice UpcomingInvoice { get; set; }
public BillingSubscription Subscription { get; set; }
public BillingCustomerDiscount Discount { get; set; }
public UserLicense License { get; set; }
public DateTime? Expiration { get; set; }
public bool UsingInAppPurchase { get; set; }
}
public class BillingCustomerDiscount
{
public BillingCustomerDiscount(SubscriptionInfo.BillingCustomerDiscount discount)
{
Id = discount.Id;
Active = discount.Active;
}
public string Id { get; set; }
public bool Active { get; set; }
}
public class BillingSubscription
{
public BillingSubscription(SubscriptionInfo.BillingSubscription sub)

View File

@@ -36,6 +36,7 @@ public class CiphersController : Controller
private readonly ICurrentContext _currentContext;
private readonly ILogger<CiphersController> _logger;
private readonly GlobalSettings _globalSettings;
private readonly Version _cipherKeyEncryptionMinimumVersion = new Version(Constants.CipherKeyEncryptionMinimumVersion);
public CiphersController(
ICipherRepository cipherRepository,
@@ -177,6 +178,8 @@ public class CiphersController : Controller
throw new NotFoundException();
}
ValidateItemLevelEncryptionIsAvailable(cipher);
var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id)).Select(c => c.CollectionId).ToList();
var modelOrgId = string.IsNullOrWhiteSpace(model.OrganizationId) ?
(Guid?)null : new Guid(model.OrganizationId);
@@ -198,6 +201,9 @@ public class CiphersController : Controller
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(id);
ValidateItemLevelEncryptionIsAvailable(cipher);
if (cipher == null || !cipher.OrganizationId.HasValue ||
!await _currentContext.EditAnyCollection(cipher.OrganizationId.Value))
{
@@ -576,6 +582,8 @@ public class CiphersController : Controller
throw new NotFoundException();
}
ValidateItemLevelEncryptionIsAvailable(cipher);
if (request.FileSize > CipherService.MAX_FILE_SIZE)
{
throw new BadRequestException($"Max file size is {CipherService.MAX_FILE_SIZE_READABLE}.");
@@ -795,4 +803,12 @@ public class CiphersController : Controller
throw new BadRequestException("Invalid content.");
}
}
private void ValidateItemLevelEncryptionIsAvailable(Cipher cipher)
{
if (cipher.Key != null && _currentContext.ClientVersion < _cipherKeyEncryptionMinimumVersion)
{
throw new BadRequestException("Cannot edit item. Update to the latest version of Bitwarden and try again.");
}
}
}

View File

@@ -18,6 +18,7 @@ public class CipherRequestModel
public string FolderId { get; set; }
public bool Favorite { get; set; }
public CipherRepromptType Reprompt { get; set; }
public string Key { get; set; }
[Required]
[EncryptedString]
[EncryptedStringLength(1000)]
@@ -86,6 +87,7 @@ public class CipherRequestModel
}
existingCipher.Reprompt = Reprompt;
existingCipher.Key = Key;
var hasAttachments2 = (Attachments2?.Count ?? 0) > 0;
var hasAttachments = (Attachments?.Count ?? 0) > 0;

View File

@@ -63,6 +63,7 @@ public class CipherMiniResponseModel : ResponseModel
CreationDate = cipher.CreationDate;
DeletedDate = cipher.DeletedDate;
Reprompt = cipher.Reprompt.GetValueOrDefault(CipherRepromptType.None);
Key = cipher.Key;
}
public Guid Id { get; set; }
@@ -83,6 +84,7 @@ public class CipherMiniResponseModel : ResponseModel
public DateTime CreationDate { get; set; }
public DateTime? DeletedDate { get; set; }
public CipherRepromptType Reprompt { get; set; }
public string Key { get; set; }
}
public class CipherResponseModel : CipherMiniResponseModel

View File

@@ -19,6 +19,8 @@ public static class Constants
/// their subscription has expired.
/// </summary>
public const int OrganizationSelfHostSubscriptionGracePeriodDays = 60;
public const string CipherKeyEncryptionMinimumVersion = "2023.9.2";
}
public static class TokenPurposes
@@ -37,6 +39,7 @@ public static class FeatureFlagKeys
public const string DisplayLowKdfIterationWarning = "display-kdf-iteration-warning";
public const string TrustedDeviceEncryption = "trusted-device-encryption";
public const string AutofillV2 = "autofill-v2";
public const string BrowserFilelessImport = "browser-fileless-import";
public static List<string> GetAllKeys()
{

View File

@@ -5,10 +5,25 @@ namespace Bit.Core.Models.Business;
public class SubscriptionInfo
{
public BillingCustomerDiscount Discount { get; set; }
public BillingSubscription Subscription { get; set; }
public BillingUpcomingInvoice UpcomingInvoice { get; set; }
public bool UsingInAppPurchase { get; set; }
public class BillingCustomerDiscount
{
public BillingCustomerDiscount() { }
public BillingCustomerDiscount(Discount discount)
{
Id = discount.Id;
Active = discount.Start != null && discount.End == null;
}
public string Id { get; }
public bool Active { get; }
}
public class BillingSubscription
{
public BillingSubscription(Subscription sub)

View File

@@ -1,5 +1,6 @@
using Bit.Core.Context;
using Bit.Core.Settings;
using LaunchDarkly.Logging;
using LaunchDarkly.Sdk.Server;
using LaunchDarkly.Sdk.Server.Integrations;
@@ -14,6 +15,7 @@ public class LaunchDarklyFeatureService : IFeatureService, IDisposable
IGlobalSettings globalSettings)
{
var ldConfig = Configuration.Builder(globalSettings.LaunchDarkly?.SdkKey);
ldConfig.Logging(Components.Logging().Level(LogLevel.Error));
if (string.IsNullOrEmpty(globalSettings.LaunchDarkly?.SdkKey))
{

View File

@@ -1557,10 +1557,19 @@ public class StripePaymentService : IPaymentService
{
var subscriptionInfo = new SubscriptionInfo();
if (subscriber.IsUser() && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
{
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId);
subscriptionInfo.UsingInAppPurchase = customer.Metadata.ContainsKey("appleReceipt");
if (customer.Discount != null)
{
subscriptionInfo.Discount = new SubscriptionInfo.BillingCustomerDiscount(customer.Discount);
}
if (subscriber.IsUser())
{
subscriptionInfo.UsingInAppPurchase = customer.Metadata.ContainsKey("appleReceipt");
}
}
if (!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))

View File

@@ -21,6 +21,7 @@ public class Cipher : ITableObject<Guid>, ICloneable
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
public DateTime? DeletedDate { get; set; }
public Enums.CipherRepromptType? Reprompt { get; set; }
public string Key { get; set; }
public void SetNewId()
{

View File

@@ -392,7 +392,8 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
SET
[Data] = TC.[Data],
[Attachments] = TC.[Attachments],
[RevisionDate] = TC.[RevisionDate]
[RevisionDate] = TC.[RevisionDate],
[Key] = TC.[Key]
FROM
[dbo].[Cipher] C
INNER JOIN
@@ -506,7 +507,8 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
[Data] = TC.[Data],
[Attachments] = TC.[Attachments],
[RevisionDate] = TC.[RevisionDate],
[DeletedDate] = TC.[DeletedDate]
[DeletedDate] = TC.[DeletedDate],
[Key] = TC.[Key]
FROM
[dbo].[Cipher] C
INNER JOIN
@@ -728,6 +730,8 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
ciphersTable.Columns.Add(deletedDateColumn);
var repromptColumn = new DataColumn(nameof(c.Reprompt), typeof(short));
ciphersTable.Columns.Add(repromptColumn);
var keyColummn = new DataColumn(nameof(c.Key), typeof(string));
ciphersTable.Columns.Add(keyColummn);
foreach (DataColumn col in ciphersTable.Columns)
{
@@ -754,6 +758,7 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
row[revisionDateColumn] = cipher.RevisionDate;
row[deletedDateColumn] = cipher.DeletedDate.HasValue ? (object)cipher.DeletedDate : DBNull.Value;
row[repromptColumn] = cipher.Reprompt;
row[keyColummn] = cipher.Key;
ciphersTable.Rows.Add(row);
}

View File

@@ -73,6 +73,7 @@ public class UserCipherDetailsQuery : IQuery<CipherDetails>
Reprompt = c.Reprompt,
ViewPassword = true,
OrganizationUseTotp = false,
Key = c.Key
});
return union;
}

View File

@@ -366,6 +366,7 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
Reprompt = c.Reprompt,
ViewPassword = true,
OrganizationUseTotp = false,
Key = c.Key
};
}
var ciphers = await cipherDetailsView.ToListAsync();
@@ -591,6 +592,7 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
trackedCipher.Attachments = cipher.Attachments;
trackedCipher.RevisionDate = cipher.RevisionDate;
trackedCipher.DeletedDate = cipher.DeletedDate;
trackedCipher.Key = cipher.Key;
await transaction.CommitAsync();

View File

@@ -29,6 +29,7 @@ public class CipherDetailsQuery : IQuery<CipherDetails>
RevisionDate = c.RevisionDate,
DeletedDate = c.DeletedDate,
Reprompt = c.Reprompt,
Key = c.Key,
Favorite = _userId.HasValue && c.Favorites != null && c.Favorites.ToLowerInvariant().Contains($"\"{_userId}\":true"),
FolderId = (_ignoreFolders || !_userId.HasValue || c.Folders == null || !c.Folders.ToLowerInvariant().Contains(_userId.Value.ToString())) ?
null :

View File

@@ -26,6 +26,7 @@ SELECT
ELSE TRY_CONVERT(UNIQUEIDENTIFIER, JSON_VALUE(C.[Folders], CONCAT('$."', @UserId, '"')))
END [FolderId],
C.[DeletedDate],
C.[Reprompt]
C.[Reprompt],
C.[Key]
FROM
[dbo].[Cipher] C

View File

@@ -15,7 +15,8 @@
@ViewPassword BIT, -- not used
@OrganizationUseTotp BIT, -- not used
@DeletedDate DATETIME2(7),
@Reprompt TINYINT
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL
AS
BEGIN
SET NOCOUNT ON
@@ -35,7 +36,8 @@ BEGIN
[CreationDate],
[RevisionDate],
[DeletedDate],
[Reprompt]
[Reprompt],
[Key]
)
VALUES
(
@@ -49,7 +51,8 @@ BEGIN
@CreationDate,
@RevisionDate,
@DeletedDate,
@Reprompt
@Reprompt,
@Key
)
IF @OrganizationId IS NOT NULL

View File

@@ -16,6 +16,7 @@
@OrganizationUseTotp BIT, -- not used
@DeletedDate DATETIME2(7),
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL,
@CollectionIds AS [dbo].[GuidIdArray] READONLY
AS
BEGIN
@@ -23,7 +24,7 @@ BEGIN
EXEC [dbo].[CipherDetails_Create] @Id, @UserId, @OrganizationId, @Type, @Data, @Favorites, @Folders,
@Attachments, @CreationDate, @RevisionDate, @FolderId, @Favorite, @Edit, @ViewPassword,
@OrganizationUseTotp, @DeletedDate, @Reprompt
@OrganizationUseTotp, @DeletedDate, @Reprompt, @Key
DECLARE @UpdateCollectionsSuccess INT
EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds

View File

@@ -6,7 +6,7 @@
@Data NVARCHAR(MAX),
@Favorites NVARCHAR(MAX), -- not used
@Folders NVARCHAR(MAX), -- not used
@Attachments NVARCHAR(MAX), -- not used
@Attachments NVARCHAR(MAX),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@FolderId UNIQUEIDENTIFIER,
@@ -15,7 +15,8 @@
@ViewPassword BIT, -- not used
@OrganizationUseTotp BIT, -- not used
@DeletedDate DATETIME2(2),
@Reprompt TINYINT
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL
AS
BEGIN
SET NOCOUNT ON
@@ -48,10 +49,12 @@ BEGIN
ELSE
JSON_MODIFY([Favorites], @UserIdPath, NULL)
END,
[Attachments] = @Attachments,
[Reprompt] = @Reprompt,
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate,
[DeletedDate] = @DeletedDate
[DeletedDate] = @DeletedDate,
[Key] = @Key
WHERE
[Id] = @Id

View File

@@ -10,7 +10,8 @@
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@DeletedDate DATETIME2(7),
@Reprompt TINYINT
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL
AS
BEGIN
SET NOCOUNT ON
@@ -28,7 +29,8 @@ BEGIN
[CreationDate],
[RevisionDate],
[DeletedDate],
[Reprompt]
[Reprompt],
[Key]
)
VALUES
(
@@ -43,7 +45,8 @@ BEGIN
@CreationDate,
@RevisionDate,
@DeletedDate,
@Reprompt
@Reprompt,
@Key
)
IF @OrganizationId IS NOT NULL

View File

@@ -11,13 +11,14 @@
@RevisionDate DATETIME2(7),
@DeletedDate DATETIME2(7),
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL,
@CollectionIds AS [dbo].[GuidIdArray] READONLY
AS
BEGIN
SET NOCOUNT ON
EXEC [dbo].[Cipher_Create] @Id, @UserId, @OrganizationId, @Type, @Data, @Favorites, @Folders,
@Attachments, @CreationDate, @RevisionDate, @DeletedDate, @Reprompt
@Attachments, @CreationDate, @RevisionDate, @DeletedDate, @Reprompt, @Key
DECLARE @UpdateCollectionsSuccess INT
EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds

View File

@@ -10,7 +10,8 @@
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@DeletedDate DATETIME2(7),
@Reprompt TINYINT
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL
AS
BEGIN
SET NOCOUNT ON
@@ -28,7 +29,8 @@ BEGIN
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate,
[DeletedDate] = @DeletedDate,
[Reprompt] = @Reprompt
[Reprompt] = @Reprompt,
[Key] = @Key
WHERE
[Id] = @Id

View File

@@ -11,6 +11,7 @@
@RevisionDate DATETIME2(7),
@DeletedDate DATETIME2(7),
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL,
@CollectionIds AS [dbo].[GuidIdArray] READONLY
AS
BEGIN
@@ -36,7 +37,8 @@ BEGIN
[Data] = @Data,
[Attachments] = @Attachments,
[RevisionDate] = @RevisionDate,
[DeletedDate] = @DeletedDate
[DeletedDate] = @DeletedDate,
[Key] = @Key
-- No need to update CreationDate, Favorites, Folders, or Type since that data will not change
WHERE
[Id] = @Id

View File

@@ -12,6 +12,7 @@ CREATE TABLE [dbo].[Cipher] (
[RevisionDate] DATETIME2 (7) NOT NULL,
[DeletedDate] DATETIME2 (7) NULL,
[Reprompt] TINYINT NULL,
[Key] VARCHAR(MAX) NULL,
CONSTRAINT [PK_Cipher] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_Cipher_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]),
CONSTRAINT [FK_Cipher_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])