1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 07:36:14 -05:00

Allow Resending Provider Setup Emails From The Admin Portal (#1497)

* Added a button for resending provider setup emails

* Fixed a case typo in a stored procedure

* Turned a couple lines of code into a method call

* Added service level validation against inviting users for MSP invites

* Code review improvements for provider invites

created a factory for provider user invites

wrote tests for provider invite permissions"

* changed a few exception types
This commit is contained in:
Addison Beck
2021-08-05 10:39:05 -04:00
committed by GitHub
parent cfc7fa071b
commit 152f1f7a9b
14 changed files with 210 additions and 73 deletions

View File

@ -133,5 +133,12 @@ namespace Bit.Admin.Controllers
return RedirectToAction("Index");
}
public async Task<IActionResult> ResendInvite(Guid ownerId, Guid providerId)
{
await _providerService.ResendProviderSetupInviteEmailAsync(providerId, ownerId);
TempData["InviteResentTo"] = ownerId;
return RedirectToAction("Edit", new { id = providerId });
}
}
}

View File

@ -14,17 +14,11 @@ namespace Bit.Admin.Models
{
Provider = provider;
UserCount = providerUsers.Count();
ProviderAdmins = string.Join(", ",
providerUsers
.Where(u => u.Type == ProviderUserType.ProviderAdmin && u.Status == ProviderUserStatusType.Confirmed)
.Select(u => u.Email));
ProviderAdmins = providerUsers.Where(u => u.Type == ProviderUserType.ProviderAdmin);
}
public int UserCount { get; set; }
public Provider Provider { get; set; }
public string ProviderAdmins { get; set; }
public IEnumerable<ProviderUserUserDetails> ProviderAdmins { get; set; }
}
}

View File

@ -0,0 +1,58 @@
@model ProviderViewModel
<h2>Provider Admins</h2>
<div class="row">
<div class="col-8">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th style="width: 190px;">Email</th>
<th style="width: 40px;">Status</th>
<th style="width: 30px;"></th>
</tr>
</thead>
<tbody>
@if(!Model.ProviderAdmins.Any())
{
<tr>
<td colspan="6">No results to list.</td>
</tr>
}
else
{
@foreach(var admin in Model.ProviderAdmins)
{
<tr>
<td class="align-middle">
@admin.Email
</td>
<td class="align-middle">
@admin.Status
</td>
<td>
@if(admin.Status.Equals(ProviderUserStatusType.Confirmed)
&& @Model.Provider.Status.Equals(ProviderStatusType.Pending))
{
@if(@TempData["InviteResentTo"] != null && @TempData["InviteResentTo"].ToString() == @admin.UserId.Value.ToString())
{
<button class="btn btn-outline-success btn-sm disabled" disabled>Invite Resent!</button>
}
else
{
<a class="btn btn-outline-secondary btn-sm"
data-id="@admin.Id" asp-controller="Providers"
asp-action="ResendInvite" asp-route-ownerId="@admin.UserId"
asp-route-providerId="@Model.Provider.Id">
Resend Setup Invite
</a>
}
}
</td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
</div>

View File

@ -7,6 +7,7 @@
<h2>Provider Information</h2>
@await Html.PartialAsync("_ViewInformation", Model)
@await Html.PartialAsync("Admins", Model)
<form method="post" id="edit-form">
<h2>General</h2>
<div class="row">

View File

@ -7,6 +7,7 @@
<h2>Information</h2>
@await Html.PartialAsync("_ViewInformation", Model)
@await Html.PartialAsync("Admins", 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>

View File

@ -9,9 +9,6 @@
<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>

View File

@ -1,5 +1,6 @@
@using Microsoft.AspNetCore.Identity
@using Bit.Admin
@using Bit.Admin.Models
@using Bit.Core.Enums.Provider
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper "*, Admin"
@addTagHelper "*, Admin"

View File

@ -67,8 +67,9 @@ namespace Bit.Api.Controllers
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User);
await _providerService.InviteUserAsync(providerId, userId.Value, new ProviderUserInvite(model));
var invite = ProviderUserInviteFactory.CreateIntialInvite(model.Emails, model.Type.Value,
_userService.GetProperUserId(User).Value, providerId);
await _providerService.InviteUserAsync(invite);
}
[HttpPost("reinvite")]
@ -79,8 +80,8 @@ namespace Bit.Api.Controllers
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User);
var result = await _providerService.ResendInvitesAsync(providerId, userId.Value, model.Ids);
var invite = ProviderUserInviteFactory.CreateReinvite(model.Ids, _userService.GetProperUserId(User).Value, providerId);
var result = await _providerService.ResendInvitesAsync(invite);
return new ListResponseModel<ProviderUserBulkResponseModel>(
result.Select(t => new ProviderUserBulkResponseModel(t.Item1.Id, t.Item2)));
}
@ -93,8 +94,9 @@ namespace Bit.Api.Controllers
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User);
await _providerService.ResendInvitesAsync(providerId, userId.Value, new [] { id });
var invite = ProviderUserInviteFactory.CreateReinvite(new [] { id },
_userService.GetProperUserId(User).Value, providerId);
await _providerService.ResendInvitesAsync(invite);
}
[HttpPost("{id:guid}/accept")]

