1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-01 16:12:49 -05:00

[AC-431] Add new organization invite process (#2737)

* [EC-435] Created _OrganizationForm partial view. Added actions for creating an Organization assigned to a provider

* [EC-435] Remove logic for creating an organization

* [EC-435] Created partial view _OrganizationFormScripts

* [EC-435] Remove unused ReferenceEventType

* [EC-435] Added TODO comment on Organization Create

* [EC-435] Checking if Provider type is Reseller on creating new assigned organization

* [EC-435] Setting the Organization plan type as TeamsMonthly by default when adding to a provider

* [EC-435] Removing unused buttons

* [EC-435] Switched hidden fields to form submit route value

* [EC-435] Moved _OrganizationForm and _OrganizationFormScripts to Shared folder

* [EC-435] Moved Create organization actions from OrganizationsController to ProvidersController

* [AC-431] Added new ReferenceEventType OrganizationCreatedByAdmin

* [AC-431] Added method IOrganizationService.CreateOrganization

* [AC-431] Creating new Organization with Pending status and assigning to Provider

* [AC-431] Added method to IMailService to send invitation to initialize org

* [AC-431] Added methods CreatePendingOrganization and InitPendingOrganization to IOrganizationService

* [AC-431] Org invite includes initOrganization parameter

* [AC-431] Modified existing Accept organization user action to initialize org

* [AC-431] Updated ProvidersController method name

* [AC-431] Created OrganizationUserInitInvitedViewModel to link to 'accept-init-organization' url

* [AC-431] Added action AcceptInit to OrganizationUsersController

* [AC-431] Resend owner invite

* [AC-431] dotnet format

* [AC-431] Removed unused parameter 'addingUserId' from IProviderService.AddOrganization

* [AC-431] Removed setting manual values for CreationDate and RevisionDate

* [AC-431] Updated OrganizationService.InitPendingOrganization to throw exceptions when the Organization does not meet the required criteria

* [AC-431] Modified OrganizationUserInitInvitedViewModel to inherit properties from OrganizationUserInvitedViewModel

* [AC-431] Removed unecessary parameter check

* [AC-431] Moved method description to IOrganizationService.InitPendingOrganization

* [AC-431] Moved ApplicationCacheService.UpsertOrganizationAbilityAsync and ReferenceEventService.RaiseEventAsync to OrganizationService

* [AC-431] Creating collection after creating organization

* [EC-435] Fixing bug on saving Organization that would have BillingEmail as null

* [AC-431] Deleted OrganizationUserInitInvitedViewModel and added parameter InitOrganization to OrganizationUserInvitedViewModel.cs

* [AC-431] Checking if the user has any existing SingleOrg policies before initializing an Org

* [AC-431] Remove commented code

* [EC-435] Added null check to Provider

* [EC-435] Moved trial buttons script logic to Edit view

* [AC-431] Added EncryptedString attribute to OrganizationUserAcceptInitRequestModel.CollectionName

* [AC-431] Refactored plan check condition

* [AC-431] Remove duplicate _applicationCacheService.UpsertOrganizationAbilityAsync call

* [AC-431] Removed IMailService.SendOrganizationInitInviteEmailAsync

* [AC-431] Added parameters ClaimsPrincipal and IUserService to IOrganizationService.CreatePendingOrganization
This commit is contained in:
Rui Tomé
2023-03-30 11:37:41 +01:00
committed by GitHub
parent 7887690c09
commit 570d239da9
19 changed files with 227 additions and 38 deletions

View File

@ -343,7 +343,7 @@ public class ProviderService : IProviderService
return result; return result;
} }
public async Task AddOrganization(Guid providerId, Guid organizationId, Guid addingUserId, string key) public async Task AddOrganization(Guid providerId, Guid organizationId, string key)
{ {
var po = await _providerOrganizationRepository.GetByOrganizationId(organizationId); var po = await _providerOrganizationRepository.GetByOrganizationId(organizationId);
if (po != null) if (po != null)

View File

@ -418,7 +418,7 @@ public class ProviderServiceTests
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task AddOrganization_OrganizationAlreadyBelongsToAProvider_Throws(Provider provider, public async Task AddOrganization_OrganizationAlreadyBelongsToAProvider_Throws(Provider provider,
Organization organization, ProviderOrganization po, User user, string key, Organization organization, ProviderOrganization po, string key,
SutProvider<ProviderService> sutProvider) SutProvider<ProviderService> sutProvider)
{ {
po.OrganizationId = organization.Id; po.OrganizationId = organization.Id;
@ -427,12 +427,12 @@ public class ProviderServiceTests
.Returns(po); .Returns(po);
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.AddOrganization(provider.Id, organization.Id, user.Id, key)); () => sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key));
Assert.Equal("Organization already belongs to a provider.", exception.Message); Assert.Equal("Organization already belongs to a provider.", exception.Message);
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task AddOrganization_Success(Provider provider, Organization organization, User user, string key, public async Task AddOrganization_Success(Provider provider, Organization organization, string key,
SutProvider<ProviderService> sutProvider) SutProvider<ProviderService> sutProvider)
{ {
organization.PlanType = PlanType.EnterpriseAnnually; organization.PlanType = PlanType.EnterpriseAnnually;
@ -442,7 +442,7 @@ public class ProviderServiceTests
providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull(); providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull();
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, user.Id, key); await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default); await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default);
await sutProvider.GetDependency<IEventService>() await sutProvider.GetDependency<IEventService>()

