mirror of
https://github.com/bitwarden/server
synced 2025-12-26 13:13:24 +00:00
[AC-1895] AC Team code ownership moves: Bitwarden Portal (#3528)
--------- Co-authored-by: Addison Beck <hello@addisonbeck.com>
This commit is contained in:
387
src/Admin/AdminConsole/Controllers/OrganizationsController.cs
Normal file
387
src/Admin/AdminConsole/Controllers/OrganizationsController.cs
Normal file
@@ -0,0 +1,387 @@
|
||||
using Bit.Admin.AdminConsole.Models;
|
||||
using Bit.Admin.Enums;
|
||||
using Bit.Admin.Services;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.OrganizationConnectionConfigs;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Controllers;
|
||||
|
||||
[Authorize]
|
||||
public class OrganizationsController : Controller
|
||||
{
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
|
||||
private readonly ISelfHostedSyncSponsorshipsCommand _syncSponsorshipsCommand;
|
||||
private readonly ICipherRepository _cipherRepository;
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly ILogger<OrganizationsController> _logger;
|
||||
private readonly IAccessControlService _accessControlService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ISecretRepository _secretRepository;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
|
||||
private readonly IRemovePaymentMethodCommand _removePaymentMethodCommand;
|
||||
|
||||
public OrganizationsController(
|
||||
IOrganizationService organizationService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationConnectionRepository organizationConnectionRepository,
|
||||
ISelfHostedSyncSponsorshipsCommand syncSponsorshipsCommand,
|
||||
ICipherRepository cipherRepository,
|
||||
ICollectionRepository collectionRepository,
|
||||
IGroupRepository groupRepository,
|
||||
IPolicyRepository policyRepository,
|
||||
IPaymentService paymentService,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
GlobalSettings globalSettings,
|
||||
IReferenceEventService referenceEventService,
|
||||
IUserService userService,
|
||||
IProviderRepository providerRepository,
|
||||
ILogger<OrganizationsController> logger,
|
||||
IAccessControlService accessControlService,
|
||||
ICurrentContext currentContext,
|
||||
ISecretRepository secretRepository,
|
||||
IProjectRepository projectRepository,
|
||||
IServiceAccountRepository serviceAccountRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
|
||||
IRemovePaymentMethodCommand removePaymentMethodCommand)
|
||||
{
|
||||
_organizationService = organizationService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_organizationConnectionRepository = organizationConnectionRepository;
|
||||
_syncSponsorshipsCommand = syncSponsorshipsCommand;
|
||||
_cipherRepository = cipherRepository;
|
||||
_collectionRepository = collectionRepository;
|
||||
_groupRepository = groupRepository;
|
||||
_policyRepository = policyRepository;
|
||||
_paymentService = paymentService;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_globalSettings = globalSettings;
|
||||
_referenceEventService = referenceEventService;
|
||||
_userService = userService;
|
||||
_providerRepository = providerRepository;
|
||||
_logger = logger;
|
||||
_accessControlService = accessControlService;
|
||||
_currentContext = currentContext;
|
||||
_secretRepository = secretRepository;
|
||||
_projectRepository = projectRepository;
|
||||
_serviceAccountRepository = serviceAccountRepository;
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
|
||||
_removePaymentMethodCommand = removePaymentMethodCommand;
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Org_List_View)]
|
||||
public async Task<IActionResult> Index(string name = null, string userEmail = null, bool? paid = null,
|
||||
int page = 1, int count = 25)
|
||||
{
|
||||
if (page < 1)
|
||||
{
|
||||
page = 1;
|
||||
}
|
||||
|
||||
if (count < 1)
|
||||
{
|
||||
count = 1;
|
||||
}
|
||||
|
||||
var skip = (page - 1) * count;
|
||||
var organizations = await _organizationRepository.SearchAsync(name, userEmail, paid, skip, count);
|
||||
return View(new OrganizationsModel
|
||||
{
|
||||
Items = organizations as List<Organization>,
|
||||
Name = string.IsNullOrWhiteSpace(name) ? null : name,
|
||||
UserEmail = string.IsNullOrWhiteSpace(userEmail) ? null : userEmail,
|
||||
Paid = paid,
|
||||
Page = page,
|
||||
Count = count,
|
||||
Action = _globalSettings.SelfHosted ? "View" : "Edit",
|
||||
SelfHosted = _globalSettings.SelfHosted
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<IActionResult> View(Guid id)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
if (organization == null)
|
||||
{
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
var provider = await _providerRepository.GetByOrganizationIdAsync(id);
|
||||
var ciphers = await _cipherRepository.GetManyByOrganizationIdAsync(id);
|
||||
var collections = await _collectionRepository.GetManyByOrganizationIdAsync(id);
|
||||
IEnumerable<Group> groups = null;
|
||||
if (organization.UseGroups)
|
||||
{
|
||||
groups = await _groupRepository.GetManyByOrganizationIdAsync(id);
|
||||
}
|
||||
IEnumerable<Policy> policies = null;
|
||||
if (organization.UsePolicies)
|
||||
{
|
||||
policies = await _policyRepository.GetManyByOrganizationIdAsync(id);
|
||||
}
|
||||
var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id);
|
||||
var billingSyncConnection = _globalSettings.EnableCloudCommunication ? await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync) : null;
|
||||
var secrets = organization.UseSecretsManager ? await _secretRepository.GetSecretsCountByOrganizationIdAsync(id) : -1;
|
||||
var projects = organization.UseSecretsManager ? await _projectRepository.GetProjectCountByOrganizationIdAsync(id) : -1;
|
||||
var serviceAccounts = organization.UseSecretsManager ? await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(id) : -1;
|
||||
var smSeats = organization.UseSecretsManager
|
||||
? await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
|
||||
: -1;
|
||||
return View(new OrganizationViewModel(organization, provider, billingSyncConnection, users, ciphers, collections, groups, policies,
|
||||
secrets, projects, serviceAccounts, smSeats));
|
||||
}
|
||||
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<IActionResult> Edit(Guid id)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
if (organization == null)
|
||||
{
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
var provider = await _providerRepository.GetByOrganizationIdAsync(id);
|
||||
var ciphers = await _cipherRepository.GetManyByOrganizationIdAsync(id);
|
||||
var collections = await _collectionRepository.GetManyByOrganizationIdAsync(id);
|
||||
IEnumerable<Group> groups = null;
|
||||
if (organization.UseGroups)
|
||||
{
|
||||
groups = await _groupRepository.GetManyByOrganizationIdAsync(id);
|
||||
}
|
||||
IEnumerable<Policy> policies = null;
|
||||
if (organization.UsePolicies)
|
||||
{
|
||||
policies = await _policyRepository.GetManyByOrganizationIdAsync(id);
|
||||
}
|
||||
var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id);
|
||||
var billingInfo = await _paymentService.GetBillingAsync(organization);
|
||||
var billingSyncConnection = _globalSettings.EnableCloudCommunication ? await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync) : null;
|
||||
var secrets = organization.UseSecretsManager ? await _secretRepository.GetSecretsCountByOrganizationIdAsync(id) : -1;
|
||||
var projects = organization.UseSecretsManager ? await _projectRepository.GetProjectCountByOrganizationIdAsync(id) : -1;
|
||||
var serviceAccounts = organization.UseSecretsManager ? await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(id) : -1;
|
||||
var smSeats = organization.UseSecretsManager
|
||||
? await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
|
||||
: -1;
|
||||
return View(new OrganizationEditModel(organization, provider, users, ciphers, collections, groups, policies,
|
||||
billingInfo, billingSyncConnection, _globalSettings, secrets, projects, serviceAccounts, smSeats));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<IActionResult> Edit(Guid id, OrganizationEditModel model)
|
||||
{
|
||||
var organization = await GetOrganization(id, model);
|
||||
|
||||
if (organization.UseSecretsManager &&
|
||||
!StaticStore.GetPlan(organization.PlanType).SupportsSecretsManager)
|
||||
{
|
||||
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)
|
||||
{
|
||||
EventRaisedByUser = _userService.GetUserName(User),
|
||||
SalesAssistedTrialStarted = model.SalesAssistedTrialStarted,
|
||||
});
|
||||
|
||||
return RedirectToAction("Edit", new { id });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequirePermission(Permission.Org_Delete)]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
if (organization != null)
|
||||
{
|
||||
await _organizationRepository.DeleteAsync(organization);
|
||||
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
|
||||
}
|
||||
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
public async Task<IActionResult> TriggerBillingSync(Guid id)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
if (organization == null)
|
||||
{
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
var connection = (await _organizationConnectionRepository.GetEnabledByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync)).FirstOrDefault();
|
||||
if (connection != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = connection.GetConfig<BillingSyncConfig>();
|
||||
await _syncSponsorshipsCommand.SyncOrganization(id, config.CloudOrganizationId, connection);
|
||||
TempData["ConnectionActivated"] = id;
|
||||
TempData["ConnectionError"] = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData["ConnectionError"] = ex.Message;
|
||||
_logger.LogWarning(ex, "Error while attempting to do billing sync for organization with id '{OrganizationId}'", id);
|
||||
}
|
||||
|
||||
if (_globalSettings.SelfHosted)
|
||||
{
|
||||
return RedirectToAction("View", new { id });
|
||||
}
|
||||
else
|
||||
{
|
||||
return RedirectToAction("Edit", new { id });
|
||||
}
|
||||
}
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> ResendOwnerInvite(Guid id)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
if (organization == null)
|
||||
{
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
var organizationUsers = await _organizationUserRepository.GetManyByOrganizationAsync(id, OrganizationUserType.Owner);
|
||||
foreach (var organizationUser in organizationUsers)
|
||||
{
|
||||
await _organizationService.ResendInviteAsync(id, null, organizationUser.Id, true);
|
||||
}
|
||||
|
||||
return Json(null);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[RequirePermission(Permission.Provider_Edit)]
|
||||
public async Task<IActionResult> UnlinkOrganizationFromProviderAsync(Guid id)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
if (organization is null)
|
||||
{
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
var provider = await _providerRepository.GetByOrganizationIdAsync(id);
|
||||
if (provider is null)
|
||||
{
|
||||
return RedirectToAction("Edit", new { id });
|
||||
}
|
||||
|
||||
var providerOrganization = await _providerOrganizationRepository.GetByOrganizationId(id);
|
||||
if (providerOrganization is null)
|
||||
{
|
||||
return RedirectToAction("Edit", new { id });
|
||||
}
|
||||
|
||||
await _removeOrganizationFromProviderCommand.RemoveOrganizationFromProvider(
|
||||
provider,
|
||||
providerOrganization,
|
||||
organization);
|
||||
|
||||
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
|
||||
|
||||
return Json(null);
|
||||
}
|
||||
private async Task<Organization> GetOrganization(Guid id, OrganizationEditModel model)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
|
||||
if (_accessControlService.UserHasPermission(Permission.Org_CheckEnabledBox))
|
||||
{
|
||||
organization.Enabled = model.Enabled;
|
||||
}
|
||||
|
||||
if (_accessControlService.UserHasPermission(Permission.Org_Plan_Edit))
|
||||
{
|
||||
organization.PlanType = model.PlanType.Value;
|
||||
organization.Plan = model.Plan;
|
||||
organization.Seats = model.Seats;
|
||||
organization.MaxAutoscaleSeats = model.MaxAutoscaleSeats;
|
||||
organization.MaxCollections = model.MaxCollections;
|
||||
organization.MaxStorageGb = model.MaxStorageGb;
|
||||
|
||||
//features
|
||||
organization.SelfHost = model.SelfHost;
|
||||
organization.Use2fa = model.Use2fa;
|
||||
organization.UseApi = model.UseApi;
|
||||
organization.UseGroups = model.UseGroups;
|
||||
organization.UsePolicies = model.UsePolicies;
|
||||
organization.UseSso = model.UseSso;
|
||||
organization.UseKeyConnector = model.UseKeyConnector;
|
||||
organization.UseScim = model.UseScim;
|
||||
organization.UseDirectory = model.UseDirectory;
|
||||
organization.UseEvents = model.UseEvents;
|
||||
organization.UseResetPassword = model.UseResetPassword;
|
||||
organization.UseCustomPermissions = model.UseCustomPermissions;
|
||||
organization.UseTotp = model.UseTotp;
|
||||
organization.UsersGetPremium = model.UsersGetPremium;
|
||||
organization.UseSecretsManager = model.UseSecretsManager;
|
||||
|
||||
//secrets
|
||||
organization.SmSeats = model.SmSeats;
|
||||
organization.MaxAutoscaleSmSeats = model.MaxAutoscaleSmSeats;
|
||||
organization.SmServiceAccounts = model.SmServiceAccounts;
|
||||
organization.MaxAutoscaleSmServiceAccounts = model.MaxAutoscaleSmServiceAccounts;
|
||||
}
|
||||
|
||||
if (_accessControlService.UserHasPermission(Permission.Org_Licensing_Edit))
|
||||
{
|
||||
organization.LicenseKey = model.LicenseKey;
|
||||
organization.ExpirationDate = model.ExpirationDate;
|
||||
}
|
||||
|
||||
if (_accessControlService.UserHasPermission(Permission.Org_Billing_Edit))
|
||||
{
|
||||
organization.BillingEmail = model.BillingEmail?.ToLowerInvariant()?.Trim();
|
||||
organization.Gateway = model.Gateway;
|
||||
organization.GatewayCustomerId = model.GatewayCustomerId;
|
||||
organization.GatewaySubscriptionId = model.GatewaySubscriptionId;
|
||||
}
|
||||
|
||||
return organization;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using Bit.Admin.Enums;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public class ProviderOrganizationsController : Controller
|
||||
{
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
|
||||
private readonly IRemovePaymentMethodCommand _removePaymentMethodCommand;
|
||||
|
||||
public ProviderOrganizationsController(IProviderRepository providerRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
|
||||
IRemovePaymentMethodCommand removePaymentMethodCommand)
|
||||
{
|
||||
_providerRepository = providerRepository;
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
|
||||
_removePaymentMethodCommand = removePaymentMethodCommand;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[RequirePermission(Permission.Provider_Edit)]
|
||||
public async Task<IActionResult> DeleteAsync(Guid providerId, Guid id)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||
if (provider is null)
|
||||
{
|
||||
return RedirectToAction("Index", "Providers");
|
||||
}
|
||||
|
||||
var providerOrganization = await _providerOrganizationRepository.GetByIdAsync(id);
|
||||
if (providerOrganization is null)
|
||||
{
|
||||
return RedirectToAction("View", "Providers", new { id = providerId });
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);
|
||||
if (organization == null)
|
||||
{
|
||||
return RedirectToAction("View", "Providers", new { id = providerId });
|
||||
}
|
||||
|
||||
await _removeOrganizationFromProviderCommand.RemoveOrganizationFromProvider(
|
||||
provider,
|
||||
providerOrganization,
|
||||
organization);
|
||||
|
||||
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
|
||||
|
||||
return Json(null);
|
||||
}
|
||||
}
|
||||
251
src/Admin/AdminConsole/Controllers/ProvidersController.cs
Normal file
251
src/Admin/AdminConsole/Controllers/ProvidersController.cs
Normal file
@@ -0,0 +1,251 @@
|
||||
using Bit.Admin.AdminConsole.Models;
|
||||
using Bit.Admin.Enums;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public class ProvidersController : Controller
|
||||
{
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IProviderService _providerService;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly ICreateProviderCommand _createProviderCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public ProvidersController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationService organizationService,
|
||||
IProviderRepository providerRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IProviderService providerService,
|
||||
GlobalSettings globalSettings,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IReferenceEventService referenceEventService,
|
||||
IUserService userService,
|
||||
ICreateProviderCommand createProviderCommand,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationService = organizationService;
|
||||
_providerRepository = providerRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
_providerService = providerService;
|
||||
_globalSettings = globalSettings;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_referenceEventService = referenceEventService;
|
||||
_userService = userService;
|
||||
_createProviderCommand = createProviderCommand;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Provider_List_View)]
|
||||
public async Task<IActionResult> Index(string name = null, string userEmail = null, int page = 1, int count = 25)
|
||||
{
|
||||
if (page < 1)
|
||||
{
|
||||
page = 1;
|
||||
}
|
||||
|
||||
if (count < 1)
|
||||
{
|
||||
count = 1;
|
||||
}
|
||||
|
||||
var skip = (page - 1) * count;
|
||||
var providers = await _providerRepository.SearchAsync(name, userEmail, skip, count);
|
||||
return View(new ProvidersModel
|
||||
{
|
||||
Items = providers as List<Provider>,
|
||||
Name = string.IsNullOrWhiteSpace(name) ? null : name,
|
||||
UserEmail = string.IsNullOrWhiteSpace(userEmail) ? null : userEmail,
|
||||
Page = page,
|
||||
Count = count,
|
||||
Action = _globalSettings.SelfHosted ? "View" : "Edit",
|
||||
SelfHosted = _globalSettings.SelfHosted
|
||||
});
|
||||
}
|
||||
|
||||
public IActionResult Create(string ownerEmail = null)
|
||||
{
|
||||
return View(new CreateProviderModel
|
||||
{
|
||||
OwnerEmail = ownerEmail
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequirePermission(Permission.Provider_Create)]
|
||||
public async Task<IActionResult> Create(CreateProviderModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var provider = model.ToProvider();
|
||||
switch (provider.Type)
|
||||
{
|
||||
case ProviderType.Msp:
|
||||
await _createProviderCommand.CreateMspAsync(provider, model.OwnerEmail);
|
||||
break;
|
||||
case ProviderType.Reseller:
|
||||
await _createProviderCommand.CreateResellerAsync(provider);
|
||||
break;
|
||||
}
|
||||
|
||||
return RedirectToAction("Edit", new { id = provider.Id });
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Provider_View)]
|
||||
public async Task<IActionResult> View(Guid id)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(id);
|
||||
if (provider == null)
|
||||
{
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id);
|
||||
var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);
|
||||
return View(new ProviderViewModel(provider, users, providerOrganizations));
|
||||
}
|
||||
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<IActionResult> Edit(Guid id)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(id);
|
||||
if (provider == null)
|
||||
{
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id);
|
||||
var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);
|
||||
return View(new ProviderEditModel(provider, users, providerOrganizations));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
[RequirePermission(Permission.Provider_Edit)]
|
||||
public async Task<IActionResult> Edit(Guid id, ProviderEditModel model)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(id);
|
||||
if (provider == null)
|
||||
{
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
model.ToProvider(provider);
|
||||
await _providerRepository.ReplaceAsync(provider);
|
||||
await _applicationCacheService.UpsertProviderAbilityAsync(provider);
|
||||
return RedirectToAction("Edit", new { id });
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Provider_ResendEmailInvite)]
|
||||
public async Task<IActionResult> ResendInvite(Guid ownerId, Guid providerId)
|
||||
{
|
||||
await _providerService.ResendProviderSetupInviteEmailAsync(providerId, ownerId);
|
||||
TempData["InviteResentTo"] = ownerId;
|
||||
return RedirectToAction("Edit", new { id = providerId });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> AddExistingOrganization(Guid id, string name = null, string ownerEmail = null, int page = 1, int count = 25)
|
||||
{
|
||||
if (page < 1)
|
||||
{
|
||||
page = 1;
|
||||
}
|
||||
|
||||
if (count < 1)
|
||||
{
|
||||
count = 1;
|
||||
}
|
||||
|
||||
var skip = (page - 1) * count;
|
||||
var unassignedOrganizations = await _organizationRepository.SearchUnassignedToProviderAsync(name, ownerEmail, skip, count);
|
||||
var viewModel = new OrganizationUnassignedToProviderSearchViewModel
|
||||
{
|
||||
OrganizationName = string.IsNullOrWhiteSpace(name) ? null : name,
|
||||
OrganizationOwnerEmail = string.IsNullOrWhiteSpace(ownerEmail) ? null : ownerEmail,
|
||||
Page = page,
|
||||
Count = count,
|
||||
Items = unassignedOrganizations.Select(uo => new OrganizationSelectableViewModel
|
||||
{
|
||||
Id = uo.Id,
|
||||
Name = uo.Name,
|
||||
PlanType = uo.PlanType
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> AddExistingOrganization(Guid id, OrganizationUnassignedToProviderSearchViewModel model)
|
||||
{
|
||||
var organizationIds = model.Items.Where(o => o.Selected).Select(o => o.Id).ToArray();
|
||||
if (organizationIds.Any())
|
||||
{
|
||||
await _providerService.AddOrganizationsToReseller(id, organizationIds);
|
||||
}
|
||||
|
||||
return RedirectToAction("Edit", "Providers", new { id = id });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> CreateOrganization(Guid providerId)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||
if (provider is not { Type: ProviderType.Reseller })
|
||||
{
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
return View(new OrganizationEditModel(provider));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateOrganization(Guid providerId, OrganizationEditModel model)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||
if (provider is not { Type: ProviderType.Reseller })
|
||||
{
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
var flexibleCollectionsSignupEnabled = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsSignup);
|
||||
var flexibleCollectionsV1Enabled = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1);
|
||||
var organization = model.CreateOrganization(provider, flexibleCollectionsSignupEnabled, flexibleCollectionsV1Enabled);
|
||||
await _organizationService.CreatePendingOrganization(organization, model.Owners, User, _userService, model.SalesAssistedTrialStarted);
|
||||
await _providerService.AddOrganization(providerId, organization.Id, null);
|
||||
|
||||
return RedirectToAction("Edit", "Providers", new { id = providerId });
|
||||
}
|
||||
}
|
||||
68
src/Admin/AdminConsole/Models/CreateProviderModel.cs
Normal file
68
src/Admin/AdminConsole/Models/CreateProviderModel.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Models;
|
||||
|
||||
public class CreateProviderModel : IValidatableObject
|
||||
{
|
||||
public CreateProviderModel() { }
|
||||
|
||||
[Display(Name = "Provider Type")]
|
||||
public ProviderType Type { get; set; }
|
||||
|
||||
[Display(Name = "Owner Email")]
|
||||
public string OwnerEmail { get; set; }
|
||||
|
||||
[Display(Name = "Name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Display(Name = "Business Name")]
|
||||
public string BusinessName { get; set; }
|
||||
|
||||
[Display(Name = "Primary Billing Email")]
|
||||
public string BillingEmail { get; set; }
|
||||
|
||||
public virtual Provider ToProvider()
|
||||
{
|
||||
return new Provider()
|
||||
{
|
||||
Type = Type,
|
||||
Name = Name,
|
||||
BusinessName = BusinessName,
|
||||
BillingEmail = BillingEmail?.ToLowerInvariant().Trim()
|
||||
};
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case ProviderType.Msp:
|
||||
if (string.IsNullOrWhiteSpace(OwnerEmail))
|
||||
{
|
||||
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(OwnerEmail);
|
||||
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
|
||||
}
|
||||
break;
|
||||
case ProviderType.Reseller:
|
||||
if (string.IsNullOrWhiteSpace(Name))
|
||||
{
|
||||
var nameDisplayName = nameof(Name).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(Name);
|
||||
yield return new ValidationResult($"The {nameDisplayName} field is required.");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(BusinessName))
|
||||
{
|
||||
var businessNameDisplayName = nameof(BusinessName).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BusinessName);
|
||||
yield return new ValidationResult($"The {businessNameDisplayName} field is required.");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(BillingEmail))
|
||||
{
|
||||
var billingEmailDisplayName = nameof(BillingEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BillingEmail);
|
||||
yield return new ValidationResult($"The {billingEmailDisplayName} field is required.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
223
src/Admin/AdminConsole/Models/OrganizationEditModel.cs
Normal file
223
src/Admin/AdminConsole/Models/OrganizationEditModel.cs
Normal file
@@ -0,0 +1,223 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Models;
|
||||
|
||||
public class OrganizationEditModel : OrganizationViewModel
|
||||
{
|
||||
public OrganizationEditModel() { }
|
||||
|
||||
public OrganizationEditModel(Provider provider)
|
||||
{
|
||||
Provider = provider;
|
||||
BillingEmail = provider.Type == ProviderType.Reseller ? provider.BillingEmail : string.Empty;
|
||||
PlanType = Core.Enums.PlanType.TeamsMonthly;
|
||||
Plan = Core.Enums.PlanType.TeamsMonthly.GetDisplayAttribute()?.GetName();
|
||||
LicenseKey = RandomLicenseKey;
|
||||
}
|
||||
|
||||
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 occupiedSmSeats)
|
||||
: base(org, provider, connections, orgUsers, ciphers, collections, groups, policies, secrets, projects,
|
||||
serviceAccounts, occupiedSmSeats)
|
||||
{
|
||||
BillingInfo = billingInfo;
|
||||
BraintreeMerchantId = globalSettings.Braintree.MerchantId;
|
||||
|
||||
Name = org.Name;
|
||||
BusinessName = org.BusinessName;
|
||||
BillingEmail = provider?.Type == ProviderType.Reseller ? provider.BillingEmail : org.BillingEmail;
|
||||
PlanType = org.PlanType;
|
||||
Plan = org.Plan;
|
||||
Seats = org.Seats;
|
||||
MaxAutoscaleSeats = org.MaxAutoscaleSeats;
|
||||
MaxCollections = org.MaxCollections;
|
||||
UsePolicies = org.UsePolicies;
|
||||
UseSso = org.UseSso;
|
||||
UseKeyConnector = org.UseKeyConnector;
|
||||
UseScim = org.UseScim;
|
||||
UseGroups = org.UseGroups;
|
||||
UseDirectory = org.UseDirectory;
|
||||
UseEvents = org.UseEvents;
|
||||
UseTotp = org.UseTotp;
|
||||
Use2fa = org.Use2fa;
|
||||
UseApi = org.UseApi;
|
||||
UseSecretsManager = org.UseSecretsManager;
|
||||
UseResetPassword = org.UseResetPassword;
|
||||
SelfHost = org.SelfHost;
|
||||
UsersGetPremium = org.UsersGetPremium;
|
||||
UseCustomPermissions = org.UseCustomPermissions;
|
||||
MaxStorageGb = org.MaxStorageGb;
|
||||
Gateway = org.Gateway;
|
||||
GatewayCustomerId = org.GatewayCustomerId;
|
||||
GatewaySubscriptionId = org.GatewaySubscriptionId;
|
||||
Enabled = org.Enabled;
|
||||
LicenseKey = org.LicenseKey;
|
||||
ExpirationDate = org.ExpirationDate;
|
||||
SmSeats = org.SmSeats;
|
||||
MaxAutoscaleSmSeats = org.MaxAutoscaleSmSeats;
|
||||
SmServiceAccounts = org.SmServiceAccounts;
|
||||
MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts;
|
||||
}
|
||||
|
||||
public BillingInfo BillingInfo { get; set; }
|
||||
public string RandomLicenseKey => CoreHelpers.SecureRandomString(20);
|
||||
public string FourteenDayExpirationDate => DateTime.Now.AddDays(14).ToString("yyyy-MM-ddTHH:mm");
|
||||
public string BraintreeMerchantId { get; set; }
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Organization Name")]
|
||||
public string Name { get; set; }
|
||||
[Display(Name = "Business Name")]
|
||||
public string BusinessName { get; set; }
|
||||
[Display(Name = "Billing Email")]
|
||||
public string BillingEmail { get; set; }
|
||||
[Required]
|
||||
[Display(Name = "Plan")]
|
||||
public PlanType? PlanType { get; set; }
|
||||
[Required]
|
||||
[Display(Name = "Plan Name")]
|
||||
public string Plan { get; set; }
|
||||
[Display(Name = "Seats")]
|
||||
public int? Seats { get; set; }
|
||||
[Display(Name = "Max. Autoscale Seats")]
|
||||
public int? MaxAutoscaleSeats { get; set; }
|
||||
[Display(Name = "Max. Collections")]
|
||||
public short? MaxCollections { get; set; }
|
||||
[Display(Name = "Policies")]
|
||||
public bool UsePolicies { get; set; }
|
||||
[Display(Name = "SSO")]
|
||||
public bool UseSso { get; set; }
|
||||
[Display(Name = "Key Connector with Customer Encryption")]
|
||||
public bool UseKeyConnector { get; set; }
|
||||
[Display(Name = "Groups")]
|
||||
public bool UseGroups { get; set; }
|
||||
[Display(Name = "Directory")]
|
||||
public bool UseDirectory { get; set; }
|
||||
[Display(Name = "Events")]
|
||||
public bool UseEvents { get; set; }
|
||||
[Display(Name = "TOTP")]
|
||||
public bool UseTotp { get; set; }
|
||||
[Display(Name = "2FA")]
|
||||
public bool Use2fa { get; set; }
|
||||
[Display(Name = "API")]
|
||||
public bool UseApi { get; set; }
|
||||
[Display(Name = "Reset Password")]
|
||||
public bool UseResetPassword { get; set; }
|
||||
[Display(Name = "SCIM")]
|
||||
public bool UseScim { get; set; }
|
||||
[Display(Name = "Secrets Manager")]
|
||||
public bool UseSecretsManager { get; set; }
|
||||
[Display(Name = "Self Host")]
|
||||
public bool SelfHost { get; set; }
|
||||
[Display(Name = "Users Get Premium")]
|
||||
public bool UsersGetPremium { get; set; }
|
||||
[Display(Name = "Custom Permissions")]
|
||||
public bool UseCustomPermissions { get; set; }
|
||||
[Display(Name = "Max. Storage GB")]
|
||||
public short? MaxStorageGb { get; set; }
|
||||
[Display(Name = "Gateway")]
|
||||
public GatewayType? Gateway { get; set; }
|
||||
[Display(Name = "Gateway Customer Id")]
|
||||
public string GatewayCustomerId { get; set; }
|
||||
[Display(Name = "Gateway Subscription Id")]
|
||||
public string GatewaySubscriptionId { get; set; }
|
||||
[Display(Name = "Enabled")]
|
||||
public bool Enabled { get; set; }
|
||||
[Display(Name = "License Key")]
|
||||
public string LicenseKey { get; set; }
|
||||
[Display(Name = "Expiration Date")]
|
||||
public DateTime? ExpirationDate { get; set; }
|
||||
public bool SalesAssistedTrialStarted { get; set; }
|
||||
[Display(Name = "Seats")]
|
||||
public int? SmSeats { get; set; }
|
||||
[Display(Name = "Max Autoscale Seats")]
|
||||
public int? MaxAutoscaleSmSeats { get; set; }
|
||||
[Display(Name = "Service Accounts")]
|
||||
public int? SmServiceAccounts { get; set; }
|
||||
[Display(Name = "Max Autoscale Service Accounts")]
|
||||
public int? MaxAutoscaleSmServiceAccounts { 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.Plans
|
||||
.Where(p => p.SupportsSecretsManager)
|
||||
.Select(p => new Dictionary<string, object>
|
||||
{
|
||||
{ "type", p.Type },
|
||||
{ "baseServiceAccount", p.SecretsManager.BaseServiceAccount }
|
||||
});
|
||||
|
||||
public Organization CreateOrganization(Provider provider, bool flexibleCollectionsSignupEnabled, bool flexibleCollectionsV1Enabled)
|
||||
{
|
||||
BillingEmail = provider.BillingEmail;
|
||||
|
||||
var newOrg = new Organization
|
||||
{
|
||||
// This feature flag indicates that new organizations should be automatically onboarded to
|
||||
// Flexible Collections enhancements
|
||||
FlexibleCollections = flexibleCollectionsSignupEnabled,
|
||||
// These collection management settings smooth the migration for existing organizations by disabling some FC behavior.
|
||||
// If the organization is onboarded to Flexible Collections on signup, we turn them OFF to enable all new behaviour.
|
||||
// If the organization is NOT onboarded now, they will have to be migrated later, so they default to ON to limit FC changes on migration.
|
||||
LimitCollectionCreationDeletion = !flexibleCollectionsSignupEnabled,
|
||||
AllowAdminAccessToAllCollectionItems = !flexibleCollectionsV1Enabled
|
||||
};
|
||||
return ToOrganization(newOrg);
|
||||
}
|
||||
|
||||
public Organization ToOrganization(Organization existingOrganization)
|
||||
{
|
||||
existingOrganization.Name = Name;
|
||||
existingOrganization.BusinessName = BusinessName;
|
||||
existingOrganization.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();
|
||||
existingOrganization.PlanType = PlanType.Value;
|
||||
existingOrganization.Plan = Plan;
|
||||
existingOrganization.Seats = Seats;
|
||||
existingOrganization.MaxCollections = MaxCollections;
|
||||
existingOrganization.UsePolicies = UsePolicies;
|
||||
existingOrganization.UseSso = UseSso;
|
||||
existingOrganization.UseKeyConnector = UseKeyConnector;
|
||||
existingOrganization.UseScim = UseScim;
|
||||
existingOrganization.UseGroups = UseGroups;
|
||||
existingOrganization.UseDirectory = UseDirectory;
|
||||
existingOrganization.UseEvents = UseEvents;
|
||||
existingOrganization.UseTotp = UseTotp;
|
||||
existingOrganization.Use2fa = Use2fa;
|
||||
existingOrganization.UseApi = UseApi;
|
||||
existingOrganization.UseSecretsManager = UseSecretsManager;
|
||||
existingOrganization.UseResetPassword = UseResetPassword;
|
||||
existingOrganization.SelfHost = SelfHost;
|
||||
existingOrganization.UsersGetPremium = UsersGetPremium;
|
||||
existingOrganization.UseCustomPermissions = UseCustomPermissions;
|
||||
existingOrganization.MaxStorageGb = MaxStorageGb;
|
||||
existingOrganization.Gateway = Gateway;
|
||||
existingOrganization.GatewayCustomerId = GatewayCustomerId;
|
||||
existingOrganization.GatewaySubscriptionId = GatewaySubscriptionId;
|
||||
existingOrganization.Enabled = Enabled;
|
||||
existingOrganization.LicenseKey = LicenseKey;
|
||||
existingOrganization.ExpirationDate = ExpirationDate;
|
||||
existingOrganization.MaxAutoscaleSeats = MaxAutoscaleSeats;
|
||||
existingOrganization.SmSeats = SmSeats;
|
||||
existingOrganization.MaxAutoscaleSmSeats = MaxAutoscaleSmSeats;
|
||||
existingOrganization.SmServiceAccounts = SmServiceAccounts;
|
||||
existingOrganization.MaxAutoscaleSmServiceAccounts = MaxAutoscaleSmServiceAccounts;
|
||||
return existingOrganization;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Models;
|
||||
|
||||
public class OrganizationSelectableViewModel : Organization
|
||||
{
|
||||
public bool Selected { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Admin.Models;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Models;
|
||||
|
||||
public class OrganizationUnassignedToProviderSearchViewModel : PagedModel<OrganizationSelectableViewModel>
|
||||
{
|
||||
[Display(Name = "Organization Name")]
|
||||
public string OrganizationName { get; set; }
|
||||
|
||||
[Display(Name = "Owner Email")]
|
||||
public string OrganizationOwnerEmail { get; set; }
|
||||
}
|
||||
68
src/Admin/AdminConsole/Models/OrganizationViewModel.cs
Normal file
68
src/Admin/AdminConsole/Models/OrganizationViewModel.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Vault.Entities;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Models;
|
||||
|
||||
public class OrganizationViewModel
|
||||
{
|
||||
public 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 occupiedSmSeatsCount)
|
||||
|
||||
{
|
||||
Organization = org;
|
||||
Provider = provider;
|
||||
Connections = connections ?? Enumerable.Empty<OrganizationConnection>();
|
||||
HasPublicPrivateKeys = org.PublicKey != null && org.PrivateKey != null;
|
||||
UserInvitedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Invited);
|
||||
UserAcceptedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Accepted);
|
||||
UserConfirmedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Confirmed);
|
||||
OccupiedSeatCount = UserInvitedCount + UserAcceptedCount + UserConfirmedCount;
|
||||
CipherCount = ciphers.Count();
|
||||
CollectionCount = collections.Count();
|
||||
GroupCount = groups?.Count() ?? 0;
|
||||
PolicyCount = policies?.Count() ?? 0;
|
||||
var organizationUserStatus = org.Status == OrganizationStatusType.Pending
|
||||
? OrganizationUserStatusType.Invited
|
||||
: OrganizationUserStatusType.Confirmed;
|
||||
Owners = string.Join(", ",
|
||||
orgUsers
|
||||
.Where(u => u.Type == OrganizationUserType.Owner && u.Status == organizationUserStatus)
|
||||
.Select(u => u.Email));
|
||||
Admins = string.Join(", ",
|
||||
orgUsers
|
||||
.Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus)
|
||||
.Select(u => u.Email));
|
||||
SecretsCount = secretsCount;
|
||||
ProjectsCount = projectCount;
|
||||
ServiceAccountsCount = serviceAccountsCount;
|
||||
OccupiedSmSeatsCount = occupiedSmSeatsCount;
|
||||
}
|
||||
|
||||
public Organization Organization { get; set; }
|
||||
public Provider Provider { get; set; }
|
||||
public IEnumerable<OrganizationConnection> Connections { get; set; }
|
||||
public string Owners { get; set; }
|
||||
public string Admins { get; set; }
|
||||
public int UserInvitedCount { get; set; }
|
||||
public int UserConfirmedCount { get; set; }
|
||||
public int UserAcceptedCount { get; set; }
|
||||
public int OccupiedSeatCount { get; set; }
|
||||
public int CipherCount { get; set; }
|
||||
public int CollectionCount { get; set; }
|
||||
public int GroupCount { get; set; }
|
||||
public int PolicyCount { get; set; }
|
||||
public bool HasPublicPrivateKeys { 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;
|
||||
}
|
||||
13
src/Admin/AdminConsole/Models/OrganizationsModel.cs
Normal file
13
src/Admin/AdminConsole/Models/OrganizationsModel.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Bit.Admin.Models;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Models;
|
||||
|
||||
public class OrganizationsModel : PagedModel<Organization>
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string UserEmail { get; set; }
|
||||
public bool? Paid { get; set; }
|
||||
public string Action { get; set; }
|
||||
public bool SelfHosted { get; set; }
|
||||
}
|
||||
35
src/Admin/AdminConsole/Models/ProviderEditModel.cs
Normal file
35
src/Admin/AdminConsole/Models/ProviderEditModel.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Models;
|
||||
|
||||
public class ProviderEditModel : ProviderViewModel
|
||||
{
|
||||
public ProviderEditModel() { }
|
||||
|
||||
public ProviderEditModel(Provider provider, IEnumerable<ProviderUserUserDetails> providerUsers, IEnumerable<ProviderOrganizationOrganizationDetails> organizations)
|
||||
: base(provider, providerUsers, organizations)
|
||||
{
|
||||
Name = provider.Name;
|
||||
BusinessName = provider.BusinessName;
|
||||
BillingEmail = provider.BillingEmail;
|
||||
BillingPhone = provider.BillingPhone;
|
||||
}
|
||||
|
||||
[Display(Name = "Billing Email")]
|
||||
public string BillingEmail { get; set; }
|
||||
[Display(Name = "Billing Phone Number")]
|
||||
public string BillingPhone { get; set; }
|
||||
[Display(Name = "Business Name")]
|
||||
public string BusinessName { get; set; }
|
||||
public string Name { get; set; }
|
||||
[Display(Name = "Events")]
|
||||
|
||||
public Provider ToProvider(Provider existingProvider)
|
||||
{
|
||||
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();
|
||||
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant()?.Trim();
|
||||
return existingProvider;
|
||||
}
|
||||
}
|
||||
24
src/Admin/AdminConsole/Models/ProviderViewModel.cs
Normal file
24
src/Admin/AdminConsole/Models/ProviderViewModel.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Models;
|
||||
|
||||
public class ProviderViewModel
|
||||
{
|
||||
public ProviderViewModel() { }
|
||||
|
||||
public ProviderViewModel(Provider provider, IEnumerable<ProviderUserUserDetails> providerUsers, IEnumerable<ProviderOrganizationOrganizationDetails> organizations)
|
||||
{
|
||||
Provider = provider;
|
||||
UserCount = providerUsers.Count();
|
||||
ProviderAdmins = providerUsers.Where(u => u.Type == ProviderUserType.ProviderAdmin);
|
||||
|
||||
ProviderOrganizations = organizations.Where(o => o.ProviderId == provider.Id);
|
||||
}
|
||||
|
||||
public int UserCount { get; set; }
|
||||
public Provider Provider { get; set; }
|
||||
public IEnumerable<ProviderUserUserDetails> ProviderAdmins { get; set; }
|
||||
public IEnumerable<ProviderOrganizationOrganizationDetails> ProviderOrganizations { get; set; }
|
||||
}
|
||||
13
src/Admin/AdminConsole/Models/ProvidersModel.cs
Normal file
13
src/Admin/AdminConsole/Models/ProvidersModel.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Bit.Admin.Models;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Models;
|
||||
|
||||
public class ProvidersModel : PagedModel<Provider>
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string UserEmail { get; set; }
|
||||
public bool? Paid { get; set; }
|
||||
public string Action { get; set; }
|
||||
public bool SelfHosted { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
@using Bit.Core.Enums
|
||||
@model OrganizationViewModel
|
||||
<h2>Connections</h2>
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 190px;">Type</th>
|
||||
<th style="width: 40px;">Status</th>
|
||||
<th style="width: 30px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if(!Model.Connections.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="6">No results to list.</td>
|
||||
</tr>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach(var connection in Model.Connections)
|
||||
{
|
||||
<tr>
|
||||
<td class="align-middle">
|
||||
@if(connection.Type == OrganizationConnectionType.CloudBillingSync)
|
||||
{
|
||||
@:Billing Sync
|
||||
}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
@if(@TempData["ConnectionError"] != null)
|
||||
{
|
||||
<span class="text-danger">
|
||||
@TempData["ConnectionError"]
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if(connection.Enabled)
|
||||
{
|
||||
@:Enabled
|
||||
}
|
||||
else
|
||||
{
|
||||
@:Disabled
|
||||
}
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if(connection.Enabled)
|
||||
{
|
||||
@if(@TempData["ConnectionActivated"] != null && @TempData["ConnectionActivated"].ToString() == @Model.Organization.Id.ToString())
|
||||
{
|
||||
@if(connection.Type.Equals(OrganizationConnectionType.CloudBillingSync))
|
||||
{
|
||||
<button class="btn btn-outline-success btn-sm disabled" disabled>Billing Synced!</button>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@if(connection.Type.Equals(OrganizationConnectionType.CloudBillingSync))
|
||||
{
|
||||
<a class="btn btn-outline-secondary btn-sm"
|
||||
data-id="@connection.Id" asp-controller="Organizations"
|
||||
asp-action="TriggerBillingSync" asp-route-id="@Model.Organization.Id">
|
||||
Manually Sync
|
||||
</a>
|
||||
}
|
||||
}
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
114
src/Admin/AdminConsole/Views/Organizations/Edit.cshtml
Normal file
114
src/Admin/AdminConsole/Views/Organizations/Edit.cshtml
Normal file
@@ -0,0 +1,114 @@
|
||||
@using Bit.Admin.Enums;
|
||||
@using Bit.Admin.Models
|
||||
@using Bit.Core.Enums
|
||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||
@model OrganizationEditModel
|
||||
@{
|
||||
ViewData["Title"] = (Model.Provider != null ? "Client " : string.Empty) + "Organization: " + Model.Organization.Name;
|
||||
|
||||
var canViewOrganizationInformation = AccessControlService.UserHasPermission(Permission.Org_OrgInformation_View);
|
||||
var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.Org_BillingInformation_View);
|
||||
var canInitiateTrial = AccessControlService.UserHasPermission(Permission.Org_InitiateTrial);
|
||||
var canDelete = AccessControlService.UserHasPermission(Permission.Org_Delete);
|
||||
var canUnlinkFromProvider = AccessControlService.UserHasPermission(Permission.Provider_Edit);
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("~/AdminConsole/Views/Shared/_OrganizationFormScripts.cshtml")
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
document.getElementById('teams-trial').addEventListener('click', () => {
|
||||
if (document.getElementById('@(nameof(Model.PlanType))').value !== '@((byte)PlanType.Free)') {
|
||||
alert('Organization is not on a free plan.');
|
||||
return;
|
||||
}
|
||||
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)PlanType.Free)') {
|
||||
alert('Organization is not on a free plan.');
|
||||
return;
|
||||
}
|
||||
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>
|
||||
}
|
||||
|
||||
<h1>@(Model.Provider != null ? "Client " : string.Empty)Organization <small>@Model.Organization.Name</small></h1>
|
||||
|
||||
@if (Model.Provider != null)
|
||||
{
|
||||
<h2>Provider Relationship</h2>
|
||||
@await Html.PartialAsync("_ProviderInformation", Model.Provider)
|
||||
}
|
||||
|
||||
@if (canViewOrganizationInformation)
|
||||
{
|
||||
<h2>Organization Information</h2>
|
||||
@await Html.PartialAsync("_ViewInformation", Model)
|
||||
}
|
||||
|
||||
@if (canViewBillingInformation)
|
||||
{
|
||||
<h2>Billing Information</h2>
|
||||
@await Html.PartialAsync("_BillingInformation",
|
||||
new BillingInformationModel { BillingInfo = Model.BillingInfo, OrganizationId = Model.Organization.Id, Entity = "Organization" })
|
||||
}
|
||||
|
||||
@await Html.PartialAsync("~/AdminConsole/Views/Shared/_OrganizationForm.cshtml", Model)
|
||||
|
||||
<div class="d-flex mt-4">
|
||||
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
|
||||
<div class="ml-auto d-flex">
|
||||
@if (canInitiateTrial && Model.Provider is null)
|
||||
{
|
||||
<button class="btn btn-secondary mr-2" type="button" id="teams-trial">
|
||||
Teams Trial
|
||||
</button>
|
||||
<button class="btn btn-secondary mr-2" type="button" id="enterprise-trial">
|
||||
Enterprise Trial
|
||||
</button>
|
||||
}
|
||||
@if (canUnlinkFromProvider && Model.Provider is not null)
|
||||
{
|
||||
<button
|
||||
class="btn btn-outline-danger mr-2"
|
||||
onclick="return unlinkProvider('@Model.Organization.Id');"
|
||||
>
|
||||
Unlink provider
|
||||
</button>
|
||||
}
|
||||
@if (canDelete)
|
||||
{
|
||||
<form asp-action="Delete" asp-route-id="@Model.Organization.Id"
|
||||
onsubmit="return confirm('Are you sure you want to delete this organization?')">
|
||||
<button class="btn btn-danger" type="submit">Delete</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
140
src/Admin/AdminConsole/Views/Organizations/Index.cshtml
Normal file
140
src/Admin/AdminConsole/Views/Organizations/Index.cshtml
Normal file
@@ -0,0 +1,140 @@
|
||||
@model OrganizationsModel
|
||||
@{
|
||||
ViewData["Title"] = "Organizations";
|
||||
}
|
||||
|
||||
<h1>Organizations</h1>
|
||||
|
||||
<form class="form-inline mb-2" method="get">
|
||||
<label class="sr-only" asp-for="Name">Name</label>
|
||||
<input type="text" class="form-control mb-2 mr-2" placeholder="Name" asp-for="Name" name="name">
|
||||
<label class="sr-only" asp-for="UserEmail">User email</label>
|
||||
<input type="text" class="form-control mb-2 mr-2" placeholder="User email" asp-for="UserEmail" name="userEmail">
|
||||
@if(!Model.SelfHosted)
|
||||
{
|
||||
<label class="sr-only" asp-for="Paid">Customer</label>
|
||||
<select class="form-control mb-2 mr-2" asp-for="Paid" name="paid">
|
||||
<option asp-selected="!Model.Paid.HasValue" value="">-- Customer --</option>
|
||||
<option asp-selected="Model.Paid.GetValueOrDefault(false)" value="true">Paid</option>
|
||||
<option asp-selected="!Model.Paid.GetValueOrDefault(true)" value="false">Freeloader</option>
|
||||
</select>
|
||||
}
|
||||
<button type="submit" class="btn btn-primary mb-2" title="Search"><i class="fa fa-search"></i> Search</button>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th style="width: 190px;">Plan</th>
|
||||
<th style="width: 80px;">Seats</th>
|
||||
<th style="width: 150px;">Created</th>
|
||||
<th style="width: 170px; min-width: 170px;">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if(!Model.Items.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="5">No results to list.</td>
|
||||
</tr>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach(var org in Model.Items)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<a asp-action="@Model.Action" asp-route-id="@org.Id">@org.Name</a>
|
||||
</td>
|
||||
<td>
|
||||
@org.Plan
|
||||
</td>
|
||||
<td>
|
||||
@org.Seats
|
||||
</td>
|
||||
<td>
|
||||
<span title="@org.CreationDate.ToString()">
|
||||
@org.CreationDate.ToShortDateString()
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
@if(!Model.SelfHosted)
|
||||
{
|
||||
if(!string.IsNullOrWhiteSpace(org.GatewaySubscriptionId))
|
||||
{
|
||||
<i class="fa fa-usd fa-lg fa-fw" title="Paid"></i>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fa fa-smile-o fa-lg fa-fw text-muted" title="Freeloader"></i>
|
||||
}
|
||||
}
|
||||
@if(org.MaxStorageGb.HasValue && org.MaxStorageGb > 1)
|
||||
{
|
||||
<i class="fa fa-plus-square fa-lg fa-fw"
|
||||
title="Additional Storage, @(org.MaxStorageGb - 1) GB"></i>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fa fa-plus-square-o fa-lg fa-fw text-muted"
|
||||
title="No Additional Storage"></i>
|
||||
}
|
||||
@if(org.Enabled)
|
||||
{
|
||||
<i class="fa fa-check-circle fa-lg fa-fw"
|
||||
title="Enabled, expires @(org.ExpirationDate?.ToShortDateString() ?? "-")"></i>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fa fa-times-circle-o fa-lg fa-fw text-muted" title="Disabled"></i>
|
||||
}
|
||||
@if(org.TwoFactorIsEnabled())
|
||||
{
|
||||
<i class="fa fa-lock fa-lg fa-fw" title="2FA Enabled"></i>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fa fa-unlock fa-lg fa-fw text-muted" title="2FA Not Enabled"></i>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<ul class="pagination">
|
||||
@if(Model.PreviousPage.HasValue)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" asp-action="Index" asp-route-page="@Model.PreviousPage.Value"
|
||||
asp-route-count="@Model.Count" asp-route-userEmail="@Model.UserEmail"
|
||||
asp-route-name="@Model.Name" asp-route-paid="@Model.Paid">Previous</a>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
}
|
||||
@if(Model.NextPage.HasValue)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" asp-action="Index" asp-route-page="@Model.NextPage.Value"
|
||||
asp-route-count="@Model.Count" asp-route-userEmail="@Model.UserEmail"
|
||||
asp-route-name="@Model.Name" asp-route-paid="@Model.Paid">Next</a>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1">Next</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
23
src/Admin/AdminConsole/Views/Organizations/View.cshtml
Normal file
23
src/Admin/AdminConsole/Views/Organizations/View.cshtml
Normal file
@@ -0,0 +1,23 @@
|
||||
@inject Bit.Core.Settings.GlobalSettings GlobalSettings
|
||||
@model OrganizationViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Organization: " + Model.Organization.Name;
|
||||
}
|
||||
|
||||
<h1>Organization <small>@Model.Organization.Name</small></h1>
|
||||
|
||||
@if (Model.Provider != null)
|
||||
{
|
||||
<h2>Provider Relationship</h2>
|
||||
@await Html.PartialAsync("_ProviderInformation", Model.Provider)
|
||||
}
|
||||
<h2>Information</h2>
|
||||
@await Html.PartialAsync("_ViewInformation", Model)
|
||||
@if(GlobalSettings.SelfHosted)
|
||||
{
|
||||
@await Html.PartialAsync("Connections", Model)
|
||||
}
|
||||
<form asp-action="Delete" asp-route-id="@Model.Organization.Id"
|
||||
onsubmit="return confirm('Are you sure you want to delete this organization?')">
|
||||
<button class="btn btn-danger" type="submit">Delete</button>
|
||||
</form>
|
||||
@@ -0,0 +1,9 @@
|
||||
@using Bit.SharedWeb.Utilities
|
||||
@model Bit.Core.AdminConsole.Entities.Provider.Provider
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4 col-lg-3">Provider Name</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.Name</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Provider Type</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@(Model.Type.GetDisplayAttribute()?.GetName())</dd>
|
||||
</dl>
|
||||
@@ -0,0 +1,61 @@
|
||||
@model OrganizationViewModel
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4 col-lg-3">Id</dt>
|
||||
<dd class="col-sm-8 col-lg-9"><code>@Model.Organization.Id</code></dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Plan</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.Organization.Plan</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Expires</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@(Model.Organization.ExpirationDate?.ToString() ?? "-")</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Users</dt>
|
||||
<dd class="col-sm-8 col-lg-9">
|
||||
@Model.OccupiedSeatCount / @(Model.Organization.Seats?.ToString() ?? "-")
|
||||
(<span title="Invited">@Model.UserInvitedCount</span> /
|
||||
<span title="Accepted">@Model.UserAcceptedCount</span> /
|
||||
<span title="Confirmed">@Model.UserConfirmedCount</span>)
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Owners</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@(string.IsNullOrWhiteSpace(Model.Owners) ? "None" : Model.Owners)</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Admins</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@(string.IsNullOrWhiteSpace(Model.Admins) ? "None" : Model.Admins)</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Using 2FA</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@(Model.Organization.TwoFactorIsEnabled() ? "Yes" : "No")</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Items</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.CipherCount</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Collections</dt>
|
||||
<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.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.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.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.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>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Policies</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.PolicyCount</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Public/Private Keys</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@(Model.HasPublicPrivateKeys ? "Yes" : "No")</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Created</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.Organization.CreationDate.ToString()</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Modified</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.Organization.RevisionDate.ToString()</dd>
|
||||
</dl>
|
||||
@@ -0,0 +1,96 @@
|
||||
@using Bit.SharedWeb.Utilities
|
||||
@model OrganizationUnassignedToProviderSearchViewModel
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Add Existing Organization";
|
||||
var providerId = ViewContext.RouteData.Values["id"];
|
||||
}
|
||||
|
||||
<h1>Add Existing Organization</h1>
|
||||
<div class="row mb-2">
|
||||
<div class="col">
|
||||
<form class="form-inline mb-2" method="get" asp-route-id="@providerId">
|
||||
<label class="sr-only" asp-for="OrganizationName"></label>
|
||||
<input type="text" class="form-control mb-2 mr-2 flex-fill" placeholder="@Html.DisplayNameFor(m => m.OrganizationName)" asp-for="OrganizationName" name="name">
|
||||
<label class="sr-only" asp-for="OrganizationOwnerEmail"></label>
|
||||
<input type="email" class="form-control mb-2 mr-2 flex-fill" placeholder="@Html.DisplayNameFor(m => m.OrganizationOwnerEmail)" asp-for="OrganizationOwnerEmail" name="ownerEmail">
|
||||
<button type="submit" class="btn btn-primary mb-2" title="Search" formmethod="get"><i class="fa fa-search"></i> Search</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" id="select-form" asp-route-id="@providerId">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 20px;">All</th>
|
||||
<th>Name</th>
|
||||
<th style="width: 190px;">Plan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (!Model.Items.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="5">No results to list.</td>
|
||||
</tr>
|
||||
}
|
||||
else
|
||||
{
|
||||
@for (var i = 0; i < Model.Items.Count; i++)
|
||||
{
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
@Html.HiddenFor(m => Model.Items[i].Id, new { @readonly = "readonly", autocomplete = "off" })
|
||||
@Html.CheckBoxFor(m => Model.Items[i].Selected)
|
||||
</td>
|
||||
<td>@Html.ActionLink(Model.Items[i].Name, "Edit", "Organizations", new { id = Model.Items[i].Id }, new { target = "_blank" })</td>
|
||||
<td>@(Model.Items[i].PlanType.GetDisplayAttribute()?.Name ?? Model.Items[i].PlanType.ToString())</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<nav>
|
||||
<ul class="pagination">
|
||||
@if (Model.PreviousPage.HasValue)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" asp-action="AddExistingOrganization" asp-route-id="@providerId" asp-route-page="@Model.PreviousPage.Value"
|
||||
asp-route-count="@Model.Count" asp-route-ownerEmail="@Model.OrganizationOwnerEmail"
|
||||
asp-route-name="@Model.OrganizationName">Previous</a>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
}
|
||||
@if (Model.NextPage.HasValue)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" asp-action="AddExistingOrganization" asp-route-id="@providerId" asp-route-page="@Model.NextPage.Value"
|
||||
asp-route-count="@Model.Count" asp-route-ownerEmail="@Model.OrganizationOwnerEmail"
|
||||
asp-route-name="@Model.OrganizationName">Next</a>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1">Next</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="submit" class="btn btn-primary" form="select-form">Add to Reseller</button>
|
||||
</div>
|
||||
</div>
|
||||
67
src/Admin/AdminConsole/Views/Providers/Admins.cshtml
Normal file
67
src/Admin/AdminConsole/Views/Providers/Admins.cshtml
Normal file
@@ -0,0 +1,67 @@
|
||||
@using Bit.Admin.Enums;
|
||||
@using Bit.Core.AdminConsole.Enums.Provider
|
||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||
@model ProviderViewModel
|
||||
|
||||
@{
|
||||
var canResendEmailInvite = AccessControlService.UserHasPermission(Permission.Provider_ResendEmailInvite);
|
||||
}
|
||||
|
||||
<h2>Provider Admins</h2>
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 190px;">Email</th>
|
||||
<th style="width: 40px;">Status</th>
|
||||
<th style="width: 30px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if(!Model.ProviderAdmins.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="6">No results to list.</td>
|
||||
</tr>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach(var admin in Model.ProviderAdmins)
|
||||
{
|
||||
<tr>
|
||||
<td class="align-middle">
|
||||
@admin.Email
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
@admin.Status
|
||||
</td>
|
||||
<td>
|
||||
@if(admin.Status.Equals(ProviderUserStatusType.Confirmed)
|
||||
&& @Model.Provider.Status.Equals(ProviderStatusType.Pending)
|
||||
&& canResendEmailInvite)
|
||||
{
|
||||
@if(@TempData["InviteResentTo"] != null && @TempData["InviteResentTo"].ToString() == @admin.UserId.Value.ToString())
|
||||
{
|
||||
<button class="btn btn-outline-success btn-sm disabled" disabled>Invite Resent!</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a class="btn btn-outline-secondary btn-sm"
|
||||
data-id="@admin.Id" asp-controller="Providers"
|
||||
asp-action="ResendInvite" asp-route-ownerId="@admin.UserId"
|
||||
asp-route-providerId="@Model.Provider.Id">
|
||||
Resend Setup Invite
|
||||
</a>
|
||||
}
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
61
src/Admin/AdminConsole/Views/Providers/Create.cshtml
Normal file
61
src/Admin/AdminConsole/Views/Providers/Create.cshtml
Normal file
@@ -0,0 +1,61 @@
|
||||
@using Bit.SharedWeb.Utilities
|
||||
@using Bit.Core.AdminConsole.Enums.Provider
|
||||
@model CreateProviderModel
|
||||
@{
|
||||
ViewData["Title"] = "Create Provider";
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function toggleProviderTypeInfo(value) {
|
||||
document.querySelectorAll('[id^="info-"]').forEach(el => { el.classList.add('d-none'); });
|
||||
document.getElementById('info-' + value).classList.remove('d-none');
|
||||
}
|
||||
</script>
|
||||
}
|
||||
|
||||
<h1>Create Provider</h1>
|
||||
|
||||
<form method="post">
|
||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Type" class="h2"></label>
|
||||
@foreach(ProviderType providerType in Enum.GetValues(typeof(ProviderType)))
|
||||
{
|
||||
var providerTypeValue = (int)providerType;
|
||||
<div class="form-check">
|
||||
@Html.RadioButtonFor(m => m.Type, providerType, new { id = $"providerType-{providerTypeValue}", @class = "form-check-input", onclick=$"toggleProviderTypeInfo({providerTypeValue})" })
|
||||
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetName(), new { @class = "form-check-label align-middle", @for = $"providerType-{providerTypeValue}" })
|
||||
<br/>
|
||||
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetDescription(), new { @class = "form-check-label small text-muted ml-3 align-top", @for = $"providerType-{providerTypeValue}" })
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div id="@($"info-{(int)ProviderType.Msp}")" class="form-group @(Model.Type != ProviderType.Msp ? "d-none" : string.Empty)">
|
||||
<h2>MSP Info</h2>
|
||||
<div class="form-group">
|
||||
<label asp-for="OwnerEmail"></label>
|
||||
<input type="text" class="form-control" asp-for="OwnerEmail">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="@($"info-{(int)ProviderType.Reseller}")" class="form-group @(Model.Type != ProviderType.Reseller ? "d-none" : string.Empty)">
|
||||
<h2>Reseller Info</h2>
|
||||
<div class="form-group">
|
||||
<label asp-for="Name"></label>
|
||||
<input type="text" class="form-control" asp-for="Name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="BusinessName"></label>
|
||||
<input type="text" class="form-control" asp-for="BusinessName">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="BillingEmail"></label>
|
||||
<input type="text" class="form-control" asp-for="BillingEmail">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
||||
</form>
|
||||
@@ -0,0 +1,27 @@
|
||||
@model OrganizationEditModel
|
||||
@{
|
||||
ViewData["Title"] = "Create Client Organization";
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("~/AdminConsole/Views/Shared/_OrganizationFormScripts.cshtml")
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
togglePlanFeatures('@((byte)Model.PlanType)');
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
|
||||
<h1>New Client Organization</h1>
|
||||
|
||||
@await Html.PartialAsync("~/AdminConsole/Views/Shared/_OrganizationForm.cshtml", Model)
|
||||
<div class="d-flex mt-4">
|
||||
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
|
||||
<div class="ml-auto d-flex">
|
||||
<form asp-controller="Providers" asp-action="Edit" asp-route-id="@Model.Provider.Id"
|
||||
onsubmit="return confirm('Are you sure you want to cancel?')">
|
||||
<button class="btn btn-outline-secondary" type="submit">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
52
src/Admin/AdminConsole/Views/Providers/Edit.cshtml
Normal file
52
src/Admin/AdminConsole/Views/Providers/Edit.cshtml
Normal file
@@ -0,0 +1,52 @@
|
||||
@using Bit.Admin.Enums;
|
||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||
|
||||
@model ProviderEditModel
|
||||
@{
|
||||
ViewData["Title"] = "Provider: " + Model.Provider.Name;
|
||||
|
||||
var canEdit = AccessControlService.UserHasPermission(Permission.Provider_Edit);
|
||||
}
|
||||
|
||||
<h1>Provider <small>@Model.Provider.Name</small></h1>
|
||||
|
||||
<h2>Provider Information</h2>
|
||||
@await Html.PartialAsync("_ViewInformation", Model)
|
||||
@await Html.PartialAsync("Admins", Model)
|
||||
<form method="post" id="edit-form">
|
||||
<h2>General</h2>
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4 col-lg-3">Name</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.Provider.Name</dd>
|
||||
</dl>
|
||||
<h2>Business Information</h2>
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4 col-lg-3">Business Name</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.Provider.BusinessName</dd>
|
||||
</dl>
|
||||
<h2>Billing</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="BillingEmail"></label>
|
||||
<input type="email" class="form-control" asp-for="BillingEmail" readonly='@(!canEdit)'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="BillingPhone"></label>
|
||||
<input type="tel" class="form-control" asp-for="BillingPhone">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@await Html.PartialAsync("Organizations", Model)
|
||||
@if (canEdit)
|
||||
{
|
||||
<div class="d-flex mt-4">
|
||||
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
102
src/Admin/AdminConsole/Views/Providers/Index.cshtml
Normal file
102
src/Admin/AdminConsole/Views/Providers/Index.cshtml
Normal file
@@ -0,0 +1,102 @@
|
||||
@using Bit.SharedWeb.Utilities
|
||||
@using Bit.Admin.Enums;
|
||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||
|
||||
@model ProvidersModel
|
||||
@{
|
||||
ViewData["Title"] = "Providers";
|
||||
|
||||
var canCreateProvider = AccessControlService.UserHasPermission(Permission.Provider_Create);
|
||||
}
|
||||
|
||||
<h1>Providers</h1>
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col">
|
||||
<form class="form-inline mb-2" method="get">
|
||||
<label class="sr-only" asp-for="Name">Name</label>
|
||||
<input type="text" class="form-control mb-2 mr-2" placeholder="Name" asp-for="Name" name="name">
|
||||
<label class="sr-only" asp-for="UserEmail">User email</label>
|
||||
<input type="text" class="form-control mb-2 mr-2" placeholder="User email" asp-for="UserEmail" name="userEmail">
|
||||
<button type="submit" class="btn btn-primary mb-2" title="Search"><i class="fa fa-search"></i> Search</button>
|
||||
</form>
|
||||
</div>
|
||||
@if (canCreateProvider)
|
||||
{
|
||||
<div class="col-auto">
|
||||
<a asp-action="Create" class="btn btn-secondary">Create Provider</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th style="width: 190px;">Provider Type</th>
|
||||
<th style="width: 190px;">Status</th>
|
||||
<th style="width: 150px;">Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if(!Model.Items.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="5">No results to list.</td>
|
||||
</tr>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach(var provider in Model.Items)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<a asp-action="@Model.Action" asp-route-id="@provider.Id">@(provider.Name ?? "Pending")</a>
|
||||
</td>
|
||||
<td>@provider.Type.GetDisplayAttribute()?.GetShortName()</td>
|
||||
<td>@provider.Status</td>
|
||||
<td>
|
||||
<span title="@provider.CreationDate.ToString()">
|
||||
@provider.CreationDate.ToShortDateString()
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<ul class="pagination">
|
||||
@if(Model.PreviousPage.HasValue)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" asp-action="Index" asp-route-page="@Model.PreviousPage.Value"
|
||||
asp-route-count="@Model.Count" asp-route-userEmail="@Model.UserEmail"
|
||||
asp-route-name="@Model.Name" asp-route-paid="@Model.Paid">Previous</a>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
}
|
||||
@if(Model.NextPage.HasValue)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" asp-action="Index" asp-route-page="@Model.NextPage.Value"
|
||||
asp-route-count="@Model.Count" asp-route-userEmail="@Model.UserEmail"
|
||||
asp-route-name="@Model.Name" asp-route-paid="@Model.Paid">Next</a>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1">Next</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
76
src/Admin/AdminConsole/Views/Providers/Organizations.cshtml
Normal file
76
src/Admin/AdminConsole/Views/Providers/Organizations.cshtml
Normal file
@@ -0,0 +1,76 @@
|
||||
@using Bit.Core.AdminConsole.Enums.Provider
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using Bit.Admin.Enums
|
||||
@using Bit.Core.Enums
|
||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||
@model ProviderViewModel
|
||||
|
||||
@{
|
||||
var canUnlinkFromProvider = AccessControlService.UserHasPermission(Permission.Provider_Edit);
|
||||
}
|
||||
|
||||
@await Html.PartialAsync("_ProviderScripts")
|
||||
@await Html.PartialAsync("_ProviderOrganizationScripts")
|
||||
|
||||
<h2>Provider Organizations</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50%;">Name</th>
|
||||
<th style="width: 50%;">Status</th>
|
||||
<th>
|
||||
@if (Model.Provider.Type == ProviderType.Reseller)
|
||||
{
|
||||
<div class="float-right text-nowrap">
|
||||
<a asp-controller="Providers" asp-action="CreateOrganization" asp-route-providerId="@Model.Provider.Id" class="btn btn-sm btn-primary">New Organization</a>
|
||||
<a asp-controller="Providers" asp-action="AddExistingOrganization" asp-route-id="@Model.Provider.Id" class="btn btn-sm btn-outline-primary">Add Existing Organization</a>
|
||||
</div>
|
||||
}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (!Model.ProviderOrganizations.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="6">No results to list.</td>
|
||||
</tr>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var providerOrganization in Model.ProviderOrganizations)
|
||||
{
|
||||
<tr>
|
||||
<td class="align-middle">
|
||||
<a asp-controller="Organizations" asp-action="Edit" asp-route-id="@providerOrganization.OrganizationId">@providerOrganization.OrganizationName</a>
|
||||
</td>
|
||||
<td>
|
||||
@providerOrganization.Status
|
||||
</td>
|
||||
<td>
|
||||
<div class="float-right">
|
||||
@if (canUnlinkFromProvider)
|
||||
{
|
||||
<a href="#" class="text-danger float-right" onclick="return unlinkProvider('@Model.Provider.Id', '@providerOrganization.Id');">
|
||||
Unlink provider
|
||||
</a>
|
||||
}
|
||||
@if (providerOrganization.Status == OrganizationStatusType.Pending)
|
||||
{
|
||||
<a href="#" class="float-right mr-3" onclick="return resendOwnerInvite('@providerOrganization.OrganizationId');">
|
||||
Resend invitation
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
11
src/Admin/AdminConsole/Views/Providers/View.cshtml
Normal file
11
src/Admin/AdminConsole/Views/Providers/View.cshtml
Normal file
@@ -0,0 +1,11 @@
|
||||
@model ProviderViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Provider: " + Model.Provider.Name;
|
||||
}
|
||||
|
||||
<h1>Provider <small>@Model.Provider.Name</small></h1>
|
||||
|
||||
<h2>Information</h2>
|
||||
@await Html.PartialAsync("_ViewInformation", Model)
|
||||
@await Html.PartialAsync("Admins", Model)
|
||||
@await Html.PartialAsync("Organizations", Model)
|
||||
@@ -0,0 +1,21 @@
|
||||
<script>
|
||||
function unlinkProvider(providerId, id) {
|
||||
if (confirm('Are you sure you want to unlink this organization from its provider?')) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: `@Url.Action("Delete", "ProviderOrganizations")?providerId=${providerId}&id=${id}`,
|
||||
dataType: 'json',
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: function (response) {
|
||||
alert("Successfully unlinked provider");
|
||||
window.location.href = `@Url.Action("Edit", "Providers")?id=${providerId}`;
|
||||
},
|
||||
error: function (response) {
|
||||
alert("Error!");
|
||||
}
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script>
|
||||
function resendOwnerInvite(orgId) {
|
||||
if (confirm('Resend invite to organization?')) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: '@Url.Action("ResendOwnerInvite", "Organizations")' + '?id=' + orgId,
|
||||
dataType: 'json',
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: function (response) {
|
||||
alert('Invitation has been resent!');
|
||||
},
|
||||
error: function (response) {
|
||||
alert("Error!");
|
||||
}
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,22 @@
|
||||
@using Bit.SharedWeb.Utilities
|
||||
@using Bit.Core.AdminConsole.Enums.Provider
|
||||
@model ProviderViewModel
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4 col-lg-3">Id</dt>
|
||||
<dd class="col-sm-8 col-lg-9"><code>@Model.Provider.Id</code></dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Status</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.Provider.Status</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Users</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@(Model.Provider.Type == ProviderType.Reseller ? "N/A" : Model.UserCount)</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Provider Type</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@(Model.Provider.Type.GetDisplayAttribute()?.GetName())</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Created</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.Provider.CreationDate.ToString()</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Modified</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.Provider.RevisionDate.ToString()</dd>
|
||||
</dl>
|
||||
326
src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml
Normal file
326
src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml
Normal file
@@ -0,0 +1,326 @@
|
||||
@using Bit.Admin.Enums;
|
||||
@using Bit.Core.Enums
|
||||
@using Bit.Core.AdminConsole.Enums.Provider
|
||||
@using Bit.SharedWeb.Utilities
|
||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService;
|
||||
|
||||
@model OrganizationEditModel
|
||||
|
||||
@{
|
||||
var canViewGeneralDetails = AccessControlService.UserHasPermission(Permission.Org_GeneralDetails_View);
|
||||
var canViewBilling = AccessControlService.UserHasPermission(Permission.Org_Billing_View);
|
||||
var canViewBusinessInformation = AccessControlService.UserHasPermission(Permission.Org_BusinessInformation_View);
|
||||
var canViewPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_View);
|
||||
var canViewLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_View);
|
||||
var canCheckEnabled = AccessControlService.UserHasPermission(Permission.Org_CheckEnabledBox);
|
||||
var canEditPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_Edit);
|
||||
var canEditLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_Edit);
|
||||
var canEditBilling = AccessControlService.UserHasPermission(Permission.Org_Billing_Edit);
|
||||
var canLaunchGateway = AccessControlService.UserHasPermission(Permission.Org_Billing_LaunchGateway);
|
||||
}
|
||||
|
||||
<form method="post" id="edit-form" asp-route-providerId="@Model.Provider?.Id">
|
||||
<input asp-for="SalesAssistedTrialStarted" type="hidden">
|
||||
@if (canViewGeneralDetails)
|
||||
{
|
||||
<h2>General</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="Name"></label>
|
||||
<input type="text" class="form-control" asp-for="Name" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.Provider?.Type == ProviderType.Reseller)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label>Client Owner Email</label>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Owners))
|
||||
{
|
||||
<input type="text" class="form-control" asp-for="Owners" readonly="readonly">
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="text" class="form-control" asp-for="Owners" required>
|
||||
}
|
||||
<label class="form-check-label small text-muted align-top">This user should be independent of the Provider. If the Provider is disassociated with the organization, this user will maintain ownership of the organization.</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (Model.Organization != null)
|
||||
{
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" class="form-check-input" asp-for="Enabled" disabled='@(canCheckEnabled ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="Enabled"></label>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (canViewBusinessInformation)
|
||||
{
|
||||
<h2>Business Information</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="BusinessName"></label>
|
||||
<input type="text" class="form-control" asp-for="BusinessName">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (canViewPlan)
|
||||
{
|
||||
<h2>Plan</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="PlanType"></label>
|
||||
@{
|
||||
var planTypes = Enum.GetValues<PlanType>()
|
||||
.Where(p =>
|
||||
Model.Provider == null ||
|
||||
(Model.Provider != null
|
||||
&& p is >= PlanType.TeamsMonthly2019 and <= PlanType.EnterpriseAnnually2019 or >= PlanType.TeamsMonthly2020 and <= PlanType.EnterpriseAnnually)
|
||||
)
|
||||
.Select(e => new SelectListItem
|
||||
{
|
||||
Value = ((int)e).ToString(),
|
||||
Text = e.GetDisplayAttribute()?.GetName() ?? e.ToString()
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
<select class="form-control" asp-for="PlanType" asp-items="planTypes" disabled='@(canEditPlan ? null : "disabled")'></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="Plan"></label>
|
||||
<input type="text" class="form-control" asp-for="Plan" required readonly='@(!canEditPlan)'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Features</h2>
|
||||
<div class="row mb-3">
|
||||
<div class="col-4">
|
||||
<h3>General</h3>
|
||||
<div class="form-check mb-2">
|
||||
<input type="checkbox" class="form-check-input" asp-for="SelfHost" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="SelfHost"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="Use2fa" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="Use2fa"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseApi" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UseApi"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseGroups" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UseGroups"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UsePolicies" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UsePolicies"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseSso" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UseSso"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseKeyConnector" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UseKeyConnector"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseScim" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UseScim"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseDirectory" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UseDirectory"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseEvents" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UseEvents"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseResetPassword" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UseResetPassword"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseCustomPermissions" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UseCustomPermissions"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<h3>Password Manager</h3>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseTotp" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UseTotp"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UsersGetPremium" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UsersGetPremium"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<h3>Secrets Manager</h3>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseSecretsManager" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UseSecretsManager"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (canViewPlan)
|
||||
{
|
||||
<h2>Password Manager Configuration</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="Seats"></label>
|
||||
<input type="number" class="form-control" asp-for="Seats" min="1" readonly='@(!canEditPlan)'>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="MaxCollections"></label>
|
||||
<input type="number" class="form-control" asp-for="MaxCollections" min="1" readonly='@(!canEditPlan)'>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="MaxStorageGb"></label>
|
||||
<input type="number" class="form-control" asp-for="MaxStorageGb" min="1" readonly='@(!canEditPlan)'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<div class="form-group">
|
||||
<label asp-for="MaxAutoscaleSeats"></label>
|
||||
<input type="number" class="form-control" asp-for="MaxAutoscaleSeats" min="1" readonly='@(!canEditPlan)'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (canViewPlan)
|
||||
{
|
||||
<div id="organization-secrets-configuration" hidden="@(!Model.UseSecretsManager)">
|
||||
<h2>Secrets Manager Configuration</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="SmSeats"></label>
|
||||
<input type="number" class="form-control" asp-for="SmSeats" min="1" readonly='@(!canEditPlan)'>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="MaxAutoscaleSmSeats"></label>
|
||||
<input type="number" class="form-control" asp-for="MaxAutoscaleSmSeats" min="1" readonly='@(!canEditPlan)'>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="SmServiceAccounts"></label>
|
||||
<input type="number" class="form-control" asp-for="SmServiceAccounts" min="1" readonly='@(!canEditPlan)'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<div class="form-group">
|
||||
<label asp-for="MaxAutoscaleSmServiceAccounts"></label>
|
||||
<input type="number" class="form-control" asp-for="MaxAutoscaleSmServiceAccounts" min="1" readonly='@(!canEditPlan)'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if(canViewLicensing)
|
||||
{
|
||||
<h2>Licensing</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="LicenseKey"></label>
|
||||
<input type="text" class="form-control" asp-for="LicenseKey" readonly='@(!canEditLicensing)'>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="ExpirationDate"></label>
|
||||
<input type="datetime-local" class="form-control" asp-for="ExpirationDate" readonly='@(!canEditLicensing)' step="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (canViewBilling)
|
||||
{
|
||||
<h2>Billing</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="BillingEmail"></label>
|
||||
<input type="email" class="form-control" asp-for="BillingEmail" readonly="readonly">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<div class="form-group">
|
||||
<label asp-for="Gateway"></label>
|
||||
<select class="form-control" asp-for="Gateway" disabled='@(canEditBilling ? null : "disabled")'
|
||||
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="GatewayCustomerId"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewayCustomerId" readonly='@(!canEditBilling)'>
|
||||
@if(canLaunchGateway)
|
||||
{
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-secondary" type="button" id="gateway-customer-link">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="GatewaySubscriptionId"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewaySubscriptionId" readonly='@(!canEditBilling)'>
|
||||
@if (canLaunchGateway)
|
||||
{
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-secondary" type="button" id="gateway-subscription-link">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
@@ -0,0 +1,154 @@
|
||||
@inject IWebHostEnvironment HostingEnvironment
|
||||
@using Bit.Admin.Utilities
|
||||
@using Bit.Core.Enums
|
||||
@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;
|
||||
togglePlanFeatures(selectEl.options[selectEl.selectedIndex].value);
|
||||
});
|
||||
document.getElementById('gateway-customer-link')?.addEventListener('click', () => {
|
||||
const gateway = document.getElementById('@(nameof(Model.Gateway))');
|
||||
const customerId = document.getElementById('@(nameof(Model.GatewayCustomerId))');
|
||||
if (!gateway || gateway.value === '' || !customerId || customerId.value === '') {
|
||||
return;
|
||||
}
|
||||
if (gateway.value === '@((byte)GatewayType.Stripe)') {
|
||||
const url = `@(HostingEnvironment.GetStripeUrl())/customers/${customerId.value}/`;
|
||||
window.open(url, '_blank');
|
||||
} else if (gateway.value === '@((byte)GatewayType.Braintree)') {
|
||||
const url = `@(HostingEnvironment.GetBraintreeMerchantUrl())/@Model.BraintreeMerchantId/${customerId.value}`;
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
});
|
||||
document.getElementById('gateway-subscription-link')?.addEventListener('click', () => {
|
||||
const gateway = document.getElementById('@(nameof(Model.Gateway))');
|
||||
const subId = document.getElementById('@(nameof(Model.GatewaySubscriptionId))');
|
||||
if (!gateway || gateway.value === '' || !subId || subId.value === '') {
|
||||
return;
|
||||
}
|
||||
if (gateway.value === '@((byte)GatewayType.Stripe)') {
|
||||
const url = `@(HostingEnvironment.GetStripeUrl())/subscriptions/${subId.value}/`;
|
||||
window.open(url, '_blank');
|
||||
} else if (gateway.value === '@((byte)GatewayType.Braintree)') {
|
||||
const url = `@(HostingEnvironment.GetBraintreeMerchantUrl())/@Model.BraintreeMerchantId/subscriptions/${subId.value}`;
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
});
|
||||
document.getElementById('@(nameof(Model.UseSecretsManager))').addEventListener('change', (event) => {
|
||||
document.getElementById('organization-secrets-configuration').hidden = !event.target.checked;
|
||||
|
||||
if (event.target.checked) {
|
||||
setInitialSecretsManagerConfiguration();
|
||||
return;
|
||||
}
|
||||
|
||||
clearSecretsManagerConfiguration();
|
||||
});
|
||||
})();
|
||||
|
||||
function togglePlanFeatures(planType) {
|
||||
switch(planType) {
|
||||
case '@((byte)PlanType.TeamsMonthly2019)':
|
||||
case '@((byte)PlanType.TeamsAnnually2019)':
|
||||
case '@((byte)PlanType.TeamsMonthly2020)':
|
||||
case '@((byte)PlanType.TeamsAnnually2020)':
|
||||
case '@((byte)PlanType.TeamsMonthly)':
|
||||
case '@((byte)PlanType.TeamsAnnually)':
|
||||
case '@((byte)PlanType.TeamsStarter)':
|
||||
document.getElementById('@(nameof(Model.UsePolicies))').checked = false;
|
||||
document.getElementById('@(nameof(Model.UseSso))').checked = false;
|
||||
document.getElementById('@(nameof(Model.UseGroups))').checked = true;
|
||||
document.getElementById('@(nameof(Model.UseDirectory))').checked = true;
|
||||
document.getElementById('@(nameof(Model.UseEvents))').checked = true;
|
||||
document.getElementById('@(nameof(Model.UsersGetPremium))').checked = true;
|
||||
document.getElementById('@(nameof(Model.UseCustomPermissions))').checked = false;
|
||||
document.getElementById('@(nameof(Model.UseTotp))').checked = true;
|
||||
document.getElementById('@(nameof(Model.Use2fa))').checked = true;
|
||||
document.getElementById('@(nameof(Model.UseApi))').checked = true;
|
||||
document.getElementById('@(nameof(Model.SelfHost))').checked = false;
|
||||
document.getElementById('@(nameof(Model.UseResetPassword))').checked = false;
|
||||
document.getElementById('@(nameof(Model.UseScim))').checked = false;
|
||||
break;
|
||||
|
||||
case '@((byte)PlanType.EnterpriseMonthly2019)':
|
||||
case '@((byte)PlanType.EnterpriseAnnually2019)':
|
||||
case '@((byte)PlanType.EnterpriseMonthly2020)':
|
||||
case '@((byte)PlanType.EnterpriseAnnually2020)':
|
||||
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;
|
||||
document.getElementById('@(nameof(Model.UseDirectory))').checked = true;
|
||||
document.getElementById('@(nameof(Model.UseEvents))').checked = true;
|
||||
document.getElementById('@(nameof(Model.UsersGetPremium))').checked = true;
|
||||
document.getElementById('@(nameof(Model.UseCustomPermissions))').checked = true;
|
||||
document.getElementById('@(nameof(Model.UseTotp))').checked = true;
|
||||
document.getElementById('@(nameof(Model.Use2fa))').checked = true;
|
||||
document.getElementById('@(nameof(Model.UseApi))').checked = true;
|
||||
document.getElementById('@(nameof(Model.SelfHost))').checked = true;
|
||||
document.getElementById('@(nameof(Model.UseResetPassword))').checked = true;
|
||||
document.getElementById('@(nameof(Model.UseScim))').checked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function unlinkProvider(id) {
|
||||
if (confirm('Are you sure you want to unlink this organization from its provider?')) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: `@Url.Action("UnlinkOrganizationFromProvider", "Organizations")?id=${id}`,
|
||||
dataType: 'json',
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: function (response) {
|
||||
alert("Successfully unlinked provider");
|
||||
window.location.href = `@Url.Action("Edit", "Organizations")?id=${id}`;
|
||||
},
|
||||
error: function (response) {
|
||||
alert("Error!");
|
||||
}
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/***
|
||||
* 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>
|
||||
5
src/Admin/AdminConsole/Views/_ViewImports.cshtml
Normal file
5
src/Admin/AdminConsole/Views/_ViewImports.cshtml
Normal file
@@ -0,0 +1,5 @@
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using Bit.Admin.AdminConsole
|
||||
@using Bit.Admin.AdminConsole.Models
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@addTagHelper "*, Admin"
|
||||
3
src/Admin/AdminConsole/Views/_ViewStart.cshtml
Normal file
3
src/Admin/AdminConsole/Views/_ViewStart.cshtml
Normal file
@@ -0,0 +1,3 @@
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
}
|
||||
Reference in New Issue
Block a user