1
0
mirror of https://github.com/bitwarden/server.git synced 2025-02-16 01:51:21 +01:00

[AC-1577] [AC-1575] Prefill Bitwarden portal values etc (#3155)

* [AC-1575] Don't overwrite existing org information when changing plans

* [AC-1577] Prefill SM configuration section when starting trial
This commit is contained in:
Thomas Rittson 2023-08-16 09:03:17 +10:00 committed by GitHub
parent 4ec01b0ef0
commit 60eab8a28e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 135 additions and 71 deletions

View File

@ -5,6 +5,7 @@ using Bit.Admin.Utilities;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories;
@ -199,6 +200,14 @@ public class OrganizationsController : Controller
{
var organization = await GetOrganization(id, model);
if (organization.UseSecretsManager &&
!organization.SecretsManagerBeta
&& StaticStore.GetSecretsManagerPlan(organization.PlanType) == null
)
{
throw new BadRequestException("Plan does not support Secrets Manager");
}
await _organizationRepository.ReplaceAsync(organization);
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.OrganizationEditedByAdmin, organization, _currentContext)

View File

@ -28,9 +28,9 @@ public class OrganizationEditModel : OrganizationViewModel
public OrganizationEditModel(Organization org, Provider provider, IEnumerable<OrganizationUserUserDetails> orgUsers,
IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections, IEnumerable<Group> groups,
IEnumerable<Policy> policies, BillingInfo billingInfo, IEnumerable<OrganizationConnection> connections,
GlobalSettings globalSettings, int secrets, int projects, int serviceAccounts, int smSeats)
GlobalSettings globalSettings, int secrets, int projects, int serviceAccounts, int occupiedSmSeats)
: base(org, provider, connections, orgUsers, ciphers, collections, groups, policies, secrets, projects,
serviceAccounts, smSeats)
serviceAccounts, occupiedSmSeats)
{
BillingInfo = billingInfo;
BraintreeMerchantId = globalSettings.Braintree.MerchantId;
@ -145,13 +145,26 @@ public class OrganizationEditModel : OrganizationViewModel
public int? SmSeats { get; set; }
[Display(Name = "Max Autoscale Seats")]
public int? MaxAutoscaleSmSeats { get; set; }
[Display(Name = "Max Service Accounts")]
[Display(Name = "Service Accounts")]
public int? SmServiceAccounts { get; set; }
[Display(Name = "Max Autoscale Service Accounts")]
public int? MaxAutoscaleSmServiceAccounts { get; set; }
[Display(Name = "Secrets Manager Beta")]
public bool SecretsManagerBeta { get; set; }
/**
* Creates a Plan[] object for use in Javascript
* This is mapped manually below to provide some type safety in case the plan objects change
* Add mappings for individual properties as you need them
*/
public IEnumerable<Dictionary<string, object>> GetPlansHelper() =>
StaticStore.SecretManagerPlans.Select(p =>
new Dictionary<string, object>
{
{ "type", p.Type },
{ "baseServiceAccount", p.BaseServiceAccount }
});
public Organization CreateOrganization(Provider provider)
{
BillingEmail = provider.BillingEmail;

View File

@ -13,7 +13,7 @@ public class OrganizationViewModel
public OrganizationViewModel(Organization org, Provider provider, IEnumerable<OrganizationConnection> connections,
IEnumerable<OrganizationUserUserDetails> orgUsers, IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
IEnumerable<Group> groups, IEnumerable<Policy> policies, int secretsCount, int projectCount, int serviceAccountsCount,
int smSeatsCount)
int occupiedSmSeatsCount)
{
Organization = org;
@ -39,10 +39,10 @@ public class OrganizationViewModel
orgUsers
.Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus)
.Select(u => u.Email));
Secrets = secretsCount;
Projects = projectCount;
ServiceAccounts = serviceAccountsCount;
SmSeats = smSeatsCount;
SecretsCount = secretsCount;
ProjectsCount = projectCount;
ServiceAccountsCount = serviceAccountsCount;
OccupiedSmSeatsCount = occupiedSmSeatsCount;
}
public Organization Organization { get; set; }
@ -59,9 +59,9 @@ public class OrganizationViewModel
public int GroupCount { get; set; }
public int PolicyCount { get; set; }
public bool HasPublicPrivateKeys { get; set; }
public int Secrets { get; set; }
public int Projects { get; set; }
public int ServiceAccounts { get; set; }
public int SmSeats { get; set; }
public int SecretsCount { get; set; }
public int ProjectsCount { get; set; }
public int ServiceAccountsCount { get; set; }
public int OccupiedSmSeatsCount { get; set; }
public bool UseSecretsManager => Organization.UseSecretsManager;
}

