1
0
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:
Rui Tomé
2023-03-30 10:54:43 +01:00
committed by GitHub
parent 7b958f275a
commit b6bd041b30
22 changed files with 591 additions and 14 deletions

View File

@ -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)
{

View File

@ -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)

View File

@ -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 });
}
}

View File

@ -0,0 +1,8 @@
using Bit.Core.Entities;
namespace Bit.Admin.Models;
public class OrganizationSelectableViewModel : Organization
{
public bool Selected { get; set; }
}

View File

@ -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; }
}

View 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>

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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);

View File

@ -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,

View File

@ -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)
{

View File

@ -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();

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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())

View File

@ -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" />

View File

@ -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

View File

@ -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" })));
}
}

View File

@ -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));
}
}

View File

@ -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