View File

@ -1,19 +1,39 @@
using System;
using System.Collections.Generic;
using Bit.Core.Enums.Provider;
using Bit.Core.Models.Api;
using Bit.Core.Models.Data;
namespace Bit.Core.Models.Business.Provider
{
public class ProviderUserInvite
public class ProviderUserInvite<T>
{
public IEnumerable<string> Emails { get; set; }
public IEnumerable<T> UserIdentifiers { get; set; }
public ProviderUserType Type { get; set; }
public Guid InvitingUserId { get; set; }
public Guid ProviderId { get; set; }
}
public ProviderUserInvite(ProviderUserInviteRequestModel requestModel)
public static class ProviderUserInviteFactory
{
public static ProviderUserInvite<string> CreateIntialInvite(IEnumerable<string> inviteeEmails, ProviderUserType type, Guid invitingUserId, Guid providerId)
{
Emails = requestModel.Emails;
Type = requestModel.Type.Value;
return new ProviderUserInvite<string>
{
UserIdentifiers = inviteeEmails,
Type = type,
InvitingUserId = invitingUserId,
ProviderId = providerId
};
}
public static ProviderUserInvite<Guid> CreateReinvite(IEnumerable<Guid> inviteeUserIds, Guid invitingUserId, Guid providerId)
{
return new ProviderUserInvite<Guid>
{
UserIdentifiers = inviteeUserIds,
InvitingUserId = invitingUserId,
ProviderId = providerId
};
}
}
}

View File

@ -14,9 +14,8 @@ namespace Bit.Core.Services
Task<Provider> 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<List<ProviderUser>> InviteUserAsync(ProviderUserInvite<string> invite);
Task<List<Tuple<ProviderUser, string>>> ResendInvitesAsync(ProviderUserInvite<Guid> invite);
Task<ProviderUser> AcceptUserAsync(Guid providerUserId, User user, string token);
Task<List<Tuple<ProviderUser, string>>> ConfirmUsersAsync(Guid providerId, Dictionary<Guid, string> keys, Guid confirmingUserId);
@ -29,5 +28,7 @@ namespace Bit.Core.Services
string clientOwnerEmail, User user);
Task RemoveOrganizationAsync(Guid providerId, Guid providerOrganizationId, Guid removingUserId);
Task LogProviderAccessToOrganizationAsync(Guid organizationId);
Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid ownerId);
}
}

View File

@ -16,9 +16,9 @@ namespace Bit.Core.Services
public Task UpdateAsync(Provider provider, bool updateBilling = false) => throw new NotImplementedException();
public Task<List<ProviderUser>> InviteUserAsync(Guid providerId, Guid invitingUserId, ProviderUserInvite providerUserInvite) => throw new NotImplementedException();
public Task<List<ProviderUser>> InviteUserAsync(ProviderUserInvite<string> invite) => throw new NotImplementedException();
public Task<List<Tuple<ProviderUser, string>>> ResendInvitesAsync(Guid providerId, Guid invitingUserId, IEnumerable<Guid> providerUsersId) => throw new NotImplementedException();
public Task<List<Tuple<ProviderUser, string>>> ResendInvitesAsync(ProviderUserInvite<Guid> invite) => throw new NotImplementedException();
public Task<ProviderUser> AcceptUserAsync(Guid providerUserId, User user, string token) => throw new NotImplementedException();
@ -29,9 +29,13 @@ namespace Bit.Core.Services
public Task<List<Tuple<ProviderUser, string>>> DeleteUsersAsync(Guid providerId, IEnumerable<Guid> providerUserIds, Guid deletingUserId) => throw new NotImplementedException();
public Task AddOrganization(Guid providerId, Guid organizationId, Guid addingUserId, string key) => throw new NotImplementedException();
public Task<ProviderOrganization> CreateOrganizationAsync(Guid providerId, OrganizationSignup organizationSignup, string clientOwnerEmail, User user) => throw new NotImplementedException();
public Task RemoveOrganizationAsync(Guid providerId, Guid providerOrganizationId, Guid removingUserId) => throw new NotImplementedException();
public Task LogProviderAccessToOrganizationAsync(Guid organizationId) => throw new NotImplementedException();
public Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid userId) => throw new NotImplementedException();
}
}

View File

@ -16,7 +16,7 @@ BEGIN
[Date] >= @StartDate
AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate)
AND (@BeforeDate IS NULL OR [Date] < @BeforeDate)
AND [Providerid] = @ProviderId
AND [ProviderId] = @ProviderId
ORDER BY [Date] DESC
OFFSET 0 ROWS
FETCH NEXT @PageSize ROWS ONLY