View File

@ -12,25 +12,45 @@
@section Scripts {
@await Html.PartialAsync("_OrganizationFormScripts")
<script>
(() => {
document.getElementById('teams-trial').addEventListener('click', () => {
if (document.getElementById('@(nameof(Model.PlanType))').value !== '@((byte)Bit.Core.Enums.PlanType.Free)') {
if (document.getElementById('@(nameof(Model.PlanType))').value !== '@((byte)PlanType.Free)') {
alert('Organization is not on a free plan.');
return;
}
togglePlanSettings('@((byte)Bit.Core.Enums.PlanType.TeamsAnnually)');
setTrialDefaults('@((byte)PlanType.TeamsAnnually)');
togglePlanFeatures('@((byte)PlanType.TeamsAnnually)');
document.getElementById('@(nameof(Model.Plan))').value = 'Teams (Trial)';
});
document.getElementById('enterprise-trial').addEventListener('click', () => {
if (document.getElementById('@(nameof(Model.PlanType))').value !== '@((byte)Bit.Core.Enums.PlanType.Free)') {
if (document.getElementById('@(nameof(Model.PlanType))').value !== '@((byte)PlanType.Free)') {
alert('Organization is not on a free plan.');
return;
}
togglePlanSettings('@((byte)Bit.Core.Enums.PlanType.EnterpriseAnnually)');
setTrialDefaults('@((byte)PlanType.EnterpriseAnnually)');
togglePlanFeatures('@((byte)PlanType.EnterpriseAnnually)');
document.getElementById('@(nameof(Model.Plan))').value = 'Enterprise (Trial)';
});
function setTrialDefaults(planType) {
// Plan
document.getElementById('@(nameof(Model.PlanType))').value = planType;
// Password Manager
document.getElementById('@(nameof(Model.Seats))').value = '10';
document.getElementById('@(nameof(Model.MaxCollections))').value = '';
document.getElementById('@(nameof(Model.MaxStorageGb))').value = '1';
// Secret Manager
if (document.getElementById('@(nameof(Model.UseSecretsManager))').checked) {
document.getElementById('@(nameof(Model.SmSeats))').value = '10';
document.getElementById('@(nameof(Model.SmServiceAccounts))').value = getPlan(planType)?.baseServiceAccount;
}
// Licensing
document.getElementById('@(nameof(Model.LicenseKey))').value = '@Model.RandomLicenseKey';
document.getElementById('@(nameof(Model.ExpirationDate))').value = '@Model.FourteenDayExpirationDate';
document.getElementById('@(nameof(Model.SalesAssistedTrialStarted))').value = true;
}
})();
</script>
}

View File

@ -33,16 +33,16 @@
<dd class="col-sm-8 col-lg-9">@Model.CollectionCount</dd>
<dt class="col-sm-4 col-lg-3">Secrets</dt>
<dd class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.Secrets: "N/A")</dd>
<dd class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.SecretsCount: "N/A")</dd>
<dt class="col-sm-4 col-lg-3">Projects</dt>
<dd class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.Projects: "N/A")</dd>
<dd class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.ProjectsCount: "N/A")</dd>
<dt class="col-sm-4 col-lg-3">Service Accounts</dt>
<dd class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.ServiceAccounts: "N/A")</dd>
<dd class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.ServiceAccountsCount: "N/A")</dd>
<dt class="col-sm-4 col-lg-3">Secrets Manager Seats</dt>
<dd class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.SmSeats: "N/A" )</dd>
<dd class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.OccupiedSmSeatsCount: "N/A" )</dd>
<dt class="col-sm-4 col-lg-3">Groups</dt>
<dd class="col-sm-8 col-lg-9">@Model.GroupCount</dd>

View File

@ -5,10 +5,10 @@
@section Scripts {
@await Html.PartialAsync("_OrganizationFormScripts")
<script>
(() => {
togglePlanSettings('@((byte)Model.PlanType)');
togglePlanFeatures('@((byte)Model.PlanType)');
})();
</script>
}

View File

