1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-15 22:57:44 -05:00

Merge branch 'master' into feature/flexible-collections

This commit is contained in:
Vincent Salucci
2023-08-30 15:16:01 -05:00
70 changed files with 1252 additions and 466 deletions

View File

@ -21,7 +21,7 @@ public class InfoController : Controller
[HttpGet("~/ip")]
public JsonResult Ip()
{
var headerSet = new HashSet<string> { "x-forwarded-for", "cf-connecting-ip", "client-ip" };
var headerSet = new HashSet<string> { "x-forwarded-for", "x-connecting-ip", "cf-connecting-ip", "client-ip", "true-client-ip" };
var headers = HttpContext.Request?.Headers
.Where(h => headerSet.Contains(h.Key.ToLower()))
.ToDictionary(h => h.Key);

View File

@ -447,8 +447,8 @@ public class OrganizationUsersController : Controller
if (additionalSmSeatsRequired > 0)
{
var organization = await _organizationRepository.GetByIdAsync(orgId);
var update = new SecretsManagerSubscriptionUpdate(organization, true);
update.AdjustSeats(additionalSmSeatsRequired);
var update = new SecretsManagerSubscriptionUpdate(organization, true)
.AdjustSeats(additionalSmSeatsRequired);
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
}

View File

@ -14,11 +14,12 @@ public class SecretsManagerSubscriptionUpdateRequestModel
public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization)
{
var orgUpdate = new SecretsManagerSubscriptionUpdate(
organization,
seatAdjustment: SeatAdjustment, maxAutoscaleSeats: MaxAutoscaleSeats,
serviceAccountAdjustment: ServiceAccountAdjustment, maxAutoscaleServiceAccounts: MaxAutoscaleServiceAccounts);
return orgUpdate;
return new SecretsManagerSubscriptionUpdate(organization, false)
{
MaxAutoscaleSmSeats = MaxAutoscaleSeats,
MaxAutoscaleSmServiceAccounts = MaxAutoscaleServiceAccounts
}
.AdjustSeats(SeatAdjustment)
.AdjustServiceAccounts(ServiceAccountAdjustment);
}
}

View File

@ -7,6 +7,7 @@ using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
@ -22,6 +23,7 @@ public class ProjectsController : Controller
private readonly ICurrentContext _currentContext;
private readonly IUserService _userService;
private readonly IProjectRepository _projectRepository;
private readonly IMaxProjectsQuery _maxProjectsQuery;
private readonly ICreateProjectCommand _createProjectCommand;
private readonly IUpdateProjectCommand _updateProjectCommand;
private readonly IDeleteProjectCommand _deleteProjectCommand;
@ -31,6 +33,7 @@ public class ProjectsController : Controller
ICurrentContext currentContext,
IUserService userService,
IProjectRepository projectRepository,
IMaxProjectsQuery maxProjectsQuery,
ICreateProjectCommand createProjectCommand,
IUpdateProjectCommand updateProjectCommand,
IDeleteProjectCommand deleteProjectCommand,
@ -39,6 +42,7 @@ public class ProjectsController : Controller
_currentContext = currentContext;
_userService = userService;
_projectRepository = projectRepository;
_maxProjectsQuery = maxProjectsQuery;
_createProjectCommand = createProjectCommand;
_updateProjectCommand = updateProjectCommand;
_deleteProjectCommand = deleteProjectCommand;
@ -74,6 +78,13 @@ public class ProjectsController : Controller
{
throw new NotFoundException();
}
var (max, atMax) = await _maxProjectsQuery.GetByOrgIdAsync(organizationId);
if (atMax != null && atMax.Value)
{
throw new BadRequestException($"You have reached the maximum number of projects ({max}) for this plan.");
}
var userId = _userService.GetProperUserId(User).Value;
var result = await _createProjectCommand.CreateAsync(project, userId, _currentContext.ClientType);

View File

@ -207,4 +207,45 @@ public class SecretsController : Controller
var responses = results.Select(r => new BulkDeleteResponseModel(r.Secret.Id, r.Error));
return new ListResponseModel<BulkDeleteResponseModel>(responses);
}
[HttpPost("secrets/get-by-ids")]
public async Task<ListResponseModel<BaseSecretResponseModel>> GetSecretsByIdsAsync(
[FromBody] GetSecretsRequestModel request)
{
var secrets = (await _secretRepository.GetManyByIds(request.Ids)).ToList();
if (!secrets.Any() || secrets.Count != request.Ids.Count())
{
throw new NotFoundException();
}
// Ensure all secrets belong to the same organization.
var organizationId = secrets.First().OrganizationId;
if (secrets.Any(secret => secret.OrganizationId != organizationId) ||
!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
foreach (var secret in secrets)
{
var authorizationResult = await _authorizationService.AuthorizeAsync(User, secret, SecretOperations.Read);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
}
if (_currentContext.ClientType == ClientType.ServiceAccount)
{
var userId = _userService.GetProperUserId(User).Value;
var org = await _organizationRepository.GetByIdAsync(organizationId);
await _eventService.LogServiceAccountSecretsEventAsync(userId, secrets, EventType.Secret_Retrieved);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.SmServiceAccountAccessedSecret, org, _currentContext));
}
var responses = secrets.Select(s => new BaseSecretResponseModel(s));
return new ListResponseModel<BaseSecretResponseModel>(responses);
}
}

View File

@ -4,6 +4,7 @@ using Bit.Api.SecretsManager.Models.Response;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.AuthorizationRequirements;
@ -125,8 +126,9 @@ public class ServiceAccountsController : Controller
if (newServiceAccountSlotsRequired > 0)
{
var org = await _organizationRepository.GetByIdAsync(organizationId);
await _updateSecretsManagerSubscriptionCommand.AdjustServiceAccountsAsync(org,
newServiceAccountSlotsRequired);
var update = new SecretsManagerSubscriptionUpdate(org, true)
.AdjustServiceAccounts(newServiceAccountSlotsRequired);
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
}
var userId = _userService.GetProperUserId(User).Value;

View File

@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.SecretsManager.Models.Request;
public class GetSecretsRequestModel
{
[Required]
public IEnumerable<Guid> Ids { get; set; }
}

View File

@ -13,7 +13,7 @@ public class SecretCreateRequestModel : IValidatableObject
[Required]
[EncryptedString]
[EncryptedStringLength(5000)]
[EncryptedStringLength(35000)]
public string Value { get; set; }
[Required]

View File

@ -13,7 +13,7 @@ public class SecretUpdateRequestModel : IValidatableObject
[Required]
[EncryptedString]
[EncryptedStringLength(5000)]
[EncryptedStringLength(35000)]
public string Value { get; set; }
[Required]

View File

@ -0,0 +1,66 @@
using Bit.Core.Models.Api;
using Bit.Core.SecretsManager.Entities;
namespace Bit.Api.SecretsManager.Models.Response;
public class BaseSecretResponseModel : ResponseModel
{
private const string _objectName = "baseSecret";
public BaseSecretResponseModel(Secret secret, string objectName = _objectName) : base(objectName)
{
if (secret == null)
{
throw new ArgumentNullException(nameof(secret));
}
Id = secret.Id;
OrganizationId = secret.OrganizationId;
Key = secret.Key;
Value = secret.Value;
Note = secret.Note;
CreationDate = secret.CreationDate;
RevisionDate = secret.RevisionDate;
Projects = secret.Projects?.Select(p => new SecretResponseInnerProject(p));
}
public BaseSecretResponseModel(string objectName = _objectName) : base(objectName)
{
}
public BaseSecretResponseModel() : base(_objectName)
{
}
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }
public string Key { get; set; }
public string Value { get; set; }
public string Note { get; set; }
public DateTime CreationDate { get; set; }
public DateTime RevisionDate { get; set; }
public IEnumerable<SecretResponseInnerProject> Projects { get; set; }
public class SecretResponseInnerProject
{
public SecretResponseInnerProject(Project project)
{
Id = project.Id;
Name = project.Name;
}
public SecretResponseInnerProject()
{
}
public Guid Id { get; set; }
public string Name { get; set; }
}
}

View File

