mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 08:02:49 -05:00
502 lines
20 KiB
C#
502 lines
20 KiB
C#
using Bit.Core.Context;
|
|
using Bit.Core.Entities;
|
|
using Bit.Core.Entities.Provider;
|
|
using Bit.Core.Enums;
|
|
using Bit.Core.Enums.Provider;
|
|
using Bit.Core.Exceptions;
|
|
using Bit.Core.Models.Business;
|
|
using Bit.Core.Models.Business.Provider;
|
|
using Bit.Core.Models.Data;
|
|
using Bit.Core.Repositories;
|
|
using Bit.Core.Services;
|
|
using Bit.Core.Settings;
|
|
using Bit.Core.Utilities;
|
|
using Microsoft.AspNetCore.DataProtection;
|
|
|
|
namespace Bit.Commercial.Core.Services;
|
|
|
|
public class ProviderService : IProviderService
|
|
{
|
|
public static PlanType[] ProviderDisallowedOrganizationTypes = new[] { PlanType.Free, PlanType.FamiliesAnnually, PlanType.FamiliesAnnually2019 };
|
|
|
|
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 IProviderOrganizationRepository _providerOrganizationRepository;
|
|
private readonly IOrganizationRepository _organizationRepository;
|
|
private readonly IUserRepository _userRepository;
|
|
private readonly IUserService _userService;
|
|
private readonly IOrganizationService _organizationService;
|
|
private readonly ICurrentContext _currentContext;
|
|
|
|
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
|
|
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
|
|
IUserService userService, IOrganizationService organizationService, IMailService mailService,
|
|
IDataProtectionProvider dataProtectionProvider, IEventService eventService,
|
|
IOrganizationRepository organizationRepository, GlobalSettings globalSettings,
|
|
ICurrentContext currentContext)
|
|
{
|
|
_providerRepository = providerRepository;
|
|
_providerUserRepository = providerUserRepository;
|
|
_providerOrganizationRepository = providerOrganizationRepository;
|
|
_organizationRepository = organizationRepository;
|
|
_userRepository = userRepository;
|
|
_userService = userService;
|
|
_organizationService = organizationService;
|
|
_mailService = mailService;
|
|
_eventService = eventService;
|
|
_globalSettings = globalSettings;
|
|
_dataProtector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
|
_currentContext = currentContext;
|
|
}
|
|
|
|
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key)
|
|
{
|
|
var owner = await _userService.GetUserByIdAsync(ownerUserId);
|
|
if (owner == null)
|
|
{
|
|
throw new BadRequestException("Invalid owner.");
|
|
}
|
|
|
|
if (provider.Status != ProviderStatusType.Pending)
|
|
{
|
|
throw new BadRequestException("Provider is already setup.");
|
|
}
|
|
|
|
if (!CoreHelpers.TokenIsValid("ProviderSetupInvite", _dataProtector, token, owner.Email, provider.Id,
|
|
_globalSettings.OrganizationInviteExpirationHours))
|
|
{
|
|
throw new BadRequestException("Invalid token.");
|
|
}
|
|
|
|
var providerUser = await _providerUserRepository.GetByProviderUserAsync(provider.Id, ownerUserId);
|
|
if (!(providerUser is { Type: ProviderUserType.ProviderAdmin }))
|
|
{
|
|
throw new BadRequestException("Invalid owner.");
|
|
}
|
|
|
|
provider.Status = ProviderStatusType.Created;
|
|
await _providerRepository.UpsertAsync(provider);
|
|
|
|
providerUser.Key = key;
|
|
await _providerUserRepository.ReplaceAsync(providerUser);
|
|
|
|
return provider;
|
|
}
|
|
|
|
public async Task UpdateAsync(Provider provider, bool updateBilling = false)
|
|
{
|
|
if (provider.Id == default)
|
|
{
|
|
throw new ArgumentException("Cannot create provider this way.");
|
|
}
|
|
|
|
await _providerRepository.ReplaceAsync(provider);
|
|
}
|
|
|
|
public async Task<List<ProviderUser>> InviteUserAsync(ProviderUserInvite<string> invite)
|
|
{
|
|
if (!_currentContext.ProviderManageUsers(invite.ProviderId))
|
|
{
|
|
throw new InvalidOperationException("Invalid permissions.");
|
|
}
|
|
|
|
var emails = invite?.UserIdentifiers;
|
|
var invitingUser = await _providerUserRepository.GetByProviderUserAsync(invite.ProviderId, invite.InvitingUserId);
|
|
|
|
var provider = await _providerRepository.GetByIdAsync(invite.ProviderId);
|
|
if (provider == null || emails == null || !emails.Any())
|
|
{
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
var providerUsers = new List<ProviderUser>();
|
|
foreach (var email in emails)
|
|
{
|
|
// Make sure user is not already invited
|
|
var existingProviderUserCount =
|
|
await _providerUserRepository.GetCountByProviderAsync(invite.ProviderId, email, false);
|
|
if (existingProviderUserCount > 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var providerUser = new ProviderUser
|
|
{
|
|
ProviderId = invite.ProviderId,
|
|
UserId = null,
|
|
Email = email.ToLowerInvariant(),
|
|
Key = null,
|
|
Type = invite.Type,
|
|
Status = ProviderUserStatusType.Invited,
|
|
CreationDate = DateTime.UtcNow,
|
|
RevisionDate = DateTime.UtcNow,
|
|
};
|
|
|
|
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(ProviderUserInvite<Guid> invite)
|
|
{
|
|
if (!_currentContext.ProviderManageUsers(invite.ProviderId))
|
|
{
|
|
throw new BadRequestException("Invalid permissions.");
|
|
}
|
|
|
|
var providerUsers = await _providerUserRepository.GetManyAsync(invite.UserIdentifiers);
|
|
var provider = await _providerRepository.GetByIdAsync(invite.ProviderId);
|
|
|
|
var result = new List<Tuple<ProviderUser, string>>();
|
|
foreach (var providerUser in providerUsers)
|
|
{
|
|
if (providerUser.Status != ProviderUserStatusType.Invited || providerUser.ProviderId != invite.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.OrganizationInviteExpirationHours))
|
|
{
|
|
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.SendProviderConfirmedEmailAsync(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 provider = await _providerRepository.GetByIdAsync(providerId);
|
|
|
|
if (provider == null)
|
|
{
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
var providerUsers = await _providerUserRepository.GetManyAsync(providerUserIds);
|
|
var users = await _userRepository.GetManyAsync(providerUsers.Where(pu => pu.UserId.HasValue)
|
|
.Select(pu => pu.UserId.Value));
|
|
var keyedUsers = users.ToDictionary(u => u.Id);
|
|
|
|
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));
|
|
|
|
var user = keyedUsers.GetValueOrDefault(providerUser.UserId.GetValueOrDefault());
|
|
var email = user == null ? providerUser.Email : user.Email;
|
|
if (!string.IsNullOrWhiteSpace(email))
|
|
{
|
|
await _mailService.SendProviderUserRemoved(provider.Name, email);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
public async Task AddOrganization(Guid providerId, Guid organizationId, string key)
|
|
{
|
|
var po = await _providerOrganizationRepository.GetByOrganizationId(organizationId);
|
|
if (po != null)
|
|
{
|
|
throw new BadRequestException("Organization already belongs to a provider.");
|
|
}
|
|
|
|
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
|
ThrowOnInvalidPlanType(organization.PlanType);
|
|
|
|
var providerOrganization = new ProviderOrganization
|
|
{
|
|
ProviderId = providerId,
|
|
OrganizationId = organizationId,
|
|
Key = key,
|
|
};
|
|
|
|
await _providerOrganizationRepository.CreateAsync(providerOrganization);
|
|
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Added);
|
|
}
|
|
|
|
public async Task AddOrganizationsToReseller(Guid providerId, IEnumerable<Guid> organizationIds)
|
|
{
|
|
var provider = await _providerRepository.GetByIdAsync(providerId);
|
|
if (provider.Type != ProviderType.Reseller)
|
|
{
|
|
throw new BadRequestException("Provider must be of type Reseller in order to assign Organizations to it.");
|
|
}
|
|
|
|
var existingProviderOrganizationsCount = await _providerOrganizationRepository.GetCountByOrganizationIdsAsync(organizationIds);
|
|
if (existingProviderOrganizationsCount > 0)
|
|
{
|
|
throw new BadRequestException("Organizations must not be assigned to any Provider.");
|
|
}
|
|
|
|
var providerOrganizationsToInsert = organizationIds.Select(orgId => new ProviderOrganization { ProviderId = providerId, OrganizationId = orgId });
|
|
var insertedProviderOrganizations = await _providerOrganizationRepository.CreateManyAsync(providerOrganizationsToInsert);
|
|
|
|
await _eventService.LogProviderOrganizationEventsAsync(insertedProviderOrganizations.Select(ipo => (ipo, EventType.ProviderOrganization_Added, (DateTime?)null)));
|
|
}
|
|
|
|
public async Task<ProviderOrganization> CreateOrganizationAsync(Guid providerId,
|
|
OrganizationSignup organizationSignup, string clientOwnerEmail, User user)
|
|
{
|
|
ThrowOnInvalidPlanType(organizationSignup.Plan);
|
|
|
|
var (organization, _) = await _organizationService.SignUpAsync(organizationSignup, true);
|
|
|
|
var providerOrganization = new ProviderOrganization
|
|
{
|
|
ProviderId = providerId,
|
|
OrganizationId = organization.Id,
|
|
Key = organizationSignup.OwnerKey,
|
|
};
|
|
|
|
await _providerOrganizationRepository.CreateAsync(providerOrganization);
|
|
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Created);
|
|
|
|
await _organizationService.InviteUsersAsync(organization.Id, user.Id,
|
|
new (OrganizationUserInvite, string)[]
|
|
{
|
|
(
|
|
new OrganizationUserInvite
|
|
{
|
|
Emails = new[] { clientOwnerEmail },
|
|
AccessAll = true,
|
|
Type = OrganizationUserType.Owner,
|
|
Permissions = null,
|
|
Collections = Array.Empty<CollectionAccessSelection>(),
|
|
},
|
|
null
|
|
)
|
|
});
|
|
|
|
return providerOrganization;
|
|
}
|
|
|
|
public async Task RemoveOrganizationAsync(Guid providerId, Guid providerOrganizationId, Guid removingUserId)
|
|
{
|
|
var providerOrganization = await _providerOrganizationRepository.GetByIdAsync(providerOrganizationId);
|
|
if (providerOrganization == null || providerOrganization.ProviderId != providerId)
|
|
{
|
|
throw new BadRequestException("Invalid organization.");
|
|
}
|
|
|
|
if (!await _organizationService.HasConfirmedOwnersExceptAsync(providerOrganization.OrganizationId, new Guid[] { }, includeProvider: false))
|
|
{
|
|
throw new BadRequestException("Organization needs to have at least one confirmed owner.");
|
|
}
|
|
|
|
await _providerOrganizationRepository.DeleteAsync(providerOrganization);
|
|
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);
|
|
}
|
|
|
|
public async Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid ownerId)
|
|
{
|
|
var provider = await _providerRepository.GetByIdAsync(providerId);
|
|
var owner = await _userRepository.GetByIdAsync(ownerId);
|
|
if (owner == null)
|
|
{
|
|
throw new BadRequestException("Invalid owner.");
|
|
}
|
|
await SendProviderSetupInviteEmailAsync(provider, owner.Email);
|
|
}
|
|
|
|
public async Task SendProviderSetupInviteEmailAsync(Provider provider, string ownerEmail)
|
|
{
|
|
var token = _dataProtector.Protect($"ProviderSetupInvite {provider.Id} {ownerEmail} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
|
await _mailService.SendProviderSetupInviteEmailAsync(provider, token, ownerEmail);
|
|
}
|
|
|
|
public async Task LogProviderAccessToOrganizationAsync(Guid organizationId)
|
|
{
|
|
if (organizationId == default)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var providerOrganization = await _providerOrganizationRepository.GetByOrganizationId(organizationId);
|
|
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
|
if (providerOrganization != null)
|
|
{
|
|
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_VaultAccessed);
|
|
}
|
|
if (organization != null)
|
|
{
|
|
await _eventService.LogOrganizationEventAsync(organization, EventType.Organization_VaultAccessed);
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
private void ThrowOnInvalidPlanType(PlanType requestedType)
|
|
{
|
|
if (ProviderDisallowedOrganizationTypes.Contains(requestedType))
|
|
{
|
|
throw new BadRequestException($"Providers cannot manage organizations with the requested plan type ({requestedType}). Only Teams and Enterprise accounts are allowed.");
|
|
}
|
|
}
|
|
}
|