View File

@ -17,6 +17,7 @@ namespace Bit.Admin.Controllers;
[Authorize] [Authorize]
public class OrganizationsController : Controller public class OrganizationsController : Controller
{ {
private readonly IOrganizationService _organizationService;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationConnectionRepository _organizationConnectionRepository; private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
@ -35,6 +36,7 @@ public class OrganizationsController : Controller
private readonly ILogger<OrganizationsController> _logger; private readonly ILogger<OrganizationsController> _logger;
public OrganizationsController( public OrganizationsController(
IOrganizationService organizationService,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IOrganizationConnectionRepository organizationConnectionRepository, IOrganizationConnectionRepository organizationConnectionRepository,
@ -52,6 +54,7 @@ public class OrganizationsController : Controller
IProviderRepository providerRepository, IProviderRepository providerRepository,
ILogger<OrganizationsController> logger) ILogger<OrganizationsController> logger)
{ {
_organizationService = organizationService;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_organizationConnectionRepository = organizationConnectionRepository; _organizationConnectionRepository = organizationConnectionRepository;
@ -219,4 +222,21 @@ public class OrganizationsController : Controller
return RedirectToAction("Index"); return RedirectToAction("Index");
} }
[HttpPost]
public async Task<IActionResult> ResendOwnerInvite(Guid id)
{
var organization = await _organizationRepository.GetByIdAsync(id);
if (organization == null)
{
return RedirectToAction("Index");
}
var organizationUsers = await _organizationUserRepository.GetManyByOrganizationAsync(id, OrganizationUserType.Owner);
foreach (var organizationUser in organizationUsers)
{
await _organizationService.ResendInviteAsync(id, null, organizationUser.Id, true);
}
return Json(null);
}
} }

View File

