mirror of
https://github.com/bitwarden/server.git
synced 2025-04-05 05:00:19 -05:00
[Provider] Server entities and models (#1370)
* Mock out provider models and service * Implement CreateAsync, CompleteSetupAsync, UpdateAsync, InviteUserAsync and ResendInvitesAsync * Implement AcceptUserAsync and ConfirmUsersAsync * Implement SaveUserAsync and DeleteUserAsync * Add email templates * Add admin operations for providers * Fix mail template names * Rename roles * Verify provider has provideradmin * Add self hosted check to admin controller * Resolve review comments * Update sql queries * Change create provider to use email instead of userId
This commit is contained in:
parent
58954f161e
commit
fe1ffb6a22
118
src/Admin/Controllers/ProvidersController.cs
Normal file
118
src/Admin/Controllers/ProvidersController.cs
Normal file
@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Admin.Models;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Admin.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public class ProvidersController : Controller
|
||||
{
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IProviderService _providerService;
|
||||
|
||||
public ProvidersController(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
|
||||
IProviderService providerService, GlobalSettings globalSettings)
|
||||
{
|
||||
_providerRepository = providerRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_providerService = providerService;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Index(string name = null, string userEmail = null, int page = 1, int count = 25)
|
||||
{
|
||||
if (page < 1)
|
||||
{
|
||||
page = 1;
|
||||
}
|
||||
|
||||
if (count < 1)
|
||||
{
|
||||
count = 1;
|
||||
}
|
||||
|
||||
var skip = (page - 1) * count;
|
||||
var providers = await _providerRepository.SearchAsync(name, userEmail, skip, count);
|
||||
return View(new ProvidersModel
|
||||
{
|
||||
Items = providers as List<Provider>,
|
||||
Name = string.IsNullOrWhiteSpace(name) ? null : name,
|
||||
UserEmail = string.IsNullOrWhiteSpace(userEmail) ? null : userEmail,
|
||||
Page = page,
|
||||
Count = count,
|
||||
Action = _globalSettings.SelfHosted ? "View" : "Edit",
|
||||
SelfHosted = _globalSettings.SelfHosted
|
||||
});
|
||||
}
|
||||
|
||||
public IActionResult Create(string ownerEmail = null)
|
||||
{
|
||||
return View(new CreateProviderModel
|
||||
{
|
||||
OwnerEmail = ownerEmail
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create(CreateProviderModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
await _providerService.CreateAsync(model.OwnerEmail);
|
||||
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
public async Task<IActionResult> View(Guid id)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(id);
|
||||
if (provider == null)
|
||||
{
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
var users = await _providerUserRepository.GetManyByProviderAsync(id);
|
||||
return View(new ProviderViewModel(provider, users));
|
||||
}
|
||||
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<IActionResult> Edit(Guid id)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(id);
|
||||
if (provider == null)
|
||||
{
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
var users = await _providerUserRepository.GetManyByProviderAsync(id);
|
||||
return View(new ProviderEditModel(provider, users));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(id);
|
||||
if (provider != null)
|
||||
{
|
||||
await _providerRepository.DeleteAsync(provider);
|
||||
}
|
||||
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
}
|
||||
}
|
13
src/Admin/Models/CreateProviderModel.cs
Normal file
13
src/Admin/Models/CreateProviderModel.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Admin.Models
|
||||
{
|
||||
public class CreateProviderModel
|
||||
{
|
||||
public CreateProviderModel() { }
|
||||
|
||||
[Display(Name = "Owner Email")]
|
||||
[Required]
|
||||
public string OwnerEmail { get; set; }
|
||||
}
|
||||
}
|
29
src/Admin/Models/ProviderEditModel.cs
Normal file
29
src/Admin/Models/ProviderEditModel.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Bit.Core.Enums.Provider;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
|
||||
namespace Bit.Admin.Models
|
||||
{
|
||||
public class ProviderEditModel : ProviderViewModel
|
||||
{
|
||||
public ProviderEditModel(Provider provider, IEnumerable<ProviderUser> providerUsers)
|
||||
: base(provider, providerUsers)
|
||||
{
|
||||
Name = provider.Name;
|
||||
BusinessName = provider.BusinessName;
|
||||
BillingEmail = provider.BillingEmail;
|
||||
Enabled = provider.Enabled;
|
||||
}
|
||||
|
||||
public string Administrators { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
public string BillingEmail { get; set; }
|
||||
|
||||
public string BusinessName { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
27
src/Admin/Models/ProviderViewModel.cs
Normal file
27
src/Admin/Models/ProviderViewModel.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Bit.Core.Enums.Provider;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
|
||||
namespace Bit.Admin.Models
|
||||
{
|
||||
public class ProviderViewModel
|
||||
{
|
||||
public ProviderViewModel(Provider provider, IEnumerable<ProviderUser> providerUsers)
|
||||
{
|
||||
Provider = provider;
|
||||
UserCount = providerUsers.Count();
|
||||
|
||||
ProviderAdmins = string.Join(", ",
|
||||
providerUsers
|
||||
.Where(u => u.Type == ProviderUserType.ProviderAdmin && u.Status == ProviderUserStatusType.Confirmed)
|
||||
.Select(u => u.Email));
|
||||
}
|
||||
|
||||
public int UserCount { get; set; }
|
||||
|
||||
public Provider Provider { get; set; }
|
||||
|
||||
public string ProviderAdmins { get; set; }
|
||||
}
|
||||
}
|
14
src/Admin/Models/ProvidersModel.cs
Normal file
14
src/Admin/Models/ProvidersModel.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
|
||||
namespace Bit.Admin.Models
|
||||
{
|
||||
public class ProvidersModel : PagedModel<Provider>
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string UserEmail { get; set; }
|
||||
public bool? Paid { get; set; }
|
||||
public string Action { get; set; }
|
||||
public bool SelfHosted { get; set; }
|
||||
}
|
||||
}
|
17
src/Admin/Views/Providers/Create.cshtml
Normal file
17
src/Admin/Views/Providers/Create.cshtml
Normal file
@ -0,0 +1,17 @@
|
||||
@model CreateProviderModel
|
||||
@{
|
||||
ViewData["Title"] = "Create Provider";
|
||||
}
|
||||
|
||||
<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">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
||||
</form>
|
51
src/Admin/Views/Providers/Edit.cshtml
Normal file
51
src/Admin/Views/Providers/Edit.cshtml
Normal file
@ -0,0 +1,51 @@
|
||||
@model ProviderEditModel
|
||||
@{
|
||||
ViewData["Title"] = "Provider: " + Model.Provider.Name;
|
||||
}
|
||||
|
||||
<h1>Provider <small>@Model.Provider.Name</small></h1>
|
||||
|
||||
<h2>Provider Information</h2>
|
||||
@await Html.PartialAsync("_ViewInformation", Model)
|
||||
<form method="post" id="edit-form">
|
||||
<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>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>
|
||||
</form>
|
||||
<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-action="Delete" asp-route-id="@Model.Provider.Id"
|
||||
onsubmit="return confirm('Are you sure you want to delete this provider (@Model.Provider.Name)?')">
|
||||
<button class="btn btn-danger" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
91
src/Admin/Views/Providers/Index.cshtml
Normal file
91
src/Admin/Views/Providers/Index.cshtml
Normal file
@ -0,0 +1,91 @@
|
||||
@model ProvidersModel
|
||||
@{
|
||||
ViewData["Title"] = "Providers";
|
||||
}
|
||||
|
||||
<h1>Providers</h1>
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col">
|
||||
<form class="form-inline mb-2" method="get">
|
||||
<label class="sr-only" asp-for="Name">Name</label>
|
||||
<input type="text" class="form-control mb-2 mr-2" placeholder="Name" asp-for="Name" name="name">
|
||||
<label class="sr-only" asp-for="UserEmail">User email</label>
|
||||
<input type="text" class="form-control mb-2 mr-2" placeholder="User email" asp-for="UserEmail" name="userEmail">
|
||||
<button type="submit" class="btn btn-primary mb-2" title="Search"><i class="fa fa-search"></i> Search</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a asp-action="Create" class="btn btn-secondary">Create Provider</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th style="width: 190px;">Status</th>
|
||||
<th style="width: 150px;">Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if(!Model.Items.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="5">No results to list.</td>
|
||||
</tr>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach(var provider in Model.Items)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<a asp-action="@Model.Action" asp-route-id="@provider.Id">@(provider.Name ?? "Pending")</a>
|
||||
</td>
|
||||
<td>@provider.Status</td>
|
||||
<td>
|
||||
<span title="@provider.CreationDate.ToString()">
|
||||
@provider.CreationDate.ToShortDateString()
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<ul class="pagination">
|
||||
@if(Model.PreviousPage.HasValue)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" asp-action="Index" asp-route-page="@Model.PreviousPage.Value"
|
||||
asp-route-count="@Model.Count" asp-route-userEmail="@Model.UserEmail"
|
||||
asp-route-name="@Model.Name" asp-route-paid="@Model.Paid">Previous</a>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
}
|
||||
@if(Model.NextPage.HasValue)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" asp-action="Index" asp-route-page="@Model.NextPage.Value"
|
||||
asp-route-count="@Model.Count" asp-route-userEmail="@Model.UserEmail"
|
||||
asp-route-name="@Model.Name" asp-route-paid="@Model.Paid">Next</a>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1">Next</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
13
src/Admin/Views/Providers/View.cshtml
Normal file
13
src/Admin/Views/Providers/View.cshtml
Normal file
@ -0,0 +1,13 @@
|
||||
@model ProviderViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Provider: " + Model.Provider.Name;
|
||||
}
|
||||
|
||||
<h1>Provider <small>@Model.Provider.Name</small></h1>
|
||||
|
||||
<h2>Information</h2>
|
||||
@await Html.PartialAsync("_ViewInformation", Model)
|
||||
<form asp-action="Delete" asp-route-id="@Model.Provider.Id"
|
||||
onsubmit="return confirm('Are you sure you want to delete this provider (@Model.Provider.Name)?')">
|
||||
<button class="btn btn-danger" type="submit">Delete</button>
|
||||
</form>
|
20
src/Admin/Views/Providers/_ViewInformation.cshtml
Normal file
20
src/Admin/Views/Providers/_ViewInformation.cshtml
Normal file
@ -0,0 +1,20 @@
|
||||
@model ProviderViewModel
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4 col-lg-3">Id</dt>
|
||||
<dd class="col-sm-8 col-lg-9"><code>@Model.Provider.Id</code></dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Status</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.Provider.Status</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Users</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.UserCount</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">ProviderAdmins</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@(string.IsNullOrWhiteSpace(Model.ProviderAdmins) ? "None" : Model.ProviderAdmins)</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Created</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.Provider.CreationDate.ToString()</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Modified</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.Provider.RevisionDate.ToString()</dd>
|
||||
</dl>
|
@ -40,6 +40,9 @@
|
||||
</li>
|
||||
@if(!GlobalSettings.SelfHosted)
|
||||
{
|
||||
<li class="nav-item" active-controller="Providers">
|
||||
<a class="nav-link" asp-controller="Providers" asp-action="Index">Providers</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown" active-controller="tools">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="toolsDropdown" role="button"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
|
@ -53,5 +53,10 @@
|
||||
// Organization_ClientExportedVault = 1602,
|
||||
|
||||
Policy_Updated = 1700,
|
||||
|
||||
ProviderUser_Invited = 1800,
|
||||
ProviderUser_Confirmed = 1801,
|
||||
ProviderUser_Updated = 1802,
|
||||
ProviderUser_Removed = 1803,
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
namespace Bit.Core.Enums.Provider
|
||||
{
|
||||
public enum ProviderOrganizationProviderUserType : byte
|
||||
{
|
||||
Administrator = 0,
|
||||
ServiceAdmin = 1,
|
||||
}
|
||||
}
|
8
src/Core/Enums/Provider/ProviderStatusType.cs
Normal file
8
src/Core/Enums/Provider/ProviderStatusType.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Bit.Core.Enums.Provider
|
||||
{
|
||||
public enum ProviderStatusType : byte
|
||||
{
|
||||
Pending = 0,
|
||||
Created = 1,
|
||||
}
|
||||
}
|
9
src/Core/Enums/Provider/ProviderUserStatusType.cs
Normal file
9
src/Core/Enums/Provider/ProviderUserStatusType.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Bit.Core.Enums.Provider
|
||||
{
|
||||
public enum ProviderUserStatusType : byte
|
||||
{
|
||||
Invited = 0,
|
||||
Accepted = 1,
|
||||
Confirmed = 2,
|
||||
}
|
||||
}
|
8
src/Core/Enums/Provider/ProviderUserType.cs
Normal file
8
src/Core/Enums/Provider/ProviderUserType.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Bit.Core.Enums.Provider
|
||||
{
|
||||
public enum ProviderUserType : byte
|
||||
{
|
||||
ProviderAdmin = 0,
|
||||
ServiceUser = 1,
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
You have been invited to setup a new Provider within Bitwarden.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
<a href="{{{Url}}}" clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
Setup Provider Now
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -0,0 +1,5 @@
|
||||
{{#>BasicTextLayout}}
|
||||
You have been invited to setup a new Provider within Bitwarden. To continue, click the following link:
|
||||
|
||||
{{{Url}}}
|
||||
{{/BasicTextLayout}}
|
@ -0,0 +1,14 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||
This email is to notify you that you have been confirmed as a user of <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{ProviderName}}</b>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
||||
You may now access the provider and manage the connected organizations.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -0,0 +1,5 @@
|
||||
{{#>BasicTextLayout}}
|
||||
This email is to notify you that you have been confirmed as a user of {{ProviderName}}.
|
||||
|
||||
You may now access the provider and manage the connected organizations.
|
||||
{{/BasicTextLayout}}
|
@ -0,0 +1,21 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
You have been invited to join the provider, <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{ProviderName}}</b>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
<a href="{{{Url}}}" clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
Join Provider Now
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
If you do not wish to join this provider, you can safely ignore this email.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -0,0 +1,7 @@
|
||||
{{#>BasicTextLayout}}
|
||||
You have been invited to join the provider, {{ProviderName}}. To accept this invite, click the following link:
|
||||
|
||||
{{{Url}}}
|
||||
|
||||
If you do not wish to join this provider, you can safely ignore this email.
|
||||
{{/BasicTextLayout}}
|
15
src/Core/Models/Business/Provider/ProviderUserInvite.cs
Normal file
15
src/Core/Models/Business/Provider/ProviderUserInvite.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
using Bit.Core.Enums.Provider;
|
||||
using Bit.Core.Models.Data;
|
||||
|
||||
namespace Bit.Core.Models.Business.Provider
|
||||
{
|
||||
public class ProviderUserInvite
|
||||
{
|
||||
public IEnumerable<string> Emails { get; set; }
|
||||
public ProviderUserType Type { get; set; }
|
||||
public Permissions Permissions { get; set; }
|
||||
|
||||
public ProviderUserInvite() {}
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
namespace Bit.Core.Models.Mail.Provider
|
||||
{
|
||||
public class ProviderSetupInviteViewModel : BaseMailModel
|
||||
{
|
||||
public string ProviderId { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string Token { get; set; }
|
||||
public string Url => string.Format("{0}/setup-provider?providerId={1}&email={2}&token={3}",
|
||||
WebVaultUrl,
|
||||
ProviderId,
|
||||
Email,
|
||||
Token);
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Models.Mail.Provider
|
||||
{
|
||||
public class ProviderUserConfirmedViewModel : BaseMailModel
|
||||
{
|
||||
public string ProviderName { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
namespace Bit.Core.Models.Mail.Provider
|
||||
{
|
||||
public class ProviderUserInvitedViewModel : BaseMailModel
|
||||
{
|
||||
public string ProviderName { get; set; }
|
||||
public string ProviderId { get; set; }
|
||||
public string ProviderUserId { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string ProviderNameUrlEncoded { get; set; }
|
||||
public string Token { get; set; }
|
||||
public string Url => string.Format("{0}/accept-provider?providerId={1}&" +
|
||||
"providerUserId={2}&email={3}&providerName={4}&token={5}",
|
||||
WebVaultUrl,
|
||||
ProviderId,
|
||||
ProviderUserId,
|
||||
Email,
|
||||
ProviderNameUrlEncoded,
|
||||
Token);
|
||||
}
|
||||
}
|
31
src/Core/Models/Table/Provider/Provider.cs
Normal file
31
src/Core/Models/Table/Provider/Provider.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using Bit.Core.Enums.Provider;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Models.Table.Provider
|
||||
{
|
||||
public class Provider : ITableObject<Guid>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string BusinessName { get; set; }
|
||||
public string BusinessAddress1 { get; set; }
|
||||
public string BusinessAddress2 { get; set; }
|
||||
public string BusinessAddress3 { get; set; }
|
||||
public string BusinessCountry { get; set; }
|
||||
public string BusinessTaxNumber { get; set; }
|
||||
public string BillingEmail { get; set; }
|
||||
public ProviderStatusType Status { get; set; }
|
||||
public bool Enabled { get; set; } = true;
|
||||
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
|
||||
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
if (Id == default)
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
src/Core/Models/Table/Provider/ProviderOrganization.cs
Normal file
24
src/Core/Models/Table/Provider/ProviderOrganization.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Models.Table.Provider
|
||||
{
|
||||
public class ProviderOrganization : ITableObject<Guid>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid ProviderId { get; set; }
|
||||
public Guid OrganizationId { get; set; }
|
||||
public string Key { get; set; }
|
||||
public string Settings { get; set; }
|
||||
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
|
||||
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
if (Id == default)
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using Bit.Core.Enums.Provider;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Models.Table.Provider
|
||||
{
|
||||
public class ProviderOrganizationProviderUser : ITableObject<Guid>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid ProviderOrganizationId { get; set; }
|
||||
public Guid ProviderUserId { get; set; }
|
||||
public ProviderOrganizationProviderUserType Type { get; set; }
|
||||
public string Permissions { get; set; }
|
||||
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
|
||||
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
if (Id == default)
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
28
src/Core/Models/Table/Provider/ProviderUser.cs
Normal file
28
src/Core/Models/Table/Provider/ProviderUser.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using Bit.Core.Enums.Provider;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Models.Table.Provider
|
||||
{
|
||||
public class ProviderUser : ITableObject<Guid>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid ProviderId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string Key { get; set; }
|
||||
public ProviderUserStatusType Status { get; set; }
|
||||
public ProviderUserType Type { get; set; }
|
||||
public string Permissions { get; set; }
|
||||
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
|
||||
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
if (Id == default)
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using System;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
|
||||
namespace Bit.Core.Repositories
|
||||
{
|
||||
public interface IProviderOrganizationProviderUserRepository : IRepository<Provider, Guid>
|
||||
{
|
||||
}
|
||||
}
|
9
src/Core/Repositories/IProviderOrganizationRepository.cs
Normal file
9
src/Core/Repositories/IProviderOrganizationRepository.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
|
||||
namespace Bit.Core.Repositories
|
||||
{
|
||||
public interface IProviderOrganizationRepository : IRepository<Provider, Guid>
|
||||
{
|
||||
}
|
||||
}
|
12
src/Core/Repositories/IProviderRepository.cs
Normal file
12
src/Core/Repositories/IProviderRepository.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
|
||||
namespace Bit.Core.Repositories
|
||||
{
|
||||
public interface IProviderRepository : IRepository<Provider, Guid>
|
||||
{
|
||||
Task<ICollection<Provider>> SearchAsync(string name, string userEmail, int skip, int take);
|
||||
}
|
||||
}
|
16
src/Core/Repositories/IProviderUserRepository.cs
Normal file
16
src/Core/Repositories/IProviderUserRepository.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums.Provider;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
|
||||
namespace Bit.Core.Repositories
|
||||
{
|
||||
public interface IProviderUserRepository : IRepository<ProviderUser, Guid>
|
||||
{
|
||||
Task<int> GetCountByProviderAsync(Guid providerId, string email, bool onlyRegisteredUsers);
|
||||
Task<ICollection<ProviderUser>> GetManyAsync(IEnumerable<Guid> ids);
|
||||
Task<ICollection<ProviderUser>> GetManyByProviderAsync(Guid providerId, ProviderUserType? type = null);
|
||||
Task DeleteManyAsync(IEnumerable<Guid> userIds);
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
namespace Bit.Core.Repositories.SqlServer
|
||||
{
|
||||
public class ProviderOrganizationProviderUserRepository : Repository<Provider, Guid>, IProviderOrganizationProviderUserRepository
|
||||
{
|
||||
public ProviderOrganizationProviderUserRepository(GlobalSettings globalSettings)
|
||||
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
|
||||
{ }
|
||||
|
||||
public ProviderOrganizationProviderUserRepository(string connectionString, string readOnlyConnectionString)
|
||||
: base(connectionString, readOnlyConnectionString)
|
||||
{ }
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
namespace Bit.Core.Repositories.SqlServer
|
||||
{
|
||||
public class ProviderOrganizationRepository : Repository<Provider, Guid>, IProviderOrganizationRepository
|
||||
{
|
||||
public ProviderOrganizationRepository(GlobalSettings globalSettings)
|
||||
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
|
||||
{ }
|
||||
|
||||
public ProviderOrganizationRepository(string connectionString, string readOnlyConnectionString)
|
||||
: base(connectionString, readOnlyConnectionString)
|
||||
{ }
|
||||
}
|
||||
}
|
38
src/Core/Repositories/SqlServer/ProviderRepository.cs
Normal file
38
src/Core/Repositories/SqlServer/ProviderRepository.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using Bit.Core.Models.Table;
|
||||
using System.Threading.Tasks;
|
||||
using System.Data.SqlClient;
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
namespace Bit.Core.Repositories.SqlServer
|
||||
{
|
||||
public class ProviderRepository : Repository<Provider, Guid>, IProviderRepository
|
||||
{
|
||||
public ProviderRepository(GlobalSettings globalSettings)
|
||||
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
|
||||
{ }
|
||||
|
||||
public ProviderRepository(string connectionString, string readOnlyConnectionString)
|
||||
: base(connectionString, readOnlyConnectionString)
|
||||
{ }
|
||||
|
||||
public async Task<ICollection<Provider>> SearchAsync(string name, string userEmail, int skip, int take)
|
||||
{
|
||||
using (var connection = new SqlConnection(ReadOnlyConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<Provider>(
|
||||
"[dbo].[Provider_Search]",
|
||||
new { Name = name, UserEmail = userEmail, Skip = skip, Take = take },
|
||||
commandType: CommandType.StoredProcedure,
|
||||
commandTimeout: 120);
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
73
src/Core/Repositories/SqlServer/ProviderUserRepository.cs
Normal file
73
src/Core/Repositories/SqlServer/ProviderUserRepository.cs
Normal file
@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums.Provider;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Dapper;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace Bit.Core.Repositories.SqlServer
|
||||
{
|
||||
public class ProviderUserRepository : Repository<ProviderUser, Guid>, IProviderUserRepository
|
||||
{
|
||||
public ProviderUserRepository(GlobalSettings globalSettings)
|
||||
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
|
||||
{ }
|
||||
|
||||
public ProviderUserRepository(string connectionString, string readOnlyConnectionString)
|
||||
: base(connectionString, readOnlyConnectionString)
|
||||
{ }
|
||||
|
||||
public async Task<int> GetCountByProviderAsync(Guid providerId, string email, bool onlyRegisteredUsers)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var result = await connection.ExecuteScalarAsync<int>(
|
||||
"[dbo].[ProviderUser_ReadCountByProviderIdEmail]",
|
||||
new { ProviderId = providerId, Email = email, OnlyUsers = onlyRegisteredUsers },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<ProviderUser>> GetManyAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<ProviderUser>(
|
||||
"[dbo].[ProviderUser_ReadByIds]",
|
||||
new { Ids = ids.ToGuidIdArrayTVP() },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<ProviderUser>> GetManyByProviderAsync(Guid providerId, ProviderUserType? type)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<ProviderUser>(
|
||||
"[dbo].[ProviderUser_ReadByProviderId]",
|
||||
new { ProviderId = providerId, Type = type },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteManyAsync(IEnumerable<Guid> providerUserIds)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
await connection.ExecuteAsync("[dbo].[ProviderUser_DeleteByIds]",
|
||||
new { Ids = providerUserIds.ToGuidIdArrayTVP() }, commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
@ -17,5 +18,8 @@ namespace Bit.Core.Services
|
||||
Task LogOrganizationUserEventAsync(OrganizationUser organizationUser, EventType type, DateTime? date = null);
|
||||
Task LogOrganizationUserEventsAsync(IEnumerable<(OrganizationUser, EventType, DateTime?)> events);
|
||||
Task LogOrganizationEventAsync(Organization organization, EventType type, DateTime? date = null);
|
||||
Task LogProviderUserEventAsync(ProviderUser providerUser, EventType type, DateTime? date = null);
|
||||
Task LogProviderUsersEventAsync(IEnumerable<(ProviderUser, EventType, DateTime?)> events);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ using Bit.Core.Models.Table;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using Bit.Core.Models.Mail;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
@ -41,5 +42,8 @@ namespace Bit.Core.Services
|
||||
Task SendEmergencyAccessRecoveryTimedOut(EmergencyAccess ea, string initiatingName, string email);
|
||||
Task SendEnqueuedMailMessageAsync(IMailQueueMessage queueMessage);
|
||||
Task SendAdminResetPasswordEmailAsync(string email, string userName, string orgName);
|
||||
Task SendProviderSetupInviteEmailAsync(Provider provider, string token, string email);
|
||||
Task SendProviderInviteEmailAsync(string providerName, ProviderUser providerUser, string token, string email);
|
||||
Task SendProviderConfirmedEmailAsync(string providerName, string email);
|
||||
}
|
||||
}
|
||||
|
31
src/Core/Services/IProviderService.cs
Normal file
31
src/Core/Services/IProviderService.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Table;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using Bit.Core.Models.Business.Provider;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public interface IProviderService
|
||||
{
|
||||
Task CreateAsync(string ownerEmail);
|
||||
Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key);
|
||||
Task UpdateAsync(Provider provider, bool updateBilling = false);
|
||||
|
||||
Task<List<ProviderUser>> InviteUserAsync(Guid providerId, Guid invitingUserId, ProviderUserInvite providerUserInvite);
|
||||
Task<List<Tuple<ProviderUser, string>>> ResendInvitesAsync(Guid providerId, Guid invitingUserId,
|
||||
IEnumerable<Guid> providerUsersId);
|
||||
Task<ProviderUser> AcceptUserAsync(Guid providerUserId, User user, string token);
|
||||
Task<List<Tuple<ProviderUser, string>>> ConfirmUsersAsync(Guid providerId, Dictionary<Guid, string> keys, Guid confirmingUserId);
|
||||
|
||||
Task SaveUserAsync(ProviderUser user, Guid savingUserId);
|
||||
Task<List<Tuple<ProviderUser, string>>> DeleteUsersAsync(Guid providerId, IEnumerable<Guid> providerUserIds,
|
||||
Guid deletingUserId);
|
||||
|
||||
Task AddOrganization(Guid providerId, Guid organizationId, Guid addingUserId, string key);
|
||||
Task RemoveOrganization(Guid providerOrganizationId, Guid removingUserId);
|
||||
|
||||
// TODO: Figure out how ProviderOrganizationProviderUsers should be managed
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
@ -222,6 +223,12 @@ namespace Bit.Core.Services
|
||||
await _eventWriteService.CreateAsync(e);
|
||||
}
|
||||
|
||||
// TODO: Implement this
|
||||
public Task LogProviderUserEventAsync(ProviderUser providerUser, EventType type, DateTime? date = null) => throw new NotImplementedException();
|
||||
|
||||
// TODO: Implement this
|
||||
public Task LogProviderUsersEventAsync(IEnumerable<(ProviderUser, EventType, DateTime?)> events) => throw new NotImplementedException();
|
||||
|
||||
private bool CanUseEvents(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)
|
||||
{
|
||||
return orgAbilities != null && orgAbilities.ContainsKey(orgId) &&
|
||||
|
@ -9,6 +9,8 @@ using System.Net;
|
||||
using Bit.Core.Utilities;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Bit.Core.Models.Mail.Provider;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
using HandlebarsDotNet;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
@ -646,5 +648,53 @@ namespace Bit.Core.Services
|
||||
message.Category = "EmergencyAccessRecoveryTimedOut";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendProviderSetupInviteEmailAsync(Provider provider, string token, string email)
|
||||
{
|
||||
var message = CreateDefaultMessage($"Create a Provider", email);
|
||||
var model = new ProviderSetupInviteViewModel
|
||||
{
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
SiteName = _globalSettings.SiteName,
|
||||
ProviderId = provider.Id.ToString(),
|
||||
Email = email,
|
||||
Token = token,
|
||||
};
|
||||
await AddMessageContentAsync(message, "Provider.ProviderSetupInvite", model);
|
||||
message.Category = "ProviderSetupInvite";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendProviderInviteEmailAsync(string providerName, ProviderUser providerUser, string token, string email)
|
||||
{
|
||||
var message = CreateDefaultMessage($"Join {providerName}", email);
|
||||
var model = new ProviderUserInvitedViewModel
|
||||
{
|
||||
ProviderName = CoreHelpers.SanitizeForEmail(providerName),
|
||||
Email = WebUtility.UrlDecode(providerUser.Email),
|
||||
ProviderId = providerUser.ProviderId.ToString(),
|
||||
ProviderUserId = providerUser.Id.ToString(),
|
||||
ProviderNameUrlEncoded = WebUtility.UrlEncode(providerName),
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
SiteName = _globalSettings.SiteName,
|
||||
};
|
||||
await AddMessageContentAsync(message, "Provider.ProviderUserInvited", model);
|
||||
message.Category = "ProviderSetupInvite";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendProviderConfirmedEmailAsync(string providerName, string email)
|
||||
{
|
||||
var message = CreateDefaultMessage($"You Have Been Confirmed To {providerName}", email);
|
||||
var model = new ProviderUserConfirmedViewModel
|
||||
{
|
||||
ProviderName = CoreHelpers.SanitizeForEmail(providerName),
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
SiteName = _globalSettings.SiteName
|
||||
};
|
||||
await AddMessageContentAsync(message, "Provider.ProviderUserConfirmed", model);
|
||||
message.Category = "ProviderUserConfirmed";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
348
src/Core/Services/Implementations/ProviderService.cs
Normal file
348
src/Core/Services/Implementations/ProviderService.cs
Normal file
@ -0,0 +1,348 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums.Provider;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business.Provider;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class ProviderService : IProviderService
|
||||
{
|
||||
private readonly IDataProtector _dataProtector;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
|
||||
IUserRepository userRepository, IUserService userService, IMailService mailService,
|
||||
IDataProtectionProvider dataProtectionProvider, IEventService eventService, GlobalSettings globalSettings)
|
||||
{
|
||||
_providerRepository = providerRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_userRepository = userRepository;
|
||||
_userService = userService;
|
||||
_mailService = mailService;
|
||||
_eventService = eventService;
|
||||
_globalSettings = globalSettings;
|
||||
_dataProtector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
||||
}
|
||||
|
||||
public async Task CreateAsync(string ownerEmail)
|
||||
{
|
||||
var owner = await _userRepository.GetByEmailAsync(ownerEmail);
|
||||
if (owner == null)
|
||||
{
|
||||
throw new BadRequestException("Invalid owner.");
|
||||
}
|
||||
|
||||
var provider = new Provider
|
||||
{
|
||||
Status = ProviderStatusType.Pending,
|
||||
Enabled = true,
|
||||
};
|
||||
await _providerRepository.CreateAsync(provider);
|
||||
|
||||
var token = _dataProtector.Protect($"ProviderSetupInvite {provider.Id} {owner.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||
await _mailService.SendProviderSetupInviteEmailAsync(provider, token, owner.Email);
|
||||
}
|
||||
|
||||
public async Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key)
|
||||
{
|
||||
var owner = await _userService.GetUserByIdAsync(ownerUserId);
|
||||
if (owner == null)
|
||||
{
|
||||
throw new BadRequestException("Invalid owner.");
|
||||
}
|
||||
|
||||
if (!CoreHelpers.TokenIsValid("ProviderSetupInvite", _dataProtector, token, owner.Email, provider.Id, _globalSettings))
|
||||
{
|
||||
throw new BadRequestException("Invalid token.");
|
||||
}
|
||||
|
||||
await _providerRepository.UpsertAsync(provider);
|
||||
|
||||
var providerUser = new ProviderUser
|
||||
{
|
||||
ProviderId = provider.Id,
|
||||
UserId = owner.Id,
|
||||
Key = key,
|
||||
Status = ProviderUserStatusType.Confirmed,
|
||||
Type = ProviderUserType.ProviderAdmin,
|
||||
};
|
||||
|
||||
await _providerUserRepository.CreateAsync(providerUser);
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(Provider provider, bool updateBilling = false)
|
||||
{
|
||||
if (provider.Id == default)
|
||||
{
|
||||
throw new ApplicationException("Cannot create provider this way.");
|
||||
}
|
||||
|
||||
await _providerRepository.ReplaceAsync(provider);
|
||||
}
|
||||
|
||||
public async Task<List<ProviderUser>> InviteUserAsync(Guid providerId, Guid invitingUserId,
|
||||
ProviderUserInvite invite)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||
if (provider == null || invite?.Emails == null || !invite.Emails.Any())
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var providerUsers = new List<ProviderUser>();
|
||||
foreach (var email in invite.Emails)
|
||||
{
|
||||
// Make sure user is not already invited
|
||||
var existingProviderUserCount =
|
||||
await _providerUserRepository.GetCountByProviderAsync(providerId, email, false);
|
||||
if (existingProviderUserCount > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var providerUser = new ProviderUser
|
||||
{
|
||||
ProviderId = providerId,
|
||||
UserId = null,
|
||||
Email = email.ToLowerInvariant(),
|
||||
Key = null,
|
||||
Type = invite.Type,
|
||||
Status = ProviderUserStatusType.Invited,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
RevisionDate = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
if (invite.Permissions != null)
|
||||
{
|
||||
providerUser.Permissions = JsonSerializer.Serialize(invite.Permissions, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
});
|
||||
}
|
||||
|
||||
await _providerUserRepository.CreateAsync(providerUser);
|
||||
|
||||
await SendInviteAsync(providerUser, provider);
|
||||
providerUsers.Add(providerUser);
|
||||
}
|
||||
|
||||
await _eventService.LogProviderUsersEventAsync(providerUsers.Select(pu => (pu, EventType.ProviderUser_Invited, null as DateTime?)));
|
||||
|
||||
return providerUsers;
|
||||
}
|
||||
|
||||
public async Task<List<Tuple<ProviderUser, string>>> ResendInvitesAsync(Guid providerId, Guid invitingUserId,
|
||||
IEnumerable<Guid> providerUsersId)
|
||||
{
|
||||
var providerUsers = await _providerUserRepository.GetManyAsync(providerUsersId);
|
||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||
|
||||
var result = new List<Tuple<ProviderUser, string>>();
|
||||
foreach (var providerUser in providerUsers)
|
||||
{
|
||||
if (providerUser.Status != ProviderUserStatusType.Invited || providerUser.ProviderId != providerId)
|
||||
{
|
||||
result.Add(Tuple.Create(providerUser, "User invalid."));
|
||||
continue;
|
||||
}
|
||||
|
||||
await SendInviteAsync(providerUser, provider);
|
||||
result.Add(Tuple.Create(providerUser, ""));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<ProviderUser> AcceptUserAsync(Guid providerUserId, User user, string token)
|
||||
{
|
||||
var providerUser = await _providerUserRepository.GetByIdAsync(providerUserId);
|
||||
if (providerUser == null)
|
||||
{
|
||||
throw new BadRequestException("User invalid.");
|
||||
}
|
||||
|
||||
if (providerUser.Status != ProviderUserStatusType.Invited)
|
||||
{
|
||||
throw new BadRequestException("Already accepted.");
|
||||
}
|
||||
|
||||
if (!CoreHelpers.TokenIsValid("ProviderUserInvite", _dataProtector, token, user.Email, providerUser.Id, _globalSettings))
|
||||
{
|
||||
throw new BadRequestException("Invalid token.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(providerUser.Email) ||
|
||||
!providerUser.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
throw new BadRequestException("User email does not match invite.");
|
||||
}
|
||||
|
||||
providerUser.Status = ProviderUserStatusType.Accepted;
|
||||
providerUser.UserId = user.Id;
|
||||
providerUser.Email = null;
|
||||
|
||||
await _providerUserRepository.ReplaceAsync(providerUser);
|
||||
|
||||
return providerUser;
|
||||
}
|
||||
|
||||
public async Task<List<Tuple<ProviderUser, string>>> ConfirmUsersAsync(Guid providerId, Dictionary<Guid, string> keys,
|
||||
Guid confirmingUserId)
|
||||
{
|
||||
var providerUsers = await _providerUserRepository.GetManyAsync(keys.Keys);
|
||||
var validProviderUsers = providerUsers
|
||||
.Where(u => u.UserId != null)
|
||||
.ToList();
|
||||
|
||||
if (!validProviderUsers.Any())
|
||||
{
|
||||
return new List<Tuple<ProviderUser, string>>();
|
||||
}
|
||||
|
||||
var validOrganizationUserIds = validProviderUsers.Select(u => u.UserId.Value).ToList();
|
||||
|
||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||
var users = await _userRepository.GetManyAsync(validOrganizationUserIds);
|
||||
|
||||
var keyedFilteredUsers = validProviderUsers.ToDictionary(u => u.UserId.Value, u => u);
|
||||
|
||||
var result = new List<Tuple<ProviderUser, string>>();
|
||||
var events = new List<(ProviderUser, EventType, DateTime?)>();
|
||||
|
||||
foreach (var user in users)
|
||||
{
|
||||
if (!keyedFilteredUsers.ContainsKey(user.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var providerUser = keyedFilteredUsers[user.Id];
|
||||
try
|
||||
{
|
||||
if (providerUser.Status != ProviderUserStatusType.Accepted || providerUser.ProviderId != providerId)
|
||||
{
|
||||
throw new BadRequestException("Invalid user.");
|
||||
}
|
||||
|
||||
providerUser.Status = ProviderUserStatusType.Confirmed;
|
||||
providerUser.Key = keys[providerUser.Id];
|
||||
providerUser.Email = null;
|
||||
|
||||
await _providerUserRepository.ReplaceAsync(providerUser);
|
||||
events.Add((providerUser, EventType.ProviderUser_Confirmed, null));
|
||||
await _mailService.SendOrganizationConfirmedEmailAsync(provider.Name, user.Email);
|
||||
result.Add(Tuple.Create(providerUser, ""));
|
||||
}
|
||||
catch (BadRequestException e)
|
||||
{
|
||||
result.Add(Tuple.Create(providerUser, e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
await _eventService.LogProviderUsersEventAsync(events);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task SaveUserAsync(ProviderUser user, Guid savingUserId)
|
||||
{
|
||||
if (user.Id.Equals(default))
|
||||
{
|
||||
throw new BadRequestException("Invite the user first.");
|
||||
}
|
||||
|
||||
if (user.Type != ProviderUserType.ProviderAdmin &&
|
||||
!await HasConfirmedProviderAdminExceptAsync(user.ProviderId, new[] {user.Id}))
|
||||
{
|
||||
throw new BadRequestException("Provider must have at least one confirmed ProviderAdmin.");
|
||||
}
|
||||
|
||||
await _providerUserRepository.ReplaceAsync(user);
|
||||
await _eventService.LogProviderUserEventAsync(user, EventType.ProviderUser_Updated);
|
||||
}
|
||||
|
||||
public async Task<List<Tuple<ProviderUser, string>>> DeleteUsersAsync(Guid providerId,
|
||||
IEnumerable<Guid> providerUserIds, Guid deletingUserId)
|
||||
{
|
||||
var providerUsers = await _providerUserRepository.GetManyAsync(providerUserIds);
|
||||
|
||||
if (!await HasConfirmedProviderAdminExceptAsync(providerId, providerUserIds))
|
||||
{
|
||||
throw new BadRequestException("Provider must have at least one confirmed ProviderAdmin.");
|
||||
}
|
||||
|
||||
var result = new List<Tuple<ProviderUser, string>>();
|
||||
var deletedUserIds = new List<Guid>();
|
||||
var events = new List<(ProviderUser, EventType, DateTime?)>();
|
||||
|
||||
foreach (var providerUser in providerUsers)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (providerUser.ProviderId != providerId)
|
||||
{
|
||||
throw new BadRequestException("Invalid user.");
|
||||
}
|
||||
if (providerUser.UserId == deletingUserId)
|
||||
{
|
||||
throw new BadRequestException("You cannot remove yourself.");
|
||||
}
|
||||
|
||||
events.Add((providerUser, EventType.ProviderUser_Removed, null));
|
||||
|
||||
result.Add(Tuple.Create(providerUser, ""));
|
||||
deletedUserIds.Add(providerUser.Id);
|
||||
}
|
||||
catch (BadRequestException e)
|
||||
{
|
||||
result.Add(Tuple.Create(providerUser, e.Message));
|
||||
}
|
||||
|
||||
await _providerUserRepository.DeleteManyAsync(deletedUserIds);
|
||||
}
|
||||
|
||||
await _eventService.LogProviderUsersEventAsync(events);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: Implement this
|
||||
public Task AddOrganization(Guid providerId, Guid organizationId, Guid addingUserId, string key) => throw new NotImplementedException();
|
||||
|
||||
// TODO: Implement this
|
||||
public Task RemoveOrganization(Guid providerOrganizationId, Guid removingUserId) => throw new NotImplementedException();
|
||||
|
||||
private async Task SendInviteAsync(ProviderUser providerUser, Provider provider)
|
||||
{
|
||||
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
|
||||
var token = _dataProtector.Protect(
|
||||
$"ProviderUserInvite {providerUser.Id} {providerUser.Email} {nowMillis}");
|
||||
await _mailService.SendProviderInviteEmailAsync(provider.Name, providerUser, token, providerUser.Email);
|
||||
}
|
||||
|
||||
private async Task<bool> HasConfirmedProviderAdminExceptAsync(Guid providerId, IEnumerable<Guid> providerUserIds)
|
||||
{
|
||||
var providerAdmins = await _providerUserRepository.GetManyByProviderAsync(providerId,
|
||||
ProviderUserType.ProviderAdmin);
|
||||
var confirmedOwners = providerAdmins.Where(o => o.Status == ProviderUserStatusType.Confirmed);
|
||||
var confirmedOwnersIds = confirmedOwners.Select(u => u.Id);
|
||||
return confirmedOwnersIds.Except(providerUserIds).Any();
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
@ -38,6 +39,16 @@ namespace Bit.Core.Services
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task LogProviderUserEventAsync(ProviderUser providerUser, EventType type, DateTime? date = null)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task LogProviderUsersEventAsync(IEnumerable<(ProviderUser, EventType, DateTime?)> events)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task LogOrganizationUserEventAsync(OrganizationUser organizationUser, EventType type,
|
||||
DateTime? date = null)
|
||||
{
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Mail;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
@ -158,10 +159,25 @@ namespace Bit.Core.Services
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
|
||||
public Task SendAdminResetPasswordEmailAsync(string email, string userName, string orgName)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendProviderSetupInviteEmailAsync(Provider provider, string token, string email)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendProviderInviteEmailAsync(string providerName, ProviderUser providerUser, string token, string email)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendProviderConfirmedEmailAsync(string providerName, string email)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -85,6 +85,10 @@ namespace Bit.Core.Utilities
|
||||
services.AddSingleton<ISendRepository, SqlServerRepos.SendRepository>();
|
||||
services.AddSingleton<ITaxRateRepository, SqlServerRepos.TaxRateRepository>();
|
||||
services.AddSingleton<IEmergencyAccessRepository, SqlServerRepos.EmergencyAccessRepository>();
|
||||
services.AddSingleton<IProviderRepository, SqlServerRepos.ProviderRepository>();
|
||||
services.AddSingleton<IProviderUserRepository, SqlServerRepos.ProviderUserRepository>();
|
||||
services.AddSingleton<IProviderOrganizationRepository, SqlServerRepos.ProviderOrganizationRepository>();
|
||||
services.AddSingleton<IProviderOrganizationProviderUserRepository, SqlServerRepos.ProviderOrganizationProviderUserRepository>();
|
||||
}
|
||||
|
||||
if (globalSettings.SelfHosted)
|
||||
@ -116,12 +120,13 @@ namespace Bit.Core.Utilities
|
||||
services.AddScoped<ICollectionService, CollectionService>();
|
||||
services.AddScoped<IGroupService, GroupService>();
|
||||
services.AddScoped<IPolicyService, PolicyService>();
|
||||
services.AddScoped<Services.IEventService, EventService>();
|
||||
services.AddScoped<IEventService, EventService>();
|
||||
services.AddScoped<IEmergencyAccessService, EmergencyAccessService>();
|
||||
services.AddSingleton<IDeviceService, DeviceService>();
|
||||
services.AddSingleton<IAppleIapService, AppleIapService>();
|
||||
services.AddSingleton<ISsoConfigService, SsoConfigService>();
|
||||
services.AddScoped<ISendService, SendService>();
|
||||
services.AddScoped<IProviderService, ProviderService>();
|
||||
}
|
||||
|
||||
public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings)
|
||||
|
@ -136,6 +136,7 @@
|
||||
<Build Include="dbo\Stored Procedures\User_BumpAccountRevisionDateByOrganizationId.sql" />
|
||||
<Build Include="dbo\Stored Procedures\User_BumpAccountRevisionDateByOrganizationUserId.sql" />
|
||||
<Build Include="dbo\Stored Procedures\User_BumpAccountRevisionDateByOrganizationUserIds.sql" />
|
||||
<Build Include="dbo\Stored Procedures\User_BumpAccountRevisionDateByProviderUserIds.sql" />
|
||||
<Build Include="dbo\Stored Procedures\Cipher_Delete.sql" />
|
||||
<Build Include="dbo\Stored Procedures\User_ReadPublicKeyById.sql" />
|
||||
<Build Include="dbo\Stored Procedures\User_ReadPublicKeysByOrganizationUserIds.sql" />
|
||||
@ -330,14 +331,18 @@
|
||||
<Build Include="dbo\Stored Procedures\Provider_Update.sql" />
|
||||
<Build Include="dbo\Stored Procedures\Provider_DeleteById.sql" />
|
||||
<Build Include="dbo\Stored Procedures\Provider_ReadById.sql" />
|
||||
<Build Include="dbo\Stored Procedures\Provider_Search.sql" />
|
||||
<Build Include="dbo\Tables\ProviderUser.sql" />
|
||||
<Build Include="dbo\Views\ProviderUserView.sql" />
|
||||
<Build Include="dbo\Stored Procedures\ProviderUser_Create.sql" />
|
||||
<Build Include="dbo\Stored Procedures\ProviderUser_Update.sql" />
|
||||
<Build Include="dbo\Stored Procedures\ProviderUser_DeleteById.sql" />
|
||||
<Build Include="dbo\Stored Procedures\ProviderUser_DeleteByIds.sql" />
|
||||
<Build Include="dbo\Stored Procedures\ProviderUser_ReadById.sql" />
|
||||
<Build Include="dbo\Stored Procedures\ProviderUser_ReadByIds.sql" />
|
||||
<Build Include="dbo\Stored Procedures\ProviderUser_ReadByProviderId.sql" />
|
||||
<Build Include="dbo\Stored Procedures\ProviderUser_ReadByUserId.sql" />
|
||||
<Build Include="dbo\Stored Procedures\ProviderUser_ReadCountByProviderIdEmail.sql" />
|
||||
<Build Include="dbo\Tables\ProviderOrganization.sql" />
|
||||
<Build Include="dbo\Views\ProviderOrganizationView.sql" />
|
||||
<Build Include="dbo\Stored Procedures\ProviderOrganization_Create.sql" />
|
||||
|
42
src/Sql/dbo/Stored Procedures/ProviderUser_DeleteByIds.sql
Normal file
42
src/Sql/dbo/Stored Procedures/ProviderUser_DeleteByIds.sql
Normal file
@ -0,0 +1,42 @@
|
||||
CREATE PROCEDURE [dbo].[ProviderUser_DeleteByIds]
|
||||
@Ids [dbo].[GuidIdArray] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByProviderUserIds] @Ids
|
||||
|
||||
DECLARE @UserAndProviderIds [dbo].[TwoGuidIdArray]
|
||||
|
||||
INSERT INTO @UserAndProviderIds
|
||||
(Id1, Id2)
|
||||
SELECT
|
||||
UserId,
|
||||
ProviderId
|
||||
FROM
|
||||
[dbo].[ProviderUser] PU
|
||||
INNER JOIN
|
||||
@Ids PUIds ON PUIds.Id = PU.Id
|
||||
WHERE
|
||||
UserId IS NOT NULL AND
|
||||
ProviderId IS NOT NULL
|
||||
|
||||
DECLARE @BatchSize INT = 100
|
||||
|
||||
-- Delete ProviderUsers
|
||||
WHILE @BatchSize > 0
|
||||
BEGIN
|
||||
BEGIN TRANSACTION ProviderUser_DeleteMany_PUs
|
||||
|
||||
DELETE TOP(@BatchSize) OU
|
||||
FROM
|
||||
[dbo].[ProviderUser] PU
|
||||
INNER JOIN
|
||||
@Ids I ON I.Id = PU.Id
|
||||
|
||||
SET @BatchSize = @@ROWCOUNT
|
||||
|
||||
COMMIT TRANSACTION ProviderUser_DeleteMany_PUs
|
||||
END
|
||||
END
|
||||
GO
|
18
src/Sql/dbo/Stored Procedures/ProviderUser_ReadByIds.sql
Normal file
18
src/Sql/dbo/Stored Procedures/ProviderUser_ReadByIds.sql
Normal file
@ -0,0 +1,18 @@
|
||||
CREATE PROCEDURE [dbo].[ProviderUser_ReadByIds]
|
||||
@Ids AS [dbo].[GuidIdArray] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
IF (SELECT COUNT(1) FROM @Ids) < 1
|
||||
BEGIN
|
||||
RETURN(-1)
|
||||
END
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[ProviderUserView]
|
||||
WHERE
|
||||
[Id] IN (SELECT [Id] FROM @Ids)
|
||||
END
|
@ -1,5 +1,6 @@
|
||||
CREATE PROCEDURE [dbo].[ProviderUser_ReadByProviderId]
|
||||
@ProviderId UNIQUEIDENTIFIER
|
||||
@ProviderId UNIQUEIDENTIFIER,
|
||||
@Type TINYINT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@ -10,4 +11,5 @@ BEGIN
|
||||
[dbo].[ProviderUserView]
|
||||
WHERE
|
||||
[ProviderId] = @ProviderId
|
||||
AND [Type] = COALESCE(@Type, [Type])
|
||||
END
|
||||
|
@ -0,0 +1,21 @@
|
||||
CREATE PROCEDURE [dbo].[ProviderUser_ReadCountByProviderIdEmail]
|
||||
@ProviderId UNIQUEIDENTIFIER,
|
||||
@Email NVARCHAR(256),
|
||||
@OnlyUsers BIT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
COUNT(1)
|
||||
FROM
|
||||
[dbo].[ProviderUser] OU
|
||||
LEFT JOIN
|
||||
[dbo].[User] U ON OU.[UserId] = U.[Id]
|
||||
WHERE
|
||||
OU.[ProviderId] = @ProviderId
|
||||
AND (
|
||||
(@OnlyUsers = 0 AND @Email IN (OU.[Email], U.[Email]))
|
||||
OR (@OnlyUsers = 1 AND U.[Email] = @Email)
|
||||
)
|
||||
END
|
41
src/Sql/dbo/Stored Procedures/Provider_Search.sql
Normal file
41
src/Sql/dbo/Stored Procedures/Provider_Search.sql
Normal file
@ -0,0 +1,41 @@
|
||||
CREATE PROCEDURE [dbo].[Provider_Search]
|
||||
@Name NVARCHAR(50),
|
||||
@UserEmail NVARCHAR(256),
|
||||
@Skip INT = 0,
|
||||
@Take INT = 25
|
||||
WITH RECOMPILE
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
DECLARE @NameLikeSearch NVARCHAR(55) = '%' + @Name + '%'
|
||||
|
||||
IF @UserEmail IS NOT NULL
|
||||
BEGIN
|
||||
SELECT
|
||||
O.*
|
||||
FROM
|
||||
[dbo].[ProviderView] O
|
||||
INNER JOIN
|
||||
[dbo].[ProviderUser] OU ON O.[Id] = OU.[ProviderId]
|
||||
INNER JOIN
|
||||
[dbo].[User] U ON U.[Id] = OU.[UserId]
|
||||
WHERE
|
||||
(@Name IS NULL OR O.[Name] LIKE @NameLikeSearch)
|
||||
AND U.[Email] = COALESCE(@UserEmail, U.[Email])
|
||||
ORDER BY O.[CreationDate] DESC
|
||||
OFFSET @Skip ROWS
|
||||
FETCH NEXT @Take ROWS ONLY
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
SELECT
|
||||
O.*
|
||||
FROM
|
||||
[dbo].[ProviderView] O
|
||||
WHERE
|
||||
(@Name IS NULL OR O.[Name] LIKE @NameLikeSearch)
|
||||
ORDER BY O.[CreationDate] DESC
|
||||
OFFSET @Skip ROWS
|
||||
FETCH NEXT @Take ROWS ONLY
|
||||
END
|
||||
END
|
@ -0,0 +1,18 @@
|
||||
CREATE PROCEDURE [dbo].[User_BumpAccountRevisionDateByProviderUserIds]
|
||||
@ProviderUserIds [dbo].[GuidIdArray] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE
|
||||
U
|
||||
SET
|
||||
U.[AccountRevisionDate] = GETUTCDATE()
|
||||
FROM
|
||||
@ProviderUserIds OUIDs
|
||||
INNER JOIN
|
||||
[dbo].[ProviderUser] PU ON OUIDs.Id = PU.Id AND PU.[Status] = 2 -- Confirmed
|
||||
INNER JOIN
|
||||
[dbo].[User] U ON PU.UserId = U.Id
|
||||
END
|
||||
GO
|
@ -1,13 +1,13 @@
|
||||
CREATE TABLE [dbo].[Provider] (
|
||||
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||
[Name] NVARCHAR (50) NOT NULL,
|
||||
[Name] NVARCHAR (50) NULL,
|
||||
[BusinessName] NVARCHAR (50) NULL,
|
||||
[BusinessAddress1] NVARCHAR (50) NULL,
|
||||
[BusinessAddress2] NVARCHAR (50) NULL,
|
||||
[BusinessAddress3] NVARCHAR (50) NULL,
|
||||
[BusinessCountry] VARCHAR (2) NULL,
|
||||
[BusinessTaxNumber] NVARCHAR (30) NULL,
|
||||
[BillingEmail] NVARCHAR (256) NOT NULL,
|
||||
[BillingEmail] NVARCHAR (256) NULL,
|
||||
[Status] TINYINT NOT NULL,
|
||||
[Enabled] BIT NOT NULL,
|
||||
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||
|
45
test/Core.Test/AutoFixture/ProviderUserFixtures.cs
Normal file
45
test/Core.Test/AutoFixture/ProviderUserFixtures.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using System.Reflection;
|
||||
using AutoFixture;
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Core.Enums.Provider;
|
||||
|
||||
namespace Bit.Core.Test.AutoFixture.ProviderUserFixtures
|
||||
{
|
||||
internal class ProviderUser : ICustomization
|
||||
{
|
||||
public ProviderUserStatusType Status { get; set; }
|
||||
public ProviderUserType Type { get; set; }
|
||||
|
||||
public ProviderUser(ProviderUserStatusType status, ProviderUserType type)
|
||||
{
|
||||
Status = status;
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
fixture.Customize<Core.Models.Table.Provider.ProviderUser>(composer => composer
|
||||
.With(o => o.Type, Type)
|
||||
.With(o => o.Status, Status));
|
||||
}
|
||||
}
|
||||
|
||||
public class ProviderUserAttribute : CustomizeAttribute
|
||||
{
|
||||
private readonly ProviderUserStatusType _status;
|
||||
private readonly ProviderUserType _type;
|
||||
|
||||
public ProviderUserAttribute(
|
||||
ProviderUserStatusType status = ProviderUserStatusType.Confirmed,
|
||||
ProviderUserType type = ProviderUserType.ProviderAdmin)
|
||||
{
|
||||
_status = status;
|
||||
_type = type;
|
||||
}
|
||||
|
||||
public override ICustomization GetCustomization(ParameterInfo parameter)
|
||||
{
|
||||
return new ProviderUser(_status, _type);
|
||||
}
|
||||
}
|
||||
}
|
392
test/Core.Test/Services/ProviderServiceTests.cs
Normal file
392
test/Core.Test/Services/ProviderServiceTests.cs
Normal file
@ -0,0 +1,392 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums.Provider;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business.Provider;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AutoFixture;
|
||||
using Bit.Core.Test.AutoFixture.Attributes;
|
||||
using Bit.Core.Test.AutoFixture.ProviderUserFixtures;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using ProviderUser = Bit.Core.Models.Table.Provider.ProviderUser;
|
||||
|
||||
namespace Bit.Core.Test.Services
|
||||
{
|
||||
public class ProviderServiceTests
|
||||
{
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
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, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
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, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task CompleteSetupAsync_UserIdIsInvalid_Throws(SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.CompleteSetupAsync(default, default, default, default));
|
||||
Assert.Contains("Invalid owner.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task CompleteSetupAsync_TokenIsInvalid_Throws(User user, Provider provider,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var userService = sutProvider.GetDependency<IUserService>();
|
||||
userService.GetUserByIdAsync(user.Id).Returns(user);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default));
|
||||
Assert.Contains("Invalid token.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task CompleteSetupAsync_Success(User user, Provider provider,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var userService = sutProvider.GetDependency<IUserService>();
|
||||
userService.GetUserByIdAsync(user.Id).Returns(user);
|
||||
|
||||
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
|
||||
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
||||
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
|
||||
.Returns(protector);
|
||||
sutProvider.Create();
|
||||
|
||||
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||
|
||||
await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, default);
|
||||
|
||||
await sutProvider.GetDependency<IProviderRepository>().Received().UpsertAsync(provider);
|
||||
await sutProvider.GetDependency<IProviderUserRepository>().Received()
|
||||
.CreateAsync(Arg.Is<ProviderUser>(pu => pu.UserId == user.Id && pu.ProviderId == provider.Id));
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task UpdateAsync_ProviderIdIsInvalid_Throws(Provider provider, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
provider.Id = default;
|
||||
|
||||
var exception = await Assert.ThrowsAsync<ApplicationException>(
|
||||
() => sutProvider.Sut.UpdateAsync(provider));
|
||||
Assert.Contains("Cannot create provider this way.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task UpdateAsync_Success(Provider provider, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
await sutProvider.Sut.UpdateAsync(provider);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task InviteUserAsync_ProviderIdIsInvalid_Throws(Provider provider, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
provider.Id = default;
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.InviteUserAsync(provider.Id, default, default));
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task InviteUserAsync_EmailsInvalid_Throws(Provider provider, ProviderUserInvite providerUserInvite,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||
|
||||
providerUserInvite.Emails = null;
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.InviteUserAsync(provider.Id, default, providerUserInvite));
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task InviteUserAsync_AlreadyInvited(Provider provider, ProviderUserInvite providerUserInvite,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetCountByProviderAsync(default, default, default).ReturnsForAnyArgs(1);
|
||||
|
||||
var result = await sutProvider.Sut.InviteUserAsync(provider.Id, default, providerUserInvite);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task InviteUserAsync_Success(Provider provider, ProviderUserInvite providerUserInvite,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetCountByProviderAsync(default, default, default).ReturnsForAnyArgs(0);
|
||||
|
||||
var result = await sutProvider.Sut.InviteUserAsync(provider.Id, default, providerUserInvite);
|
||||
Assert.Equal(providerUserInvite.Emails.Count(), result.Count);
|
||||
Assert.True(result.TrueForAll(pu => pu.Status == ProviderUserStatusType.Invited), "Status must be invited");
|
||||
Assert.True(result.TrueForAll(pu => pu.ProviderId == provider.Id), "Provider Id must be correct");
|
||||
}
|
||||
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task ResendInvitesAsync_Errors(Provider provider,
|
||||
[ProviderUser(ProviderUserStatusType.Invited)]ProviderUser pu1,
|
||||
[ProviderUser(ProviderUserStatusType.Accepted)]ProviderUser pu2,
|
||||
[ProviderUser(ProviderUserStatusType.Confirmed)]ProviderUser pu3,
|
||||
[ProviderUser(ProviderUserStatusType.Invited)]ProviderUser pu4,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var providerUsers = new[] {pu1, pu2, pu3, pu4};
|
||||
pu1.ProviderId = pu2.ProviderId = pu3.ProviderId = provider.Id;
|
||||
|
||||
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetManyAsync(default).ReturnsForAnyArgs(providerUsers.ToList());
|
||||
|
||||
var result = await sutProvider.Sut.ResendInvitesAsync(provider.Id, default, providerUsers.Select(pu => pu.Id));
|
||||
Assert.Equal("", result[0].Item2);
|
||||
Assert.Equal("User invalid.", result[1].Item2);
|
||||
Assert.Equal("User invalid.", result[2].Item2);
|
||||
Assert.Equal("User invalid.", result[3].Item2);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task ResendInvitesAsync_Success(Provider provider, IEnumerable<ProviderUser> providerUsers,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
foreach (var providerUser in providerUsers)
|
||||
{
|
||||
providerUser.ProviderId = provider.Id;
|
||||
providerUser.Status = ProviderUserStatusType.Invited;
|
||||
}
|
||||
|
||||
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetManyAsync(default).ReturnsForAnyArgs(providerUsers.ToList());
|
||||
|
||||
var result = await sutProvider.Sut.ResendInvitesAsync(provider.Id, default, providerUsers.Select(pu => pu.Id));
|
||||
Assert.True(result.All(r => r.Item2 == ""));
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task AcceptUserAsync_UserIsInvalid_Throws(ProviderUser providerUser, User user,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.AcceptUserAsync(default, default, default));
|
||||
Assert.Equal("User invalid.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task AcceptUserAsync_AlreadyAccepted_Throws(
|
||||
[ProviderUser(ProviderUserStatusType.Accepted)]ProviderUser providerUser, User user,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetByIdAsync(providerUser.Id).Returns(providerUser);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.AcceptUserAsync(providerUser.Id, user, default));
|
||||
Assert.Equal("Already accepted.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task AcceptUserAsync_TokenIsInvalid_Throws(
|
||||
[ProviderUser(ProviderUserStatusType.Invited)]ProviderUser providerUser, User user,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetByIdAsync(providerUser.Id).Returns(providerUser);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.AcceptUserAsync(providerUser.Id, user, default));
|
||||
Assert.Equal("Invalid token.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task AcceptUserAsync_WrongEmail_Throws(
|
||||
[ProviderUser(ProviderUserStatusType.Invited)]ProviderUser providerUser, User user,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetByIdAsync(providerUser.Id).Returns(providerUser);
|
||||
|
||||
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
|
||||
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
||||
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
|
||||
.Returns(protector);
|
||||
sutProvider.Create();
|
||||
|
||||
var token = protector.Protect($"ProviderUserInvite {providerUser.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.AcceptUserAsync(providerUser.Id, user, token));
|
||||
Assert.Equal("User email does not match invite.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task AcceptUserAsync_Success(
|
||||
[ProviderUser(ProviderUserStatusType.Invited)]ProviderUser providerUser, User user,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetByIdAsync(providerUser.Id).Returns(providerUser);
|
||||
|
||||
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
|
||||
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
||||
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
|
||||
.Returns(protector);
|
||||
sutProvider.Create();
|
||||
|
||||
providerUser.Email = user.Email;
|
||||
var token = protector.Protect($"ProviderUserInvite {providerUser.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||
|
||||
var pu = await sutProvider.Sut.AcceptUserAsync(providerUser.Id, user, token);
|
||||
Assert.Null(pu.Email);
|
||||
Assert.Equal(ProviderUserStatusType.Accepted, pu.Status);
|
||||
Assert.Equal(user.Id, pu.UserId);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task ConfirmUsersAsync_NoValid(
|
||||
[ProviderUser(ProviderUserStatusType.Invited)]ProviderUser pu1,
|
||||
[ProviderUser(ProviderUserStatusType.Accepted)]ProviderUser pu2,
|
||||
[ProviderUser(ProviderUserStatusType.Confirmed)]ProviderUser pu3,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
pu1.ProviderId = pu3.ProviderId;
|
||||
var providerUsers = new[] {pu1, pu2, pu3};
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetManyAsync(default).ReturnsForAnyArgs(providerUsers);
|
||||
|
||||
var dict = providerUsers.ToDictionary(pu => pu.Id, _ => "key");
|
||||
var result = await sutProvider.Sut.ConfirmUsersAsync(pu1.ProviderId, dict, default);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task ConfirmUsersAsync_Success(
|
||||
[ProviderUser(ProviderUserStatusType.Invited)]ProviderUser pu1, User u1,
|
||||
[ProviderUser(ProviderUserStatusType.Accepted)]ProviderUser pu2, User u2,
|
||||
[ProviderUser(ProviderUserStatusType.Confirmed)]ProviderUser pu3, User u3,
|
||||
Provider provider, User user, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
pu1.ProviderId = pu2.ProviderId = pu3.ProviderId = provider.Id;
|
||||
pu1.UserId = u1.Id;
|
||||
pu2.UserId = u2.Id;
|
||||
pu3.UserId = u3.Id;
|
||||
var providerUsers = new[] {pu1, pu2, pu3};
|
||||
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetManyAsync(default).ReturnsForAnyArgs(providerUsers);
|
||||
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] {u1, u2, u3});
|
||||
|
||||
var dict = providerUsers.ToDictionary(pu => pu.Id, _ => "key");
|
||||
var result = await sutProvider.Sut.ConfirmUsersAsync(pu1.ProviderId, dict, user.Id);
|
||||
|
||||
Assert.Equal("Invalid user.", result[0].Item2);
|
||||
Assert.Equal("", result[1].Item2);
|
||||
Assert.Equal("Invalid user.", result[2].Item2);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task SaveUserAsync_UserIdIsInvalid_Throws(ProviderUser providerUser,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
providerUser.Id = default;
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveUserAsync(providerUser, default));
|
||||
Assert.Equal("Invite the user first.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task SaveUserAsync_Success(
|
||||
[ProviderUser(type: ProviderUserType.ProviderAdmin)]ProviderUser providerUser, User savingUser,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetByIdAsync(providerUser.Id).Returns(providerUser);
|
||||
|
||||
await sutProvider.Sut.SaveUserAsync(providerUser, savingUser.Id);
|
||||
await providerUserRepository.Received().ReplaceAsync(providerUser);
|
||||
await sutProvider.GetDependency<IEventService>().Received()
|
||||
.LogProviderUserEventAsync(providerUser, EventType.ProviderUser_Updated, null);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task DeleteUsersAsync_NoRemainingOwner_Throws(Provider provider, User deletingUser,
|
||||
ICollection<ProviderUser> providerUsers, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var userIds = providerUsers.Select(pu => pu.Id);
|
||||
|
||||
providerUsers.First().UserId = deletingUser.Id;
|
||||
foreach (var providerUser in providerUsers)
|
||||
{
|
||||
providerUser.ProviderId = provider.Id;
|
||||
}
|
||||
providerUsers.Last().ProviderId = default;
|
||||
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetManyAsync(default).ReturnsForAnyArgs(providerUsers);
|
||||
providerUserRepository.GetManyByProviderAsync(default, default).ReturnsForAnyArgs(new ProviderUser[] {});
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.DeleteUsersAsync(provider.Id, userIds, deletingUser.Id));
|
||||
Assert.Equal("Provider must have at least one confirmed ProviderAdmin.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task DeleteUsersAsync_Success(Provider provider, User deletingUser, ICollection<ProviderUser> providerUsers,
|
||||
[ProviderUser(ProviderUserStatusType.Confirmed, ProviderUserType.ProviderAdmin)]ProviderUser remainingOwner,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var userIds = providerUsers.Select(pu => pu.Id);
|
||||
|
||||
providerUsers.First().UserId = deletingUser.Id;
|
||||
foreach (var providerUser in providerUsers)
|
||||
{
|
||||
providerUser.ProviderId = provider.Id;
|
||||
}
|
||||
providerUsers.Last().ProviderId = default;
|
||||
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetManyAsync(default).ReturnsForAnyArgs(providerUsers);
|
||||
providerUserRepository.GetManyByProviderAsync(default, default).ReturnsForAnyArgs(new[] {remainingOwner});
|
||||
|
||||
var result = await sutProvider.Sut.DeleteUsersAsync(provider.Id, userIds, deletingUser.Id);
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
Assert.Equal("You cannot remove yourself.", result[0].Item2);
|
||||
Assert.Equal("", result[1].Item2);
|
||||
Assert.Equal("Invalid user.", result[2].Item2);
|
||||
}
|
||||
}
|
||||
}
|
@ -95,6 +95,12 @@ BEGIN
|
||||
END
|
||||
GO
|
||||
|
||||
ALTER TABLE [dbo].[Provider] ALTER COLUMN [Name] NVARCHAR (50) NULL;
|
||||
GO
|
||||
|
||||
ALTER TABLE [dbo].[Provider] ALTER COLUMN [BillingEmail] NVARCHAR (256) NULL;
|
||||
GO
|
||||
|
||||
IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'ProviderView')
|
||||
BEGIN
|
||||
DROP VIEW [dbo].[ProviderView];
|
||||
@ -454,7 +460,8 @@ END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[ProviderUser_ReadByProviderId]
|
||||
@ProviderId UNIQUEIDENTIFIER
|
||||
@ProviderId UNIQUEIDENTIFIER,
|
||||
@Type TINYINT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@ -465,6 +472,7 @@ BEGIN
|
||||
[dbo].[ProviderUserView]
|
||||
WHERE
|
||||
[ProviderId] = @ProviderId
|
||||
AND [Type] = COALESCE(@Type, [Type])
|
||||
END
|
||||
GO
|
||||
|
||||
@ -797,3 +805,182 @@ BEGIN
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByProviderUserId] @ProviderUserId
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID('[dbo].[ProviderUser_ReadCountByProviderIdEmail]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[ProviderUser_ReadCountByProviderIdEmail]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[ProviderUser_ReadCountByProviderIdEmail]
|
||||
@ProviderId UNIQUEIDENTIFIER,
|
||||
@Email NVARCHAR(256),
|
||||
@OnlyUsers BIT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
COUNT(1)
|
||||
FROM
|
||||
[dbo].[ProviderUser] OU
|
||||
LEFT JOIN
|
||||
[dbo].[User] U ON OU.[UserId] = U.[Id]
|
||||
WHERE
|
||||
OU.[ProviderId] = @ProviderId
|
||||
AND (
|
||||
(@OnlyUsers = 0 AND @Email IN (OU.[Email], U.[Email]))
|
||||
OR (@OnlyUsers = 1 AND U.[Email] = @Email)
|
||||
)
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID('[dbo].[ProviderUser_ReadByIds]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[ProviderUser_ReadByIds]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[ProviderUser_ReadByIds]
|
||||
@Ids AS [dbo].[GuidIdArray] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
IF (SELECT COUNT(1) FROM @Ids) < 1
|
||||
BEGIN
|
||||
RETURN(-1)
|
||||
END
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[ProviderUserView]
|
||||
WHERE
|
||||
[Id] IN (SELECT [Id] FROM @Ids)
|
||||
END
|
||||
GO
|
||||
|
||||
|
||||
IF OBJECT_ID('[dbo].[User_BumpAccountRevisionDateByProviderUserIds]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[User_BumpAccountRevisionDateByProviderUserIds]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[User_BumpAccountRevisionDateByProviderUserIds]
|
||||
@ProviderUserIds [dbo].[GuidIdArray] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE
|
||||
U
|
||||
SET
|
||||
U.[AccountRevisionDate] = GETUTCDATE()
|
||||
FROM
|
||||
@ProviderUserIds OUIDs
|
||||
INNER JOIN
|
||||
[dbo].[ProviderUser] PU ON OUIDs.Id = PU.Id AND PU.[Status] = 2 -- Confirmed
|
||||
INNER JOIN
|
||||
[dbo].[User] U ON PU.UserId = U.Id
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID('[dbo].[ProviderUser_DeleteByIds]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[ProviderUser_DeleteByIds]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[ProviderUser_DeleteByIds]
|
||||
@Ids [dbo].[GuidIdArray] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByProviderUserIds] @Ids
|
||||
|
||||
DECLARE @UserAndProviderIds [dbo].[TwoGuidIdArray]
|
||||
|
||||
INSERT INTO @UserAndProviderIds
|
||||
(Id1, Id2)
|
||||
SELECT
|
||||
UserId,
|
||||
ProviderId
|
||||
FROM
|
||||
[dbo].[ProviderUser] PU
|
||||
INNER JOIN
|
||||
@Ids PUIds ON PUIds.Id = PU.Id
|
||||
WHERE
|
||||
UserId IS NOT NULL AND
|
||||
ProviderId IS NOT NULL
|
||||
|
||||
DECLARE @BatchSize INT = 100
|
||||
|
||||
-- Delete ProviderUsers
|
||||
WHILE @BatchSize > 0
|
||||
BEGIN
|
||||
BEGIN TRANSACTION ProviderUser_DeleteMany_PUs
|
||||
|
||||
DELETE TOP(@BatchSize) OU
|
||||
FROM
|
||||
[dbo].[ProviderUser] PU
|
||||
INNER JOIN
|
||||
@Ids I ON I.Id = PU.Id
|
||||
|
||||
SET @BatchSize = @@ROWCOUNT
|
||||
|
||||
COMMIT TRANSACTION ProviderUser_DeleteMany_PUs
|
||||
END
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID('[dbo].[Provider_Search]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[Provider_Search]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[Provider_Search]
|
||||
@Name NVARCHAR(50),
|
||||
@UserEmail NVARCHAR(256),
|
||||
@Skip INT = 0,
|
||||
@Take INT = 25
|
||||
WITH RECOMPILE
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
DECLARE @NameLikeSearch NVARCHAR(55) = '%' + @Name + '%'
|
||||
|
||||
IF @UserEmail IS NOT NULL
|
||||
BEGIN
|
||||
SELECT
|
||||
O.*
|
||||
FROM
|
||||
[dbo].[ProviderView] O
|
||||
INNER JOIN
|
||||
[dbo].[ProviderUser] OU ON O.[Id] = OU.[ProviderId]
|
||||
INNER JOIN
|
||||
[dbo].[User] U ON U.[Id] = OU.[UserId]
|
||||
WHERE
|
||||
(@Name IS NULL OR O.[Name] LIKE @NameLikeSearch)
|
||||
AND U.[Email] = COALESCE(@UserEmail, U.[Email])
|
||||
ORDER BY O.[CreationDate] DESC
|
||||
OFFSET @Skip ROWS
|
||||
FETCH NEXT @Take ROWS ONLY
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
SELECT
|
||||
O.*
|
||||
FROM
|
||||
[dbo].[ProviderView] O
|
||||
WHERE
|
||||
(@Name IS NULL OR O.[Name] LIKE @NameLikeSearch)
|
||||
ORDER BY O.[CreationDate] DESC
|
||||
OFFSET @Skip ROWS
|
||||
FETCH NEXT @Take ROWS ONLY
|
||||
END
|
||||
END
|
||||
GO
|
Loading…
x
Reference in New Issue
Block a user