@ -1,28 +1,13 @@
using Bit.Core.Models.Api;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Entities;
namespace Bit.Api.SecretsManager.Models.Response;
public class SecretResponseModel : ResponseModel
public class SecretResponseModel : BaseSecretResponseModel
{
private const string _objectName = "secret";
public SecretResponseModel(Secret secret, bool read, bool write) : base(_objectName)
public SecretResponseModel(Secret secret, bool read, bool write) : base(secret, _objectName)
{
if (secret == null)
{
throw new ArgumentNullException(nameof(secret));
}
Id = secret.Id;
OrganizationId = secret.OrganizationId;
Key = secret.Key;
Value = secret.Value;
Note = secret.Note;
CreationDate = secret.CreationDate;
RevisionDate = secret.RevisionDate;
Projects = secret.Projects?.Select(p => new SecretResponseInnerProject(p));
Read = read;
Write = write;
}
@ -31,39 +16,7 @@ public class SecretResponseModel : ResponseModel
{
}
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }
public string Key { get; set; }
public string Value { get; set; }
public string Note { get; set; }
public DateTime CreationDate { get; set; }
public DateTime RevisionDate { get; set; }
public IEnumerable<SecretResponseInnerProject> Projects { get; set; }
public bool Read { get; set; }
public bool Write { get; set; }
public class SecretResponseInnerProject
{
public SecretResponseInnerProject(Project project)
{
Id = project.Id;
Name = project.Name;
}
public SecretResponseInnerProject()
{
}
public Guid Id { get; set; }
public string Name { get; set; }
}
}

View File

@ -79,7 +79,7 @@
"IpRateLimitOptions": {
"EnableEndpointRateLimiting": true,
"StackBlockedRequests": false,
"RealIpHeader": "CF-Connecting-IP",
"RealIpHeader": "X-Connecting-IP",
"ClientIdHeader": "X-ClientId",
"HttpStatusCode": 429,
"IpWhitelist": [],

View File

@ -10,4 +10,5 @@ public static class HandledStripeWebhook
public const string PaymentSucceeded = "invoice.payment_succeeded";
public const string PaymentFailed = "invoice.payment_failed";
public const string InvoiceCreated = "invoice.created";
public const string PaymentMethodAttached = "payment_method.attached";
}

View File

@ -0,0 +1,10 @@
namespace Bit.Billing.Constants;
public static class StripeInvoiceStatus
{
public const string Draft = "draft";
public const string Open = "open";
public const string Paid = "paid";
public const string Void = "void";
public const string Uncollectible = "uncollectible";
}

View File