@ -16,31 +16,40 @@ namespace Bit.Admin.Controllers;
public class ProvidersController : Controller public class ProvidersController : Controller
{ {
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationService _organizationService;
private readonly IProviderRepository _providerRepository; private readonly IProviderRepository _providerRepository;
private readonly IProviderUserRepository _providerUserRepository; private readonly IProviderUserRepository _providerUserRepository;
private readonly IProviderOrganizationRepository _providerOrganizationRepository; private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly IApplicationCacheService _applicationCacheService; private readonly IApplicationCacheService _applicationCacheService;
private readonly IProviderService _providerService; private readonly IProviderService _providerService;
private readonly IReferenceEventService _referenceEventService;
private readonly IUserService _userService;
private readonly ICreateProviderCommand _createProviderCommand; private readonly ICreateProviderCommand _createProviderCommand;
public ProvidersController( public ProvidersController(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationService organizationService,
IProviderRepository providerRepository, IProviderRepository providerRepository,
IProviderUserRepository providerUserRepository, IProviderUserRepository providerUserRepository,
IProviderOrganizationRepository providerOrganizationRepository, IProviderOrganizationRepository providerOrganizationRepository,
IProviderService providerService, IProviderService providerService,
GlobalSettings globalSettings, GlobalSettings globalSettings,
IApplicationCacheService applicationCacheService, IApplicationCacheService applicationCacheService,
IReferenceEventService referenceEventService,
IUserService userService,
ICreateProviderCommand createProviderCommand) ICreateProviderCommand createProviderCommand)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationService = organizationService;
_providerRepository = providerRepository; _providerRepository = providerRepository;
_providerUserRepository = providerUserRepository; _providerUserRepository = providerUserRepository;
_providerOrganizationRepository = providerOrganizationRepository; _providerOrganizationRepository = providerOrganizationRepository;
_providerService = providerService; _providerService = providerService;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_applicationCacheService = applicationCacheService; _applicationCacheService = applicationCacheService;
_referenceEventService = referenceEventService;
_userService = userService;
_createProviderCommand = createProviderCommand; _createProviderCommand = createProviderCommand;
} }
@ -211,7 +220,15 @@ public class ProvidersController : Controller
[HttpPost] [HttpPost]
public async Task<IActionResult> CreateOrganization(Guid providerId, OrganizationEditModel model) public async Task<IActionResult> CreateOrganization(Guid providerId, OrganizationEditModel model)
{ {
// TODO : Insert logic to create the new Organization entry, create an OrganizationUser entry for the owner and send the invitation email var provider = await _providerRepository.GetByIdAsync(providerId);
if (provider is not { Type: ProviderType.Reseller })
{
return RedirectToAction("Index");
}
var organization = model.CreateOrganization(provider);
await _organizationService.CreatePendingOrganization(organization, model.Owners, User, _userService, model.SalesAssistedTrialStarted);
await _providerService.AddOrganization(providerId, organization.Id, null);
return RedirectToAction("Edit", "Providers", new { id = providerId }); return RedirectToAction("Edit", "Providers", new { id = providerId });
} }

View File

