1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-02 16:42:50 -05:00

[Provider] Setup provider (#1378)

This commit is contained in:
Oscar Hinton
2021-06-30 09:35:26 +02:00
committed by GitHub
parent 08f508f536
commit 43f7271147
85 changed files with 1810 additions and 113 deletions

View File

@ -9,6 +9,7 @@ namespace Bit.Core.Services
public interface IApplicationCacheService
{
Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync();
Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync();
Task UpsertOrganizationAbilityAsync(Organization organization);
Task DeleteOrganizationAbilityAsync(Guid organizationId);
}

View File

@ -10,7 +10,7 @@ namespace Bit.Core.Services
public interface IProviderService
{
Task CreateAsync(string ownerEmail);
Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key);
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);

View File

@ -223,16 +223,45 @@ namespace Bit.Core.Services
await _eventWriteService.CreateAsync(e);
}
// TODO: Implement this
public Task LogProviderUserEventAsync(ProviderUser providerUser, EventType type, DateTime? date = null) => throw new NotImplementedException();
public async Task LogProviderUserEventAsync(ProviderUser providerUser, EventType type, DateTime? date = null)
{
await LogProviderUsersEventAsync(new[] { (providerUser, type, date) });
}
// TODO: Implement this
public Task LogProviderUsersEventAsync(IEnumerable<(ProviderUser, EventType, DateTime?)> events) => throw new NotImplementedException();
public async Task LogProviderUsersEventAsync(IEnumerable<(ProviderUser, EventType, DateTime?)> events)
{
var providerAbilities = await _applicationCacheService.GetProviderAbilitiesAsync();
var eventMessages = new List<IEvent>();
foreach (var (providerUser, type, date) in events)
{
if (!CanUseProviderEvents(providerAbilities, providerUser.ProviderId))
{
continue;
}
eventMessages.Add(new EventMessage
{
ProviderId = providerUser.ProviderId,
UserId = providerUser.UserId,
ProviderUserId = providerUser.Id,
Type = type,
ActingUserId = _currentContext?.UserId,
Date = date.GetValueOrDefault(DateTime.UtcNow)
});
}
await _eventWriteService.CreateManyAsync(eventMessages);
}
private bool CanUseEvents(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)
{
return orgAbilities != null && orgAbilities.ContainsKey(orgId) &&
orgAbilities[orgId].Enabled && orgAbilities[orgId].UseEvents;
}
private bool CanUseProviderEvents(IDictionary<Guid, ProviderAbility> providerAbilities, Guid providerId)
{
return providerAbilities != null && providerAbilities.ContainsKey(providerId) &&
providerAbilities[providerId].Enabled && providerAbilities[providerId].UseEvents;
}
}
}

View File

@ -675,6 +675,7 @@ namespace Bit.Core.Services
ProviderId = providerUser.ProviderId.ToString(),
ProviderUserId = providerUser.Id.ToString(),
ProviderNameUrlEncoded = WebUtility.UrlEncode(providerName),
Token = token,
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
};

View File

@ -11,14 +11,18 @@ namespace Bit.Core.Services
public class InMemoryApplicationCacheService : IApplicationCacheService
{
private readonly IOrganizationRepository _organizationRepository;
private readonly IProviderRepository _providerRepository;
private DateTime _lastOrgAbilityRefresh = DateTime.MinValue;
private IDictionary<Guid, OrganizationAbility> _orgAbilities;
private TimeSpan _orgAbilitiesRefreshInterval = TimeSpan.FromMinutes(10);
private IDictionary<Guid, ProviderAbility> _providerAbilities;
public InMemoryApplicationCacheService(
IOrganizationRepository organizationRepository)
IOrganizationRepository organizationRepository, IProviderRepository providerRepository)
{
_organizationRepository = organizationRepository;
_providerRepository = providerRepository;
}
public virtual async Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync()
@ -27,6 +31,12 @@ namespace Bit.Core.Services
return _orgAbilities;
}
public virtual async Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync()
{
await InitProviderAbilitiesAsync();
return _providerAbilities;
}
public virtual async Task UpsertOrganizationAbilityAsync(Organization organization)
{
await InitOrganizationAbilitiesAsync();
@ -62,5 +72,16 @@ namespace Bit.Core.Services
_lastOrgAbilityRefresh = now;
}
}
private async Task InitProviderAbilitiesAsync()
{
var now = DateTime.UtcNow;
if (_providerAbilities == null || (now - _lastOrgAbilityRefresh) > _orgAbilitiesRefreshInterval)
{
var abilities = await _providerRepository.GetManyAbilitiesAsync();
_providerAbilities = abilities.ToDictionary(a => a.Id);
_lastOrgAbilityRefresh = now;
}
}
}
}

View File

@ -16,8 +16,9 @@ namespace Bit.Core.Services
public InMemoryServiceBusApplicationCacheService(
IOrganizationRepository organizationRepository,
IProviderRepository providerRepository,
GlobalSettings globalSettings)
: base(organizationRepository)
: base(organizationRepository, providerRepository)
{
_subName = CoreHelpers.GetApplicationCacheServiceBusSubcriptionName(globalSettings);
_topicClient = new TopicClient(globalSettings.ServiceBus.ConnectionString,

View File

@ -1,348 +0,0 @@
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();
}
}
}

View File

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Models.Business.Provider;
using Bit.Core.Models.Table;
using Bit.Core.Models.Table.Provider;
namespace Bit.Core.Services
{
public class NoopProviderService : IProviderService
{
public Task CreateAsync(string ownerEmail) => throw new NotImplementedException();
public Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key) => throw new NotImplementedException();
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<Tuple<ProviderUser, string>>> ResendInvitesAsync(Guid providerId, Guid invitingUserId, IEnumerable<Guid> providerUsersId) => throw new NotImplementedException();
public Task<ProviderUser> AcceptUserAsync(Guid providerUserId, User user, string token) => throw new NotImplementedException();
public Task<List<Tuple<ProviderUser, string>>> ConfirmUsersAsync(Guid providerId, Dictionary<Guid, string> keys, Guid confirmingUserId) => throw new NotImplementedException();
public Task SaveUserAsync(ProviderUser user, Guid savingUserId) => 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 RemoveOrganization(Guid providerOrganizationId, Guid removingUserId) => throw new NotImplementedException();
}
}