@ -83,7 +83,7 @@
var planTypes = Enum.GetValues<PlanType>()
.Where(p => Model.Provider == null || p is >= PlanType.TeamsMonthly and <= PlanType.EnterpriseAnnually)
.Select(e => new SelectListItem
{
{
Value = ((int)e).ToString(),
Text = e.GetDisplayAttribute()?.GetName() ?? e.ToString()
})
@ -176,7 +176,7 @@
</div>
</div>
}
@if (canViewPlan)
{
<h2>Password Manager Configuration</h2>
@ -212,7 +212,7 @@
@if (canViewPlan)
{
<div id="organization-secrets-configuration" hidden="@(!Model.UseSecretsManager)">
<div id="organization-secrets-configuration" hidden="@(!Model.UseSecretsManager || Model.SecretsManagerBeta)">
<h2>Secrets Manager Configuration</h2>
<div class="row">
<div class="col-sm">
@ -265,7 +265,7 @@
}
@if (canViewBilling)
{
{
<h2>Billing</h2>
<div class="row">
<div class="col-sm">
@ -315,7 +315,7 @@
<label asp-for="GatewaySubscriptionId"></label>
<div class="input-group">
<input type="text" class="form-control" asp-for="GatewaySubscriptionId" readonly='@(!canEditBilling)'>
@if (canLaunchGateway)
@if (canLaunchGateway)
{
<div class="input-group-append">
<button class="btn btn-secondary" type="button" id="gateway-subscription-link">
@ -328,4 +328,4 @@
</div>
</div>
}
</form>
</form>

View File

@ -1,10 +1,12 @@
@model OrganizationEditModel
<script>
(() => {
document.getElementById('@(nameof(Model.PlanType))').addEventListener('change', () => {
const selectEl = document.getElementById('@(nameof(Model.PlanType))');
const selectText = selectEl.options[selectEl.selectedIndex].text;
document.getElementById('@(nameof(Model.Plan))').value = selectText;
togglePlanSettings(selectEl.options[selectEl.selectedIndex].value);
togglePlanFeatures(selectEl.options[selectEl.selectedIndex].value);
});
document.getElementById('gateway-customer-link')?.addEventListener('click', () => {
const gateway = document.getElementById('@(nameof(Model.Gateway))');
@ -12,9 +14,9 @@
if (!gateway || gateway.value === '' || !customerId || customerId.value === '') {
return;
}
if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Stripe)') {
if (gateway.value === '@((byte)GatewayType.Stripe)') {
window.open('https://dashboard.stripe.com/customers/' + customerId.value, '_blank');
} else if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Braintree)') {
} else if (gateway.value === '@((byte)GatewayType.Braintree)') {
window.open('https://www.braintreegateway.com/merchants/@(Model.BraintreeMerchantId)/'
+ customerId.value, '_blank');
}
@ -25,41 +27,45 @@
if (!gateway || gateway.value === '' || !subId || subId.value === '') {
return;
}
if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Stripe)') {
if (gateway.value === '@((byte)GatewayType.Stripe)') {
window.open('https://dashboard.stripe.com/subscriptions/' + subId.value, '_blank');
} else if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Braintree)') {
} else if (gateway.value === '@((byte)GatewayType.Braintree)') {
window.open('https://www.braintreegateway.com/merchants/@(Model.BraintreeMerchantId)/' +
'subscriptions/' + subId.value, '_blank');
}
});
document.getElementById('@(nameof(Model.UseSecretsManager))').addEventListener('change', (event) => {
document.getElementById('organization-secrets-configuration').hidden = !event.target.checked;
document.getElementById('organization-secrets-configuration').hidden = !event.target.checked;
if (event.target.checked) {
setInitialSecretsManagerConfiguration();
return;
}
document.getElementById('@(nameof(Model.SmSeats))').value = '';
document.getElementById('@(nameof(Model.MaxAutoscaleSmSeats))').value = '';
document.getElementById('@(nameof(Model.SmServiceAccounts))').value = '';
document.getElementById('@(nameof(Model.MaxAutoscaleSmServiceAccounts))').value = '';
// SM beta requires SM access
document.getElementById('@(nameof(Model.SecretsManagerBeta))').checked = false;
clearSecretsManagerConfiguration();
});
document.getElementById('@(nameof(Model.SecretsManagerBeta))').addEventListener('change', (event) => {
document.getElementById('organization-secrets-configuration').hidden = event.target.checked;
if (event.target.checked) {
// SM beta requires SM access
document.getElementById('@(nameof(Model.UseSecretsManager))').checked = true;
// SM Beta orgs do not have subscription limits
clearSecretsManagerConfiguration();
return;
}
setInitialSecretsManagerConfiguration();
});
})();
function togglePlanSettings(planType) {
document.getElementById('@(nameof(Model.PlanType))').value = planType;
function togglePlanFeatures(planType) {
switch(planType) {
case '@((byte)Bit.Core.Enums.PlanType.TeamsMonthly)':
case '@((byte)Bit.Core.Enums.PlanType.TeamsAnnually)':
// Plan
document.getElementById('@(nameof(Model.Seats))').value = '10';
document.getElementById('@(nameof(Model.MaxCollections))').value = '';
document.getElementById('@(nameof(Model.MaxStorageGb))').value = '1';
// Secrets
if (document.getElementById('@(nameof(Model.UseSecretsManager))').checked) {
document.getElementById('@(nameof(Model.SmServiceAccounts))').value = '50';
}
// Features
case '@((byte)PlanType.TeamsMonthly)':
case '@((byte)PlanType.TeamsAnnually)':
document.getElementById('@(nameof(Model.UsePolicies))').checked = false;
document.getElementById('@(nameof(Model.UseSso))').checked = false;
document.getElementById('@(nameof(Model.UseGroups))').checked = true;
@ -73,23 +79,10 @@
document.getElementById('@(nameof(Model.SelfHost))').checked = false;
document.getElementById('@(nameof(Model.UseResetPassword))').checked = false;
document.getElementById('@(nameof(Model.UseScim))').checked = false;
// Licensing
document.getElementById('@(nameof(Model.LicenseKey))').value = '@Model.RandomLicenseKey';
document.getElementById('@(nameof(Model.ExpirationDate))').value = '@Model.FourteenDayExpirationDate';
document.getElementById('@(nameof(Model.SalesAssistedTrialStarted))').value = true;
break;
case '@((byte)Bit.Core.Enums.PlanType.EnterpriseMonthly)':
case '@((byte)Bit.Core.Enums.PlanType.EnterpriseAnnually)':
// Plan
document.getElementById('@(nameof(Model.Seats))').value = '10';
document.getElementById('@(nameof(Model.MaxCollections))').value = '';
document.getElementById('@(nameof(Model.MaxStorageGb))').value = '1';
// Secrets
if (document.getElementById('@(nameof(Model.UseSecretsManager))').checked) {
document.getElementById('@(nameof(Model.SmServiceAccounts))').value = '200';
}
// Features
case '@((byte)PlanType.EnterpriseMonthly)':
case '@((byte)PlanType.EnterpriseAnnually)':
document.getElementById('@(nameof(Model.UsePolicies))').checked = true;
document.getElementById('@(nameof(Model.UseSso))').checked = true;
document.getElementById('@(nameof(Model.UseGroups))').checked = true;
@ -103,12 +96,41 @@
document.getElementById('@(nameof(Model.SelfHost))').checked = true;
document.getElementById('@(nameof(Model.UseResetPassword))').checked = true;
document.getElementById('@(nameof(Model.UseScim))').checked = true;
// Licensing
document.getElementById('@(nameof(Model.LicenseKey))').value = '@Model.RandomLicenseKey';
document.getElementById('@(nameof(Model.ExpirationDate))').value = '@Model.FourteenDayExpirationDate';
document.getElementById('@(nameof(Model.SalesAssistedTrialStarted))').value = true;
break;
}
}
/***
* Set Secrets Manager values based on current usage (for migrating from SM beta or reinstating an old subscription)
*/
function setInitialSecretsManagerConfiguration() {
const planType = document.getElementById('@(nameof(Model.PlanType))').value;
// Seats
document.getElementById('@(nameof(Model.SmSeats))').value = Math.max(@Model.OccupiedSmSeatsCount, 1);
// Service accounts
const baseServiceAccounts = getPlan(planType)?.baseServiceAccount ?? 0;
if (planType !== '@((byte)PlanType.Free)' && @Model.ServiceAccountsCount > baseServiceAccounts) {
document.getElementById('@(nameof(Model.SmServiceAccounts))').value = @Model.ServiceAccountsCount;
} else {
document.getElementById('@(nameof(Model.SmServiceAccounts))').value = baseServiceAccounts;
}
// Clear autoscale values (no defaults)
document.getElementById('@(nameof(Model.MaxAutoscaleSmSeats))').value = '';
document.getElementById('@(nameof(Model.MaxAutoscaleSmServiceAccounts))').value = '';
}
function clearSecretsManagerConfiguration() {
document.getElementById('@(nameof(Model.SmSeats))').value = '';
document.getElementById('@(nameof(Model.SmServiceAccounts))').value = '';
document.getElementById('@(nameof(Model.MaxAutoscaleSmSeats))').value = '';
document.getElementById('@(nameof(Model.MaxAutoscaleSmServiceAccounts))').value = '';
}
function getPlan(planType) {
const plans = @Html.Raw(Json.Serialize(Model.GetPlansHelper()));
return plans.find(p => p.type == planType);
}
</script>