@ -135,6 +135,13 @@ public class OrganizationEditModel : OrganizationViewModel
public DateTime? ExpirationDate { get; set; } public DateTime? ExpirationDate { get; set; }
public bool SalesAssistedTrialStarted { get; set; } public bool SalesAssistedTrialStarted { get; set; }
public Organization CreateOrganization(Provider provider)
{
BillingEmail = provider.BillingEmail;
return ToOrganization(new Organization());
}
public Organization ToOrganization(Organization existingOrganization) public Organization ToOrganization(Organization existingOrganization)
{ {
existingOrganization.Name = Name; existingOrganization.Name = Name;

View File

@ -1,4 +1,7 @@
@model ProviderViewModel @model ProviderViewModel
@await Html.PartialAsync("_ProviderScripts")
<h2>Provider Organizations</h2> <h2>Provider Organizations</h2>
<div class="row"> <div class="row">
<div class="col-sm"> <div class="col-sm">
@ -41,7 +44,7 @@
<div class="float-right"> <div class="float-right">
@if (org.Status == OrganizationStatusType.Pending) @if (org.Status == OrganizationStatusType.Pending)
{ {
<a asp-controller="Organizations" asp-action="ResendInvite" asp-route-id="@org.Id" class="float-right"> <a href="#" class="float-right" onclick="return resendOwnerInvite('@org.OrganizationId', '@org.OrganizationName');">
<i class="fa fa-envelope-o fa-lg" title="Resend Setup Invite"></i> <i class="fa fa-envelope-o fa-lg" title="Resend Setup Invite"></i>
</a> </a>
} }

View File

@ -0,0 +1,20 @@
<script>
function resendOwnerInvite(orgId, orgName) {
if (confirm('Resend invite to "' + orgName + '"?')) {
$.ajax({
type: "POST",
url: '@Url.Action("ResendOwnerInvite", "Organizations")' + '?id=' + orgId,
dataType: 'json',
contentType: false,
processData: false,
success: function (response) {
alert('Invitation has been resent!');
},
error: function (response) {
alert("Error!");
}
});
}
return false;
}
</script>

View File

@ -177,6 +177,20 @@ public class OrganizationUsersController : Controller
await _organizationService.ResendInviteAsync(orgGuidId, userId.Value, new Guid(id)); await _organizationService.ResendInviteAsync(orgGuidId, userId.Value, new Guid(id));
} }
[HttpPost("{organizationUserId}/accept-init")]
public async Task AcceptInit(Guid orgId, Guid organizationUserId, [FromBody] OrganizationUserAcceptInitRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
await _organizationService.InitPendingOrganization(user.Id, orgId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName);
await _organizationService.AcceptUserAsync(organizationUserId, user, model.Token, _userService);
await _organizationService.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id, _userService);
}
[HttpPost("{organizationUserId}/accept")] [HttpPost("{organizationUserId}/accept")]
public async Task Accept(Guid orgId, Guid organizationUserId, [FromBody] OrganizationUserAcceptRequestModel model) public async Task Accept(Guid orgId, Guid organizationUserId, [FromBody] OrganizationUserAcceptRequestModel model)
{ {
@ -188,11 +202,9 @@ public class OrganizationUsersController : Controller
var masterPasswordPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword); var masterPasswordPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
var useMasterPasswordPolicy = masterPasswordPolicy != null && var useMasterPasswordPolicy = masterPasswordPolicy != null &&
masterPasswordPolicy.Enabled && masterPasswordPolicy.Enabled &&
masterPasswordPolicy.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled; masterPasswordPolicy.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled;
if (useMasterPasswordPolicy && string.IsNullOrWhiteSpace(model.ResetPasswordKey))
if (useMasterPasswordPolicy &&
string.IsNullOrWhiteSpace(model.ResetPasswordKey))
{ {
throw new BadRequestException(string.Empty, "Master Password reset is required, but not provided."); throw new BadRequestException(string.Empty, "Master Password reset is required, but not provided.");
} }

View File

@ -54,9 +54,7 @@ public class ProviderOrganizationsController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
var userId = _userService.GetProperUserId(User).Value; await _providerService.AddOrganization(providerId, model.OrganizationId, model.Key);
await _providerService.AddOrganization(providerId, model.OrganizationId, userId, model.Key);
} }
[HttpPost("")] [HttpPost("")]

View File

@ -37,6 +37,19 @@ public class OrganizationUserInviteRequestModel
} }
} }
public class OrganizationUserAcceptInitRequestModel
{
[Required]
public string Token { get; set; }
[Required]
public string Key { get; set; }
[Required]
public OrganizationKeysRequestModel Keys { get; set; }
[EncryptedString]
[EncryptedStringLength(1000)]
public string CollectionName { get; set; }
}
public class OrganizationUserAcceptRequestModel public class OrganizationUserAcceptRequestModel
{ {
[Required] [Required]

View File

@ -39,5 +39,7 @@ public enum ReferenceEventType
[EnumMember(Value = "collection-created")] [EnumMember(Value = "collection-created")]
CollectionCreated, CollectionCreated,
[EnumMember(Value = "organization-edited-by-admin")] [EnumMember(Value = "organization-edited-by-admin")]
OrganizationEditedByAdmin OrganizationEditedByAdmin,
[EnumMember(Value = "organization-created-by-admin")]
OrganizationCreatedByAdmin
} }

View File

