mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 16:12:49 -05:00
[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
This commit is contained in:
@ -365,6 +365,20 @@ 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("Organization must be of type Reseller in order to assign Organizations to it.");
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
@ -450,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("Organization 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)
|
||||
|
@ -15,6 +15,7 @@ namespace Bit.Admin.Controllers;
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public class ProvidersController : Controller
|
||||
{
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
@ -24,6 +25,7 @@ public class ProvidersController : Controller
|
||||
private readonly ICreateProviderCommand _createProviderCommand;
|
||||
|
||||
public ProvidersController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IProviderRepository providerRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
@ -32,6 +34,7 @@ public class ProvidersController : Controller
|
||||
IApplicationCacheService applicationCacheService,
|
||||
ICreateProviderCommand createProviderCommand)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_providerRepository = providerRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
@ -148,4 +151,48 @@ 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 });
|
||||
}
|
||||
}
|
||||
|
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; }
|
||||
}
|
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>
|
@ -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,7 @@ 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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -20,6 +20,7 @@ public interface IProviderService
|
||||
Guid deletingUserId);
|
||||
|
||||
Task AddOrganization(Guid providerId, Guid organizationId, Guid addingUserId, 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);
|
||||
|
@ -332,11 +332,19 @@ 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();
|
||||
var eventMessages = new List<IEvent>();
|
||||
foreach (var (providerOrganization, type, date) in events)
|
||||
{
|
||||
if (!CanUseProviderEvents(providerAbilities, providerOrganization.ProviderId))
|
||||
{
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
var e = new EventMessage(_currentContext)
|
||||
@ -347,7 +355,11 @@ public class EventService : IEventService
|
||||
ActingUserId = _currentContext?.UserId,
|
||||
Date = date.GetValueOrDefault(DateTime.UtcNow)
|
||||
};
|
||||
await _eventWriteService.CreateAsync(e);
|
||||
|
||||
eventMessages.Add(e);
|
||||
}
|
||||
|
||||
await _eventWriteService.CreateManyAsync(eventMessages);
|
||||
}
|
||||
|
||||
public async Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type,
|
||||
|
@ -70,6 +70,11 @@ 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)
|
||||
{
|
||||
|
@ -25,6 +25,8 @@ public class NoopProviderService : IProviderService
|
||||
|
||||
public Task AddOrganization(Guid providerId, Guid organizationId, Guid addingUserId, 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();
|
||||
|
||||
public Task RemoveOrganizationAsync(Guid providerId, Guid providerOrganizationId, Guid removingUserId) => 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,56 @@ public class ProviderOrganizationRepository : Repository<ProviderOrganization, G
|
||||
return results.SingleOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -91,6 +91,32 @@ 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))
|
||||
{
|
||||
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 u.Email == ownerEmail && ou.Type == OrganizationUserType.Owner
|
||||
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())
|
||||
|
@ -264,6 +264,7 @@
|
||||
<Build Include="dbo\Stored Procedures\Policy_Update.sql" />
|
||||
<Build Include="dbo\Stored Procedures\ProviderOrganizationOrganizationDetails_ReadByProviderId.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" />
|
||||
|
@ -0,0 +1,45 @@
|
||||
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 + '%'
|
||||
|
||||
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] = @OwnerEmail)
|
||||
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
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,53 @@
|
||||
-- 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 + '%'
|
||||
|
||||
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] = @OwnerEmail)
|
||||
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
|
Reference in New Issue
Block a user