@ -0,0 +1,13 @@
namespace Bit.Billing.Constants;
public static class StripeSubscriptionStatus
{
public const string Trialing = "trialing";
public const string Active = "active";
public const string Incomplete = "incomplete";
public const string IncompleteExpired = "incomplete_expired";
public const string PastDue = "past_due";
public const string Canceled = "canceled";
public const string Unpaid = "unpaid";
public const string Paused = "paused";
}

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using System.Text;
using System.Web;
using Bit.Billing.Models;
using Bit.Core.Repositories;
using Bit.Core.Settings;
@ -77,7 +78,9 @@ public class FreshdeskController : Controller
foreach (var org in orgs)
{
var orgNote = $"{org.Name} ({org.Seats.GetValueOrDefault()}): " +
// Prevent org names from injecting any additional HTML
var orgName = HttpUtility.HtmlEncode(org.Name);
var orgNote = $"{orgName} ({org.Seats.GetValueOrDefault()}): " +
$"{_globalSettings.BaseServiceUri.Admin}/organizations/edit/{org.Id}";
note += $"<li>Org, {orgNote}</li>";
if (!customFields.Any(kvp => kvp.Key == _billingSettings.FreshDesk.OrgFieldName))

View File

@ -10,12 +10,19 @@ using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Braintree;
using Braintree.Exceptions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Options;
using Stripe;
using Customer = Stripe.Customer;
using Event = Stripe.Event;
using PaymentMethod = Stripe.PaymentMethod;
using Subscription = Stripe.Subscription;
using TaxRate = Bit.Core.Entities.TaxRate;
using Transaction = Bit.Core.Entities.Transaction;
using TransactionType = Bit.Core.Enums.TransactionType;
namespace Bit.Billing.Controllers;
@ -132,10 +139,10 @@ public class StripeController : Controller
var ids = GetIdsFromMetaData(subscription.Metadata);
var organizationId = ids.Item1 ?? Guid.Empty;
var userId = ids.Item2 ?? Guid.Empty;
var subCanceled = subDeleted && subscription.Status == "canceled";
var subUnpaid = subUpdated && subscription.Status == "unpaid";
var subActive = subUpdated && subscription.Status == "active";
var subIncompleteExpired = subUpdated && subscription.Status == "incomplete_expired";
var subCanceled = subDeleted && subscription.Status == StripeSubscriptionStatus.Canceled;
var subUnpaid = subUpdated && subscription.Status == StripeSubscriptionStatus.Unpaid;
var subActive = subUpdated && subscription.Status == StripeSubscriptionStatus.Active;
var subIncompleteExpired = subUpdated && subscription.Status == StripeSubscriptionStatus.IncompleteExpired;
if (subCanceled || subUnpaid || subIncompleteExpired)
{
@ -147,7 +154,17 @@ public class StripeController : Controller
// user
else if (userId != Guid.Empty)
{
await _userService.DisablePremiumAsync(userId, subscription.CurrentPeriodEnd);
if (subUnpaid && subscription.Items.Any(i => i.Price.Id is PremiumPlanId or PremiumPlanIdAppStore))
{
await CancelSubscription(subscription.Id);
await VoidOpenInvoices(subscription.Id);
}
var user = await _userService.GetUserByIdAsync(userId);
if (user.Premium)
{
await _userService.DisablePremiumAsync(userId, subscription.CurrentPeriodEnd);
}
}
}
@ -265,7 +282,7 @@ public class StripeController : Controller
});
foreach (var sub in subscriptions)
{
if (sub.Status != "canceled" && sub.Status != "incomplete_expired")
if (sub.Status != StripeSubscriptionStatus.Canceled && sub.Status != StripeSubscriptionStatus.IncompleteExpired)
{
ids = GetIdsFromMetaData(sub.Metadata);
if (ids.Item1.HasValue || ids.Item2.HasValue)
@ -415,7 +432,7 @@ public class StripeController : Controller
{
var subscriptionService = new SubscriptionService();
var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId);
if (subscription?.Status == "active")
if (subscription?.Status == StripeSubscriptionStatus.Active)
{
if (DateTime.UtcNow - invoice.Created < TimeSpan.FromMinutes(1))
{
@ -472,6 +489,11 @@ public class StripeController : Controller
await AttemptToPayInvoiceAsync(invoice);
}
}
else if (parsedEvent.Type.Equals(HandledStripeWebhook.PaymentMethodAttached))
{
var paymentMethod = await GetPaymentMethodAsync(parsedEvent);
await HandlePaymentMethodAttachedAsync(paymentMethod);
}
else
{
_logger.LogWarning("Unsupported event received. " + parsedEvent.Type);
@ -490,60 +512,163 @@ public class StripeController : Controller
/// <exception cref="Exception"></exception>
private async Task<bool> ValidateCloudRegionAsync(Event parsedEvent)
{
string customerRegion;
var serverRegion = _globalSettings.BaseServiceUri.CloudRegion;
var eventType = parsedEvent.Type;
var expandOptions = new List<string> { "customer" };
switch (eventType)
try
{
case HandledStripeWebhook.SubscriptionDeleted:
case HandledStripeWebhook.SubscriptionUpdated:
{
var subscription = await GetSubscriptionAsync(parsedEvent, true, new List<string> { "customer" });
customerRegion = GetCustomerRegionFromMetadata(subscription.Customer.Metadata);
Dictionary<string, string> customerMetadata;
switch (eventType)
{
case HandledStripeWebhook.SubscriptionDeleted:
case HandledStripeWebhook.SubscriptionUpdated:
customerMetadata = (await GetSubscriptionAsync(parsedEvent, true, expandOptions))?.Customer
?.Metadata;
break;
}
case HandledStripeWebhook.ChargeSucceeded:
case HandledStripeWebhook.ChargeRefunded:
{
var charge = await GetChargeAsync(parsedEvent, true, new List<string> { "customer" });
customerRegion = GetCustomerRegionFromMetadata(charge.Customer.Metadata);
case HandledStripeWebhook.ChargeSucceeded:
case HandledStripeWebhook.ChargeRefunded:
customerMetadata = (await GetChargeAsync(parsedEvent, true, expandOptions))?.Customer?.Metadata;
break;
}
case HandledStripeWebhook.UpcomingInvoice:
var eventInvoice = await GetInvoiceAsync(parsedEvent);
var customer = await GetCustomerAsync(eventInvoice.CustomerId);
customerRegion = GetCustomerRegionFromMetadata(customer.Metadata);
break;
case HandledStripeWebhook.PaymentSucceeded:
case HandledStripeWebhook.PaymentFailed:
case HandledStripeWebhook.InvoiceCreated:
{
var invoice = await GetInvoiceAsync(parsedEvent, true, new List<string> { "customer" });
customerRegion = GetCustomerRegionFromMetadata(invoice.Customer.Metadata);
case HandledStripeWebhook.UpcomingInvoice:
customerMetadata = (await GetInvoiceAsync(parsedEvent))?.Customer?.Metadata;
break;
}
default:
{
// For all Stripe events that we're not listening to, just return 200
return false;
}
}
case HandledStripeWebhook.PaymentSucceeded:
case HandledStripeWebhook.PaymentFailed:
case HandledStripeWebhook.InvoiceCreated:
customerMetadata = (await GetInvoiceAsync(parsedEvent, true, expandOptions))?.Customer?.Metadata;
break;
case HandledStripeWebhook.PaymentMethodAttached:
customerMetadata = (await GetPaymentMethodAsync(parsedEvent, true, expandOptions))
?.Customer
?.Metadata;
break;
default:
customerMetadata = null;
break;
}
return customerRegion == serverRegion;
if (customerMetadata is null)
{
return false;
}
var customerRegion = GetCustomerRegionFromMetadata(customerMetadata);
return customerRegion == serverRegion;
}
catch (Exception e)
{
_logger.LogError(e, "Encountered unexpected error while validating cloud region");
throw;
}
}
/// <summary>
/// Gets the region from the customer metadata. If no region is present, defaults to "US"
/// Gets the customer's region from the metadata.
/// </summary>
/// <param name="customerMetadata"></param>
/// <returns></returns>
private static string GetCustomerRegionFromMetadata(Dictionary<string, string> customerMetadata)
/// <param name="customerMetadata">The metadata of the customer.</param>
/// <returns>The region of the customer. If the region is not specified, it returns "US", if metadata is null,
/// it returns null. It is case insensitive.</returns>
private static string GetCustomerRegionFromMetadata(IDictionary<string, string> customerMetadata)
{
return customerMetadata.TryGetValue("region", out var value)
? value
: "US";
const string defaultRegion = "US";
if (customerMetadata is null)
{
return null;
}
if (customerMetadata.TryGetValue("region", out var value))
{
return value;
}
var miscasedRegionKey = customerMetadata.Keys
.FirstOrDefault(key =>
key.Equals("region", StringComparison.OrdinalIgnoreCase));
if (miscasedRegionKey is null)
{
return defaultRegion;
}
_ = customerMetadata.TryGetValue(miscasedRegionKey, out var regionValue);
return !string.IsNullOrWhiteSpace(regionValue)
? regionValue
: defaultRegion;
}
private async Task HandlePaymentMethodAttachedAsync(PaymentMethod paymentMethod)
{
if (paymentMethod is null)
{
_logger.LogWarning("Attempted to handle the event payment_method.attached but paymentMethod was null");
return;
}
var subscriptionService = new SubscriptionService();
var subscriptionListOptions = new SubscriptionListOptions
{
Customer = paymentMethod.CustomerId,
Status = StripeSubscriptionStatus.Unpaid,
Expand = new List<string> { "data.latest_invoice" }
};
StripeList<Subscription> unpaidSubscriptions;
try
{
unpaidSubscriptions = await subscriptionService.ListAsync(subscriptionListOptions);
}
catch (Exception e)
{
_logger.LogError(e,
"Attempted to get unpaid invoices for customer {CustomerId} but encountered an error while calling Stripe",
paymentMethod.CustomerId);
return;
}
foreach (var unpaidSubscription in unpaidSubscriptions)
{
await AttemptToPayOpenSubscriptionAsync(unpaidSubscription);
}
}
private async Task AttemptToPayOpenSubscriptionAsync(Subscription unpaidSubscription)
{
var latestInvoice = unpaidSubscription.LatestInvoice;
if (unpaidSubscription.LatestInvoice is null)
{
_logger.LogWarning(
"Attempted to pay unpaid subscription {SubscriptionId} but latest invoice didn't exist",
unpaidSubscription.Id);
return;
}
if (latestInvoice.Status != StripeInvoiceStatus.Open)
{
_logger.LogWarning(
"Attempted to pay unpaid subscription {SubscriptionId} but latest invoice wasn't \"open\"",
unpaidSubscription.Id);
return;
}
try
{
await AttemptToPayInvoiceAsync(latestInvoice, true);
}
catch (Exception e)
{
_logger.LogError(e,
"Attempted to pay open invoice {InvoiceId} on unpaid subscription {SubscriptionId} but encountered an error",
latestInvoice.Id, unpaidSubscription.Id);
throw;
}
}
private Tuple<Guid?, Guid?> GetIdsFromMetaData(IDictionary<string, string> metaData)
@ -598,7 +723,7 @@ public class StripeController : Controller
}
}
private async Task<bool> AttemptToPayInvoiceAsync(Invoice invoice)
private async Task<bool> AttemptToPayInvoiceAsync(Invoice invoice, bool attemptToPayWithStripe = false)
{
var customerService = new CustomerService();
var customer = await customerService.GetAsync(invoice.CustomerId);
@ -606,10 +731,17 @@ public class StripeController : Controller
{
return await AttemptToPayInvoiceWithAppleReceiptAsync(invoice, customer);
}
else if (customer?.Metadata?.ContainsKey("btCustomerId") ?? false)
if (customer?.Metadata?.ContainsKey("btCustomerId") ?? false)
{
return await AttemptToPayInvoiceWithBraintreeAsync(invoice, customer);
}
if (attemptToPayWithStripe)
{
return await AttemptToPayInvoiceWithStripeAsync(invoice);
}
return false;
}
@ -708,8 +840,11 @@ public class StripeController : Controller
private async Task<bool> AttemptToPayInvoiceWithBraintreeAsync(Invoice invoice, Customer customer)
{
_logger.LogDebug("Attempting to pay invoice with Braintree");
if (!customer?.Metadata?.ContainsKey("btCustomerId") ?? true)
{
_logger.LogWarning(
"Attempted to pay invoice with Braintree but btCustomerId wasn't on Stripe customer metadata");
return false;
}
@ -718,6 +853,8 @@ public class StripeController : Controller
var ids = GetIdsFromMetaData(subscription?.Metadata);
if (!ids.Item1.HasValue && !ids.Item2.HasValue)
{
_logger.LogWarning(
"Attempted to pay invoice with Braintree but Stripe subscription metadata didn't contain either a organizationId or userId");
return false;
}
@ -740,25 +877,36 @@ public class StripeController : Controller
return false;
}
var transactionResult = await _btGateway.Transaction.SaleAsync(
new Braintree.TransactionRequest
{
Amount = btInvoiceAmount,
CustomerId = customer.Metadata["btCustomerId"],
Options = new Braintree.TransactionOptionsRequest
Result<Braintree.Transaction> transactionResult;
try
{
transactionResult = await _btGateway.Transaction.SaleAsync(
new Braintree.TransactionRequest
{
SubmitForSettlement = true,
PayPal = new Braintree.TransactionOptionsPayPalRequest
Amount = btInvoiceAmount,
CustomerId = customer.Metadata["btCustomerId"],
Options = new Braintree.TransactionOptionsRequest
{
CustomField = $"{btObjIdField}:{btObjId},region:{_globalSettings.BaseServiceUri.CloudRegion}"
SubmitForSettlement = true,
PayPal = new Braintree.TransactionOptionsPayPalRequest
{
CustomField =
$"{btObjIdField}:{btObjId},region:{_globalSettings.BaseServiceUri.CloudRegion}"
}
},
CustomFields = new Dictionary<string, string>
{
[btObjIdField] = btObjId.ToString(),
["region"] = _globalSettings.BaseServiceUri.CloudRegion
}
},
CustomFields = new Dictionary<string, string>
{
[btObjIdField] = btObjId.ToString(),
["region"] = _globalSettings.BaseServiceUri.CloudRegion
}
});
});
}
catch (NotFoundException e)
{
_logger.LogError(e,
"Attempted to make a payment with Braintree, but customer did not exist for the given btCustomerId present on the Stripe metadata");
throw;
}
if (!transactionResult.IsSuccess())
{
@ -802,6 +950,25 @@ public class StripeController : Controller
return true;
}
private async Task<bool> AttemptToPayInvoiceWithStripeAsync(Invoice invoice)
{
try
{
var invoiceService = new InvoiceService();
await invoiceService.PayAsync(invoice.Id);
return true;
}
catch (Exception e)
{
_logger.LogWarning(
e,
"Exception occurred while trying to pay Stripe invoice with Id: {InvoiceId}",
invoice.Id);
throw;
}
}
private bool UnpaidAutoChargeInvoiceForSubscriptionCycle(Invoice invoice)
{
return invoice.AmountDue > 0 && !invoice.Paid && invoice.CollectionMethod == "charge_automatically" &&
@ -886,6 +1053,31 @@ public class StripeController : Controller
return customer;
}
private async Task<PaymentMethod> GetPaymentMethodAsync(Event parsedEvent, bool fresh = false,
List<string> expandOptions = null)
{
if (parsedEvent.Data.Object is not PaymentMethod eventPaymentMethod)
{
throw new Exception("Invoice is null (from parsed event). " + parsedEvent.Id);
}
if (!fresh)
{
return eventPaymentMethod;
}
var paymentMethodService = new PaymentMethodService();
var paymentMethodGetOptions = new PaymentMethodGetOptions { Expand = expandOptions };
var paymentMethod = await paymentMethodService.GetAsync(eventPaymentMethod.Id, paymentMethodGetOptions);
if (paymentMethod == null)
{
throw new Exception($"Payment method is null. {eventPaymentMethod.Id}");
}
return paymentMethod;
}
private async Task<Subscription> VerifyCorrectTaxRateForCharge(Invoice invoice, Subscription subscription)
{
if (!string.IsNullOrWhiteSpace(invoice?.CustomerAddress?.Country) && !string.IsNullOrWhiteSpace(invoice?.CustomerAddress?.PostalCode))
@ -922,12 +1114,8 @@ public class StripeController : Controller
var subscriptionService = new SubscriptionService();
var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId);
// attempt count 4 = 11 days after initial failure
if (invoice.AttemptCount > 3 && subscription.Items.Any(i => i.Price.Id == PremiumPlanId || i.Price.Id == PremiumPlanIdAppStore))
{
await CancelSubscription(invoice.SubscriptionId);
await VoidOpenInvoices(invoice.SubscriptionId);
}
else
if (invoice.AttemptCount <= 3 ||
!subscription.Items.Any(i => i.Price.Id is PremiumPlanId or PremiumPlanIdAppStore))
{
await AttemptToPayInvoiceAsync(invoice);
}
@ -944,7 +1132,7 @@ public class StripeController : Controller
var invoiceService = new InvoiceService();
var options = new InvoiceListOptions
{
Status = "open",
Status = StripeInvoiceStatus.Open,
Subscription = subscriptionId
};
var invoices = invoiceService.List(options);