@ -9,12 +9,14 @@ public class OrganizationUserInvitedViewModel : BaseMailModel
public string OrganizationNameUrlEncoded { get; set; } public string OrganizationNameUrlEncoded { get; set; }
public string Token { get; set; } public string Token { get; set; }
public string ExpirationDate { get; set; } public string ExpirationDate { get; set; }
public bool InitOrganization { get; set; }
public string Url => string.Format("{0}/accept-organization?organizationId={1}&" + public string Url => string.Format("{0}/accept-organization?organizationId={1}&" +
"organizationUserId={2}&email={3}&organizationName={4}&token={5}", "organizationUserId={2}&email={3}&organizationName={4}&token={5}&initOrganization={6}",
WebVaultUrl, WebVaultUrl,
OrganizationId, OrganizationId,
OrganizationUserId, OrganizationUserId,
Email, Email,
OrganizationNameUrlEncoded, OrganizationNameUrlEncoded,
Token); Token,
InitOrganization);
} }

View File

@ -15,8 +15,8 @@ public interface IMailService
Task SendTwoFactorEmailAsync(string email, string token); Task SendTwoFactorEmailAsync(string email, string token);
Task SendNoMasterPasswordHintEmailAsync(string email); Task SendNoMasterPasswordHintEmailAsync(string email);
Task SendMasterPasswordHintEmailAsync(string email, string hint); Task SendMasterPasswordHintEmailAsync(string email, string hint);
Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token); Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token, bool initOrganization = false);
Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites); Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites, bool initOrganization = false);
Task SendOrganizationMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails); Task SendOrganizationMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
Task SendOrganizationAutoscaledEmailAsync(Organization organization, int initialSeatCount, IEnumerable<string> ownerEmails); Task SendOrganizationAutoscaledEmailAsync(Organization organization, int initialSeatCount, IEnumerable<string> ownerEmails);
Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable<string> adminEmails); Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable<string> adminEmails);

View File

@ -1,4 +1,5 @@
using Bit.Core.Entities; using System.Security.Claims;
using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
@ -37,9 +38,8 @@ public interface IOrganizationService
Task<OrganizationUser> InviteUserAsync(Guid organizationId, EventSystemUser systemUser, string email, Task<OrganizationUser> InviteUserAsync(Guid organizationId, EventSystemUser systemUser, string email,
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections, IEnumerable<Guid> groups); OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections, IEnumerable<Guid> groups);
Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId); Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId);
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId); Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false);
Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token, Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token, IUserService userService);
IUserService userService);
Task<OrganizationUser> AcceptUserAsync(string orgIdentifier, User user, IUserService userService); Task<OrganizationUser> AcceptUserAsync(string orgIdentifier, User user, IUserService userService);
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
Guid confirmingUserId, IUserService userService); Guid confirmingUserId, IUserService userService);
@ -69,5 +69,13 @@ public interface IOrganizationService
Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser, IUserService userService); Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser, IUserService userService);
Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId, Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService); IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService);
Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted);
/// <summary>
/// Update an Organization entry by setting the public/private keys, set it as 'Enabled' and move the Status from 'Pending' to 'Created'.
/// </summary>
/// <remarks>
/// This method must target a disabled Organization that has null keys and status as 'Pending'.
/// </remarks>
Task InitPendingOrganization(Guid userId, Guid organizationId, string publicKey, string privateKey, string collectionName);
Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null); Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null);
} }

View File

@ -19,7 +19,7 @@ public interface IProviderService
Task<List<Tuple<ProviderUser, string>>> DeleteUsersAsync(Guid providerId, IEnumerable<Guid> providerUserIds, Task<List<Tuple<ProviderUser, string>>> DeleteUsersAsync(Guid providerId, IEnumerable<Guid> providerUserIds,
Guid deletingUserId); Guid deletingUserId);
Task AddOrganization(Guid providerId, Guid organizationId, Guid addingUserId, string key); Task AddOrganization(Guid providerId, Guid organizationId, string key);
Task AddOrganizationsToReseller(Guid providerId, IEnumerable<Guid> organizationIds); Task AddOrganizationsToReseller(Guid providerId, IEnumerable<Guid> organizationIds);
Task<ProviderOrganization> CreateOrganizationAsync(Guid providerId, OrganizationSignup organizationSignup, Task<ProviderOrganization> CreateOrganizationAsync(Guid providerId, OrganizationSignup organizationSignup,
string clientOwnerEmail, User user); string clientOwnerEmail, User user);

