mirror of
https://github.com/bitwarden/server.git
synced 2024-11-25 12:45:18 +01:00
[AC-1081] Merge feature/billing-obfuscation (#2665)
* [EC-1014] Create Organization Status (Pending/Created) (#2610) * [EC-427] Add columns 'Type' and 'BillingPhone' to Provider table * [EC-427] Provider table Type and BillingPhone MySql migrations * [EC-427] Provider table Type and BillingPhone Postgres migrations * [EC-427] Add mysql migration script * [EC-427] Add mysql migration script * [EC-427] Updated Provider sql script to include default column value * [EC-427] Removed default value from Provider.Type column * [EC-427] Changed migration script to include a default value constraint instead of updating the null type * [EC-427] Updated Sql project Provider table script * [EC-427] Changed migration script to use 'Create OR Alter' for views and sprocs * [EC-427] Added default values for 'BillingPhone' and 'Type' fields on sprocs [dbo].[Provider_Create] and [dbo].[Provider_Update] * [EC-427] Adjusting metadata in migration script * [EC-427] Updated Provider sprocs SQL script files * [EC-427] Fixed migration script * [EC-427] Added sqlite migration * [EC-427] Add missing Provider_Update sproc default value * [EC-427] Added missing GO action to migration script * [EC-428] Redirect to Edit after creating Provider * Revert "[EC-428] Redirect to Edit after creating Provider" This reverts commit6347bca1ed
. * [EC-1014] Create OrganizationStatusType and add Status column to Organizations table * [EC-1014] Added EF migrations * [EC-1014] dotnet format * [EC-1014] Changed Organization.Status from SMALLINT to TINYINT * [EC-1014] Set Organization.Status default value = 1 * [EC-1014] Setting Organization.Status default value as 1 * [EC-459 / EC-428] Admin panel: Add Provider Type to list and creation flow (#2593) * [EC-427] Add columns 'Type' and 'BillingPhone' to Provider table * [EC-427] Provider table Type and BillingPhone MySql migrations * [EC-427] Provider table Type and BillingPhone Postgres migrations * [EC-427] Add mysql migration script * [EC-427] Add mysql migration script * [EC-427] Updated Provider sql script to include default column value * [EC-427] Removed default value from Provider.Type column * [EC-427] Changed migration script to include a default value constraint instead of updating the null type * [EC-427] Updated Sql project Provider table script * [EC-427] Changed migration script to use 'Create OR Alter' for views and sprocs * [EC-427] Added default values for 'BillingPhone' and 'Type' fields on sprocs [dbo].[Provider_Create] and [dbo].[Provider_Update] * [EC-427] Adjusting metadata in migration script * [EC-427] Updated Provider sprocs SQL script files * [EC-427] Fixed migration script * [EC-427] Added sqlite migration * [EC-427] Add missing Provider_Update sproc default value * [EC-427] Added missing GO action to migration script * [EC-459] Added Type column to Providers list * [EC-428] Added Type, BusinessName and BillingEmail to CreateProviderModel * [EC-428] Updated Create Provider view to include new fields * [EC-428] Updated ProviderService to not create a ProviderUser for the type Reseller * [EC-428] Added custom validation for Provider fields depending on selected Type * [EC-428] Redirect to Edit after creating Provider * [EC-428] Setting Provider status as Created for Resellers * [EC-428] Redirect on Provider creation depending if self host server * [EC-428] Split ProviderService.CreateAsync into two methods: CreateMspAsync and CreateResellerAsync * [EC-428] Created ICreateProviderCommand and added service for injection on Admin.Startup * [EC-428] Modified Provider views to use DisplayName attribute values * [EC-428] Moved ICreateProviderCommand to Core project * [EC-428] Adding ICreateProviderCommand injection next to IProviderService * [EC-428] Moved CreateProviderCommand to Commercial.Core project * [EC-459] Added Type column to Providers list * [EC-428] Added Type, BusinessName and BillingEmail to CreateProviderModel * [EC-428] Updated Create Provider view to include new fields * [EC-428] Updated ProviderService to not create a ProviderUser for the type Reseller * [EC-428] Added custom validation for Provider fields depending on selected Type * [EC-428] Redirect to Edit after creating Provider * [EC-428] Setting Provider status as Created for Resellers * [EC-428] Redirect on Provider creation depending if self host server * [EC-428] Split ProviderService.CreateAsync into two methods: CreateMspAsync and CreateResellerAsync * [EC-428] Created ICreateProviderCommand and added service for injection on Admin.Startup * [EC-428] Modified Provider views to use DisplayName attribute values * [EC-428] Moved ICreateProviderCommand to Core project * [EC-428] Adding ICreateProviderCommand injection next to IProviderService * [EC-428] Moved CreateProviderCommand to Commercial.Core project * [EC-428] Moved CreateProviderCommand to namespace Bit.Commercial.Core.Providers * [EC-429] Provider details screen updated with Type, BillingPhone and Organization details (#2666) * [EC-430] Admin portal: Update organization information screen (#2672) * [EC-430] Added ProviderOrganizationProviderDetailsView to get Provider details for an Organization * [EC-430] Added Provider information to Organization Edit/View on Admin panel * [EC-430] Remove "Add to Reseller" button * [EC-430] Removed unused property OrganizationEditModel.ClientOwnerEmail * [EC-430] Replaced IProviderOrganizationRepository.GetProviderDetailsByOrganizationAsync with IProviderRepository.GetByOrganizationIdAsync * [EC-430] Deleted ProviderOrganizationProviderDetails and ProviderOrganizationProviderDetailsReadByOrganizationIdQuery * [EC-429] Only show Create/Add Existing Organization buttons for Reseller providers (#2723) * [EC-432] Add existing Organizations to Provider (#2683) * [EC-432] Added ProviderOrganizationUnassignedOrganizationDetails_Search stored procedure * [EC-432] Added IProviderOrganizationRepository.SearchAsync * [EC-432] Created controller ProviderOrganizationsController to assign Organizations to a Provider * [EC-432] Filter existing organizations by plans Enterprise or Team * [EC-432] Existing Organization name links to edit page * [EC-432] EF filtering out existing organizations by plan type enterprise or teams * [EC-432] Creating multiple ProviderOrganization records * [EC-432] Added ProviderOrganizationUnassignedOrganizationDetails_Search stored procedure * [EC-432] Added IProviderOrganizationRepository.SearchAsync * [EC-432] Created controller ProviderOrganizationsController to assign Organizations to a Provider * [EC-432] Filter existing organizations by plans Enterprise or Team * [EC-432] Existing Organization name links to edit page * [EC-432] EF filtering out existing organizations by plan type enterprise or teams * [EC-432] Creating multiple ProviderOrganization records * [EC-432] Renamed migration script and added missing sproc * [EC-432] Saving multiple events for the created ProviderOrganizations * [EC-432] Included unit testing for ProviderService.AddOrganizations and EventService.LogProviderOrganizationEventsAsync * [EC-432] Removed async from NoopEventService.LogProviderOrganizationEventsAsync * [EC-432] Remove unused dependency setup in ProviderServiceTests.AddOrganizations_Success * [EC-432] Renamed AddOrganizations to AddOrganizationsToReseller and removed addingUserId and key arguments * [EC-432] Added DisplayName attributes to ProviderOrganizationViewModel and used them in the view * [EC-432] Reverted changes to input fields * [EC-432] Moved unassigned organizations search to Organizations repo * [EC-432] Moved AddExistingOrganization action to ProvidersController * [EC-432] dotnet format * [EC-432] Fixed unit test issues * [EC-432] Removed unnecessary Html.DisplayNameFor for labels * [EC-432] Renamed OrganizationSearchViewModel to OrganizationUnassignedToProviderSearchViewModel * [EC-432] Modified IEventService.LogProviderOrganizationEventsAsync to receive an IEnumerable as parameter * [EC-432] Updated IProviderOrganizationRepository and replaced CreateWithManyOrganizations method with CreateManyAsync * [EC-432] Deleted ProviderOrganization_CreateWithManyOrganizations * [AC-432] Simplified Organization_UnassignedToProviderSearch query * [AC-432] Removed unnecessary setup * [EC-432] Checking if stored procedure exists before creating * [EC-432] Renamed migration file to recent date * [EC-435] Admin Portal: Add new Organization creation flow UI (#2707) * [EC-435] Created _OrganizationForm partial view. Added actions for creating an Organization assigned to a provider * [EC-435] Remove logic for creating an organization * [EC-435] Created partial view _OrganizationFormScripts * [EC-435] Remove unused ReferenceEventType * [EC-435] Added TODO comment on Organization Create * [EC-435] Checking if Provider type is Reseller on creating new assigned organization * [EC-435] Setting the Organization plan type as TeamsMonthly by default when adding to a provider * [EC-435] Removing unused buttons * [EC-435] Switched hidden fields to form submit route value * [EC-435] Moved _OrganizationForm and _OrganizationFormScripts to Shared folder * [EC-435] Moved Create organization actions from OrganizationsController to ProvidersController * [EC-435] Fixing bug on saving Organization that would have BillingEmail as null * [EC-435] Added null check to Provider * [EC-435] Moved trial buttons script logic to Edit view * [AC-431] Add new organization invite process (#2737) * [EC-435] Created _OrganizationForm partial view. Added actions for creating an Organization assigned to a provider * [EC-435] Remove logic for creating an organization * [EC-435] Created partial view _OrganizationFormScripts * [EC-435] Remove unused ReferenceEventType * [EC-435] Added TODO comment on Organization Create * [EC-435] Checking if Provider type is Reseller on creating new assigned organization * [EC-435] Setting the Organization plan type as TeamsMonthly by default when adding to a provider * [EC-435] Removing unused buttons * [EC-435] Switched hidden fields to form submit route value * [EC-435] Moved _OrganizationForm and _OrganizationFormScripts to Shared folder * [EC-435] Moved Create organization actions from OrganizationsController to ProvidersController * [AC-431] Added new ReferenceEventType OrganizationCreatedByAdmin * [AC-431] Added method IOrganizationService.CreateOrganization * [AC-431] Creating new Organization with Pending status and assigning to Provider * [AC-431] Added method to IMailService to send invitation to initialize org * [AC-431] Added methods CreatePendingOrganization and InitPendingOrganization to IOrganizationService * [AC-431] Org invite includes initOrganization parameter * [AC-431] Modified existing Accept organization user action to initialize org * [AC-431] Updated ProvidersController method name * [AC-431] Created OrganizationUserInitInvitedViewModel to link to 'accept-init-organization' url * [AC-431] Added action AcceptInit to OrganizationUsersController * [AC-431] Resend owner invite * [AC-431] dotnet format * [AC-431] Removed unused parameter 'addingUserId' from IProviderService.AddOrganization * [AC-431] Removed setting manual values for CreationDate and RevisionDate * [AC-431] Updated OrganizationService.InitPendingOrganization to throw exceptions when the Organization does not meet the required criteria * [AC-431] Modified OrganizationUserInitInvitedViewModel to inherit properties from OrganizationUserInvitedViewModel * [AC-431] Removed unecessary parameter check * [AC-431] Moved method description to IOrganizationService.InitPendingOrganization * [AC-431] Moved ApplicationCacheService.UpsertOrganizationAbilityAsync and ReferenceEventService.RaiseEventAsync to OrganizationService * [AC-431] Creating collection after creating organization * [EC-435] Fixing bug on saving Organization that would have BillingEmail as null * [AC-431] Deleted OrganizationUserInitInvitedViewModel and added parameter InitOrganization to OrganizationUserInvitedViewModel.cs * [AC-431] Checking if the user has any existing SingleOrg policies before initializing an Org * [AC-431] Remove commented code * [EC-435] Added null check to Provider * [EC-435] Moved trial buttons script logic to Edit view * [AC-431] Added EncryptedString attribute to OrganizationUserAcceptInitRequestModel.CollectionName * [AC-431] Refactored plan check condition * [AC-431] Remove duplicate _applicationCacheService.UpsertOrganizationAbilityAsync call * [AC-431] Removed IMailService.SendOrganizationInitInviteEmailAsync * [AC-431] Added parameters ClaimsPrincipal and IUserService to IOrganizationService.CreatePendingOrganization * [AC-434] Hide Billing screen for Reseller clients (#2783) * [AC-434] Added ProviderType to ProfileOrganizationResponseModel * [AC-434] Migration script * [AC-434] Fixed indentation on migration script * [AC-434] Hiding sensitive subscription data if the user does not have permissions * [AC-434] Fixed missing dependency in unit test * [AC-434] Altered BillingSubscription.Amount and BillingSubscriptionUpcomingInvoice.Amount to nullable * [AC-434] Replaced CurrentContext.ManageBilling with ViewBillingHistory, ViewSubscription, EditSubscription and EditPaymentMethods * [AC-434] Reverted change on BillingSubscription.Amount and now setting Subscription.Items = null when User does not have permission * [AC-434] Added ProviderOrganizationProviderDetails_ReadByUserId * [AC-434] Added IProviderOrganizationRepository.GetManyByUserAsync * [AC-434] Added CurrentContext.GetOrganizationProviderDetails * [AC-434] Remove unneeded join Organization table * [AC-1255] Search Existing Organizations by partial Email (#2830) * [AC-1255] Added email search field input validation * [AC-1255] Reverted added email pattern * [AC-1255] Modified Organization search by Email to search using substring * [AC-1276] Displaying an Organizations pending owners if the Organization is in a Pending status (#2834) * [AC-432] Checking that an existing Organization is not assigned to any Provider before being assigned (#2840) * [AC-432] Checking if any of the selected Organizations is already assigned to a Provider * [AC-432] Changed ProviderOrganization_ReadByOrganizationIds to only get count * [AC-432] Replaced IProviderOrganizationRepository.GetCountByOrganizationIdsAsync with call to IProviderOrganizationRepository.GetByOrganizationId * [AC-432] undo new line * [AC-432] Fixed unit test * Revert "[AC-432] Replaced IProviderOrganizationRepository.GetCountByOrganizationIdsAsync with call to IProviderOrganizationRepository.GetByOrganizationId" This reverts commitee6e095e88
. # Conflicts: # util/Migrator/DbScripts/2023-03-22_00_ProviderAddExistingOrganizations.sql * [AC-432] Created new migration script for ProviderOrganization_ReadCountByOrganizationIds
This commit is contained in:
parent
03c740dffb
commit
f5a8cf5c9c
@ -0,0 +1,62 @@
|
||||
using Bit.Core.Entities.Provider;
|
||||
using Bit.Core.Enums.Provider;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Providers.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Commercial.Core.Providers;
|
||||
|
||||
public class CreateProviderCommand : ICreateProviderCommand
|
||||
{
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IProviderService _providerService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public CreateProviderCommand(
|
||||
IProviderRepository providerRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IProviderService providerService,
|
||||
IUserRepository userRepository)
|
||||
{
|
||||
_providerRepository = providerRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_providerService = providerService;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public async Task CreateMspAsync(Provider provider, string ownerEmail)
|
||||
{
|
||||
var owner = await _userRepository.GetByEmailAsync(ownerEmail);
|
||||
if (owner == null)
|
||||
{
|
||||
throw new BadRequestException("Invalid owner. Owner must be an existing Bitwarden user.");
|
||||
}
|
||||
|
||||
await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Pending);
|
||||
|
||||
var providerUser = new ProviderUser
|
||||
{
|
||||
ProviderId = provider.Id,
|
||||
UserId = owner.Id,
|
||||
Type = ProviderUserType.ProviderAdmin,
|
||||
Status = ProviderUserStatusType.Confirmed,
|
||||
};
|
||||
await _providerUserRepository.CreateAsync(providerUser);
|
||||
await _providerService.SendProviderSetupInviteEmailAsync(provider, owner.Email);
|
||||
}
|
||||
|
||||
public async Task CreateResellerAsync(Provider provider)
|
||||
{
|
||||
await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Created);
|
||||
}
|
||||
|
||||
private async Task ProviderRepositoryCreateAsync(Provider provider, ProviderStatusType status)
|
||||
{
|
||||
provider.Status = status;
|
||||
provider.Enabled = true;
|
||||
provider.UseEvents = true;
|
||||
await _providerRepository.CreateAsync(provider);
|
||||
}
|
||||
}
|
@ -53,33 +53,6 @@ public class ProviderService : IProviderService
|
||||
_currentContext = currentContext;
|
||||
}
|
||||
|
||||
public async Task CreateAsync(string ownerEmail)
|
||||
{
|
||||
var owner = await _userRepository.GetByEmailAsync(ownerEmail);
|
||||
if (owner == null)
|
||||
{
|
||||
throw new BadRequestException("Invalid owner. Owner must be an existing Bitwarden user.");
|
||||
}
|
||||
|
||||
var provider = new Provider
|
||||
{
|
||||
Status = ProviderStatusType.Pending,
|
||||
Enabled = true,
|
||||
UseEvents = true,
|
||||
};
|
||||
await _providerRepository.CreateAsync(provider);
|
||||
|
||||
var providerUser = new ProviderUser
|
||||
{
|
||||
ProviderId = provider.Id,
|
||||
UserId = owner.Id,
|
||||
Type = ProviderUserType.ProviderAdmin,
|
||||
Status = ProviderUserStatusType.Confirmed,
|
||||
};
|
||||
await _providerUserRepository.CreateAsync(providerUser);
|
||||
await SendProviderSetupInviteEmailAsync(provider, owner.Email);
|
||||
}
|
||||
|
||||
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key)
|
||||
{
|
||||
var owner = await _userService.GetUserByIdAsync(ownerUserId);
|
||||
@ -370,7 +343,7 @@ public class ProviderService : IProviderService
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task AddOrganization(Guid providerId, Guid organizationId, Guid addingUserId, string key)
|
||||
public async Task AddOrganization(Guid providerId, Guid organizationId, string key)
|
||||
{
|
||||
var po = await _providerOrganizationRepository.GetByOrganizationId(organizationId);
|
||||
if (po != null)
|
||||
@ -392,6 +365,26 @@ public class ProviderService : IProviderService
|
||||
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Added);
|
||||
}
|
||||
|
||||
public async Task AddOrganizationsToReseller(Guid providerId, IEnumerable<Guid> organizationIds)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||
if (provider.Type != ProviderType.Reseller)
|
||||
{
|
||||
throw new BadRequestException("Provider must be of type Reseller in order to assign Organizations to it.");
|
||||
}
|
||||
|
||||
var existingProviderOrganizationsCount = await _providerOrganizationRepository.GetCountByOrganizationIdsAsync(organizationIds);
|
||||
if (existingProviderOrganizationsCount > 0)
|
||||
{
|
||||
throw new BadRequestException("Organizations must not be assigned to any Provider.");
|
||||
}
|
||||
|
||||
var providerOrganizationsToInsert = organizationIds.Select(orgId => new ProviderOrganization { ProviderId = providerId, OrganizationId = orgId });
|
||||
var insertedProviderOrganizations = await _providerOrganizationRepository.CreateManyAsync(providerOrganizationsToInsert);
|
||||
|
||||
await _eventService.LogProviderOrganizationEventsAsync(insertedProviderOrganizations.Select(ipo => (ipo, EventType.ProviderOrganization_Added, (DateTime?)null)));
|
||||
}
|
||||
|
||||
public async Task<ProviderOrganization> CreateOrganizationAsync(Guid providerId,
|
||||
OrganizationSignup organizationSignup, string clientOwnerEmail, User user)
|
||||
{
|
||||
@ -456,7 +449,7 @@ public class ProviderService : IProviderService
|
||||
await SendProviderSetupInviteEmailAsync(provider, owner.Email);
|
||||
}
|
||||
|
||||
private async Task SendProviderSetupInviteEmailAsync(Provider provider, string ownerEmail)
|
||||
public async Task SendProviderSetupInviteEmailAsync(Provider provider, string ownerEmail)
|
||||
{
|
||||
var token = _dataProtector.Protect($"ProviderSetupInvite {provider.Id} {ownerEmail} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||
await _mailService.SendProviderSetupInviteEmailAsync(provider, token, ownerEmail);
|
||||
|
@ -1,4 +1,6 @@
|
||||
using Bit.Commercial.Core.Services;
|
||||
using Bit.Commercial.Core.Providers;
|
||||
using Bit.Commercial.Core.Services;
|
||||
using Bit.Core.Providers.Interfaces;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@ -9,5 +11,6 @@ public static class ServiceCollectionExtensions
|
||||
public static void AddCommercialCoreServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IProviderService, ProviderService>();
|
||||
services.AddScoped<ICreateProviderCommand, CreateProviderCommand>();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,52 @@
|
||||
using Bit.Commercial.Core.Providers;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Entities.Provider;
|
||||
using Bit.Core.Enums.Provider;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.ProviderFeatures;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class CreateProviderCommandTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateMspAsync_UserIdIsInvalid_Throws(Provider provider, SutProvider<CreateProviderCommand> sutProvider)
|
||||
{
|
||||
provider.Type = ProviderType.Msp;
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.CreateMspAsync(provider, default));
|
||||
Assert.Contains("Invalid owner.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateMspAsync_Success(Provider provider, User user, SutProvider<CreateProviderCommand> sutProvider)
|
||||
{
|
||||
provider.Type = ProviderType.Msp;
|
||||
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
userRepository.GetByEmailAsync(user.Email).Returns(user);
|
||||
|
||||
await sutProvider.Sut.CreateMspAsync(provider, user.Email);
|
||||
|
||||
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
|
||||
await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateResellerAsync_Success(Provider provider, SutProvider<CreateProviderCommand> sutProvider)
|
||||
{
|
||||
provider.Type = ProviderType.Reseller;
|
||||
|
||||
await sutProvider.Sut.CreateResellerAsync(provider);
|
||||
|
||||
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
|
||||
await sutProvider.GetDependency<IProviderService>().DidNotReceiveWithAnyArgs().SendProviderSetupInviteEmailAsync(default, default);
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ using Microsoft.AspNetCore.DataProtection;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Xunit;
|
||||
using Provider = Bit.Core.Entities.Provider.Provider;
|
||||
using ProviderUser = Bit.Core.Entities.Provider.ProviderUser;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.Services;
|
||||
@ -24,26 +25,6 @@ namespace Bit.Commercial.Core.Test.Services;
|
||||
[SutProviderCustomize]
|
||||
public class ProviderServiceTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_UserIdIsInvalid_Throws(SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.CreateAsync(default));
|
||||
Assert.Contains("Invalid owner.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_Success(User user, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
userRepository.GetByEmailAsync(user.Email).Returns(user);
|
||||
|
||||
await sutProvider.Sut.CreateAsync(user.Email);
|
||||
|
||||
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
|
||||
await sutProvider.GetDependency<IMailService>().ReceivedWithAnyArgs().SendProviderSetupInviteEmailAsync(default, default, default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CompleteSetupAsync_UserIdIsInvalid_Throws(SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
@ -229,6 +210,14 @@ public class ProviderServiceTests
|
||||
Assert.True(result.All(r => r.Item2 == ""));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SendProviderSetupInviteEmailAsync_Success(Provider provider, string email, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
await sutProvider.Sut.SendProviderSetupInviteEmailAsync(provider, email);
|
||||
|
||||
await sutProvider.GetDependency<IMailService>().Received(1).SendProviderSetupInviteEmailAsync(provider, Arg.Any<string>(), email);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AcceptUserAsync_UserIsInvalid_Throws(SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
@ -429,7 +418,7 @@ public class ProviderServiceTests
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AddOrganization_OrganizationAlreadyBelongsToAProvider_Throws(Provider provider,
|
||||
Organization organization, ProviderOrganization po, User user, string key,
|
||||
Organization organization, ProviderOrganization po, string key,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
po.OrganizationId = organization.Id;
|
||||
@ -438,12 +427,12 @@ public class ProviderServiceTests
|
||||
.Returns(po);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.AddOrganization(provider.Id, organization.Id, user.Id, key));
|
||||
() => sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key));
|
||||
Assert.Equal("Organization already belongs to a provider.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AddOrganization_Success(Provider provider, Organization organization, User user, string key,
|
||||
public async Task AddOrganization_Success(Provider provider, Organization organization, string key,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
@ -453,7 +442,7 @@ public class ProviderServiceTests
|
||||
providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull();
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, user.Id, key);
|
||||
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
|
||||
|
||||
await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
@ -461,6 +450,49 @@ public class ProviderServiceTests
|
||||
EventType.ProviderOrganization_Added);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AddOrganizationsToReseller_WithResellerProvider_Success(Provider provider, ICollection<Organization> organizations, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
provider.Type = ProviderType.Reseller;
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
|
||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||
foreach (var organization in organizations)
|
||||
{
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
}
|
||||
|
||||
var organizationIds = organizations.Select(o => o.Id).ToArray();
|
||||
|
||||
await sutProvider.Sut.AddOrganizationsToReseller(provider.Id, organizationIds);
|
||||
|
||||
await providerOrganizationRepository.Received(1).CreateManyAsync(Arg.Is<IEnumerable<ProviderOrganization>>(i => i.All(po => po.ProviderId == provider.Id && organizations.Any(o => o.Id == po.OrganizationId))));
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogProviderOrganizationEventsAsync(
|
||||
Arg.Is<IEnumerable<(ProviderOrganization, EventType, DateTime?)>>(events => events.All(e =>
|
||||
e.Item1.ProviderId == provider.Id && organizationIds.Contains(e.Item1.OrganizationId) && e.Item2 == EventType.ProviderOrganization_Added)));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AddOrganizationsToReseller_WithMspProvider_Throws(Provider provider, ICollection<Organization> organizations, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
provider.Type = ProviderType.Msp;
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
|
||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||
foreach (var organization in organizations)
|
||||
{
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
}
|
||||
|
||||
var organizationIds = organizations.Select(o => o.Id).ToArray();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.AddOrganizationsToReseller(provider.Id, organizationIds));
|
||||
Assert.Contains("Provider must be of type Reseller in order to assign Organizations to it.", exception.Message);
|
||||
|
||||
await providerOrganizationRepository.DidNotReceiveWithAnyArgs().CreateManyAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogProviderOrganizationEventsAsync(default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateOrganizationAsync_Success(Provider provider, OrganizationSignup organizationSignup,
|
||||
Organization organization, string clientOwnerEmail, User user, SutProvider<ProviderService> sutProvider)
|
||||
|
@ -17,6 +17,7 @@ namespace Bit.Admin.Controllers;
|
||||
[Authorize]
|
||||
public class OrganizationsController : Controller
|
||||
{
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
|
||||
@ -31,9 +32,11 @@ public class OrganizationsController : Controller
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly ILogger<OrganizationsController> _logger;
|
||||
|
||||
public OrganizationsController(
|
||||
IOrganizationService organizationService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationConnectionRepository organizationConnectionRepository,
|
||||
@ -48,8 +51,10 @@ public class OrganizationsController : Controller
|
||||
GlobalSettings globalSettings,
|
||||
IReferenceEventService referenceEventService,
|
||||
IUserService userService,
|
||||
IProviderRepository providerRepository,
|
||||
ILogger<OrganizationsController> logger)
|
||||
{
|
||||
_organizationService = organizationService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_organizationConnectionRepository = organizationConnectionRepository;
|
||||
@ -64,6 +69,7 @@ public class OrganizationsController : Controller
|
||||
_globalSettings = globalSettings;
|
||||
_referenceEventService = referenceEventService;
|
||||
_userService = userService;
|
||||
_providerRepository = providerRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@ -103,6 +109,7 @@ public class OrganizationsController : Controller
|
||||
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;
|
||||
@ -117,7 +124,7 @@ public class OrganizationsController : Controller
|
||||
}
|
||||
var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id);
|
||||
var billingSyncConnection = _globalSettings.EnableCloudCommunication ? await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync) : null;
|
||||
return View(new OrganizationViewModel(organization, billingSyncConnection, users, ciphers, collections, groups, policies));
|
||||
return View(new OrganizationViewModel(organization, provider, billingSyncConnection, users, ciphers, collections, groups, policies));
|
||||
}
|
||||
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
@ -129,6 +136,7 @@ public class OrganizationsController : Controller
|
||||
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;
|
||||
@ -144,7 +152,7 @@ public class OrganizationsController : Controller
|
||||
var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id);
|
||||
var billingInfo = await _paymentService.GetBillingAsync(organization);
|
||||
var billingSyncConnection = _globalSettings.EnableCloudCommunication ? await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync) : null;
|
||||
return View(new OrganizationEditModel(organization, users, ciphers, collections, groups, policies,
|
||||
return View(new OrganizationEditModel(organization, provider, users, ciphers, collections, groups, policies,
|
||||
billingInfo, billingSyncConnection, _globalSettings));
|
||||
}
|
||||
|
||||
@ -214,4 +222,21 @@ public class OrganizationsController : Controller
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
using Bit.Admin.Models;
|
||||
using Bit.Core.Entities.Provider;
|
||||
using Bit.Core.Enums.Provider;
|
||||
using Bit.Core.Providers.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@ -13,23 +15,42 @@ namespace Bit.Admin.Controllers;
|
||||
[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;
|
||||
|
||||
public ProvidersController(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository, IProviderService providerService,
|
||||
GlobalSettings globalSettings, IApplicationCacheService applicationCacheService)
|
||||
public ProvidersController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationService organizationService,
|
||||
IProviderRepository providerRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IProviderService providerService,
|
||||
GlobalSettings globalSettings,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IReferenceEventService referenceEventService,
|
||||
IUserService userService,
|
||||
ICreateProviderCommand createProviderCommand)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationService = organizationService;
|
||||
_providerRepository = providerRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
_providerService = providerService;
|
||||
_globalSettings = globalSettings;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_referenceEventService = referenceEventService;
|
||||
_userService = userService;
|
||||
_createProviderCommand = createProviderCommand;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Index(string name = null, string userEmail = null, int page = 1, int count = 25)
|
||||
@ -75,9 +96,18 @@ public class ProvidersController : Controller
|
||||
return View(model);
|
||||
}
|
||||
|
||||
await _providerService.CreateAsync(model.OwnerEmail);
|
||||
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("Index");
|
||||
return RedirectToAction("Edit", new { id = provider.Id });
|
||||
}
|
||||
|
||||
public async Task<IActionResult> View(Guid id)
|
||||
@ -130,4 +160,76 @@ public class ProvidersController : Controller
|
||||
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 organization = model.CreateOrganization(provider);
|
||||
await _organizationService.CreatePendingOrganization(organization, model.Owners, User, _userService, model.SalesAssistedTrialStarted);
|
||||
await _providerService.AddOrganization(providerId, organization.Id, null);
|
||||
|
||||
return RedirectToAction("Edit", "Providers", new { id = providerId });
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,59 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Entities.Provider;
|
||||
using Bit.Core.Enums.Provider;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
|
||||
namespace Bit.Admin.Models;
|
||||
|
||||
public class CreateProviderModel
|
||||
public class CreateProviderModel : IValidatableObject
|
||||
{
|
||||
public CreateProviderModel() { }
|
||||
|
||||
[Display(Name = "Provider Type")]
|
||||
public ProviderType Type { get; set; }
|
||||
|
||||
[Display(Name = "Owner Email")]
|
||||
[Required]
|
||||
public string OwnerEmail { 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,
|
||||
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();
|
||||
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
|
||||
}
|
||||
break;
|
||||
case ProviderType.Reseller:
|
||||
if (string.IsNullOrWhiteSpace(BusinessName))
|
||||
{
|
||||
var businessNameDisplayName = nameof(BusinessName).GetDisplayAttribute<CreateProviderModel>()?.GetName();
|
||||
yield return new ValidationResult($"The {businessNameDisplayName} field is required.");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(BillingEmail))
|
||||
{
|
||||
var billingEmailDisplayName = nameof(BillingEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName();
|
||||
yield return new ValidationResult($"The {billingEmailDisplayName} field is required.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Entities.Provider;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums.Provider;
|
||||
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.Models;
|
||||
|
||||
@ -13,18 +16,26 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
{
|
||||
public OrganizationEditModel() { }
|
||||
|
||||
public OrganizationEditModel(Organization org, IEnumerable<OrganizationUserUserDetails> orgUsers,
|
||||
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();
|
||||
}
|
||||
|
||||
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)
|
||||
: base(org, connections, orgUsers, ciphers, collections, groups, policies)
|
||||
: base(org, provider, connections, orgUsers, ciphers, collections, groups, policies)
|
||||
{
|
||||
BillingInfo = billingInfo;
|
||||
BraintreeMerchantId = globalSettings.Braintree.MerchantId;
|
||||
|
||||
Name = org.Name;
|
||||
BusinessName = org.BusinessName;
|
||||
BillingEmail = org.BillingEmail;
|
||||
BillingEmail = provider?.Type == ProviderType.Reseller ? provider.BillingEmail : org.BillingEmail;
|
||||
PlanType = org.PlanType;
|
||||
Plan = org.Plan;
|
||||
Seats = org.Seats;
|
||||
@ -60,7 +71,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
public string BraintreeMerchantId { get; set; }
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Name")]
|
||||
[Display(Name = "Organization Name")]
|
||||
public string Name { get; set; }
|
||||
[Display(Name = "Business Name")]
|
||||
public string BusinessName { get; set; }
|
||||
@ -124,6 +135,13 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
public DateTime? ExpirationDate { get; set; }
|
||||
public bool SalesAssistedTrialStarted { get; set; }
|
||||
|
||||
public Organization CreateOrganization(Provider provider)
|
||||
{
|
||||
BillingEmail = provider.BillingEmail;
|
||||
|
||||
return ToOrganization(new Organization());
|
||||
}
|
||||
|
||||
public Organization ToOrganization(Organization existingOrganization)
|
||||
{
|
||||
existingOrganization.Name = Name;
|
||||
|
8
src/Admin/Models/OrganizationSelectableViewModel.cs
Normal file
8
src/Admin/Models/OrganizationSelectableViewModel.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Admin.Models;
|
||||
|
||||
public class OrganizationSelectableViewModel : Organization
|
||||
{
|
||||
public bool Selected { get; set; }
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Admin.Models;
|
||||
|
||||
public class OrganizationUnassignedToProviderSearchViewModel : PagedModel<OrganizationSelectableViewModel>
|
||||
{
|
||||
[Display(Name = "Organization Name")]
|
||||
public string OrganizationName { get; set; }
|
||||
|
||||
[Display(Name = "Owner Email")]
|
||||
public string OrganizationOwnerEmail { get; set; }
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Entities.Provider;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Vault.Entities;
|
||||
@ -9,11 +10,12 @@ public class OrganizationViewModel
|
||||
{
|
||||
public OrganizationViewModel() { }
|
||||
|
||||
public OrganizationViewModel(Organization org, IEnumerable<OrganizationConnection> connections,
|
||||
public OrganizationViewModel(Organization org, Provider provider, IEnumerable<OrganizationConnection> connections,
|
||||
IEnumerable<OrganizationUserUserDetails> orgUsers, IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
|
||||
IEnumerable<Group> groups, IEnumerable<Policy> policies)
|
||||
{
|
||||
Organization = org;
|
||||
Provider = provider;
|
||||
Connections = connections ?? Enumerable.Empty<OrganizationConnection>();
|
||||
HasPublicPrivateKeys = org.PublicKey != null && org.PrivateKey != null;
|
||||
UserInvitedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Invited);
|
||||
@ -24,17 +26,21 @@ public class OrganizationViewModel
|
||||
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 == OrganizationUserStatusType.Confirmed)
|
||||
.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 == OrganizationUserStatusType.Confirmed)
|
||||
.Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus)
|
||||
.Select(u => u.Email));
|
||||
}
|
||||
|
||||
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; }
|
||||
|
@ -14,10 +14,13 @@ public class ProviderEditModel : ProviderViewModel
|
||||
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; }
|
||||
@ -28,6 +31,7 @@ public class ProviderEditModel : ProviderViewModel
|
||||
existingProvider.Name = Name;
|
||||
existingProvider.BusinessName = BusinessName;
|
||||
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();
|
||||
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant()?.Trim();
|
||||
return existingProvider;
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,20 @@
|
||||
@model OrganizationEditModel
|
||||
@{
|
||||
ViewData["Title"] = "Organization: " + Model.Organization.Name;
|
||||
ViewData["Title"] = (Model.Provider != null ? "Client " : string.Empty) + "Organization: " + Model.Organization.Name;
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_OrganizationFormScripts")
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
document.getElementById('teams-trial').addEventListener('click', () => {
|
||||
if (document.getElementById('@(nameof(Model.PlanType))').value !==
|
||||
'@((byte)Bit.Core.Enums.PlanType.Free)') {
|
||||
if (document.getElementById('@(nameof(Model.PlanType))').value !== '@((byte)Bit.Core.Enums.PlanType.Free)') {
|
||||
alert('Organization is not on a free plan.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Plan
|
||||
document.getElementById('@(nameof(Model.PlanType))').value =
|
||||
'@((byte)Bit.Core.Enums.PlanType.TeamsAnnually)';
|
||||
document.getElementById('@(nameof(Model.PlanType))').value = '@((byte)Bit.Core.Enums.PlanType.TeamsAnnually)';
|
||||
document.getElementById('@(nameof(Model.Plan))').value = 'Teams (Trial)';
|
||||
document.getElementById('@(nameof(Model.Seats))').value = '10';
|
||||
document.getElementById('@(nameof(Model.MaxCollections))').value = '';
|
||||
@ -38,19 +37,14 @@
|
||||
document.getElementById('@(nameof(Model.LicenseKey))').value = '@Model.RandomLicenseKey';
|
||||
document.getElementById('@(nameof(Model.ExpirationDate))').value = '@Model.FourteenDayExpirationDate';
|
||||
document.getElementById('@(nameof(Model.SalesAssistedTrialStarted))').value = true;
|
||||
|
||||
});
|
||||
|
||||
document.getElementById('enterprise-trial').addEventListener('click', () => {
|
||||
if (document.getElementById('@(nameof(Model.PlanType))').value !==
|
||||
'@((byte)Bit.Core.Enums.PlanType.Free)') {
|
||||
if (document.getElementById('@(nameof(Model.PlanType))').value !== '@((byte)Bit.Core.Enums.PlanType.Free)') {
|
||||
alert('Organization is not on a free plan.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Plan
|
||||
document.getElementById('@(nameof(Model.PlanType))').value =
|
||||
'@((byte)Bit.Core.Enums.PlanType.EnterpriseAnnually)';
|
||||
document.getElementById('@(nameof(Model.PlanType))').value = '@((byte)Bit.Core.Enums.PlanType.EnterpriseAnnually)';
|
||||
document.getElementById('@(nameof(Model.Plan))').value = 'Enterprise (Trial)';
|
||||
document.getElementById('@(nameof(Model.Seats))').value = '10';
|
||||
document.getElementById('@(nameof(Model.MaxCollections))').value = '';
|
||||
@ -73,260 +67,24 @@
|
||||
document.getElementById('@(nameof(Model.LicenseKey))').value = '@Model.RandomLicenseKey';
|
||||
document.getElementById('@(nameof(Model.ExpirationDate))').value = '@Model.FourteenDayExpirationDate';
|
||||
document.getElementById('@(nameof(Model.SalesAssistedTrialStarted))').value = true;
|
||||
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
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)Bit.Core.Enums.GatewayType.Stripe)') {
|
||||
window.open('https://dashboard.stripe.com/customers/' + customerId.value, '_blank');
|
||||
} else if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Braintree)') {
|
||||
window.open('https://www.braintreegateway.com/merchants/@(Model.BraintreeMerchantId)/'
|
||||
+ customerId.value, '_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)Bit.Core.Enums.GatewayType.Stripe)') {
|
||||
window.open('https://dashboard.stripe.com/subscriptions/' + subId.value, '_blank');
|
||||
} else if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Braintree)') {
|
||||
window.open('https://www.braintreegateway.com/merchants/@(Model.BraintreeMerchantId)/' +
|
||||
'subscriptions/' + subId.value, '_blank');
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
|
||||
<h1>Organization <small>@Model.Organization.Name</small></h1>
|
||||
<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)
|
||||
}
|
||||
<h2>Organization Information</h2>
|
||||
@await Html.PartialAsync("_ViewInformation", Model)
|
||||
<h2>Billing Information</h2>
|
||||
@await Html.PartialAsync("_BillingInformation",
|
||||
new BillingInformationModel { BillingInfo = Model.BillingInfo, OrganizationId = Model.Organization.Id })
|
||||
<form method="post" id="edit-form">
|
||||
<input asp-for="SalesAssistedTrialStarted" type="hidden">
|
||||
<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>
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" class="form-check-input" asp-for="Enabled">
|
||||
<label class="form-check-label" asp-for="Enabled"></label>
|
||||
</div>
|
||||
<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>
|
||||
<h2>Plan</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="PlanType"></label>
|
||||
<select class="form-control" asp-for="PlanType"
|
||||
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.PlanType>()"></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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
</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">
|
||||
</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">
|
||||
</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">
|
||||
</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">
|
||||
<label class="form-check-label" asp-for="SelfHost"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="Use2fa">
|
||||
<label class="form-check-label" asp-for="Use2fa"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseApi">
|
||||
<label class="form-check-label" asp-for="UseApi"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseGroups">
|
||||
<label class="form-check-label" asp-for="UseGroups"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UsePolicies">
|
||||
<label class="form-check-label" asp-for="UsePolicies"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseSso">
|
||||
<label class="form-check-label" asp-for="UseSso"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseKeyConnector">
|
||||
<label class="form-check-label" asp-for="UseKeyConnector"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseScim">
|
||||
<label class="form-check-label" asp-for="UseScim"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseDirectory">
|
||||
<label class="form-check-label" asp-for="UseDirectory"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseEvents">
|
||||
<label class="form-check-label" asp-for="UseEvents"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseResetPassword">
|
||||
<label class="form-check-label" asp-for="UseResetPassword"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseCustomPermissions">
|
||||
<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">
|
||||
<label class="form-check-label" asp-for="UseTotp"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UsersGetPremium">
|
||||
<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">
|
||||
<label class="form-check-label" asp-for="UseSecretsManager"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
</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">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
</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"
|
||||
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">
|
||||
<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">
|
||||
<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>
|
||||
@await Html.PartialAsync("_OrganizationForm", 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">
|
||||
|
@ -6,6 +6,11 @@
|
||||
|
||||
<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)
|
||||
|
@ -0,0 +1,9 @@
|
||||
@using Bit.SharedWeb.Utilities
|
||||
@model Bit.Core.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>
|
97
src/Admin/Views/Providers/AddExistingOrganization.cshtml
Normal file
97
src/Admin/Views/Providers/AddExistingOrganization.cshtml
Normal file
@ -0,0 +1,97 @@
|
||||
@using Bit.SharedWeb.Utilities
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@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>
|
@ -1,16 +1,55 @@
|
||||
@model CreateProviderModel
|
||||
@using Bit.SharedWeb.Utilities
|
||||
@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="OwnerEmail"></label>
|
||||
<input type="text" class="form-control" asp-for="OwnerEmail">
|
||||
<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="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>
|
||||
|
21
src/Admin/Views/Providers/CreateOrganization.cshtml
Normal file
21
src/Admin/Views/Providers/CreateOrganization.cshtml
Normal file
@ -0,0 +1,21 @@
|
||||
@model OrganizationEditModel
|
||||
@{
|
||||
ViewData["Title"] = "Create Client Organization";
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_OrganizationFormScripts")
|
||||
}
|
||||
|
||||
<h1>New Client Organization</h1>
|
||||
|
||||
@await Html.PartialAsync("_OrganizationForm", 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>
|
@ -36,6 +36,14 @@
|
||||
</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)
|
||||
<div class="d-flex mt-4">
|
||||
|
@ -1,4 +1,5 @@
|
||||
@model ProvidersModel
|
||||
@using Bit.SharedWeb.Utilities
|
||||
@model ProvidersModel
|
||||
@{
|
||||
ViewData["Title"] = "Providers";
|
||||
}
|
||||
@ -25,6 +26,7 @@
|
||||
<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>
|
||||
@ -44,6 +46,7 @@
|
||||
<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()">
|
||||
|
@ -1,4 +1,7 @@
|
||||
@model ProviderViewModel
|
||||
|
||||
@await Html.PartialAsync("_ProviderScripts")
|
||||
|
||||
<h2>Provider Organizations</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
@ -6,7 +9,17 @@
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<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>
|
||||
@ -22,8 +35,24 @@
|
||||
{
|
||||
<tr>
|
||||
<td class="align-middle">
|
||||
<a asp-controller="Organizations" asp-action="Edit"
|
||||
asp-route-id="@org.OrganizationId">@org.OrganizationName</a>
|
||||
<a asp-controller="Organizations" asp-action="Edit" asp-route-id="@org.OrganizationId">@org.OrganizationName</a>
|
||||
</td>
|
||||
<td>
|
||||
@org.Status
|
||||
</td>
|
||||
<td>
|
||||
<div class="float-right">
|
||||
@if (org.Status == OrganizationStatusType.Pending)
|
||||
{
|
||||
<a href="#" class="float-right" onclick="return resendOwnerInvite('@org.OrganizationId', '@org.OrganizationName');">
|
||||
<i class="fa fa-envelope-o fa-lg" title="Resend Setup Invite"></i>
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fa fa-envelope-o fa-lg text-secondary"></i>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
20
src/Admin/Views/Providers/_ProviderScripts.cshtml
Normal file
20
src/Admin/Views/Providers/_ProviderScripts.cshtml
Normal file
@ -0,0 +1,20 @@
|
||||
<script>
|
||||
function resendOwnerInvite(orgId, orgName) {
|
||||
if (confirm('Resend invite to "' + orgName + '"?')) {
|
||||
$.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>
|
@ -1,4 +1,5 @@
|
||||
@model ProviderViewModel
|
||||
@using Bit.SharedWeb.Utilities
|
||||
@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>
|
||||
@ -7,7 +8,10 @@
|
||||
<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.UserCount</dd>
|
||||
<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>
|
||||
|
247
src/Admin/Views/Shared/_OrganizationForm.cshtml
Normal file
247
src/Admin/Views/Shared/_OrganizationForm.cshtml
Normal file
@ -0,0 +1,247 @@
|
||||
@using Bit.SharedWeb.Utilities
|
||||
@model OrganizationEditModel
|
||||
|
||||
<form method="post" id="edit-form" asp-route-providerId="@Model.Provider?.Id">
|
||||
<input asp-for="SalesAssistedTrialStarted" type="hidden">
|
||||
<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">
|
||||
<label class="form-check-label" asp-for="Enabled"></label>
|
||||
</div>
|
||||
}
|
||||
<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>
|
||||
<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 || p is >= PlanType.TeamsMonthly 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"></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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
</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">
|
||||
</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">
|
||||
</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">
|
||||
</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">
|
||||
<label class="form-check-label" asp-for="SelfHost"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="Use2fa">
|
||||
<label class="form-check-label" asp-for="Use2fa"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseApi">
|
||||
<label class="form-check-label" asp-for="UseApi"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseGroups">
|
||||
<label class="form-check-label" asp-for="UseGroups"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UsePolicies">
|
||||
<label class="form-check-label" asp-for="UsePolicies"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseSso">
|
||||
<label class="form-check-label" asp-for="UseSso"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseKeyConnector">
|
||||
<label class="form-check-label" asp-for="UseKeyConnector"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseScim">
|
||||
<label class="form-check-label" asp-for="UseScim"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseDirectory">
|
||||
<label class="form-check-label" asp-for="UseDirectory"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseEvents">
|
||||
<label class="form-check-label" asp-for="UseEvents"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseResetPassword">
|
||||
<label class="form-check-label" asp-for="UseResetPassword"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseCustomPermissions">
|
||||
<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">
|
||||
<label class="form-check-label" asp-for="UseTotp"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UsersGetPremium">
|
||||
<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">
|
||||
<label class="form-check-label" asp-for="UseSecretsManager"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
</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">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Billing</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="BillingEmail"></label>
|
||||
@if (Model.Provider?.Type == ProviderType.Reseller)
|
||||
{
|
||||
<input type="email" class="form-control" asp-for="BillingEmail" readonly="readonly">
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="email" class="form-control" asp-for="BillingEmail">
|
||||
}
|
||||
</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"
|
||||
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">
|
||||
<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">
|
||||
<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>
|
35
src/Admin/Views/Shared/_OrganizationFormScripts.cshtml
Normal file
35
src/Admin/Views/Shared/_OrganizationFormScripts.cshtml
Normal file
@ -0,0 +1,35 @@
|
||||
<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;
|
||||
});
|
||||
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)Bit.Core.Enums.GatewayType.Stripe)') {
|
||||
window.open('https://dashboard.stripe.com/customers/' + customerId.value, '_blank');
|
||||
} else if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Braintree)') {
|
||||
window.open('https://www.braintreegateway.com/merchants/@(Model.BraintreeMerchantId)/'
|
||||
+ customerId.value, '_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)Bit.Core.Enums.GatewayType.Stripe)') {
|
||||
window.open('https://dashboard.stripe.com/subscriptions/' + subId.value, '_blank');
|
||||
} else if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Braintree)') {
|
||||
window.open('https://www.braintreegateway.com/merchants/@(Model.BraintreeMerchantId)/' +
|
||||
'subscriptions/' + subId.value, '_blank');
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
@ -177,6 +177,20 @@ public class OrganizationUsersController : Controller
|
||||
await _organizationService.ResendInviteAsync(orgGuidId, userId.Value, new Guid(id));
|
||||
}
|
||||
|
||||
[HttpPost("{organizationUserId}/accept-init")]
|
||||
public async Task AcceptInit(Guid orgId, Guid organizationUserId, [FromBody] OrganizationUserAcceptInitRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
await _organizationService.InitPendingOrganization(user.Id, orgId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName);
|
||||
await _organizationService.AcceptUserAsync(organizationUserId, user, model.Token, _userService);
|
||||
await _organizationService.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id, _userService);
|
||||
}
|
||||
|
||||
[HttpPost("{organizationUserId}/accept")]
|
||||
public async Task Accept(Guid orgId, Guid organizationUserId, [FromBody] OrganizationUserAcceptRequestModel model)
|
||||
{
|
||||
@ -188,11 +202,9 @@ public class OrganizationUsersController : Controller
|
||||
|
||||
var masterPasswordPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
||||
var useMasterPasswordPolicy = masterPasswordPolicy != null &&
|
||||
masterPasswordPolicy.Enabled &&
|
||||
masterPasswordPolicy.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled;
|
||||
|
||||
if (useMasterPasswordPolicy &&
|
||||
string.IsNullOrWhiteSpace(model.ResetPasswordKey))
|
||||
masterPasswordPolicy.Enabled &&
|
||||
masterPasswordPolicy.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled;
|
||||
if (useMasterPasswordPolicy && string.IsNullOrWhiteSpace(model.ResetPasswordKey))
|
||||
{
|
||||
throw new BadRequestException(string.Empty, "Master Password reset is required, but not provided.");
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ public class OrganizationsController : Controller
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IPaymentService _paymentService;
|
||||
@ -46,6 +47,7 @@ public class OrganizationsController : Controller
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IPolicyRepository policyRepository,
|
||||
IProviderRepository providerRepository,
|
||||
IOrganizationService organizationService,
|
||||
IUserService userService,
|
||||
IPaymentService paymentService,
|
||||
@ -63,6 +65,7 @@ public class OrganizationsController : Controller
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_policyRepository = policyRepository;
|
||||
_providerRepository = providerRepository;
|
||||
_organizationService = organizationService;
|
||||
_userService = userService;
|
||||
_paymentService = paymentService;
|
||||
@ -101,7 +104,7 @@ public class OrganizationsController : Controller
|
||||
public async Task<BillingResponseModel> GetBilling(string id)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.ManageBilling(orgIdGuid))
|
||||
if (!await _currentContext.ViewBillingHistory(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@ -120,7 +123,7 @@ public class OrganizationsController : Controller
|
||||
public async Task<OrganizationSubscriptionResponseModel> GetSubscription(string id)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.ManageBilling(orgIdGuid))
|
||||
if (!await _currentContext.ViewSubscription(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@ -139,7 +142,9 @@ public class OrganizationsController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new OrganizationSubscriptionResponseModel(organization, subscriptionInfo);
|
||||
var hideSensitiveData = !await _currentContext.EditSubscription(orgIdGuid);
|
||||
|
||||
return new OrganizationSubscriptionResponseModel(organization, subscriptionInfo, hideSensitiveData);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -240,7 +245,7 @@ public class OrganizationsController : Controller
|
||||
model.BillingEmail != organization.BillingEmail);
|
||||
|
||||
var hasRequiredPermissions = updateBilling
|
||||
? await _currentContext.ManageBilling(orgIdGuid)
|
||||
? await _currentContext.EditSubscription(orgIdGuid)
|
||||
: await _currentContext.OrganizationOwner(orgIdGuid);
|
||||
|
||||
if (!hasRequiredPermissions)
|
||||
@ -257,7 +262,7 @@ public class OrganizationsController : Controller
|
||||
public async Task PostPayment(string id, [FromBody] PaymentRequestModel model)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.ManageBilling(orgIdGuid))
|
||||
if (!await _currentContext.EditPaymentMethods(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@ -280,7 +285,7 @@ public class OrganizationsController : Controller
|
||||
public async Task<PaymentResponseModel> PostUpgrade(string id, [FromBody] OrganizationUpgradeRequestModel model)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.ManageBilling(orgIdGuid))
|
||||
if (!await _currentContext.EditSubscription(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@ -294,7 +299,7 @@ public class OrganizationsController : Controller
|
||||
public async Task PostSubscription(string id, [FromBody] OrganizationSubscriptionUpdateRequestModel model)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.ManageBilling(orgIdGuid))
|
||||
if (!await _currentContext.EditSubscription(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@ -307,7 +312,7 @@ public class OrganizationsController : Controller
|
||||
public async Task<PaymentResponseModel> PostSeat(string id, [FromBody] OrganizationSeatRequestModel model)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.ManageBilling(orgIdGuid))
|
||||
if (!await _currentContext.EditSubscription(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@ -321,7 +326,7 @@ public class OrganizationsController : Controller
|
||||
public async Task<PaymentResponseModel> PostStorage(string id, [FromBody] StorageRequestModel model)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.ManageBilling(orgIdGuid))
|
||||
if (!await _currentContext.EditSubscription(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@ -335,7 +340,7 @@ public class OrganizationsController : Controller
|
||||
public async Task PostVerifyBank(string id, [FromBody] OrganizationVerifyBankRequestModel model)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.ManageBilling(orgIdGuid))
|
||||
if (!await _currentContext.EditSubscription(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@ -348,7 +353,7 @@ public class OrganizationsController : Controller
|
||||
public async Task PostCancel(string id)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.ManageBilling(orgIdGuid))
|
||||
if (!await _currentContext.EditSubscription(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@ -361,7 +366,7 @@ public class OrganizationsController : Controller
|
||||
public async Task PostReinstate(string id)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.ManageBilling(orgIdGuid))
|
||||
if (!await _currentContext.EditSubscription(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
@ -54,9 +54,7 @@ public class ProviderOrganizationsController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
|
||||
await _providerService.AddOrganization(providerId, model.OrganizationId, userId, model.Key);
|
||||
await _providerService.AddOrganization(providerId, model.OrganizationId, model.Key);
|
||||
}
|
||||
|
||||
[HttpPost("")]
|
||||
|
@ -37,6 +37,19 @@ public class OrganizationUserInviteRequestModel
|
||||
}
|
||||
}
|
||||
|
||||
public class OrganizationUserAcceptInitRequestModel
|
||||
{
|
||||
[Required]
|
||||
public string Token { get; set; }
|
||||
[Required]
|
||||
public string Key { get; set; }
|
||||
[Required]
|
||||
public OrganizationKeysRequestModel Keys { get; set; }
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(1000)]
|
||||
public string CollectionName { get; set; }
|
||||
}
|
||||
|
||||
public class OrganizationUserAcceptRequestModel
|
||||
{
|
||||
[Required]
|
||||
|
@ -84,28 +84,30 @@ public class OrganizationResponseModel : ResponseModel
|
||||
|
||||
public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
|
||||
{
|
||||
public OrganizationSubscriptionResponseModel(Organization organization, SubscriptionInfo subscription = null)
|
||||
: base(organization, "organizationSubscription")
|
||||
public OrganizationSubscriptionResponseModel(Organization organization) : base(organization, "organizationSubscription")
|
||||
{
|
||||
if (subscription != null)
|
||||
{
|
||||
Subscription = subscription.Subscription != null ?
|
||||
new BillingSubscription(subscription.Subscription) : null;
|
||||
UpcomingInvoice = subscription.UpcomingInvoice != null ?
|
||||
new BillingSubscriptionUpcomingInvoice(subscription.UpcomingInvoice) : null;
|
||||
Expiration = DateTime.UtcNow.AddYears(1); // Not used, so just give it a value.
|
||||
}
|
||||
else
|
||||
{
|
||||
Expiration = organization.ExpirationDate;
|
||||
}
|
||||
|
||||
Expiration = organization.ExpirationDate;
|
||||
StorageName = organization.Storage.HasValue ?
|
||||
CoreHelpers.ReadableBytesSize(organization.Storage.Value) : null;
|
||||
StorageGb = organization.Storage.HasValue ?
|
||||
Math.Round(organization.Storage.Value / 1073741824D, 2) : 0; // 1 GB
|
||||
}
|
||||
|
||||
public OrganizationSubscriptionResponseModel(Organization organization, SubscriptionInfo subscription, bool hideSensitiveData)
|
||||
: this(organization)
|
||||
{
|
||||
Subscription = subscription.Subscription != null ? new BillingSubscription(subscription.Subscription) : null;
|
||||
UpcomingInvoice = subscription.UpcomingInvoice != null ? new BillingSubscriptionUpcomingInvoice(subscription.UpcomingInvoice) : null;
|
||||
Expiration = DateTime.UtcNow.AddYears(1); // Not used, so just give it a value.
|
||||
|
||||
if (hideSensitiveData)
|
||||
{
|
||||
BillingEmail = null;
|
||||
Subscription.Items = null;
|
||||
UpcomingInvoice.Amount = null;
|
||||
}
|
||||
}
|
||||
|
||||
public string StorageName { get; set; }
|
||||
public double? StorageGb { get; set; }
|
||||
public BillingSubscription Subscription { get; set; }
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums.Provider;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
@ -46,6 +47,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
UserId = organization.UserId?.ToString();
|
||||
ProviderId = organization.ProviderId?.ToString();
|
||||
ProviderName = organization.ProviderName;
|
||||
ProviderType = organization.ProviderType;
|
||||
FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName;
|
||||
FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null &&
|
||||
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
|
||||
@ -97,6 +99,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
public bool HasPublicAndPrivateKeys { get; set; }
|
||||
public string ProviderId { get; set; }
|
||||
public string ProviderName { get; set; }
|
||||
public ProviderType? ProviderType { get; set; }
|
||||
public string FamilySponsorshipFriendlyName { get; set; }
|
||||
public bool FamilySponsorshipAvailable { get; set; }
|
||||
public ProductType PlanProductType { get; set; }
|
||||
|
@ -100,6 +100,6 @@ public class BillingSubscriptionUpcomingInvoice
|
||||
Date = inv.Date;
|
||||
}
|
||||
|
||||
public decimal Amount { get; set; }
|
||||
public decimal? Amount { get; set; }
|
||||
public DateTime? Date { get; set; }
|
||||
}
|
||||
|
@ -13,9 +13,11 @@ namespace Bit.Core.Context;
|
||||
|
||||
public class CurrentContext : ICurrentContext
|
||||
{
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private bool _builtHttpContext;
|
||||
private bool _builtClaimsPrincipal;
|
||||
private IEnumerable<ProviderOrganizationProviderDetails> _providerOrganizationProviderDetails;
|
||||
private IEnumerable<ProviderUserOrganizationDetails> _providerUserOrganizations;
|
||||
|
||||
public virtual HttpContext HttpContext { get; set; }
|
||||
@ -37,8 +39,11 @@ public class CurrentContext : ICurrentContext
|
||||
public virtual ClientType ClientType { get; set; }
|
||||
public virtual Guid? ServiceAccountOrganizationId { get; set; }
|
||||
|
||||
public CurrentContext(IProviderUserRepository providerUserRepository)
|
||||
public CurrentContext(
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IProviderUserRepository providerUserRepository)
|
||||
{
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
}
|
||||
|
||||
@ -392,15 +397,34 @@ public class CurrentContext : ICurrentContext
|
||||
&& (o.Permissions?.ManageResetPassword ?? false)) ?? false);
|
||||
}
|
||||
|
||||
public async Task<bool> ManageBilling(Guid orgId)
|
||||
public async Task<bool> ViewSubscription(Guid orgId)
|
||||
{
|
||||
var orgManagedByProvider = await ProviderIdForOrg(orgId) != null;
|
||||
var orgManagedByMspProvider = (await GetOrganizationProviderDetails()).Any(po => po.OrganizationId == orgId && po.ProviderType == ProviderType.Msp);
|
||||
|
||||
return orgManagedByMspProvider
|
||||
? await ProviderUserForOrgAsync(orgId)
|
||||
: await OrganizationOwner(orgId);
|
||||
}
|
||||
|
||||
public async Task<bool> EditSubscription(Guid orgId)
|
||||
{
|
||||
var orgManagedByProvider = (await GetOrganizationProviderDetails()).Any(po => po.OrganizationId == orgId);
|
||||
|
||||
return orgManagedByProvider
|
||||
? await ProviderUserForOrgAsync(orgId)
|
||||
: await OrganizationOwner(orgId);
|
||||
}
|
||||
|
||||
public async Task<bool> EditPaymentMethods(Guid orgId)
|
||||
{
|
||||
return await EditSubscription(orgId);
|
||||
}
|
||||
|
||||
public async Task<bool> ViewBillingHistory(Guid orgId)
|
||||
{
|
||||
return await EditSubscription(orgId);
|
||||
}
|
||||
|
||||
public bool ProviderProviderAdmin(Guid providerId)
|
||||
{
|
||||
return Providers?.Any(o => o.Id == providerId && o.Type == ProviderUserType.ProviderAdmin) ?? false;
|
||||
@ -433,7 +457,7 @@ public class CurrentContext : ICurrentContext
|
||||
|
||||
public async Task<bool> ProviderUserForOrgAsync(Guid orgId)
|
||||
{
|
||||
return (await GetProviderOrganizations()).Any(po => po.OrganizationId == orgId);
|
||||
return (await GetProviderUserOrganizations()).Any(po => po.OrganizationId == orgId);
|
||||
}
|
||||
|
||||
public async Task<Guid?> ProviderIdForOrg(Guid orgId)
|
||||
@ -443,7 +467,7 @@ public class CurrentContext : ICurrentContext
|
||||
return null;
|
||||
}
|
||||
|
||||
var po = (await GetProviderOrganizations())
|
||||
var po = (await GetProviderUserOrganizations())
|
||||
?.FirstOrDefault(po => po.OrganizationId == orgId);
|
||||
|
||||
return po?.ProviderId;
|
||||
@ -520,7 +544,7 @@ public class CurrentContext : ICurrentContext
|
||||
};
|
||||
}
|
||||
|
||||
protected async Task<IEnumerable<ProviderUserOrganizationDetails>> GetProviderOrganizations()
|
||||
protected async Task<IEnumerable<ProviderUserOrganizationDetails>> GetProviderUserOrganizations()
|
||||
{
|
||||
if (_providerUserOrganizations == null && UserId.HasValue)
|
||||
{
|
||||
@ -529,4 +553,14 @@ public class CurrentContext : ICurrentContext
|
||||
|
||||
return _providerUserOrganizations;
|
||||
}
|
||||
|
||||
protected async Task<IEnumerable<ProviderOrganizationProviderDetails>> GetOrganizationProviderDetails()
|
||||
{
|
||||
if (_providerOrganizationProviderDetails == null && UserId.HasValue)
|
||||
{
|
||||
_providerOrganizationProviderDetails = await _providerOrganizationRepository.GetManyByUserAsync(UserId.Value);
|
||||
}
|
||||
|
||||
return _providerOrganizationProviderDetails;
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,10 @@ public interface ICurrentContext
|
||||
Task<bool> ManageUsers(Guid orgId);
|
||||
Task<bool> ManageScim(Guid orgId);
|
||||
Task<bool> ManageResetPassword(Guid orgId);
|
||||
Task<bool> ManageBilling(Guid orgId);
|
||||
Task<bool> ViewSubscription(Guid orgId);
|
||||
Task<bool> EditSubscription(Guid orgId);
|
||||
Task<bool> EditPaymentMethods(Guid orgId);
|
||||
Task<bool> ViewBillingHistory(Guid orgId);
|
||||
Task<bool> ProviderUserForOrgAsync(Guid orgId);
|
||||
bool ProviderProviderAdmin(Guid providerId);
|
||||
bool ProviderUser(Guid providerId);
|
||||
|
@ -69,6 +69,7 @@ public class Organization : ITableObject<Guid>, ISubscriber, IStorable, IStorabl
|
||||
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
||||
public int? MaxAutoscaleSeats { get; set; } = null;
|
||||
public DateTime? OwnersNotifiedOfAutoscaling { get; set; } = null;
|
||||
public OrganizationStatusType Status { get; set; }
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
|
7
src/Core/Enums/OrganizationStatusType.cs
Normal file
7
src/Core/Enums/OrganizationStatusType.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Enums;
|
||||
|
||||
public enum OrganizationStatusType : byte
|
||||
{
|
||||
Pending = 0,
|
||||
Created = 1
|
||||
}
|
@ -1,7 +1,11 @@
|
||||
namespace Bit.Core.Enums.Provider;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Core.Enums.Provider;
|
||||
|
||||
public enum ProviderType : byte
|
||||
{
|
||||
[Display(ShortName = "MSP", Name = "Managed Service Provider", Description = "Access to clients organization")]
|
||||
Msp = 0,
|
||||
[Display(ShortName = "Reseller", Name = "Reseller", Description = "Access to clients billing")]
|
||||
Reseller = 1,
|
||||
}
|
||||
|
@ -40,6 +40,8 @@ public enum ReferenceEventType
|
||||
CollectionCreated,
|
||||
[EnumMember(Value = "organization-edited-by-admin")]
|
||||
OrganizationEditedByAdmin,
|
||||
[EnumMember(Value = "organization-created-by-admin")]
|
||||
OrganizationCreatedByAdmin,
|
||||
[EnumMember(Value = "sm-service-account-accessed-secret")]
|
||||
SmServiceAccountAccessedSecret,
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Enums.Provider;
|
||||
|
||||
namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
public class OrganizationUserOrganizationDetails
|
||||
{
|
||||
@ -36,6 +38,7 @@ public class OrganizationUserOrganizationDetails
|
||||
public string PrivateKey { get; set; }
|
||||
public Guid? ProviderId { get; set; }
|
||||
public string ProviderName { get; set; }
|
||||
public ProviderType? ProviderType { get; set; }
|
||||
public string FamilySponsorshipFriendlyName { get; set; }
|
||||
public string SsoConfig { get; set; }
|
||||
public DateTime? FamilySponsorshipLastSyncDate { get; set; }
|
||||
|
@ -1,4 +1,6 @@
|
||||
namespace Bit.Core.Models.Data;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Data;
|
||||
|
||||
public class ProviderOrganizationOrganizationDetails
|
||||
{
|
||||
@ -13,4 +15,5 @@ public class ProviderOrganizationOrganizationDetails
|
||||
public int UserCount { get; set; }
|
||||
public int? Seats { get; set; }
|
||||
public string Plan { get; set; }
|
||||
public OrganizationStatusType Status { get; set; }
|
||||
}
|
||||
|
@ -0,0 +1,12 @@
|
||||
using Bit.Core.Enums.Provider;
|
||||
|
||||
namespace Bit.Core.Models.Data;
|
||||
|
||||
public class ProviderOrganizationProviderDetails
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid ProviderId { get; set; }
|
||||
public Guid OrganizationId { get; set; }
|
||||
public string ProviderName { get; set; }
|
||||
public ProviderType ProviderType { get; set; }
|
||||
}
|
@ -9,12 +9,14 @@ public class OrganizationUserInvitedViewModel : BaseTitleContactUsMailModel
|
||||
public string OrganizationNameUrlEncoded { get; set; }
|
||||
public string Token { get; set; }
|
||||
public string ExpirationDate { get; set; }
|
||||
public bool InitOrganization { get; set; }
|
||||
public string Url => string.Format("{0}/accept-organization?organizationId={1}&" +
|
||||
"organizationUserId={2}&email={3}&organizationName={4}&token={5}",
|
||||
"organizationUserId={2}&email={3}&organizationName={4}&token={5}&initOrganization={6}",
|
||||
WebVaultUrl,
|
||||
OrganizationId,
|
||||
OrganizationUserId,
|
||||
Email,
|
||||
OrganizationNameUrlEncoded,
|
||||
Token);
|
||||
Token,
|
||||
InitOrganization);
|
||||
}
|
||||
|
9
src/Core/Providers/Interfaces/ICreateProviderCommand.cs
Normal file
9
src/Core/Providers/Interfaces/ICreateProviderCommand.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using Bit.Core.Entities.Provider;
|
||||
|
||||
namespace Bit.Core.Providers.Interfaces;
|
||||
|
||||
public interface ICreateProviderCommand
|
||||
{
|
||||
Task CreateMspAsync(Provider provider, string ownerEmail);
|
||||
Task CreateResellerAsync(Provider provider);
|
||||
}
|
@ -13,4 +13,5 @@ public interface IOrganizationRepository : IRepository<Organization, Guid>
|
||||
Task<ICollection<OrganizationAbility>> GetManyAbilitiesAsync();
|
||||
Task<Organization> GetByLicenseKeyAsync(string licenseKey);
|
||||
Task<SelfHostedOrganizationDetails> GetSelfHostedOrganizationDetailsById(Guid id);
|
||||
Task<ICollection<Organization>> SearchUnassignedToProviderAsync(string name, string ownerEmail, int skip, int take);
|
||||
}
|
||||
|
@ -5,6 +5,9 @@ namespace Bit.Core.Repositories;
|
||||
|
||||
public interface IProviderOrganizationRepository : IRepository<ProviderOrganization, Guid>
|
||||
{
|
||||
Task<ICollection<ProviderOrganization>> CreateManyAsync(IEnumerable<ProviderOrganization> providerOrganizations);
|
||||
Task<ICollection<ProviderOrganizationOrganizationDetails>> GetManyDetailsByProviderAsync(Guid providerId);
|
||||
Task<ProviderOrganization> GetByOrganizationId(Guid organizationId);
|
||||
Task<IEnumerable<ProviderOrganizationProviderDetails>> GetManyByUserAsync(Guid userId);
|
||||
Task<int> GetCountByOrganizationIdsAsync(IEnumerable<Guid> organizationIds);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ namespace Bit.Core.Repositories;
|
||||
|
||||
public interface IProviderRepository : IRepository<Provider, Guid>
|
||||
{
|
||||
Task<Provider> GetByOrganizationIdAsync(Guid organizationId);
|
||||
Task<ICollection<Provider>> SearchAsync(string name, string userEmail, int skip, int take);
|
||||
Task<ICollection<ProviderAbility>> GetManyAbilitiesAsync();
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ public interface IEventService
|
||||
Task LogProviderUserEventAsync(ProviderUser providerUser, EventType type, DateTime? date = null);
|
||||
Task LogProviderUsersEventAsync(IEnumerable<(ProviderUser, EventType, DateTime?)> events);
|
||||
Task LogProviderOrganizationEventAsync(ProviderOrganization providerOrganization, EventType type, DateTime? date = null);
|
||||
Task LogProviderOrganizationEventsAsync(IEnumerable<(ProviderOrganization, EventType, DateTime?)> events);
|
||||
Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type, DateTime? date = null);
|
||||
Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type, EventSystemUser systemUser, DateTime? date = null);
|
||||
Task LogServiceAccountSecretEventAsync(Guid serviceAccountId, Secret secret, EventType type, DateTime? date = null);
|
||||
|
@ -15,8 +15,8 @@ public interface IMailService
|
||||
Task SendTwoFactorEmailAsync(string email, string token);
|
||||
Task SendNoMasterPasswordHintEmailAsync(string email);
|
||||
Task SendMasterPasswordHintEmailAsync(string email, string hint);
|
||||
Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token, bool isFreeOrg);
|
||||
Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites, bool isFreeOrg);
|
||||
Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token, bool isFreeOrg, bool initOrganization = false);
|
||||
Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites, bool isFreeOrg, bool initOrganization = false);
|
||||
Task SendOrganizationMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
|
||||
Task SendOrganizationAutoscaledEmailAsync(Organization organization, int initialSeatCount, IEnumerable<string> ownerEmails);
|
||||
Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable<string> adminEmails);
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Entities;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data;
|
||||
@ -37,9 +38,8 @@ public interface IOrganizationService
|
||||
Task<OrganizationUser> InviteUserAsync(Guid organizationId, EventSystemUser systemUser, string email,
|
||||
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections, IEnumerable<Guid> groups);
|
||||
Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId);
|
||||
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId);
|
||||
Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token,
|
||||
IUserService userService);
|
||||
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false);
|
||||
Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token, IUserService userService);
|
||||
Task<OrganizationUser> AcceptUserAsync(string orgIdentifier, User user, IUserService userService);
|
||||
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
||||
Guid confirmingUserId, IUserService userService);
|
||||
@ -69,5 +69,13 @@ public interface IOrganizationService
|
||||
Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser, IUserService userService);
|
||||
Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
|
||||
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService);
|
||||
Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted);
|
||||
/// <summary>
|
||||
/// Update an Organization entry by setting the public/private keys, set it as 'Enabled' and move the Status from 'Pending' to 'Created'.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method must target a disabled Organization that has null keys and status as 'Pending'.
|
||||
/// </remarks>
|
||||
Task InitPendingOrganization(Guid userId, Guid organizationId, string publicKey, string privateKey, string collectionName);
|
||||
Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null);
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ namespace Bit.Core.Services;
|
||||
|
||||
public interface IProviderService
|
||||
{
|
||||
Task CreateAsync(string ownerEmail);
|
||||
Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key);
|
||||
Task UpdateAsync(Provider provider, bool updateBilling = false);
|
||||
|
||||
@ -20,11 +19,13 @@ public interface IProviderService
|
||||
Task<List<Tuple<ProviderUser, string>>> DeleteUsersAsync(Guid providerId, IEnumerable<Guid> providerUserIds,
|
||||
Guid deletingUserId);
|
||||
|
||||
Task AddOrganization(Guid providerId, Guid organizationId, Guid addingUserId, string key);
|
||||
Task AddOrganization(Guid providerId, Guid organizationId, string key);
|
||||
Task AddOrganizationsToReseller(Guid providerId, IEnumerable<Guid> organizationIds);
|
||||
Task<ProviderOrganization> CreateOrganizationAsync(Guid providerId, OrganizationSignup organizationSignup,
|
||||
string clientOwnerEmail, User user);
|
||||
Task RemoveOrganizationAsync(Guid providerId, Guid providerOrganizationId, Guid removingUserId);
|
||||
Task LogProviderAccessToOrganizationAsync(Guid organizationId);
|
||||
Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid ownerId);
|
||||
Task SendProviderSetupInviteEmailAsync(Provider provider, string ownerEmail);
|
||||
}
|
||||
|
||||
|
@ -332,26 +332,38 @@ public class EventService : IEventService
|
||||
|
||||
public async Task LogProviderOrganizationEventAsync(ProviderOrganization providerOrganization, EventType type,
|
||||
DateTime? date = null)
|
||||
{
|
||||
await LogProviderOrganizationEventsAsync(new[] { (providerOrganization, type, date) });
|
||||
}
|
||||
|
||||
public async Task LogProviderOrganizationEventsAsync(IEnumerable<(ProviderOrganization, EventType, DateTime?)> events)
|
||||
{
|
||||
var providerAbilities = await _applicationCacheService.GetProviderAbilitiesAsync();
|
||||
if (!CanUseProviderEvents(providerAbilities, providerOrganization.ProviderId))
|
||||
var eventMessages = new List<IEvent>();
|
||||
foreach (var (providerOrganization, type, date) in events)
|
||||
{
|
||||
return;
|
||||
if (!CanUseProviderEvents(providerAbilities, providerOrganization.ProviderId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var e = new EventMessage(_currentContext)
|
||||
{
|
||||
ProviderId = providerOrganization.ProviderId,
|
||||
ProviderOrganizationId = providerOrganization.Id,
|
||||
Type = type,
|
||||
ActingUserId = _currentContext?.UserId,
|
||||
Date = date.GetValueOrDefault(DateTime.UtcNow)
|
||||
};
|
||||
|
||||
eventMessages.Add(e);
|
||||
}
|
||||
|
||||
var e = new EventMessage(_currentContext)
|
||||
{
|
||||
ProviderId = providerOrganization.ProviderId,
|
||||
ProviderOrganizationId = providerOrganization.Id,
|
||||
Type = type,
|
||||
ActingUserId = _currentContext?.UserId,
|
||||
Date = date.GetValueOrDefault(DateTime.UtcNow)
|
||||
};
|
||||
await _eventWriteService.CreateAsync(e);
|
||||
await _eventWriteService.CreateManyAsync(eventMessages);
|
||||
}
|
||||
|
||||
public async Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type,
|
||||
DateTime? date = null)
|
||||
DateTime? date = null)
|
||||
{
|
||||
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
if (!CanUseEvents(orgAbilities, organizationDomain.OrganizationId))
|
||||
|
@ -203,10 +203,10 @@ public class HandlebarsMailService : IMailService
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token, bool isFreeOrg) =>
|
||||
BulkSendOrganizationInviteEmailAsync(organizationName, new[] { (orgUser, token) }, isFreeOrg);
|
||||
public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token, bool isFreeOrg, bool initOrganization = false) =>
|
||||
BulkSendOrganizationInviteEmailAsync(organizationName, new[] { (orgUser, token) }, isFreeOrg, initOrganization);
|
||||
|
||||
public async Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites, bool isFreeOrg)
|
||||
public async Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites, bool isFreeOrg, bool initOrganization = false)
|
||||
{
|
||||
MailQueueMessage CreateMessage(string email, object model)
|
||||
{
|
||||
@ -229,6 +229,7 @@ public class HandlebarsMailService : IMailService
|
||||
OrganizationNameUrlEncoded = WebUtility.UrlEncode(organizationName),
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
SiteName = _globalSettings.SiteName,
|
||||
InitOrganization = initOrganization
|
||||
}
|
||||
));
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -598,7 +599,7 @@ public class OrganizationService : IOrganizationService
|
||||
bool provider = false)
|
||||
{
|
||||
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == signup.Plan);
|
||||
if (!(plan is { LegacyYear: null }))
|
||||
if (plan is not { LegacyYear: null })
|
||||
{
|
||||
throw new BadRequestException("Invalid plan selected.");
|
||||
}
|
||||
@ -649,6 +650,7 @@ public class OrganizationService : IOrganizationService
|
||||
PrivateKey = signup.PrivateKey,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
RevisionDate = DateTime.UtcNow,
|
||||
Status = OrganizationStatusType.Created
|
||||
};
|
||||
|
||||
if (plan.Type == PlanType.Free && !provider)
|
||||
@ -748,7 +750,8 @@ public class OrganizationService : IOrganizationService
|
||||
PublicKey = publicKey,
|
||||
PrivateKey = privateKey,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
RevisionDate = DateTime.UtcNow
|
||||
RevisionDate = DateTime.UtcNow,
|
||||
Status = OrganizationStatusType.Created
|
||||
};
|
||||
|
||||
var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);
|
||||
@ -1167,14 +1170,14 @@ public class OrganizationService : IOrganizationService
|
||||
continue;
|
||||
}
|
||||
|
||||
await SendInviteAsync(orgUser, org);
|
||||
await SendInviteAsync(orgUser, org, false);
|
||||
result.Add(Tuple.Create(orgUser, ""));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId)
|
||||
public async Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
||||
if (orgUser == null || orgUser.OrganizationId != organizationId ||
|
||||
@ -1184,7 +1187,7 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
|
||||
var org = await GetOrgById(orgUser.OrganizationId);
|
||||
await SendInviteAsync(orgUser, org);
|
||||
await SendInviteAsync(orgUser, org, initOrganization);
|
||||
}
|
||||
|
||||
private async Task SendInvitesAsync(IEnumerable<OrganizationUser> orgUsers, Organization organization)
|
||||
@ -1196,13 +1199,13 @@ public class OrganizationService : IOrganizationService
|
||||
orgUsers.Select(o => (o, new ExpiringToken(MakeToken(o), DateTime.UtcNow.AddDays(5)))), organization.PlanType == PlanType.Free);
|
||||
}
|
||||
|
||||
private async Task SendInviteAsync(OrganizationUser orgUser, Organization organization)
|
||||
private async Task SendInviteAsync(OrganizationUser orgUser, Organization organization, bool initOrganization)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var nowMillis = CoreHelpers.ToEpocMilliseconds(now);
|
||||
var token = _dataProtector.Protect(
|
||||
$"OrganizationUserInvite {orgUser.Id} {orgUser.Email} {nowMillis}");
|
||||
await _mailService.SendOrganizationInviteEmailAsync(organization.Name, orgUser, new ExpiringToken(token, now.AddDays(5)), organization.PlanType == PlanType.Free);
|
||||
await _mailService.SendOrganizationInviteEmailAsync(organization.Name, orgUser, new ExpiringToken(token, now.AddDays(5)), organization.PlanType == PlanType.Free, initOrganization);
|
||||
}
|
||||
|
||||
public async Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token,
|
||||
@ -2425,4 +2428,89 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
public async Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted)
|
||||
{
|
||||
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
|
||||
if (plan is not { LegacyYear: null })
|
||||
{
|
||||
throw new BadRequestException("Invalid plan selected.");
|
||||
}
|
||||
|
||||
if (plan.Disabled)
|
||||
{
|
||||
throw new BadRequestException("Plan not found.");
|
||||
}
|
||||
|
||||
organization.Id = CoreHelpers.GenerateComb();
|
||||
organization.Enabled = false;
|
||||
organization.Status = OrganizationStatusType.Pending;
|
||||
|
||||
await SignUpAsync(organization, default, null, null, true);
|
||||
|
||||
var ownerOrganizationUser = new OrganizationUser
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = null,
|
||||
Email = ownerEmail,
|
||||
Key = null,
|
||||
Type = OrganizationUserType.Owner,
|
||||
Status = OrganizationUserStatusType.Invited,
|
||||
AccessAll = true
|
||||
};
|
||||
await _organizationUserRepository.CreateAsync(ownerOrganizationUser);
|
||||
|
||||
await SendInviteAsync(ownerOrganizationUser, organization, true);
|
||||
await _eventService.LogOrganizationUserEventAsync(ownerOrganizationUser, EventType.OrganizationUser_Invited);
|
||||
|
||||
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.OrganizationCreatedByAdmin, organization)
|
||||
{
|
||||
EventRaisedByUser = userService.GetUserName(user),
|
||||
SalesAssistedTrialStarted = salesAssistedTrialStarted,
|
||||
});
|
||||
}
|
||||
|
||||
public async Task InitPendingOrganization(Guid userId, Guid organizationId, string publicKey, string privateKey, string collectionName)
|
||||
{
|
||||
await ValidateSignUpPoliciesAsync(userId);
|
||||
|
||||
var org = await GetOrgById(organizationId);
|
||||
|
||||
if (org.Enabled)
|
||||
{
|
||||
throw new BadRequestException("Organization is already enabled.");
|
||||
}
|
||||
|
||||
if (org.Status != OrganizationStatusType.Pending)
|
||||
{
|
||||
throw new BadRequestException("Organization is not on a Pending status.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(org.PublicKey))
|
||||
{
|
||||
throw new BadRequestException("Organization already has a Public Key.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(org.PrivateKey))
|
||||
{
|
||||
throw new BadRequestException("Organization already has a Private Key.");
|
||||
}
|
||||
|
||||
org.Enabled = true;
|
||||
org.Status = OrganizationStatusType.Created;
|
||||
org.PublicKey = publicKey;
|
||||
org.PrivateKey = privateKey;
|
||||
|
||||
await UpdateAsync(org);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(collectionName))
|
||||
{
|
||||
var defaultCollection = new Collection
|
||||
{
|
||||
Name = collectionName,
|
||||
OrganizationId = org.Id
|
||||
};
|
||||
await _collectionRepository.CreateAsync(defaultCollection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,8 +70,13 @@ public class NoopEventService : IEventService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task LogProviderOrganizationEventsAsync(IEnumerable<(ProviderOrganization, EventType, DateTime?)> events)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type,
|
||||
DateTime? date = null)
|
||||
DateTime? date = null)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
@ -52,12 +52,12 @@ public class NoopMailService : IMailService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token, bool isFreeOrg)
|
||||
public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token, bool isFreeOrg, bool initOrganization = false)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites, bool isFreeOrg)
|
||||
public Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites, bool isFreeOrg, bool initOrganization = false)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
@ -7,8 +7,6 @@ namespace Bit.Core.Services;
|
||||
|
||||
public class NoopProviderService : IProviderService
|
||||
{
|
||||
public Task CreateAsync(string ownerEmail) => throw new NotImplementedException();
|
||||
|
||||
public Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key) => throw new NotImplementedException();
|
||||
|
||||
public Task UpdateAsync(Provider provider, bool updateBilling = false) => throw new NotImplementedException();
|
||||
@ -25,7 +23,9 @@ public class NoopProviderService : IProviderService
|
||||
|
||||
public Task<List<Tuple<ProviderUser, string>>> DeleteUsersAsync(Guid providerId, IEnumerable<Guid> providerUserIds, Guid deletingUserId) => throw new NotImplementedException();
|
||||
|
||||
public Task AddOrganization(Guid providerId, Guid organizationId, Guid addingUserId, string key) => throw new NotImplementedException();
|
||||
public Task AddOrganization(Guid providerId, Guid organizationId, string key) => throw new NotImplementedException();
|
||||
|
||||
public Task AddOrganizationsToReseller(Guid providerId, IEnumerable<Guid> organizationIds) => throw new NotImplementedException();
|
||||
|
||||
public Task<ProviderOrganization> CreateOrganizationAsync(Guid providerId, OrganizationSignup organizationSignup, string clientOwnerEmail, User user) => throw new NotImplementedException();
|
||||
|
||||
@ -34,4 +34,5 @@ public class NoopProviderService : IProviderService
|
||||
public Task LogProviderAccessToOrganizationAsync(Guid organizationId) => throw new NotImplementedException();
|
||||
|
||||
public Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid userId) => throw new NotImplementedException();
|
||||
public Task SendProviderSetupInviteEmailAsync(Provider provider, string ownerEmail) => throw new NotImplementedException();
|
||||
}
|
||||
|
@ -134,4 +134,18 @@ public class OrganizationRepository : Repository<Organization, Guid>, IOrganizat
|
||||
return selfHostOrganization;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<Organization>> SearchUnassignedToProviderAsync(string name, string ownerEmail, int skip, int take)
|
||||
{
|
||||
using (var connection = new SqlConnection(ReadOnlyConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<Organization>(
|
||||
"[dbo].[Organization_UnassignedToProviderSearch]",
|
||||
new { Name = name, OwnerEmail = ownerEmail, Skip = skip, Take = take },
|
||||
commandType: CommandType.StoredProcedure,
|
||||
commandTimeout: 120);
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,48 @@ public class ProviderOrganizationRepository : Repository<ProviderOrganization, G
|
||||
: base(connectionString, readOnlyConnectionString)
|
||||
{ }
|
||||
|
||||
public async Task<ICollection<ProviderOrganization>> CreateManyAsync(IEnumerable<ProviderOrganization> providerOrganizations)
|
||||
{
|
||||
var entities = providerOrganizations.ToList();
|
||||
|
||||
if (!entities.Any())
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
foreach (var providerOrganization in entities)
|
||||
{
|
||||
providerOrganization.SetNewId();
|
||||
}
|
||||
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
connection.Open();
|
||||
|
||||
using (var transaction = connection.BeginTransaction())
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))
|
||||
{
|
||||
bulkCopy.DestinationTableName = "[dbo].[ProviderOrganization]";
|
||||
var dataTable = BuildProviderOrganizationsTable(bulkCopy, entities);
|
||||
await bulkCopy.WriteToServerAsync(dataTable);
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
|
||||
return entities.ToList();
|
||||
}
|
||||
catch
|
||||
{
|
||||
transaction.Rollback();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<ProviderOrganizationOrganizationDetails>> GetManyDetailsByProviderAsync(Guid providerId)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
@ -43,4 +85,83 @@ public class ProviderOrganizationRepository : Repository<ProviderOrganization, G
|
||||
return results.SingleOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ProviderOrganizationProviderDetails>> GetManyByUserAsync(Guid userId)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<ProviderOrganizationProviderDetails>(
|
||||
"[dbo].[ProviderOrganizationProviderDetails_ReadByUserId]",
|
||||
new { UserId = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> GetCountByOrganizationIdsAsync(
|
||||
IEnumerable<Guid> organizationIds)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.ExecuteScalarAsync<int>(
|
||||
$"[{Schema}].[ProviderOrganization_ReadCountByOrganizationIds]",
|
||||
new { Ids = organizationIds.ToGuidIdArrayTVP() },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
private DataTable BuildProviderOrganizationsTable(SqlBulkCopy bulkCopy, IEnumerable<ProviderOrganization> providerOrganizations)
|
||||
{
|
||||
var po = providerOrganizations.FirstOrDefault();
|
||||
if (po == null)
|
||||
{
|
||||
throw new ApplicationException("Must have some ProviderOrganizations to bulk import.");
|
||||
}
|
||||
|
||||
var providerOrganizationsTable = new DataTable("ProviderOrganizationDataTable");
|
||||
|
||||
var idColumn = new DataColumn(nameof(po.Id), typeof(Guid));
|
||||
providerOrganizationsTable.Columns.Add(idColumn);
|
||||
var providerIdColumn = new DataColumn(nameof(po.ProviderId), typeof(Guid));
|
||||
providerOrganizationsTable.Columns.Add(providerIdColumn);
|
||||
var organizationIdColumn = new DataColumn(nameof(po.OrganizationId), typeof(Guid));
|
||||
providerOrganizationsTable.Columns.Add(organizationIdColumn);
|
||||
var keyColumn = new DataColumn(nameof(po.Key), typeof(string));
|
||||
providerOrganizationsTable.Columns.Add(keyColumn);
|
||||
var settingsColumn = new DataColumn(nameof(po.Settings), typeof(string));
|
||||
providerOrganizationsTable.Columns.Add(settingsColumn);
|
||||
var creationDateColumn = new DataColumn(nameof(po.CreationDate), po.CreationDate.GetType());
|
||||
providerOrganizationsTable.Columns.Add(creationDateColumn);
|
||||
var revisionDateColumn = new DataColumn(nameof(po.RevisionDate), po.RevisionDate.GetType());
|
||||
providerOrganizationsTable.Columns.Add(revisionDateColumn);
|
||||
|
||||
foreach (DataColumn col in providerOrganizationsTable.Columns)
|
||||
{
|
||||
bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);
|
||||
}
|
||||
|
||||
var keys = new DataColumn[1];
|
||||
keys[0] = idColumn;
|
||||
providerOrganizationsTable.PrimaryKey = keys;
|
||||
|
||||
foreach (var providerOrganization in providerOrganizations)
|
||||
{
|
||||
var row = providerOrganizationsTable.NewRow();
|
||||
|
||||
row[idColumn] = providerOrganization.Id;
|
||||
row[providerIdColumn] = providerOrganization.ProviderId;
|
||||
row[organizationIdColumn] = providerOrganization.OrganizationId;
|
||||
row[keyColumn] = providerOrganization.Key;
|
||||
row[settingsColumn] = providerOrganization.Settings;
|
||||
row[creationDateColumn] = providerOrganization.CreationDate;
|
||||
row[revisionDateColumn] = providerOrganization.RevisionDate;
|
||||
|
||||
providerOrganizationsTable.Rows.Add(row);
|
||||
}
|
||||
|
||||
return providerOrganizationsTable;
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,19 @@ public class ProviderRepository : Repository<Provider, Guid>, IProviderRepositor
|
||||
: base(connectionString, readOnlyConnectionString)
|
||||
{ }
|
||||
|
||||
public async Task<Provider> GetByOrganizationIdAsync(Guid organizationId)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<Provider>(
|
||||
"[dbo].[Provider_ReadByOrganizationId]",
|
||||
new { OrganizationId = organizationId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<Provider>> SearchAsync(string name, string userEmail, int skip, int take)
|
||||
{
|
||||
using (var connection = new SqlConnection(ReadOnlyConnectionString))
|
||||
|
@ -91,6 +91,45 @@ public class OrganizationRepository : Repository<Core.Entities.Organization, Org
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<Core.Entities.Organization>> SearchUnassignedToProviderAsync(string name, string ownerEmail, int skip, int take)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var query = from o in dbContext.Organizations
|
||||
where o.PlanType >= PlanType.TeamsMonthly && o.PlanType <= PlanType.EnterpriseAnnually &&
|
||||
!dbContext.ProviderOrganizations.Any(po => po.OrganizationId == o.Id) &&
|
||||
(string.IsNullOrWhiteSpace(name) || EF.Functions.Like(o.Name, $"%{name}%"))
|
||||
select o;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ownerEmail))
|
||||
{
|
||||
if (dbContext.Database.IsNpgsql())
|
||||
{
|
||||
query = from o in query
|
||||
join ou in dbContext.OrganizationUsers
|
||||
on o.Id equals ou.OrganizationId
|
||||
join u in dbContext.Users
|
||||
on ou.UserId equals u.Id
|
||||
where ou.Type == OrganizationUserType.Owner && EF.Functions.ILike(EF.Functions.Collate(u.Email, "default"), $"{ownerEmail}%")
|
||||
select o;
|
||||
}
|
||||
else
|
||||
{
|
||||
query = from o in query
|
||||
join ou in dbContext.OrganizationUsers
|
||||
on o.Id equals ou.OrganizationId
|
||||
join u in dbContext.Users
|
||||
on ou.UserId equals u.Id
|
||||
where ou.Type == OrganizationUserType.Owner && EF.Functions.Like(u.Email, $"{ownerEmail}%")
|
||||
select o;
|
||||
}
|
||||
}
|
||||
|
||||
return await query.OrderByDescending(o => o.CreationDate).Skip(skip).Take(take).ToArrayAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateStorageAsync(Guid id)
|
||||
{
|
||||
await OrganizationUpdateStorage(id);
|
||||
|
@ -15,6 +15,30 @@ public class ProviderOrganizationRepository :
|
||||
: base(serviceScopeFactory, mapper, context => context.ProviderOrganizations)
|
||||
{ }
|
||||
|
||||
public async Task<ICollection<ProviderOrganization>> CreateManyAsync(IEnumerable<ProviderOrganization> providerOrganizations)
|
||||
{
|
||||
var entities = providerOrganizations.ToList();
|
||||
|
||||
if (!entities.Any())
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
foreach (var providerOrganization in entities)
|
||||
{
|
||||
providerOrganization.SetNewId();
|
||||
}
|
||||
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
await dbContext.AddRangeAsync(entities);
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
public async Task<ICollection<ProviderOrganizationOrganizationDetails>> GetManyDetailsByProviderAsync(Guid providerId)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
@ -32,4 +56,21 @@ public class ProviderOrganizationRepository :
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
return await GetDbSet(dbContext).Where(po => po.OrganizationId == organizationId).FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ProviderOrganizationProviderDetails>> GetManyByUserAsync(Guid userId)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var query = new ProviderOrganizationReadByUserIdQuery(userId);
|
||||
var data = await query.Run(dbContext).ToListAsync();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> GetCountByOrganizationIdsAsync(IEnumerable<Guid> organizationIds)
|
||||
{
|
||||
var query = new ProviderOrganizationCountByOrganizationIdsQuery(organizationIds);
|
||||
return await GetCountFromQuery(query);
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,20 @@ public class ProviderRepository : Repository<Provider, Models.Provider, Guid>, I
|
||||
await base.DeleteAsync(provider);
|
||||
}
|
||||
|
||||
public async Task<Provider> GetByOrganizationIdAsync(Guid organizationId)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var query = from p in dbContext.Providers
|
||||
join po in dbContext.ProviderOrganizations
|
||||
on p.Id equals po.ProviderId
|
||||
where po.OrganizationId == organizationId
|
||||
select p;
|
||||
return await query.FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<Provider>> SearchAsync(string name, string userEmail, int skip, int take)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
|
@ -55,6 +55,7 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery<OrganizationU
|
||||
Permissions = ou.Permissions,
|
||||
ProviderId = p.Id,
|
||||
ProviderName = p.Name,
|
||||
ProviderType = p.Type,
|
||||
SsoConfig = ss.Data,
|
||||
FamilySponsorshipFriendlyName = os.FriendlyName,
|
||||
FamilySponsorshipLastSyncDate = os.LastSyncDate,
|
||||
|
@ -0,0 +1,21 @@
|
||||
using Bit.Core.Entities.Provider;
|
||||
|
||||
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
||||
|
||||
public class ProviderOrganizationCountByOrganizationIdsQuery : IQuery<ProviderOrganization>
|
||||
{
|
||||
private readonly IEnumerable<Guid> _organizationIds;
|
||||
|
||||
public ProviderOrganizationCountByOrganizationIdsQuery(IEnumerable<Guid> organizationIds)
|
||||
{
|
||||
_organizationIds = organizationIds;
|
||||
}
|
||||
|
||||
public IQueryable<ProviderOrganization> Run(DatabaseContext dbContext)
|
||||
{
|
||||
var query = from po in dbContext.ProviderOrganizations
|
||||
where _organizationIds.Contains(po.OrganizationId)
|
||||
select po;
|
||||
return query;
|
||||
}
|
||||
}
|
@ -31,7 +31,8 @@ public class ProviderOrganizationOrganizationDetailsReadByProviderIdQuery : IQue
|
||||
RevisionDate = x.po.RevisionDate,
|
||||
UserCount = x.o.OrganizationUsers.Count(ou => ou.Status == Core.Enums.OrganizationUserStatusType.Confirmed),
|
||||
Seats = x.o.Seats,
|
||||
Plan = x.o.Plan
|
||||
Plan = x.o.Plan,
|
||||
Status = x.o.Status
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,32 @@
|
||||
using Bit.Core.Models.Data;
|
||||
|
||||
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
||||
|
||||
public class ProviderOrganizationReadByUserIdQuery : IQuery<ProviderOrganizationProviderDetails>
|
||||
{
|
||||
private readonly Guid _userId;
|
||||
|
||||
public ProviderOrganizationReadByUserIdQuery(Guid userId)
|
||||
{
|
||||
_userId = userId;
|
||||
}
|
||||
|
||||
public IQueryable<ProviderOrganizationProviderDetails> Run(DatabaseContext dbContext)
|
||||
{
|
||||
var query = from po in dbContext.ProviderOrganizations
|
||||
join ou in dbContext.OrganizationUsers
|
||||
on po.OrganizationId equals ou.OrganizationId
|
||||
join p in dbContext.Providers
|
||||
on po.ProviderId equals p.Id
|
||||
where ou.UserId == _userId
|
||||
select new ProviderOrganizationProviderDetails
|
||||
{
|
||||
Id = po.Id,
|
||||
OrganizationId = po.OrganizationId,
|
||||
ProviderId = po.ProviderId,
|
||||
ProviderName = p.Name,
|
||||
ProviderType = p.Type
|
||||
};
|
||||
return query;
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@ public class NotificationsHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
var currentContext = new CurrentContext(null);
|
||||
var currentContext = new CurrentContext(null, null);
|
||||
await currentContext.BuildAsync(Context.User, _globalSettings);
|
||||
if (currentContext.Organizations != null)
|
||||
{
|
||||
@ -33,7 +33,7 @@ public class NotificationsHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
|
||||
public override async Task OnDisconnectedAsync(Exception exception)
|
||||
{
|
||||
var currentContext = new CurrentContext(null);
|
||||
var currentContext = new CurrentContext(null, null);
|
||||
await currentContext.BuildAsync(Context.User, _globalSettings);
|
||||
if (currentContext.Organizations != null)
|
||||
{
|
||||
|
21
src/SharedWeb/Utilities/DisplayAttributeHelpers.cs
Normal file
21
src/SharedWeb/Utilities/DisplayAttributeHelpers.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Bit.SharedWeb.Utilities;
|
||||
|
||||
public static class DisplayAttributeHelpers
|
||||
{
|
||||
public static DisplayAttribute GetDisplayAttribute(this Enum enumValue)
|
||||
{
|
||||
return enumValue.GetType()
|
||||
.GetMember(enumValue.ToString())
|
||||
.First()
|
||||
.GetCustomAttribute<DisplayAttribute>();
|
||||
}
|
||||
|
||||
public static DisplayAttribute GetDisplayAttribute<T>(this string property)
|
||||
{
|
||||
MemberInfo propertyInfo = typeof(T).GetProperty(property);
|
||||
return propertyInfo?.GetCustomAttribute(typeof(DisplayAttribute)) as DisplayAttribute;
|
||||
}
|
||||
}
|
@ -263,6 +263,10 @@
|
||||
<Build Include="dbo\Stored Procedures\Policy_ReadByUserId.sql" />
|
||||
<Build Include="dbo\Stored Procedures\Policy_Update.sql" />
|
||||
<Build Include="dbo\Stored Procedures\ProviderOrganizationOrganizationDetails_ReadByProviderId.sql" />
|
||||
<Build Include="dbo\Stored Procedures\ProviderOrganizationProviderDetails_ReadByUserId.sql" />
|
||||
<Build Include="dbo\Stored Procedures\ProviderOrganization_ReadCountByOrganizationIds.sql" />
|
||||
<Build Include="dbo\Stored Procedures\Provider_ReadByOrganizationId.sql" />
|
||||
<Build Include="dbo\Stored Procedures\Organization_UnassignedToProviderSearch.sql" />
|
||||
<Build Include="dbo\Stored Procedures\ProviderOrganization_Create.sql" />
|
||||
<Build Include="dbo\Stored Procedures\ProviderOrganization_DeleteById.sql" />
|
||||
<Build Include="dbo\Stored Procedures\ProviderOrganization_ReadById.sql" />
|
||||
|
@ -43,7 +43,8 @@
|
||||
@UseKeyConnector BIT = 0,
|
||||
@UseScim BIT = 0,
|
||||
@UseCustomPermissions BIT = 0,
|
||||
@UseSecretsManager BIT = 0
|
||||
@UseSecretsManager BIT = 0,
|
||||
@Status TINYINT = 0
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@ -94,7 +95,8 @@ BEGIN
|
||||
[UseKeyConnector],
|
||||
[UseScim],
|
||||
[UseCustomPermissions],
|
||||
[UseSecretsManager]
|
||||
[UseSecretsManager],
|
||||
[Status]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@ -142,6 +144,7 @@ BEGIN
|
||||
@UseKeyConnector,
|
||||
@UseScim,
|
||||
@UseCustomPermissions,
|
||||
@UseSecretsManager
|
||||
@UseSecretsManager,
|
||||
@Status
|
||||
)
|
||||
END
|
@ -0,0 +1,46 @@
|
||||
CREATE PROCEDURE [dbo].[Organization_UnassignedToProviderSearch]
|
||||
@Name NVARCHAR(50),
|
||||
@OwnerEmail NVARCHAR(256),
|
||||
@Skip INT = 0,
|
||||
@Take INT = 25
|
||||
WITH RECOMPILE
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
DECLARE @NameLikeSearch NVARCHAR(55) = '%' + @Name + '%'
|
||||
DECLARE @OwnerLikeSearch NVARCHAR(55) = @OwnerEmail + '%'
|
||||
|
||||
IF @OwnerEmail IS NOT NULL
|
||||
BEGIN
|
||||
SELECT
|
||||
O.*
|
||||
FROM
|
||||
[dbo].[OrganizationView] O
|
||||
INNER JOIN
|
||||
[dbo].[OrganizationUser] OU ON O.[Id] = OU.[OrganizationId]
|
||||
INNER JOIN
|
||||
[dbo].[User] U ON U.[Id] = OU.[UserId]
|
||||
WHERE
|
||||
O.[PlanType] >= 8 AND O.[PlanType] <= 11 -- Get 'Team' and 'Enterprise' Organizations
|
||||
AND NOT EXISTS (SELECT * FROM [dbo].[ProviderOrganizationView] PO WHERE PO.[OrganizationId] = O.[Id])
|
||||
AND (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch)
|
||||
AND (U.[Email] LIKE @OwnerLikeSearch)
|
||||
ORDER BY O.[CreationDate] DESC
|
||||
OFFSET @Skip ROWS
|
||||
FETCH NEXT @Take ROWS ONLY
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
SELECT
|
||||
O.*
|
||||
FROM
|
||||
[dbo].[OrganizationView] O
|
||||
WHERE
|
||||
O.[PlanType] >= 8 AND O.[PlanType] <= 11 -- Get 'Team' and 'Enterprise' Organizations
|
||||
AND NOT EXISTS (SELECT * FROM [dbo].[ProviderOrganizationView] PO WHERE PO.[OrganizationId] = O.[Id])
|
||||
AND (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch)
|
||||
ORDER BY O.[CreationDate] DESC
|
||||
OFFSET @Skip ROWS
|
||||
FETCH NEXT @Take ROWS ONLY
|
||||
END
|
||||
END
|
@ -43,7 +43,8 @@
|
||||
@UseKeyConnector BIT = 0,
|
||||
@UseScim BIT = 0,
|
||||
@UseCustomPermissions BIT = 0,
|
||||
@UseSecretsManager BIT = 0
|
||||
@UseSecretsManager BIT = 0,
|
||||
@Status TINYINT = 0
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@ -94,7 +95,8 @@ BEGIN
|
||||
[UseKeyConnector] = @UseKeyConnector,
|
||||
[UseScim] = @UseScim,
|
||||
[UseCustomPermissions] = @UseCustomPermissions,
|
||||
[UseSecretsManager] = @UseSecretsManager
|
||||
[UseSecretsManager] = @UseSecretsManager,
|
||||
[Status] = @Status
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
||||
|
@ -0,0 +1,21 @@
|
||||
CREATE PROCEDURE [dbo].[ProviderOrganizationProviderDetails_ReadByUserId]
|
||||
@UserId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
PO.Id,
|
||||
PO.OrganizationId,
|
||||
PO.ProviderId,
|
||||
P.Name as ProviderName,
|
||||
P.[Type] as ProviderType
|
||||
FROM
|
||||
[dbo].[ProviderOrganizationView] PO
|
||||
INNER JOIN
|
||||
[dbo].[OrganizationUser] OU ON PO.OrganizationId = OU.OrganizationId
|
||||
INNER JOIN
|
||||
[dbo].[Provider] P ON PO.ProviderId = P.Id
|
||||
WHERE
|
||||
OU.UserId = @UserId
|
||||
END
|
@ -0,0 +1,18 @@
|
||||
CREATE PROCEDURE [dbo].[ProviderOrganization_ReadCountByOrganizationIds]
|
||||
@Ids AS [dbo].[GuidIdArray] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
IF (SELECT COUNT(1) FROM @Ids) < 1
|
||||
BEGIN
|
||||
RETURN(-1)
|
||||
END
|
||||
|
||||
SELECT
|
||||
COUNT(1)
|
||||
FROM
|
||||
[dbo].[ProviderOrganizationView]
|
||||
WHERE
|
||||
[OrganizationId] IN (SELECT [Id] FROM @Ids)
|
||||
END
|
@ -0,0 +1,15 @@
|
||||
CREATE PROCEDURE [dbo].[Provider_ReadByOrganizationId]
|
||||
@OrganizationId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
P.*
|
||||
FROM
|
||||
[dbo].[ProviderView] P
|
||||
INNER JOIN
|
||||
[dbo].[ProviderOrganization] PO ON PO.[ProviderId] = P.[Id]
|
||||
WHERE
|
||||
PO.[OrganizationId] = @OrganizationId
|
||||
END
|
@ -44,6 +44,7 @@
|
||||
[UseScim] BIT NOT NULL CONSTRAINT [DF_Organization_UseScim] DEFAULT (0),
|
||||
[UseCustomPermissions] BIT NOT NULL CONSTRAINT [DF_Organization_UseCustomPermissions] DEFAULT (0),
|
||||
[UseSecretsManager] BIT NOT NULL CONSTRAINT [DF_Organization_UseSecretsManager] DEFAULT (0),
|
||||
[Status] TINYINT NOT NULL CONSTRAINT [DF_Organization_Status] DEFAULT (1),
|
||||
CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC)
|
||||
);
|
||||
|
||||
|
@ -35,6 +35,7 @@ SELECT
|
||||
OU.[Permissions],
|
||||
PO.[ProviderId],
|
||||
P.[Name] ProviderName,
|
||||
P.[Type] ProviderType,
|
||||
SS.[Data] SsoConfig,
|
||||
OS.[FriendlyName] FamilySponsorshipFriendlyName,
|
||||
OS.[LastSyncDate] FamilySponsorshipLastSyncDate,
|
||||
|
@ -11,7 +11,8 @@ SELECT
|
||||
PO.[RevisionDate],
|
||||
(SELECT COUNT(1) FROM [dbo].[OrganizationUser] OU WHERE OU.OrganizationId = PO.OrganizationId AND OU.Status = 2) UserCount,
|
||||
O.[Seats],
|
||||
O.[Plan]
|
||||
O.[Plan],
|
||||
O.[Status]
|
||||
FROM
|
||||
[dbo].[ProviderOrganization] PO
|
||||
LEFT JOIN
|
||||
|
@ -24,6 +24,7 @@ public class OrganizationsControllerTests : IDisposable
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly ISsoConfigService _ssoConfigService;
|
||||
private readonly IUserService _userService;
|
||||
@ -46,6 +47,7 @@ public class OrganizationsControllerTests : IDisposable
|
||||
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||
_paymentService = Substitute.For<IPaymentService>();
|
||||
_policyRepository = Substitute.For<IPolicyRepository>();
|
||||
_providerRepository = Substitute.For<IProviderRepository>();
|
||||
_ssoConfigRepository = Substitute.For<ISsoConfigRepository>();
|
||||
_ssoConfigService = Substitute.For<ISsoConfigService>();
|
||||
_getOrganizationApiKeyQuery = Substitute.For<IGetOrganizationApiKeyQuery>();
|
||||
@ -57,7 +59,7 @@ public class OrganizationsControllerTests : IDisposable
|
||||
_updateOrganizationLicenseCommand = Substitute.For<IUpdateOrganizationLicenseCommand>();
|
||||
|
||||
_sut = new OrganizationsController(_organizationRepository, _organizationUserRepository,
|
||||
_policyRepository, _organizationService, _userService, _paymentService, _currentContext,
|
||||
_policyRepository, _providerRepository, _organizationService, _userService, _paymentService, _currentContext,
|
||||
_ssoConfigRepository, _ssoConfigService, _getOrganizationApiKeyQuery, _rotateOrganizationApiKeyCommand,
|
||||
_createOrganizationApiKeyCommand, _organizationApiKeyRepository, _updateOrganizationLicenseCommand,
|
||||
_cloudGetOrganizationLicenseQuery, _globalSettings);
|
||||
|
@ -214,4 +214,40 @@ public class EventServiceTests
|
||||
|
||||
await sutProvider.GetDependency<IEventWriteService>().Received(1).CreateManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual<IEvent>(expected, new[] { "IdempotencyId" })));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task LogProviderOrganizationEventsAsync_LogsRequiredInfo(Provider provider, ICollection<ProviderOrganization> providerOrganizations, EventType eventType, DateTime date,
|
||||
Guid actingUserId, Guid providerId, string ipAddress, DeviceType deviceType, SutProvider<EventService> sutProvider)
|
||||
{
|
||||
foreach (var providerOrganization in providerOrganizations)
|
||||
{
|
||||
providerOrganization.ProviderId = provider.Id;
|
||||
}
|
||||
|
||||
var providerAbilities = new Dictionary<Guid, ProviderAbility>()
|
||||
{
|
||||
{ provider.Id, new ProviderAbility() { UseEvents = true, Enabled = true } }
|
||||
};
|
||||
sutProvider.GetDependency<IApplicationCacheService>().GetProviderAbilitiesAsync().Returns(providerAbilities);
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().IpAddress.Returns(ipAddress);
|
||||
sutProvider.GetDependency<ICurrentContext>().DeviceType.Returns(deviceType);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderIdForOrg(Arg.Any<Guid>()).Returns(providerId);
|
||||
|
||||
await sutProvider.Sut.LogProviderOrganizationEventsAsync(providerOrganizations.Select(po => (po, eventType, (DateTime?)date)));
|
||||
|
||||
var expected = providerOrganizations.Select(po =>
|
||||
new EventMessage()
|
||||
{
|
||||
DeviceType = deviceType,
|
||||
IpAddress = ipAddress,
|
||||
ProviderId = provider.Id,
|
||||
ProviderOrganizationId = po.Id,
|
||||
Type = eventType,
|
||||
ActingUserId = actingUserId,
|
||||
Date = date
|
||||
}).ToList();
|
||||
|
||||
await sutProvider.GetDependency<IEventWriteService>().Received(1).CreateManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual<IEvent>(expected, new[] { "IdempotencyId" })));
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Test.AutoFixture.Attributes;
|
||||
using Bit.Infrastructure.EFIntegration.Test.AutoFixture;
|
||||
using Bit.Infrastructure.EFIntegration.Test.Repositories.EqualityComparers;
|
||||
@ -146,4 +148,42 @@ public class OrganizationRepositoryTests
|
||||
list.Concat(await sqlOrganizationRepo.GetManyAbilitiesAsync());
|
||||
Assert.True(list.All(x => x.GetType() == typeof(OrganizationAbility)));
|
||||
}
|
||||
|
||||
[CiSkippedTheory, EfOrganizationUserAutoData]
|
||||
public async void SearchUnassignedAsync_Works(OrganizationUser orgUser, User user, Organization org,
|
||||
List<EfRepo.OrganizationUserRepository> efOrgUserRepos, List<EfRepo.OrganizationRepository> efOrgRepos, List<EfRepo.UserRepository> efUserRepos,
|
||||
SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.OrganizationRepository sqlOrgRepo, SqlRepo.UserRepository sqlUserRepo)
|
||||
{
|
||||
orgUser.Type = OrganizationUserType.Owner;
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
|
||||
var efList = new List<Organization>();
|
||||
foreach (var efOrgUserRepo in efOrgUserRepos)
|
||||
{
|
||||
var i = efOrgUserRepos.IndexOf(efOrgUserRepo);
|
||||
var postEfUser = await efUserRepos[i].CreateAsync(user);
|
||||
var postEfOrg = await efOrgRepos[i].CreateAsync(org);
|
||||
efOrgUserRepo.ClearChangeTracking();
|
||||
|
||||
orgUser.UserId = postEfUser.Id;
|
||||
orgUser.OrganizationId = postEfOrg.Id;
|
||||
await efOrgUserRepo.CreateAsync(orgUser);
|
||||
efOrgUserRepo.ClearChangeTracking();
|
||||
|
||||
efList.AddRange(await efOrgRepos[i].SearchUnassignedToProviderAsync(org.Name, user.Email, 0, 10));
|
||||
}
|
||||
|
||||
var postSqlUser = await sqlUserRepo.CreateAsync(user);
|
||||
var postSqlOrg = await sqlOrgRepo.CreateAsync(org);
|
||||
|
||||
orgUser.UserId = postSqlUser.Id;
|
||||
orgUser.OrganizationId = postSqlOrg.Id;
|
||||
await sqlOrgUserRepo.CreateAsync(orgUser);
|
||||
var sqlResult = await sqlOrgRepo.SearchUnassignedToProviderAsync(org.Name, user.Email, 0, 10);
|
||||
|
||||
Assert.Equal(efOrgRepos.Count, efList.Count);
|
||||
Assert.True(efList.All(o => o.Name == org.Name));
|
||||
Assert.Equal(1, sqlResult.Count);
|
||||
Assert.True(sqlResult.All(o => o.Name == org.Name));
|
||||
}
|
||||
}
|
||||
|
291
util/Migrator/DbScripts/2023-01-20_00_OrganizationStatus.sql
Normal file
291
util/Migrator/DbScripts/2023-01-20_00_OrganizationStatus.sql
Normal file
@ -0,0 +1,291 @@
|
||||
--Add column 'Status' to 'Organization' table
|
||||
IF COL_LENGTH('[dbo].[Organization]', 'Status') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE
|
||||
[dbo].[Organization]
|
||||
ADD
|
||||
[Status] TINYINT NOT NULL CONSTRAINT [DF_Organization_Status] DEFAULT (1)
|
||||
END
|
||||
GO
|
||||
|
||||
--Insert value in column 'Status'
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Organization_Create]
|
||||
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||
@Identifier NVARCHAR(50),
|
||||
@Name NVARCHAR(50),
|
||||
@BusinessName NVARCHAR(50),
|
||||
@BusinessAddress1 NVARCHAR(50),
|
||||
@BusinessAddress2 NVARCHAR(50),
|
||||
@BusinessAddress3 NVARCHAR(50),
|
||||
@BusinessCountry VARCHAR(2),
|
||||
@BusinessTaxNumber NVARCHAR(30),
|
||||
@BillingEmail NVARCHAR(256),
|
||||
@Plan NVARCHAR(50),
|
||||
@PlanType TINYINT,
|
||||
@Seats INT,
|
||||
@MaxCollections SMALLINT,
|
||||
@UsePolicies BIT,
|
||||
@UseSso BIT,
|
||||
@UseGroups BIT,
|
||||
@UseDirectory BIT,
|
||||
@UseEvents BIT,
|
||||
@UseTotp BIT,
|
||||
@Use2fa BIT,
|
||||
@UseApi BIT,
|
||||
@UseResetPassword BIT,
|
||||
@SelfHost BIT,
|
||||
@UsersGetPremium BIT,
|
||||
@Storage BIGINT,
|
||||
@MaxStorageGb SMALLINT,
|
||||
@Gateway TINYINT,
|
||||
@GatewayCustomerId VARCHAR(50),
|
||||
@GatewaySubscriptionId VARCHAR(50),
|
||||
@ReferenceData VARCHAR(MAX),
|
||||
@Enabled BIT,
|
||||
@LicenseKey VARCHAR(100),
|
||||
@PublicKey VARCHAR(MAX),
|
||||
@PrivateKey VARCHAR(MAX),
|
||||
@TwoFactorProviders NVARCHAR(MAX),
|
||||
@ExpirationDate DATETIME2(7),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@OwnersNotifiedOfAutoscaling DATETIME2(7),
|
||||
@MaxAutoscaleSeats INT,
|
||||
@UseKeyConnector BIT = 0,
|
||||
@UseScim BIT = 0,
|
||||
@UseCustomPermissions BIT = 0,
|
||||
@UseSecretsManager BIT = 0,
|
||||
@Status TINYINT = 0
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
INSERT INTO [dbo].[Organization]
|
||||
(
|
||||
[Id],
|
||||
[Identifier],
|
||||
[Name],
|
||||
[BusinessName],
|
||||
[BusinessAddress1],
|
||||
[BusinessAddress2],
|
||||
[BusinessAddress3],
|
||||
[BusinessCountry],
|
||||
[BusinessTaxNumber],
|
||||
[BillingEmail],
|
||||
[Plan],
|
||||
[PlanType],
|
||||
[Seats],
|
||||
[MaxCollections],
|
||||
[UsePolicies],
|
||||
[UseSso],
|
||||
[UseGroups],
|
||||
[UseDirectory],
|
||||
[UseEvents],
|
||||
[UseTotp],
|
||||
[Use2fa],
|
||||
[UseApi],
|
||||
[UseResetPassword],
|
||||
[SelfHost],
|
||||
[UsersGetPremium],
|
||||
[Storage],
|
||||
[MaxStorageGb],
|
||||
[Gateway],
|
||||
[GatewayCustomerId],
|
||||
[GatewaySubscriptionId],
|
||||
[ReferenceData],
|
||||
[Enabled],
|
||||
[LicenseKey],
|
||||
[PublicKey],
|
||||
[PrivateKey],
|
||||
[TwoFactorProviders],
|
||||
[ExpirationDate],
|
||||
[CreationDate],
|
||||
[RevisionDate],
|
||||
[OwnersNotifiedOfAutoscaling],
|
||||
[MaxAutoscaleSeats],
|
||||
[UseKeyConnector],
|
||||
[UseScim],
|
||||
[UseCustomPermissions],
|
||||
[UseSecretsManager],
|
||||
[Status]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@Id,
|
||||
@Identifier,
|
||||
@Name,
|
||||
@BusinessName,
|
||||
@BusinessAddress1,
|
||||
@BusinessAddress2,
|
||||
@BusinessAddress3,
|
||||
@BusinessCountry,
|
||||
@BusinessTaxNumber,
|
||||
@BillingEmail,
|
||||
@Plan,
|
||||
@PlanType,
|
||||
@Seats,
|
||||
@MaxCollections,
|
||||
@UsePolicies,
|
||||
@UseSso,
|
||||
@UseGroups,
|
||||
@UseDirectory,
|
||||
@UseEvents,
|
||||
@UseTotp,
|
||||
@Use2fa,
|
||||
@UseApi,
|
||||
@UseResetPassword,
|
||||
@SelfHost,
|
||||
@UsersGetPremium,
|
||||
@Storage,
|
||||
@MaxStorageGb,
|
||||
@Gateway,
|
||||
@GatewayCustomerId,
|
||||
@GatewaySubscriptionId,
|
||||
@ReferenceData,
|
||||
@Enabled,
|
||||
@LicenseKey,
|
||||
@PublicKey,
|
||||
@PrivateKey,
|
||||
@TwoFactorProviders,
|
||||
@ExpirationDate,
|
||||
@CreationDate,
|
||||
@RevisionDate,
|
||||
@OwnersNotifiedOfAutoscaling,
|
||||
@MaxAutoscaleSeats,
|
||||
@UseKeyConnector,
|
||||
@UseScim,
|
||||
@UseCustomPermissions,
|
||||
@UseSecretsManager,
|
||||
@Status
|
||||
)
|
||||
END
|
||||
GO
|
||||
|
||||
--Update column 'Status'
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Organization_Update]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@Identifier NVARCHAR(50),
|
||||
@Name NVARCHAR(50),
|
||||
@BusinessName NVARCHAR(50),
|
||||
@BusinessAddress1 NVARCHAR(50),
|
||||
@BusinessAddress2 NVARCHAR(50),
|
||||
@BusinessAddress3 NVARCHAR(50),
|
||||
@BusinessCountry VARCHAR(2),
|
||||
@BusinessTaxNumber NVARCHAR(30),
|
||||
@BillingEmail NVARCHAR(256),
|
||||
@Plan NVARCHAR(50),
|
||||
@PlanType TINYINT,
|
||||
@Seats INT,
|
||||
@MaxCollections SMALLINT,
|
||||
@UsePolicies BIT,
|
||||
@UseSso BIT,
|
||||
@UseGroups BIT,
|
||||
@UseDirectory BIT,
|
||||
@UseEvents BIT,
|
||||
@UseTotp BIT,
|
||||
@Use2fa BIT,
|
||||
@UseApi BIT,
|
||||
@UseResetPassword BIT,
|
||||
@SelfHost BIT,
|
||||
@UsersGetPremium BIT,
|
||||
@Storage BIGINT,
|
||||
@MaxStorageGb SMALLINT,
|
||||
@Gateway TINYINT,
|
||||
@GatewayCustomerId VARCHAR(50),
|
||||
@GatewaySubscriptionId VARCHAR(50),
|
||||
@ReferenceData VARCHAR(MAX),
|
||||
@Enabled BIT,
|
||||
@LicenseKey VARCHAR(100),
|
||||
@PublicKey VARCHAR(MAX),
|
||||
@PrivateKey VARCHAR(MAX),
|
||||
@TwoFactorProviders NVARCHAR(MAX),
|
||||
@ExpirationDate DATETIME2(7),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@OwnersNotifiedOfAutoscaling DATETIME2(7),
|
||||
@MaxAutoscaleSeats INT,
|
||||
@UseKeyConnector BIT = 0,
|
||||
@UseScim BIT = 0,
|
||||
@UseCustomPermissions BIT = 0,
|
||||
@UseSecretsManager BIT = 0,
|
||||
@Status TINYINT = 0
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE
|
||||
[dbo].[Organization]
|
||||
SET
|
||||
[Identifier] = @Identifier,
|
||||
[Name] = @Name,
|
||||
[BusinessName] = @BusinessName,
|
||||
[BusinessAddress1] = @BusinessAddress1,
|
||||
[BusinessAddress2] = @BusinessAddress2,
|
||||
[BusinessAddress3] = @BusinessAddress3,
|
||||
[BusinessCountry] = @BusinessCountry,
|
||||
[BusinessTaxNumber] = @BusinessTaxNumber,
|
||||
[BillingEmail] = @BillingEmail,
|
||||
[Plan] = @Plan,
|
||||
[PlanType] = @PlanType,
|
||||
[Seats] = @Seats,
|
||||
[MaxCollections] = @MaxCollections,
|
||||
[UsePolicies] = @UsePolicies,
|
||||
[UseSso] = @UseSso,
|
||||
[UseGroups] = @UseGroups,
|
||||
[UseDirectory] = @UseDirectory,
|
||||
[UseEvents] = @UseEvents,
|
||||
[UseTotp] = @UseTotp,
|
||||
[Use2fa] = @Use2fa,
|
||||
[UseApi] = @UseApi,
|
||||
[UseResetPassword] = @UseResetPassword,
|
||||
[SelfHost] = @SelfHost,
|
||||
[UsersGetPremium] = @UsersGetPremium,
|
||||
[Storage] = @Storage,
|
||||
[MaxStorageGb] = @MaxStorageGb,
|
||||
[Gateway] = @Gateway,
|
||||
[GatewayCustomerId] = @GatewayCustomerId,
|
||||
[GatewaySubscriptionId] = @GatewaySubscriptionId,
|
||||
[ReferenceData] = @ReferenceData,
|
||||
[Enabled] = @Enabled,
|
||||
[LicenseKey] = @LicenseKey,
|
||||
[PublicKey] = @PublicKey,
|
||||
[PrivateKey] = @PrivateKey,
|
||||
[TwoFactorProviders] = @TwoFactorProviders,
|
||||
[ExpirationDate] = @ExpirationDate,
|
||||
[CreationDate] = @CreationDate,
|
||||
[RevisionDate] = @RevisionDate,
|
||||
[OwnersNotifiedOfAutoscaling] = @OwnersNotifiedOfAutoscaling,
|
||||
[MaxAutoscaleSeats] = @MaxAutoscaleSeats,
|
||||
[UseKeyConnector] = @UseKeyConnector,
|
||||
[UseScim] = @UseScim,
|
||||
[UseCustomPermissions] = @UseCustomPermissions,
|
||||
[UseSecretsManager] = @UseSecretsManager,
|
||||
[Status] = @Status
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
||||
GO
|
||||
|
||||
--Add column 'Status'
|
||||
CREATE OR ALTER VIEW [dbo].[ProviderOrganizationOrganizationDetailsView]
|
||||
AS
|
||||
SELECT
|
||||
PO.[Id],
|
||||
PO.[ProviderId],
|
||||
PO.[OrganizationId],
|
||||
O.[Name] OrganizationName,
|
||||
PO.[Key],
|
||||
PO.[Settings],
|
||||
PO.[CreationDate],
|
||||
PO.[RevisionDate],
|
||||
(SELECT COUNT(1) FROM [dbo].[OrganizationUser] OU WHERE OU.OrganizationId = PO.OrganizationId AND OU.Status = 2) UserCount,
|
||||
O.[Seats],
|
||||
O.[Plan],
|
||||
O.[Status]
|
||||
FROM
|
||||
[dbo].[ProviderOrganization] PO
|
||||
LEFT JOIN
|
||||
[dbo].[Organization] O ON O.[Id] = PO.[OrganizationId]
|
||||
GO
|
||||
|
||||
|
@ -0,0 +1,16 @@
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Provider_ReadByOrganizationId]
|
||||
@OrganizationId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
P.*
|
||||
FROM
|
||||
[dbo].[ProviderView] P
|
||||
INNER JOIN
|
||||
[dbo].[ProviderOrganization] PO ON PO.[ProviderId] = P.[Id]
|
||||
WHERE
|
||||
PO.[OrganizationId] = @OrganizationId
|
||||
END
|
||||
GO
|
@ -0,0 +1,82 @@
|
||||
CREATE OR ALTER VIEW [dbo].[OrganizationUserOrganizationDetailsView]
|
||||
AS
|
||||
SELECT
|
||||
OU.[UserId],
|
||||
OU.[OrganizationId],
|
||||
O.[Name],
|
||||
O.[Enabled],
|
||||
O.[PlanType],
|
||||
O.[UsePolicies],
|
||||
O.[UseSso],
|
||||
O.[UseKeyConnector],
|
||||
O.[UseScim],
|
||||
O.[UseGroups],
|
||||
O.[UseDirectory],
|
||||
O.[UseEvents],
|
||||
O.[UseTotp],
|
||||
O.[Use2fa],
|
||||
O.[UseApi],
|
||||
O.[UseResetPassword],
|
||||
O.[SelfHost],
|
||||
O.[UsersGetPremium],
|
||||
O.[UseCustomPermissions],
|
||||
O.[UseSecretsManager],
|
||||
O.[Seats],
|
||||
O.[MaxCollections],
|
||||
O.[MaxStorageGb],
|
||||
O.[Identifier],
|
||||
OU.[Key],
|
||||
OU.[ResetPasswordKey],
|
||||
O.[PublicKey],
|
||||
O.[PrivateKey],
|
||||
OU.[Status],
|
||||
OU.[Type],
|
||||
SU.[ExternalId] SsoExternalId,
|
||||
OU.[Permissions],
|
||||
PO.[ProviderId],
|
||||
P.[Name] ProviderName,
|
||||
P.[Type] ProviderType,
|
||||
SS.[Data] SsoConfig,
|
||||
OS.[FriendlyName] FamilySponsorshipFriendlyName,
|
||||
OS.[LastSyncDate] FamilySponsorshipLastSyncDate,
|
||||
OS.[ToDelete] FamilySponsorshipToDelete,
|
||||
OS.[ValidUntil] FamilySponsorshipValidUntil,
|
||||
OU.[AccessSecretsManager]
|
||||
FROM
|
||||
[dbo].[OrganizationUser] OU
|
||||
LEFT JOIN
|
||||
[dbo].[Organization] O ON O.[Id] = OU.[OrganizationId]
|
||||
LEFT JOIN
|
||||
[dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId]
|
||||
LEFT JOIN
|
||||
[dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id]
|
||||
LEFT JOIN
|
||||
[dbo].[Provider] P ON P.[Id] = PO.[ProviderId]
|
||||
LEFT JOIN
|
||||
[dbo].[SsoConfig] SS ON SS.[OrganizationId] = OU.[OrganizationId]
|
||||
LEFT JOIN
|
||||
[dbo].[OrganizationSponsorship] OS ON OS.[SponsoringOrganizationUserID] = OU.[Id]
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[ProviderOrganizationProviderDetails_ReadByUserId]
|
||||
@UserId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
PO.Id,
|
||||
PO.OrganizationId,
|
||||
PO.ProviderId,
|
||||
P.Name as ProviderName,
|
||||
P.[Type] as ProviderType
|
||||
FROM
|
||||
[dbo].[ProviderOrganizationView] PO
|
||||
INNER JOIN
|
||||
[dbo].[OrganizationUser] OU ON PO.OrganizationId = OU.OrganizationId
|
||||
INNER JOIN
|
||||
[dbo].[Provider] P ON PO.ProviderId = P.Id
|
||||
WHERE
|
||||
OU.UserId = @UserId
|
||||
END
|
||||
GO
|
@ -0,0 +1,54 @@
|
||||
-- Drop existing SPROC
|
||||
IF OBJECT_ID('[dbo].[Organization_UnassignedToProviderSearch]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[Organization_UnassignedToProviderSearch]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[Organization_UnassignedToProviderSearch]
|
||||
@Name NVARCHAR(50),
|
||||
@OwnerEmail NVARCHAR(256),
|
||||
@Skip INT = 0,
|
||||
@Take INT = 25
|
||||
WITH RECOMPILE
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
DECLARE @NameLikeSearch NVARCHAR(55) = '%' + @Name + '%'
|
||||
DECLARE @OwnerLikeSearch NVARCHAR(55) = @OwnerEmail + '%'
|
||||
|
||||
IF @OwnerEmail IS NOT NULL
|
||||
BEGIN
|
||||
SELECT
|
||||
O.*
|
||||
FROM
|
||||
[dbo].[OrganizationView] O
|
||||
INNER JOIN
|
||||
[dbo].[OrganizationUser] OU ON O.[Id] = OU.[OrganizationId]
|
||||
INNER JOIN
|
||||
[dbo].[User] U ON U.[Id] = OU.[UserId]
|
||||
WHERE
|
||||
O.[PlanType] >= 8 AND O.[PlanType] <= 11 -- Get 'Team' and 'Enterprise' Organizations
|
||||
AND NOT EXISTS (SELECT * FROM [dbo].[ProviderOrganizationView] PO WHERE PO.[OrganizationId] = O.[Id])
|
||||
AND (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch)
|
||||
AND (U.[Email] LIKE @OwnerLikeSearch)
|
||||
ORDER BY O.[CreationDate] DESC
|
||||
OFFSET @Skip ROWS
|
||||
FETCH NEXT @Take ROWS ONLY
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
SELECT
|
||||
O.*
|
||||
FROM
|
||||
[dbo].[OrganizationView] O
|
||||
WHERE
|
||||
O.[PlanType] >= 8 AND O.[PlanType] <= 11 -- Get 'Team' and 'Enterprise' Organizations
|
||||
AND NOT EXISTS (SELECT * FROM [dbo].[ProviderOrganizationView] PO WHERE PO.[OrganizationId] = O.[Id])
|
||||
AND (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch)
|
||||
ORDER BY O.[CreationDate] DESC
|
||||
OFFSET @Skip ROWS
|
||||
FETCH NEXT @Take ROWS ONLY
|
||||
END
|
||||
END
|
||||
GO
|
@ -0,0 +1,19 @@
|
||||
CREATE OR ALTER PROCEDURE [dbo].[ProviderOrganization_ReadCountByOrganizationIds]
|
||||
@Ids AS [dbo].[GuidIdArray] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
IF (SELECT COUNT(1) FROM @Ids) < 1
|
||||
BEGIN
|
||||
RETURN(-1)
|
||||
END
|
||||
|
||||
SELECT
|
||||
COUNT(1)
|
||||
FROM
|
||||
[dbo].[ProviderOrganizationView]
|
||||
WHERE
|
||||
[OrganizationId] IN (SELECT [Id] FROM @Ids)
|
||||
END
|
||||
GO
|
2107
util/MySqlMigrations/Migrations/20230120160248_OrganizationStatus.Designer.cs
generated
Normal file
2107
util/MySqlMigrations/Migrations/20230120160248_OrganizationStatus.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.MySqlMigrations.Migrations;
|
||||
|
||||
public partial class OrganizationStatus : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<byte>(
|
||||
name: "Status",
|
||||
table: "Organization",
|
||||
type: "tinyint unsigned",
|
||||
nullable: false,
|
||||
defaultValue: (byte)1);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Status",
|
||||
table: "Organization");
|
||||
}
|
||||
}
|
@ -609,6 +609,9 @@ namespace Bit.MySqlMigrations.Migrations
|
||||
b.Property<bool>("SelfHost")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<byte>("Status")
|
||||
.HasColumnType("tinyint unsigned");
|
||||
|
||||
b.Property<long?>("Storage")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
|
2118
util/PostgresMigrations/Migrations/20230120160253_OrganizationStatus.Designer.cs
generated
Normal file
2118
util/PostgresMigrations/Migrations/20230120160253_OrganizationStatus.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.PostgresMigrations.Migrations;
|
||||
|
||||
public partial class OrganizationStatus : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<byte>(
|
||||
name: "Status",
|
||||
table: "Organization",
|
||||
type: "smallint",
|
||||
nullable: false,
|
||||
defaultValue: (byte)1);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Status",
|
||||
table: "Organization");
|
||||
}
|
||||
}
|
@ -540,6 +540,9 @@ namespace Bit.PostgresMigrations.Migrations
|
||||
b.Property<bool>("SelfHost")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<byte>("Status")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.Property<long?>("Storage")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
|
2105
util/SqliteMigrations/Migrations/20230120160257_OrganizationStatus.Designer.cs
generated
Normal file
2105
util/SqliteMigrations/Migrations/20230120160257_OrganizationStatus.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.SqliteMigrations.Migrations;
|
||||
|
||||
public partial class OrganizationStatus : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<byte>(
|
||||
name: "Status",
|
||||
table: "Organization",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: (byte)1);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Status",
|
||||
table: "Organization");
|
||||
}
|
||||
}
|
@ -533,6 +533,9 @@ namespace Bit.SqliteMigrations.Migrations
|
||||
b.Property<bool>("SelfHost")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long?>("Storage")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user