View File

@ -37,6 +37,7 @@ public static class FeatureFlagKeys
public const string DisplayLowKdfIterationWarning = "display-kdf-iteration-warning";
public const string TrustedDeviceEncryption = "trusted-device-encryption";
public const string SecretsManagerBilling = "sm-ga-billing";
public const string AutofillV2 = "autofill-v2";
public static List<string> GetAllKeys()
{

View File

@ -26,8 +26,8 @@ public class CurrentContext : ICurrentContext
public virtual string DeviceIdentifier { get; set; }
public virtual DeviceType? DeviceType { get; set; }
public virtual string IpAddress { get; set; }
public virtual List<CurrentContentOrganization> Organizations { get; set; }
public virtual List<CurrentContentProvider> Providers { get; set; }
public virtual List<CurrentContextOrganization> Organizations { get; set; }
public virtual List<CurrentContextProvider> Providers { get; set; }
public virtual Guid? InstallationId { get; set; }
public virtual Guid? OrganizationId { get; set; }
public virtual bool CloudflareWorkerProxied { get; set; }
@ -166,17 +166,17 @@ public class CurrentContext : ICurrentContext
return Task.FromResult(0);
}
private List<CurrentContentOrganization> GetOrganizations(Dictionary<string, IEnumerable<Claim>> claimsDict, bool orgApi)
private List<CurrentContextOrganization> GetOrganizations(Dictionary<string, IEnumerable<Claim>> claimsDict, bool orgApi)
{
var accessSecretsManager = claimsDict.ContainsKey(Claims.SecretsManagerAccess)
? claimsDict[Claims.SecretsManagerAccess].ToDictionary(s => s.Value, _ => true)
: new Dictionary<string, bool>();
var organizations = new List<CurrentContentOrganization>();
var organizations = new List<CurrentContextOrganization>();
if (claimsDict.ContainsKey(Claims.OrganizationOwner))
{
organizations.AddRange(claimsDict[Claims.OrganizationOwner].Select(c =>
new CurrentContentOrganization
new CurrentContextOrganization
{
Id = new Guid(c.Value),
Type = OrganizationUserType.Owner,
@ -185,7 +185,7 @@ public class CurrentContext : ICurrentContext
}
else if (orgApi && OrganizationId.HasValue)
{
organizations.Add(new CurrentContentOrganization
organizations.Add(new CurrentContextOrganization
{
Id = OrganizationId.Value,
Type = OrganizationUserType.Owner,
@ -195,7 +195,7 @@ public class CurrentContext : ICurrentContext
if (claimsDict.ContainsKey(Claims.OrganizationAdmin))
{
organizations.AddRange(claimsDict[Claims.OrganizationAdmin].Select(c =>
new CurrentContentOrganization
new CurrentContextOrganization
{
Id = new Guid(c.Value),
Type = OrganizationUserType.Admin,
@ -206,7 +206,7 @@ public class CurrentContext : ICurrentContext
if (claimsDict.ContainsKey(Claims.OrganizationUser))
{
organizations.AddRange(claimsDict[Claims.OrganizationUser].Select(c =>
new CurrentContentOrganization
new CurrentContextOrganization
{
Id = new Guid(c.Value),
Type = OrganizationUserType.User,
@ -217,7 +217,7 @@ public class CurrentContext : ICurrentContext
if (claimsDict.ContainsKey(Claims.OrganizationManager))
{
organizations.AddRange(claimsDict[Claims.OrganizationManager].Select(c =>
new CurrentContentOrganization
new CurrentContextOrganization
{
Id = new Guid(c.Value),
Type = OrganizationUserType.Manager,
@ -228,7 +228,7 @@ public class CurrentContext : ICurrentContext
if (claimsDict.ContainsKey(Claims.OrganizationCustom))
{
organizations.AddRange(claimsDict[Claims.OrganizationCustom].Select(c =>
new CurrentContentOrganization
new CurrentContextOrganization
{
Id = new Guid(c.Value),
Type = OrganizationUserType.Custom,
@ -240,13 +240,13 @@ public class CurrentContext : ICurrentContext
return organizations;
}
private List<CurrentContentProvider> GetProviders(Dictionary<string, IEnumerable<Claim>> claimsDict)
private List<CurrentContextProvider> GetProviders(Dictionary<string, IEnumerable<Claim>> claimsDict)
{
var providers = new List<CurrentContentProvider>();
var providers = new List<CurrentContextProvider>();
if (claimsDict.ContainsKey(Claims.ProviderAdmin))
{
providers.AddRange(claimsDict[Claims.ProviderAdmin].Select(c =>
new CurrentContentProvider
new CurrentContextProvider
{
Id = new Guid(c.Value),
Type = ProviderUserType.ProviderAdmin
@ -256,7 +256,7 @@ public class CurrentContext : ICurrentContext
if (claimsDict.ContainsKey(Claims.ProviderServiceUser))
{
providers.AddRange(claimsDict[Claims.ProviderServiceUser].Select(c =>
new CurrentContentProvider
new CurrentContextProvider
{
Id = new Guid(c.Value),
Type = ProviderUserType.ServiceUser
@ -483,26 +483,26 @@ public class CurrentContext : ICurrentContext
return Organizations?.Any(o => o.Id == orgId && o.AccessSecretsManager) ?? false;
}
public async Task<ICollection<CurrentContentOrganization>> OrganizationMembershipAsync(
public async Task<ICollection<CurrentContextOrganization>> OrganizationMembershipAsync(
IOrganizationUserRepository organizationUserRepository, Guid userId)
{
if (Organizations == null)
{
var userOrgs = await organizationUserRepository.GetManyDetailsByUserAsync(userId);
Organizations = userOrgs.Where(ou => ou.Status == OrganizationUserStatusType.Confirmed)
.Select(ou => new CurrentContentOrganization(ou)).ToList();
.Select(ou => new CurrentContextOrganization(ou)).ToList();
}
return Organizations;
}
public async Task<ICollection<CurrentContentProvider>> ProviderMembershipAsync(
public async Task<ICollection<CurrentContextProvider>> ProviderMembershipAsync(
IProviderUserRepository providerUserRepository, Guid userId)
{
if (Providers == null)
{
var userProviders = await providerUserRepository.GetManyByUserAsync(userId);
Providers = userProviders.Where(ou => ou.Status == ProviderUserStatusType.Confirmed)
.Select(ou => new CurrentContentProvider(ou)).ToList();
.Select(ou => new CurrentContextProvider(ou)).ToList();
}
return Providers;
}

View File

@ -5,11 +5,11 @@ using Bit.Core.Utilities;
namespace Bit.Core.Context;
public class CurrentContentOrganization
public class CurrentContextOrganization
{
public CurrentContentOrganization() { }
public CurrentContextOrganization() { }
public CurrentContentOrganization(OrganizationUserOrganizationDetails orgUser)
public CurrentContextOrganization(OrganizationUserOrganizationDetails orgUser)
{
Id = orgUser.OrganizationId;
Type = orgUser.Type;

View File

@ -5,11 +5,11 @@ using Bit.Core.Utilities;
namespace Bit.Core.Context;
public class CurrentContentProvider
public class CurrentContextProvider
{
public CurrentContentProvider() { }
public CurrentContextProvider() { }
public CurrentContentProvider(ProviderUser providerUser)
public CurrentContextProvider(ProviderUser providerUser)
{
Id = providerUser.ProviderId;
Type = providerUser.Type;

View File

@ -16,7 +16,7 @@ public interface ICurrentContext
string DeviceIdentifier { get; set; }
DeviceType? DeviceType { get; set; }
string IpAddress { get; set; }
List<CurrentContentOrganization> Organizations { get; set; }
List<CurrentContextOrganization> Organizations { get; set; }
Guid? InstallationId { get; set; }
Guid? OrganizationId { get; set; }
ClientType ClientType { get; set; }
@ -64,10 +64,10 @@ public interface ICurrentContext
bool AccessProviderOrganizations(Guid providerId);
bool ManageProviderOrganizations(Guid providerId);
Task<ICollection<CurrentContentOrganization>> OrganizationMembershipAsync(
Task<ICollection<CurrentContextOrganization>> OrganizationMembershipAsync(
IOrganizationUserRepository organizationUserRepository, Guid userId);
Task<ICollection<CurrentContentProvider>> ProviderMembershipAsync(
Task<ICollection<CurrentContextProvider>> ProviderMembershipAsync(
IProviderUserRepository providerUserRepository, Guid userId);
Task<Guid?> ProviderIdForOrg(Guid orgId);

View File

@ -6,7 +6,7 @@ namespace Bit.Core.Models.Business;
public class SecretsManagerSubscriptionUpdate
{
public Organization Organization { get; set; }
public Organization Organization { get; }
/// <summary>
/// The total seats the organization will have after the update, including any base seats included in the plan
@ -14,8 +14,7 @@ public class SecretsManagerSubscriptionUpdate
public int? SmSeats { get; set; }
/// <summary>
/// The new autoscale limit for seats, expressed as a total (not an adjustment).
/// This may or may not be the same as the current autoscale limit.
/// The new autoscale limit for seats after the update
/// </summary>
public int? MaxAutoscaleSmSeats { get; set; }
@ -26,8 +25,7 @@ public class SecretsManagerSubscriptionUpdate
public int? SmServiceAccounts { get; set; }
/// <summary>
/// The new autoscale limit for service accounts, expressed as a total (not an adjustment).
/// This may or may not be the same as the current autoscale limit.
/// The new autoscale limit for service accounts after the update
/// </summary>
public int? MaxAutoscaleSmServiceAccounts { get; set; }
@ -39,7 +37,7 @@ public class SecretsManagerSubscriptionUpdate
/// <summary>
/// Whether the subscription update is a result of autoscaling
/// </summary>
public bool Autoscaling { get; init; }
public bool Autoscaling { get; }
/// <summary>
/// The seats the organization will have after the update, excluding the base seats included in the plan
@ -57,18 +55,11 @@ public class SecretsManagerSubscriptionUpdate
public bool MaxAutoscaleSmServiceAccountsChanged =>
MaxAutoscaleSmServiceAccounts != Organization.MaxAutoscaleSmServiceAccounts;
public Plan Plan => Utilities.StaticStore.GetSecretsManagerPlan(Organization.PlanType);
public bool SmSeatAutoscaleLimitReached => SmSeats.HasValue && MaxAutoscaleSmSeats.HasValue && SmSeats == MaxAutoscaleSmSeats;
public SecretsManagerSubscriptionUpdate(
Organization organization,
int seatAdjustment, int? maxAutoscaleSeats,
int serviceAccountAdjustment, int? maxAutoscaleServiceAccounts) : this(organization, false)
{
AdjustSeats(seatAdjustment);
AdjustServiceAccounts(serviceAccountAdjustment);
MaxAutoscaleSmSeats = maxAutoscaleSeats;
MaxAutoscaleSmServiceAccounts = maxAutoscaleServiceAccounts;
}
public bool SmServiceAccountAutoscaleLimitReached => SmServiceAccounts.HasValue &&
MaxAutoscaleSmServiceAccounts.HasValue &&
SmServiceAccounts == MaxAutoscaleSmServiceAccounts;
public SecretsManagerSubscriptionUpdate(Organization organization, bool autoscaling)
{
@ -91,13 +82,15 @@ public class SecretsManagerSubscriptionUpdate
Autoscaling = autoscaling;
}
public void AdjustSeats(int adjustment)
public SecretsManagerSubscriptionUpdate AdjustSeats(int adjustment)
{
SmSeats = SmSeats.GetValueOrDefault() + adjustment;
return this;
}
public void AdjustServiceAccounts(int adjustment)
public SecretsManagerSubscriptionUpdate AdjustServiceAccounts(int adjustment)
{
SmServiceAccounts = SmServiceAccounts.GetValueOrDefault() + adjustment;
return this;
}
}

View File

@ -20,6 +20,7 @@ public class OrganizationAbility
UseScim = organization.UseScim;
UseResetPassword = organization.UseResetPassword;
UseCustomPermissions = organization.UseCustomPermissions;
UsePolicies = organization.UsePolicies;
}
public Guid Id { get; set; }
@ -33,4 +34,5 @@ public class OrganizationAbility
public bool UseScim { get; set; }
public bool UseResetPassword { get; set; }
public bool UseCustomPermissions { get; set; }
public bool UsePolicies { get; set; }
}

View File

@ -62,6 +62,17 @@ public class AddSecretsManagerSubscriptionCommand : IAddSecretsManagerSubscripti
throw new NotFoundException();
}
if (organization.SecretsManagerBeta)
{
throw new BadRequestException("Organization is enrolled in Secrets Manager Beta. " +
"Please contact Customer Success to add Secrets Manager to your subscription.");
}
if (organization.UseSecretsManager)
{
throw new BadRequestException("Organization already uses Secrets Manager.");
}
var plan = StaticStore.GetSecretsManagerPlan(organization.PlanType);
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId) && plan.Product != ProductType.Free)
{

View File

@ -1,11 +1,9 @@
using Bit.Core.Entities;
using Bit.Core.Models.Business;
using Bit.Core.Models.Business;
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
public interface IUpdateSecretsManagerSubscriptionCommand
{
Task UpdateSubscriptionAsync(SecretsManagerSubscriptionUpdate update);
Task AdjustServiceAccountsAsync(Organization organization, int smServiceAccountsAdjustment);
Task ValidateUpdate(SecretsManagerSubscriptionUpdate update);
}

View File

@ -2,13 +2,11 @@
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.Extensions.Logging;
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
@ -51,82 +49,46 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
{
await ValidateUpdate(update);
await FinalizeSubscriptionAdjustmentAsync(update.Organization, update.Plan, update);
await FinalizeSubscriptionAdjustmentAsync(update);
await SendEmailIfAutoscaleLimitReached(update.Organization);
}
public async Task AdjustServiceAccountsAsync(Organization organization, int smServiceAccountsAdjustment)
{
var update = new SecretsManagerSubscriptionUpdate(
organization, seatAdjustment: 0, maxAutoscaleSeats: organization?.MaxAutoscaleSmSeats,
serviceAccountAdjustment: smServiceAccountsAdjustment, maxAutoscaleServiceAccounts: organization?.MaxAutoscaleSmServiceAccounts)
if (update.SmSeatAutoscaleLimitReached)
{
Autoscaling = true
};
await SendSeatLimitEmailAsync(update.Organization);
}
await UpdateSubscriptionAsync(update);
if (update.SmServiceAccountAutoscaleLimitReached)
{
await SendServiceAccountLimitEmailAsync(update.Organization);
}
}
private async Task FinalizeSubscriptionAdjustmentAsync(Organization organization,
Plan plan, SecretsManagerSubscriptionUpdate update)
private async Task FinalizeSubscriptionAdjustmentAsync(SecretsManagerSubscriptionUpdate update)
{
if (update.SmSeatsChanged)
{
await ProcessChargesAndRaiseEventsForAdjustSeatsAsync(organization, plan, update);
organization.SmSeats = update.SmSeats;
await _paymentService.AdjustSeatsAsync(update.Organization, update.Plan, update.SmSeatsExcludingBase, update.ProrationDate);
// TODO: call ReferenceEventService - see AC-1481
}
if (update.SmServiceAccountsChanged)
{
await ProcessChargesAndRaiseEventsForAdjustServiceAccountsAsync(organization, plan, update);
organization.SmServiceAccounts = update.SmServiceAccounts;
await _paymentService.AdjustServiceAccountsAsync(update.Organization, update.Plan,
update.SmServiceAccountsExcludingBase, update.ProrationDate);
// TODO: call ReferenceEventService - see AC-1481
}
if (update.MaxAutoscaleSmSeatsChanged)
{
organization.MaxAutoscaleSmSeats = update.MaxAutoscaleSmSeats;
}
if (update.MaxAutoscaleSmServiceAccountsChanged)
{
organization.MaxAutoscaleSmServiceAccounts = update.MaxAutoscaleSmServiceAccounts;
}
var organization = update.Organization;
organization.SmSeats = update.SmSeats;
organization.SmServiceAccounts = update.SmServiceAccounts;
organization.MaxAutoscaleSmSeats = update.MaxAutoscaleSmSeats;
organization.MaxAutoscaleSmServiceAccounts = update.MaxAutoscaleSmServiceAccounts;
await ReplaceAndUpdateCacheAsync(organization);
}
private async Task ProcessChargesAndRaiseEventsForAdjustSeatsAsync(Organization organization, Plan plan,
SecretsManagerSubscriptionUpdate update)
{
await _paymentService.AdjustSeatsAsync(organization, plan, update.SmSeatsExcludingBase, update.ProrationDate);
// TODO: call ReferenceEventService - see AC-1481
}
private async Task ProcessChargesAndRaiseEventsForAdjustServiceAccountsAsync(Organization organization, Plan plan,
SecretsManagerSubscriptionUpdate update)
{
await _paymentService.AdjustServiceAccountsAsync(organization, plan,
update.SmServiceAccountsExcludingBase, update.ProrationDate);
// TODO: call ReferenceEventService - see AC-1481
}
private async Task SendEmailIfAutoscaleLimitReached(Organization organization)
{
if (organization.SmSeats.HasValue && organization.MaxAutoscaleSmSeats.HasValue && organization.SmSeats == organization.MaxAutoscaleSmSeats)
{
await SendSeatLimitEmailAsync(organization, organization.MaxAutoscaleSmSeats.Value);
}
if (organization.SmServiceAccounts.HasValue && organization.MaxAutoscaleSmServiceAccounts.HasValue && organization.SmServiceAccounts == organization.MaxAutoscaleSmServiceAccounts)
{
await SendServiceAccountLimitEmailAsync(organization, organization.MaxAutoscaleSmServiceAccounts.Value);
}
}
private async Task SendSeatLimitEmailAsync(Organization organization, int MaxAutoscaleValue)
private async Task SendSeatLimitEmailAsync(Organization organization)
{
try
{
@ -134,16 +96,16 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
OrganizationUserType.Owner))
.Select(u => u.Email).Distinct();
await _mailService.SendSecretsManagerMaxSeatLimitReachedEmailAsync(organization, MaxAutoscaleValue, ownerEmails);
await _mailService.SendSecretsManagerMaxSeatLimitReachedEmailAsync(organization, organization.MaxAutoscaleSmSeats.Value, ownerEmails);
}
catch (Exception e)
{
_logger.LogError(e, $"Error encountered notifying organization owners of Seats limit reached.");
_logger.LogError(e, $"Error encountered notifying organization owners of seats limit reached.");
}
}
private async Task SendServiceAccountLimitEmailAsync(Organization organization, int MaxAutoscaleValue)
private async Task SendServiceAccountLimitEmailAsync(Organization organization)
{
try
{
@ -151,12 +113,12 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
OrganizationUserType.Owner))
.Select(u => u.Email).Distinct();
await _mailService.SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(organization, MaxAutoscaleValue, ownerEmails);
await _mailService.SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(organization, organization.MaxAutoscaleSmServiceAccounts.Value, ownerEmails);
}
catch (Exception e)
{
_logger.LogError(e, $"Error encountered notifying organization owners of Service Accounts limit reached.");
_logger.LogError(e, $"Error encountered notifying organization owners of service accounts limit reached.");
}
}
@ -171,46 +133,45 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
throw new BadRequestException(message);
}
var organization = update.Organization;
ValidateOrganization(organization);
var plan = GetPlanForOrganization(organization);
ValidateOrganization(update);
if (update.SmSeatsChanged)
{
await ValidateSmSeatsUpdateAsync(organization, update, plan);
await ValidateSmSeatsUpdateAsync(update);
}
if (update.SmServiceAccountsChanged)
{
await ValidateSmServiceAccountsUpdateAsync(organization, update, plan);
await ValidateSmServiceAccountsUpdateAsync(update);
}
if (update.MaxAutoscaleSmSeatsChanged)
{
ValidateMaxAutoscaleSmSeatsUpdateAsync(organization, update.MaxAutoscaleSmSeats, plan);
ValidateMaxAutoscaleSmSeatsUpdateAsync(update);
}
if (update.MaxAutoscaleSmServiceAccountsChanged)
{
ValidateMaxAutoscaleSmServiceAccountUpdate(organization, update.MaxAutoscaleSmServiceAccounts, plan);
ValidateMaxAutoscaleSmServiceAccountUpdate(update);
}
}
private void ValidateOrganization(Organization organization)
private void ValidateOrganization(SecretsManagerSubscriptionUpdate update)
{
if (organization == null)
{
throw new NotFoundException("Organization is not found.");
}
var organization = update.Organization;
if (!organization.UseSecretsManager)
{
throw new BadRequestException("Organization has no access to Secrets Manager.");
}
var plan = GetPlanForOrganization(organization);
if (plan.Product == ProductType.Free)
if (organization.SecretsManagerBeta)
{
throw new BadRequestException("Organization is enrolled in Secrets Manager Beta. " +
"Please contact Customer Success to add Secrets Manager to your subscription.");
}
if (update.Plan.Product == ProductType.Free)
{
// No need to check the organization is set up with Stripe
return;
@ -227,18 +188,11 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
}
}
private Plan GetPlanForOrganization(Organization organization)
private async Task ValidateSmSeatsUpdateAsync(SecretsManagerSubscriptionUpdate update)
{
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(p => p.Type == organization.PlanType);
if (plan == null)
{
throw new BadRequestException("Existing plan not found.");
}
return plan;
}
var organization = update.Organization;
var plan = update.Plan;
private async Task ValidateSmSeatsUpdateAsync(Organization organization, SecretsManagerSubscriptionUpdate update, Plan plan)
{
// Check if the organization has unlimited seats
if (organization.SmSeats == null)
{
@ -282,21 +236,24 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
// Check minimum seats currently in use by the organization
if (organization.SmSeats.Value > update.SmSeats.Value)
{
var currentSeats = await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id);
if (currentSeats > update.SmSeats.Value)
var occupiedSeats = await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id);
if (occupiedSeats > update.SmSeats.Value)
{
throw new BadRequestException($"Your organization currently has {currentSeats} Secrets Manager seats. " +
$"Your plan only allows {update.SmSeats} Secrets Manager seats. Remove some Secrets Manager users.");
throw new BadRequestException($"{occupiedSeats} users are currently occupying Secrets Manager seats. " +
"You cannot decrease your subscription below your current occupied seat count.");
}
}
}
private async Task ValidateSmServiceAccountsUpdateAsync(Organization organization, SecretsManagerSubscriptionUpdate update, Plan plan)
private async Task ValidateSmServiceAccountsUpdateAsync(SecretsManagerSubscriptionUpdate update)
{
var organization = update.Organization;
var plan = update.Plan;
// Check if the organization has unlimited service accounts
if (organization.SmServiceAccounts == null)
{
throw new BadRequestException("Organization has no Service Accounts limit, no need to adjust Service Accounts");
throw new BadRequestException("Organization has no service accounts limit, no need to adjust service accounts");
}
if (update.Autoscaling && update.SmServiceAccounts.Value < organization.SmServiceAccounts.Value)
@ -326,13 +283,13 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
// Check minimum service accounts included with plan
if (plan.BaseServiceAccount.HasValue && plan.BaseServiceAccount.Value > update.SmServiceAccounts.Value)
{
throw new BadRequestException($"Plan has a minimum of {plan.BaseServiceAccount} Service Accounts.");
throw new BadRequestException($"Plan has a minimum of {plan.BaseServiceAccount} service accounts.");
}
// Check minimum service accounts required by business logic
if (update.SmServiceAccounts.Value <= 0)
{
throw new BadRequestException("You must have at least 1 Service Account.");
throw new BadRequestException("You must have at least 1 service account.");
}
// Check minimum service accounts currently in use by the organization
@ -341,30 +298,32 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
var currentServiceAccounts = await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organization.Id);
if (currentServiceAccounts > update.SmServiceAccounts)
{
throw new BadRequestException($"Your organization currently has {currentServiceAccounts} Service Accounts. " +
$"Your plan only allows {update.SmServiceAccounts} Service Accounts. Remove some Service Accounts.");
throw new BadRequestException($"Your organization currently has {currentServiceAccounts} service accounts. " +
$"You cannot decrease your subscription below your current service account usage.");
}
}
}
private void ValidateMaxAutoscaleSmSeatsUpdateAsync(Organization organization, int? maxAutoscaleSeats, Plan plan)
private void ValidateMaxAutoscaleSmSeatsUpdateAsync(SecretsManagerSubscriptionUpdate update)
{
if (!maxAutoscaleSeats.HasValue)
var plan = update.Plan;
if (!update.MaxAutoscaleSmSeats.HasValue)
{
// autoscale limit has been turned off, no validation required
return;
}
if (organization.SmSeats.HasValue && maxAutoscaleSeats.Value < organization.SmSeats.Value)
if (update.SmSeats.HasValue && update.MaxAutoscaleSmSeats.Value < update.SmSeats.Value)
{
throw new BadRequestException($"Cannot set max Secrets Manager seat autoscaling below current Secrets Manager seat count.");
}
if (plan.MaxUsers.HasValue && maxAutoscaleSeats.Value > plan.MaxUsers)
if (plan.MaxUsers.HasValue && update.MaxAutoscaleSmSeats.Value > plan.MaxUsers)
{
throw new BadRequestException(string.Concat(
$"Your plan has a Secrets Manager seat limit of {plan.MaxUsers}, ",
$"but you have specified a max autoscale count of {maxAutoscaleSeats}.",
$"but you have specified a max autoscale count of {update.MaxAutoscaleSmSeats}.",
"Reduce your max autoscale count."));
}
@ -374,30 +333,32 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
}
}
private void ValidateMaxAutoscaleSmServiceAccountUpdate(Organization organization, int? maxAutoscaleServiceAccounts, Plan plan)
private void ValidateMaxAutoscaleSmServiceAccountUpdate(SecretsManagerSubscriptionUpdate update)
{
if (!maxAutoscaleServiceAccounts.HasValue)
var plan = update.Plan;
if (!update.MaxAutoscaleSmServiceAccounts.HasValue)
{
// autoscale limit has been turned off, no validation required
return;
}
if (organization.SmServiceAccounts.HasValue && maxAutoscaleServiceAccounts.Value < organization.SmServiceAccounts.Value)
if (update.SmServiceAccounts.HasValue && update.MaxAutoscaleSmServiceAccounts.Value < update.SmServiceAccounts.Value)
{
throw new BadRequestException(
$"Cannot set max Service Accounts autoscaling below current Service Accounts count.");
$"Cannot set max service accounts autoscaling below current service accounts count.");
}
if (!plan.AllowServiceAccountsAutoscale)
{
throw new BadRequestException("Your plan does not allow Service Accounts autoscaling.");
throw new BadRequestException("Your plan does not allow service accounts autoscaling.");
}
if (plan.MaxServiceAccounts.HasValue && maxAutoscaleServiceAccounts.Value > plan.MaxServiceAccounts)
if (plan.MaxServiceAccounts.HasValue && update.MaxAutoscaleSmServiceAccounts.Value > plan.MaxServiceAccounts)
{
throw new BadRequestException(string.Concat(
$"Your plan has a Service Accounts limit of {plan.MaxServiceAccounts}, ",
$"but you have specified a max autoscale count of {maxAutoscaleServiceAccounts}.",
$"Your plan has a service account limit of {plan.MaxServiceAccounts}, ",
$"but you have specified a max autoscale count of {update.MaxAutoscaleSmServiceAccounts}.",
"Reduce your max autoscale count."));
}
}

View File

@ -9,6 +9,7 @@ public class SecretOperationRequirement : OperationAuthorizationRequirement
public static class SecretOperations
{
public static readonly SecretOperationRequirement Create = new() { Name = nameof(Create) };
public static readonly SecretOperationRequirement Read = new() { Name = nameof(Read) };
public static readonly SecretOperationRequirement Update = new() { Name = nameof(Update) };
public static readonly SecretOperationRequirement Delete = new() { Name = nameof(Delete) };
}

View File

@ -4,8 +4,6 @@ namespace Bit.Core.SecretsManager.Models.Data;
public class ApiKeyDetails : ApiKey
{
public string ClientSecret { get; set; } // Deprecated as of 2023-05-17
protected ApiKeyDetails() { }
protected ApiKeyDetails(ApiKey apiKey)

View File

@ -0,0 +1,6 @@
namespace Bit.Core.SecretsManager.Queries.Projects.Interfaces;
public interface IMaxProjectsQuery
{
Task<(short? max, bool? atMax)> GetByOrgIdAsync(Guid organizationId);
}

View File

@ -29,4 +29,5 @@ public interface IEventService
Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type, DateTime? date = null);
Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type, EventSystemUser systemUser, DateTime? date = null);
Task LogServiceAccountSecretEventAsync(Guid serviceAccountId, Secret secret, EventType type, DateTime? date = null);
Task LogServiceAccountSecretsEventAsync(Guid serviceAccountId, IEnumerable<Secret> secrets, EventType type, DateTime? date = null);
}

View File

@ -406,22 +406,34 @@ public class EventService : IEventService
}
public async Task LogServiceAccountSecretEventAsync(Guid serviceAccountId, Secret secret, EventType type, DateTime? date = null)
{
await LogServiceAccountSecretsEventAsync(serviceAccountId, new[] { secret }, type, date);
}
public async Task LogServiceAccountSecretsEventAsync(Guid serviceAccountId, IEnumerable<Secret> secrets, EventType type, DateTime? date = null)
{
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
if (!CanUseEvents(orgAbilities, secret.OrganizationId))
var eventMessages = new List<IEvent>();
foreach (var secret in secrets)
{
return;
if (!CanUseEvents(orgAbilities, secret.OrganizationId))
{
continue;
}
var e = new EventMessage(_currentContext)
{
OrganizationId = secret.OrganizationId,
Type = type,
SecretId = secret.Id,
ServiceAccountId = serviceAccountId,
Date = date.GetValueOrDefault(DateTime.UtcNow)
};
eventMessages.Add(e);
}
var e = new EventMessage(_currentContext)
{
OrganizationId = secret.OrganizationId,
Type = type,
SecretId = secret.Id,
ServiceAccountId = serviceAccountId,
Date = date.GetValueOrDefault(DateTime.UtcNow)
};
await _eventWriteService.CreateAsync(e);
await _eventWriteService.CreateManyAsync(eventMessages);
}
private async Task<Guid?> GetProviderIdAsync(Guid? orgId)

View File

@ -861,8 +861,8 @@ public class OrganizationService : IOrganizationService
var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organization.Id, inviteWithSmAccessCount);
if (additionalSmSeatsRequired > 0)
{
smSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, true);
smSubscriptionUpdate.AdjustSeats(additionalSmSeatsRequired);
smSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, true)
.AdjustSeats(additionalSmSeatsRequired);
await _updateSecretsManagerSubscriptionCommand.ValidateUpdate(smSubscriptionUpdate);
}
@ -1418,8 +1418,8 @@ public class OrganizationService : IOrganizationService
if (additionalSmSeatsRequired > 0)
{
var organization = await _organizationRepository.GetByIdAsync(user.OrganizationId);
var update = new SecretsManagerSubscriptionUpdate(organization, true);
update.AdjustSeats(additionalSmSeatsRequired);
var update = new SecretsManagerSubscriptionUpdate(organization, true)
.AdjustSeats(additionalSmSeatsRequired);
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
}
}

View File

@ -12,6 +12,7 @@ namespace Bit.Core.Services;
public class PolicyService : IPolicyService
{
private readonly IApplicationCacheService _applicationCacheService;
private readonly IEventService _eventService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
@ -21,6 +22,7 @@ public class PolicyService : IPolicyService
private readonly GlobalSettings _globalSettings;
public PolicyService(
IApplicationCacheService applicationCacheService,
IEventService eventService,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
@ -29,6 +31,7 @@ public class PolicyService : IPolicyService
IMailService mailService,
GlobalSettings globalSettings)
{
_applicationCacheService = applicationCacheService;
_eventService = eventService;
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@ -199,7 +202,9 @@ public class PolicyService : IPolicyService
{
var organizationUserPolicyDetails = await _organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(userId, policyType);
var excludedUserTypes = GetUserTypesExcludedFromPolicy(policyType);
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
return organizationUserPolicyDetails.Where(o =>
(!orgAbilities.ContainsKey(o.OrganizationId) || orgAbilities[o.OrganizationId].UsePolicies) &&
o.PolicyEnabled &&
!excludedUserTypes.Contains(o.OrganizationUserType) &&
o.OrganizationUserStatus >= minStatus &&

View File

@ -119,4 +119,10 @@ public class NoopEventService : IEventService
{
return Task.FromResult(0);
}
public Task LogServiceAccountSecretsEventAsync(Guid serviceAccountId, IEnumerable<Secret> secrets, EventType type,
DateTime? date = null)
{
return Task.FromResult(0);
}
}

View File

@ -29,7 +29,7 @@ public static class CoreHelpers
private static readonly DateTime _epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static readonly DateTime _max = new DateTime(9999, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static readonly Random _random = new Random();
private static readonly string CloudFlareConnectingIp = "CF-Connecting-IP";
private static readonly string RealConnectingIp = "X-Connecting-IP";
/// <summary>
/// Generate sequential Guid for Sql Server.
@ -50,20 +50,20 @@ public static class CoreHelpers
{
var guidArray = startingGuid.ToByteArray();
// Get the days and milliseconds which will be used to build the byte string
// Get the days and milliseconds which will be used to build the byte string
var days = new TimeSpan(time.Ticks - _baseDateTicks);
var msecs = time.TimeOfDay;
// Convert to a byte array
// Note that SQL Server is accurate to 1/300th of a millisecond so we divide by 3.333333
// Convert to a byte array
// Note that SQL Server is accurate to 1/300th of a millisecond so we divide by 3.333333
var daysArray = BitConverter.GetBytes(days.Days);
var msecsArray = BitConverter.GetBytes((long)(msecs.TotalMilliseconds / 3.333333));
// Reverse the bytes to match SQL Servers ordering
// Reverse the bytes to match SQL Servers ordering
Array.Reverse(daysArray);
Array.Reverse(msecsArray);
// Copy the bytes into the guid
// Copy the bytes into the guid
Array.Copy(daysArray, daysArray.Length - 2, guidArray, guidArray.Length - 6, 2);
Array.Copy(msecsArray, msecsArray.Length - 4, guidArray, guidArray.Length - 4, 4);
@ -557,9 +557,9 @@ public static class CoreHelpers
return null;
}
if (!globalSettings.SelfHosted && httpContext.Request.Headers.ContainsKey(CloudFlareConnectingIp))
if (!globalSettings.SelfHosted && httpContext.Request.Headers.ContainsKey(RealConnectingIp))
{
return httpContext.Request.Headers[CloudFlareConnectingIp].ToString();
return httpContext.Request.Headers[RealConnectingIp].ToString();
}
return httpContext.Connection?.RemoteIpAddress?.ToString();
@ -624,8 +624,8 @@ public static class CoreHelpers
return configDict;
}
public static List<KeyValuePair<string, string>> BuildIdentityClaims(User user, ICollection<CurrentContentOrganization> orgs,
ICollection<CurrentContentProvider> providers, bool isPremium)
public static List<KeyValuePair<string, string>> BuildIdentityClaims(User user, ICollection<CurrentContextOrganization> orgs,
ICollection<CurrentContextProvider> providers, bool isPremium)
{
var claims = new List<KeyValuePair<string, string>>()
{
@ -817,4 +817,19 @@ public static class CoreHelpers
.ToString();
}
public static string GetEmailDomain(string email)
{
if (!string.IsNullOrWhiteSpace(email))
{
var emailParts = email.Split('@', StringSplitOptions.RemoveEmptyEntries);
if (emailParts.Length == 2)
{
return emailParts[1].Trim();
}
}
return null;
}
}

View File

@ -107,11 +107,6 @@ public class ClientStore : IClientStore
break;
}
if (string.IsNullOrEmpty(apiKey.ClientSecretHash))
{
apiKey.ClientSecretHash = apiKey.ClientSecret.Sha256();
}
var client = new Client
{
ClientId = clientId,

View File

@ -69,7 +69,7 @@
"IpRateLimitOptions": {
"EnableEndpointRateLimiting": true,
"StackBlockedRequests": false,
"RealIpHeader": "CF-Connecting-IP",
"RealIpHeader": "X-Connecting-IP",
"ClientIdHeader": "X-ClientId",
"HttpStatusCode": 429,
"IpWhitelist": [],

View File

@ -87,7 +87,8 @@ public class OrganizationRepository : Repository<Core.Entities.Organization, Org
UseKeyConnector = e.UseKeyConnector,
UseResetPassword = e.UseResetPassword,
UseScim = e.UseScim,
UseCustomPermissions = e.UseCustomPermissions
UseCustomPermissions = e.UseCustomPermissions,
UsePolicies = e.UsePolicies
}).ToListAsync();
}
}

View File

@ -2,8 +2,7 @@ CREATE PROCEDURE [dbo].[ApiKey_Create]
@Id UNIQUEIDENTIFIER OUTPUT,
@ServiceAccountId UNIQUEIDENTIFIER,
@Name VARCHAR(200),
@ClientSecret VARCHAR(30) = 'migrated', -- Deprecated as of 2023-05-17
@ClientSecretHash VARCHAR(128) = NULL,
@ClientSecretHash VARCHAR(128),
@Scope NVARCHAR(4000),
@EncryptedPayload NVARCHAR(4000),
@Key VARCHAR(MAX),
@ -14,18 +13,11 @@ AS
BEGIN
SET NOCOUNT ON
IF (@ClientSecretHash IS NULL)
BEGIN
DECLARE @hb VARBINARY(128) = HASHBYTES('SHA2_256', @ClientSecret);
SET @ClientSecretHash = CAST(N'' as xml).value('xs:base64Binary(sql:variable("@hb"))', 'VARCHAR(128)');
END
INSERT INTO [dbo].[ApiKey]
INSERT INTO [dbo].[ApiKey]
(
[Id],
[ServiceAccountId],
[Name],
[ClientSecret],
[ClientSecretHash],
[Scope],
[EncryptedPayload],
@ -34,12 +26,11 @@ BEGIN
[CreationDate],
[RevisionDate]
)
VALUES
VALUES
(
@Id,
@ServiceAccountId,
@Name,
@ClientSecret,
@ClientSecretHash,
@Scope,
@EncryptedPayload,

View File

@ -2,7 +2,6 @@
[Id] UNIQUEIDENTIFIER,
[ServiceAccountId] UNIQUEIDENTIFIER NULL,
[Name] VARCHAR(200) NOT NULL,
[ClientSecret] VARCHAR(30) NOT NULL,
[ClientSecretHash] VARCHAR(128) NULL,
[Scope] NVARCHAR (4000) NOT NULL,
[EncryptedPayload] NVARCHAR (4000) NOT NULL,

View File

@ -19,6 +19,7 @@ BEGIN
[UseKeyConnector],
[UseScim],
[UseResetPassword],
[UsePolicies],
[Enabled]
FROM
[dbo].[Organization]

View File

@ -1,42 +0,0 @@
CREATE PROCEDURE [dbo].[ApiKey_Create]
@Id UNIQUEIDENTIFIER OUTPUT,
@ServiceAccountId UNIQUEIDENTIFIER,
@Name VARCHAR(200),
@ClientSecretHash VARCHAR(128),
@Scope NVARCHAR(4000),
@EncryptedPayload NVARCHAR(4000),
@Key VARCHAR(MAX),
@ExpireAt DATETIME2(7),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7)
AS
BEGIN
SET NOCOUNT ON
INSERT INTO [dbo].[ApiKey]
(
[Id],
[ServiceAccountId],
[Name],
[ClientSecretHash],
[Scope],
[EncryptedPayload],
[Key],
[ExpireAt],
[CreationDate],
[RevisionDate]
)
VALUES
(
@Id,
@ServiceAccountId,
@Name,
@ClientSecretHash,
@Scope,
@EncryptedPayload,
@Key,
@ExpireAt,
@CreationDate,
@RevisionDate
)
END

View File

@ -1,18 +0,0 @@
CREATE TABLE [dbo].[ApiKey] (
[Id] UNIQUEIDENTIFIER,
[ServiceAccountId] UNIQUEIDENTIFIER NULL,
[Name] VARCHAR(200) NOT NULL,
[ClientSecretHash] VARCHAR(128) NULL,
[Scope] NVARCHAR (4000) NOT NULL,
[EncryptedPayload] NVARCHAR (4000) NOT NULL,
[Key] VARCHAR (MAX) NOT NULL,
[ExpireAt] DATETIME2(7) NULL,
[CreationDate] DATETIME2(7) NOT NULL,
[RevisionDate] DATETIME2(7) NOT NULL,
CONSTRAINT [PK_ApiKey] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_ApiKey_ServiceAccountId] FOREIGN KEY ([ServiceAccountId]) REFERENCES [dbo].[ServiceAccount] ([Id])
);
GO
CREATE NONCLUSTERED INDEX [IX_ApiKey_ServiceAccountId]
ON [dbo].[ApiKey]([ServiceAccountId] ASC);