View File

@ -200,10 +200,10 @@ public class HandlebarsMailService : IMailService
await _mailDeliveryService.SendEmailAsync(message); await _mailDeliveryService.SendEmailAsync(message);
} }
public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token) => public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token, bool initOrganization = false) =>
BulkSendOrganizationInviteEmailAsync(organizationName, new[] { (orgUser, token) }); BulkSendOrganizationInviteEmailAsync(organizationName, new[] { (orgUser, token) }, initOrganization);
public async Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites) public async Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites, bool initOrganization = false)
{ {
MailQueueMessage CreateMessage(string email, object model) MailQueueMessage CreateMessage(string email, object model)
{ {
@ -223,6 +223,7 @@ public class HandlebarsMailService : IMailService
OrganizationNameUrlEncoded = WebUtility.UrlEncode(organizationName), OrganizationNameUrlEncoded = WebUtility.UrlEncode(organizationName),
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName, SiteName = _globalSettings.SiteName,
InitOrganization = initOrganization
} }
)); ));

View File

@ -1,4 +1,5 @@
using System.Text.Json; using System.Security.Claims;
using System.Text.Json;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -598,7 +599,7 @@ public class OrganizationService : IOrganizationService
bool provider = false) bool provider = false)
{ {
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == signup.Plan); var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == signup.Plan);
if (!(plan is { LegacyYear: null })) if (plan is not { LegacyYear: null })
{ {
throw new BadRequestException("Invalid plan selected."); throw new BadRequestException("Invalid plan selected.");
} }
@ -1169,14 +1170,14 @@ public class OrganizationService : IOrganizationService
continue; continue;
} }
await SendInviteAsync(orgUser, org); await SendInviteAsync(orgUser, org, false);
result.Add(Tuple.Create(orgUser, "")); result.Add(Tuple.Create(orgUser, ""));
} }
return result; return result;
} }
public async Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId) public async Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false)
{ {
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
if (orgUser == null || orgUser.OrganizationId != organizationId || if (orgUser == null || orgUser.OrganizationId != organizationId ||
@ -1186,7 +1187,7 @@ public class OrganizationService : IOrganizationService
} }
var org = await GetOrgById(orgUser.OrganizationId); var org = await GetOrgById(orgUser.OrganizationId);
await SendInviteAsync(orgUser, org); await SendInviteAsync(orgUser, org, initOrganization);
} }
private async Task SendInvitesAsync(IEnumerable<OrganizationUser> orgUsers, Organization organization) private async Task SendInvitesAsync(IEnumerable<OrganizationUser> orgUsers, Organization organization)
@ -1198,14 +1199,14 @@ public class OrganizationService : IOrganizationService
orgUsers.Select(o => (o, new ExpiringToken(MakeToken(o), DateTime.UtcNow.AddDays(5))))); orgUsers.Select(o => (o, new ExpiringToken(MakeToken(o), DateTime.UtcNow.AddDays(5)))));
} }
private async Task SendInviteAsync(OrganizationUser orgUser, Organization organization) private async Task SendInviteAsync(OrganizationUser orgUser, Organization organization, bool initOrganization)
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var nowMillis = CoreHelpers.ToEpocMilliseconds(now); var nowMillis = CoreHelpers.ToEpocMilliseconds(now);
var token = _dataProtector.Protect( var token = _dataProtector.Protect(
$"OrganizationUserInvite {orgUser.Id} {orgUser.Email} {nowMillis}"); $"OrganizationUserInvite {orgUser.Id} {orgUser.Email} {nowMillis}");
await _mailService.SendOrganizationInviteEmailAsync(organization.Name, orgUser, new ExpiringToken(token, now.AddDays(5))); await _mailService.SendOrganizationInviteEmailAsync(organization.Name, orgUser, new ExpiringToken(token, now.AddDays(5)), initOrganization);
} }
public async Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token, public async Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token,
@ -2428,4 +2429,89 @@ public class OrganizationService : IOrganizationService
return status; return status;
} }
public async Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted)
{
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
if (plan is not { LegacyYear: null })
{
throw new BadRequestException("Invalid plan selected.");
}
if (plan.Disabled)
{
throw new BadRequestException("Plan not found.");
}
organization.Id = CoreHelpers.GenerateComb();
organization.Enabled = false;
organization.Status = OrganizationStatusType.Pending;
await SignUpAsync(organization, default, null, null, true);
var ownerOrganizationUser = new OrganizationUser
{
OrganizationId = organization.Id,
UserId = null,
Email = ownerEmail,
Key = null,
Type = OrganizationUserType.Owner,
Status = OrganizationUserStatusType.Invited,
AccessAll = true
};
await _organizationUserRepository.CreateAsync(ownerOrganizationUser);
await SendInviteAsync(ownerOrganizationUser, organization, true);
await _eventService.LogOrganizationUserEventAsync(ownerOrganizationUser, EventType.OrganizationUser_Invited);
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.OrganizationCreatedByAdmin, organization)
{
EventRaisedByUser = userService.GetUserName(user),
SalesAssistedTrialStarted = salesAssistedTrialStarted,
});
}
public async Task InitPendingOrganization(Guid userId, Guid organizationId, string publicKey, string privateKey, string collectionName)
{
await ValidateSignUpPoliciesAsync(userId);
var org = await GetOrgById(organizationId);
if (org.Enabled)
{
throw new BadRequestException("Organization is already enabled.");
}
if (org.Status != OrganizationStatusType.Pending)
{
throw new BadRequestException("Organization is not on a Pending status.");
}
if (!string.IsNullOrEmpty(org.PublicKey))
{
throw new BadRequestException("Organization already has a Public Key.");
}
if (!string.IsNullOrEmpty(org.PrivateKey))
{
throw new BadRequestException("Organization already has a Private Key.");
}
org.Enabled = true;
org.Status = OrganizationStatusType.Created;
org.PublicKey = publicKey;
org.PrivateKey = privateKey;
await UpdateAsync(org);
if (!string.IsNullOrWhiteSpace(collectionName))
{
var defaultCollection = new Collection
{
Name = collectionName,
OrganizationId = org.Id
};
await _collectionRepository.CreateAsync(defaultCollection);
}
}
} }

View File

@ -52,12 +52,12 @@ public class NoopMailService : IMailService
return Task.FromResult(0); return Task.FromResult(0);
} }
public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token) public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token, bool initOrganization = false)
{ {
return Task.FromResult(0); return Task.FromResult(0);
} }
public Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites) public Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites, bool initOrganization = false)
{ {
return Task.FromResult(0); return Task.FromResult(0);
} }

View File

@ -23,7 +23,7 @@ public class NoopProviderService : IProviderService
public Task<List<Tuple<ProviderUser, string>>> DeleteUsersAsync(Guid providerId, IEnumerable<Guid> providerUserIds, Guid deletingUserId) => throw new NotImplementedException(); public Task<List<Tuple<ProviderUser, string>>> DeleteUsersAsync(Guid providerId, IEnumerable<Guid> providerUserIds, Guid deletingUserId) => throw new NotImplementedException();
public Task AddOrganization(Guid providerId, Guid organizationId, Guid addingUserId, string key) => throw new NotImplementedException(); public Task AddOrganization(Guid providerId, Guid organizationId, string key) => throw new NotImplementedException();
public Task AddOrganizationsToReseller(Guid providerId, IEnumerable<Guid> organizationIds) => throw new NotImplementedException(); public Task AddOrganizationsToReseller(Guid providerId, IEnumerable<Guid> organizationIds) => throw new NotImplementedException();