1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-25 12:45:18 +01:00

app cache with org ability checks on events

This commit is contained in:
Kyle Spearrin 2017-12-19 16:02:39 -05:00
parent d75ca51d75
commit e9116f8c44
11 changed files with 252 additions and 35 deletions

View File

@ -0,0 +1,21 @@
using System;
using Bit.Core.Models.Table;
namespace Bit.Core.Models.Data
{
public class OrganizationAbility
{
public OrganizationAbility() { }
public OrganizationAbility(Organization organization)
{
Id = organization.Id;
UseEvents = organization.UseEvents;
Enabled = organization.Enabled;
}
public Guid Id { get; set; }
public bool UseEvents { get; set; }
public bool Enabled { get; set; }
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Models.Data;
using Bit.Core.Models.Table;
namespace Bit.Core.Repositories
@ -10,5 +11,6 @@ namespace Bit.Core.Repositories
Task<ICollection<Organization>> GetManyByEnabledAsync();
Task<ICollection<Organization>> GetManyByUserIdAsync(Guid userId);
Task UpdateStorageAsync(Guid id);
Task<ICollection<OrganizationAbility>> GetManyAbilitiesAsync();
}
}

View File

@ -6,6 +6,7 @@ using System.Data;
using Dapper;
using System.Linq;
using System.Collections.Generic;
using Bit.Core.Models.Data;
namespace Bit.Core.Repositories.SqlServer
{
@ -55,5 +56,17 @@ namespace Bit.Core.Repositories.SqlServer
commandTimeout: 180);
}
}
public async Task<ICollection<OrganizationAbility>> GetManyAbilitiesAsync()
{
using(var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<OrganizationAbility>(
"[dbo].[Organization_ReadAbilities]",
commandType: CommandType.StoredProcedure);
return results.ToList();
}
}
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Models.Data;
using Bit.Core.Models.Table;
namespace Bit.Core.Services
{
public interface IApplicationCacheService
{
Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync();
Task UpsertOrganizationAbilityAsync(Organization organization);
Task DeleteOrganizationAbilityAsync(Guid organizationId);
}
}

View File

@ -13,17 +13,20 @@ namespace Bit.Core.Services
{
private readonly IEventWriteService _eventWriteService;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IApplicationCacheService _applicationCacheService;
private readonly CurrentContext _currentContext;
private readonly GlobalSettings _globalSettings;
public EventService(
IEventWriteService eventWriteService,
IOrganizationUserRepository organizationUserRepository,
IApplicationCacheService applicationCacheService,
CurrentContext currentContext,
GlobalSettings globalSettings)
{
_eventWriteService = eventWriteService;
_organizationUserRepository = organizationUserRepository;
_applicationCacheService = applicationCacheService;
_currentContext = currentContext;
_globalSettings = globalSettings;
}
@ -42,22 +45,26 @@ namespace Bit.Core.Services
}
};
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
IEnumerable<IEvent> orgEvents;
if(_currentContext.UserId.HasValue)
{
orgEvents = _currentContext.Organizations.Select(o => new EventMessage(_currentContext)
{
OrganizationId = o.Id,
UserId = userId,
ActingUserId = userId,
Type = type,
Date = DateTime.UtcNow
});
orgEvents = _currentContext.Organizations
.Where(o => CanUseEvents(orgAbilities, o.Id))
.Select(o => new EventMessage(_currentContext)
{
OrganizationId = o.Id,
UserId = userId,
ActingUserId = userId,
Type = type,
Date = DateTime.UtcNow
});
}
else
{
var orgs = await _organizationUserRepository.GetManyByUserAsync(userId);
orgEvents = orgs.Where(o => o.Status == OrganizationUserStatusType.Confirmed)
orgEvents = orgs
.Where(o => o.Status == OrganizationUserStatusType.Confirmed && CanUseEvents(orgAbilities, o.Id))
.Select(o => new EventMessage(_currentContext)
{
OrganizationId = o.OrganizationId,
@ -87,6 +94,15 @@ namespace Bit.Core.Services
return;
}
if(cipher.OrganizationId.HasValue)
{
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
if(!CanUseEvents(orgAbilities, cipher.OrganizationId.Value))
{
return;
}
}
var e = new EventMessage(_currentContext)
{
OrganizationId = cipher.OrganizationId,
@ -101,6 +117,12 @@ namespace Bit.Core.Services
public async Task LogCollectionEventAsync(Collection collection, EventType type)
{
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
if(!CanUseEvents(orgAbilities, collection.OrganizationId))
{
return;
}
var e = new EventMessage(_currentContext)
{
OrganizationId = collection.OrganizationId,
@ -114,6 +136,12 @@ namespace Bit.Core.Services
public async Task LogGroupEventAsync(Group group, EventType type)
{
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
if(!CanUseEvents(orgAbilities, group.OrganizationId))
{
return;
}
var e = new EventMessage(_currentContext)
{
OrganizationId = group.OrganizationId,
@ -127,6 +155,12 @@ namespace Bit.Core.Services
public async Task LogOrganizationUserEventAsync(OrganizationUser organizationUser, EventType type)
{
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
if(!CanUseEvents(orgAbilities, organizationUser.OrganizationId))
{
return;
}
var e = new EventMessage(_currentContext)
{
OrganizationId = organizationUser.OrganizationId,
@ -141,6 +175,11 @@ namespace Bit.Core.Services
public async Task LogOrganizationEventAsync(Organization organization, EventType type)
{
if(!organization.Enabled || !organization.UseEvents)
{
return;
}
var e = new EventMessage(_currentContext)
{
OrganizationId = organization.Id,
@ -150,5 +189,11 @@ namespace Bit.Core.Services
};
await _eventWriteService.CreateAsync(e);
}
private bool CanUseEvents(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)
{
return orgAbilities != null && orgAbilities.ContainsKey(orgId) &&
orgAbilities[orgId].Enabled && orgAbilities[orgId].UseEvents;
}
}
}

View File

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.Core.Models.Data;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
namespace Bit.Core.Services
{
public class InMemoryApplicationCacheService : IApplicationCacheService
{
private readonly IOrganizationRepository _organizationRepository;
private DateTime _lastOrgAbilityRefresh = DateTime.MinValue;
private IDictionary<Guid, OrganizationAbility> _orgAbilities;
private TimeSpan _orgAbilitiesRefreshInterval = TimeSpan.FromMinutes(10);
public InMemoryApplicationCacheService(
IOrganizationRepository organizationRepository)
{
_organizationRepository = organizationRepository;
}
public async Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync()
{
await InitOrganizationAbilitiesAsync();
return _orgAbilities;
}
public async Task UpsertOrganizationAbilityAsync(Organization organization)
{
await InitOrganizationAbilitiesAsync();
var newAbility = new OrganizationAbility(organization);
if(_orgAbilities.ContainsKey(organization.Id))
{
_orgAbilities[organization.Id] = newAbility;
}
else
{
_orgAbilities.Add(organization.Id, newAbility);
}
}
public Task DeleteOrganizationAbilityAsync(Guid organizationId)
{
if(_orgAbilities != null && _orgAbilities.ContainsKey(organizationId))
{
_orgAbilities.Remove(organizationId);
}
return Task.FromResult(0);
}
private async Task InitOrganizationAbilitiesAsync()
{
var now = DateTime.UtcNow;
if(_orgAbilities == null || (now - _lastOrgAbilityRefresh) > _orgAbilitiesRefreshInterval)
{
var abilities = await _organizationRepository.GetManyAbilitiesAsync();
_orgAbilities = abilities.ToDictionary(a => a.Id);
_lastOrgAbilityRefresh = now;
}
}
}
}

View File

@ -31,6 +31,7 @@ namespace Bit.Core.Services
private readonly ILicensingService _licensingService;
private readonly IEventService _eventService;
private readonly IInstallationRepository _installationRepository;
private readonly IApplicationCacheService _applicationCacheService;
private readonly StripePaymentService _stripePaymentService;
private readonly GlobalSettings _globalSettings;
@ -48,6 +49,7 @@ namespace Bit.Core.Services
ILicensingService licensingService,
IEventService eventService,
IInstallationRepository installationRepository,
IApplicationCacheService applicationCacheService,
GlobalSettings globalSettings)
{
_organizationRepository = organizationRepository;
@ -63,13 +65,14 @@ namespace Bit.Core.Services
_licensingService = licensingService;
_eventService = eventService;
_installationRepository = installationRepository;
_applicationCacheService = applicationCacheService;
_stripePaymentService = new StripePaymentService();
_globalSettings = globalSettings;
}
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken)
{
var organization = await _organizationRepository.GetByIdAsync(organizationId);
var organization = await GetOrgById(organizationId);
if(organization == null)
{
throw new NotFoundException();
@ -78,13 +81,13 @@ namespace Bit.Core.Services
var updated = await _stripePaymentService.UpdatePaymentMethodAsync(organization, paymentToken);
if(updated)
{
await _organizationRepository.ReplaceAsync(organization);
await ReplaceAndUpdateCache(organization);
}
}
public async Task CancelSubscriptionAsync(Guid organizationId, bool endOfPeriod = false)
{
var organization = await _organizationRepository.GetByIdAsync(organizationId);
var organization = await GetOrgById(organizationId);
if(organization == null)
{
throw new NotFoundException();
@ -95,7 +98,7 @@ namespace Bit.Core.Services
public async Task ReinstateSubscriptionAsync(Guid organizationId)
{
var organization = await _organizationRepository.GetByIdAsync(organizationId);
var organization = await GetOrgById(organizationId);
if(organization == null)
{
throw new NotFoundException();
@ -106,7 +109,7 @@ namespace Bit.Core.Services
public async Task UpgradePlanAsync(Guid organizationId, PlanType plan, int additionalSeats)
{
var organization = await _organizationRepository.GetByIdAsync(organizationId);
var organization = await GetOrgById(organizationId);
if(organization == null)
{
throw new NotFoundException();
@ -243,7 +246,7 @@ namespace Bit.Core.Services
public async Task AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb)
{
var organization = await _organizationRepository.GetByIdAsync(organizationId);
var organization = await GetOrgById(organizationId);
if(organization == null)
{
throw new NotFoundException();
@ -262,12 +265,12 @@ namespace Bit.Core.Services
await BillingHelpers.AdjustStorageAsync(_stripePaymentService, organization, storageAdjustmentGb,
plan.StripStoragePlanId);
await _organizationRepository.ReplaceAsync(organization);
await ReplaceAndUpdateCache(organization);
}
public async Task AdjustSeatsAsync(Guid organizationId, int seatAdjustment)
{
var organization = await _organizationRepository.GetByIdAsync(organizationId);
var organization = await GetOrgById(organizationId);
if(organization == null)
{
throw new NotFoundException();
@ -361,12 +364,12 @@ namespace Bit.Core.Services
}
organization.Seats = (short?)newSeatTotal;
await _organizationRepository.ReplaceAsync(organization);
await ReplaceAndUpdateCache(organization);
}
public async Task VerifyBankAsync(Guid organizationId, int amount1, int amount2)
{
var organization = await _organizationRepository.GetByIdAsync(organizationId);
var organization = await GetOrgById(organizationId);
if(organization == null)
{
throw new NotFoundException();
@ -622,6 +625,7 @@ namespace Bit.Core.Services
try
{
await _organizationRepository.CreateAsync(organization);
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
var orgUser = new OrganizationUser
{
@ -666,6 +670,7 @@ namespace Bit.Core.Services
if(organization.Id != default(Guid))
{
await _organizationRepository.DeleteAsync(organization);
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
}
throw;
@ -674,7 +679,7 @@ namespace Bit.Core.Services
public async Task UpdateLicenseAsync(Guid organizationId, OrganizationLicense license)
{
var organization = await _organizationRepository.GetByIdAsync(organizationId);
var organization = await GetOrgById(organizationId);
if(organization == null)
{
throw new NotFoundException();
@ -753,7 +758,7 @@ namespace Bit.Core.Services
organization.ExpirationDate = license.Expires;
organization.LicenseKey = license.LicenseKey;
organization.RevisionDate = DateTime.UtcNow;
await _organizationRepository.ReplaceAsync(organization);
await ReplaceAndUpdateCache(organization);
}
public async Task DeleteAsync(Organization organization)
@ -764,17 +769,18 @@ namespace Bit.Core.Services
}
await _organizationRepository.DeleteAsync(organization);
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
}
public async Task DisableAsync(Guid organizationId, DateTime? expirationDate)
{
var org = await _organizationRepository.GetByIdAsync(organizationId);
var org = await GetOrgById(organizationId);
if(org != null && org.Enabled)
{
org.Enabled = false;
org.ExpirationDate = expirationDate;
org.RevisionDate = DateTime.UtcNow;
await _organizationRepository.ReplaceAsync(org);
await ReplaceAndUpdateCache(org);
// TODO: send email to owners?
}
@ -782,22 +788,22 @@ namespace Bit.Core.Services
public async Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate)
{
var org = await _organizationRepository.GetByIdAsync(organizationId);
var org = await GetOrgById(organizationId);
if(org != null)
{
org.ExpirationDate = expirationDate;
org.RevisionDate = DateTime.UtcNow;
await _organizationRepository.ReplaceAsync(org);
await ReplaceAndUpdateCache(org);
}
}
public async Task EnableAsync(Guid organizationId)
{
var org = await _organizationRepository.GetByIdAsync(organizationId);
var org = await GetOrgById(organizationId);
if(org != null && !org.Enabled)
{
org.Enabled = true;
await _organizationRepository.ReplaceAsync(org);
await ReplaceAndUpdateCache(org);
}
}
@ -808,8 +814,7 @@ namespace Bit.Core.Services
throw new ApplicationException("Cannot create org this way. Call SignUpAsync.");
}
await _organizationRepository.ReplaceAsync(organization);
await _eventService.LogOrganizationEventAsync(organization, EventType.Organization_Updated);
await ReplaceAndUpdateCache(organization, EventType.Organization_Updated);
if(updateBilling && !string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
{
@ -834,7 +839,7 @@ namespace Bit.Core.Services
IEnumerable<string> emails, OrganizationUserType type, bool accessAll, string externalId,
IEnumerable<SelectionReadOnly> collections)
{
var organization = await _organizationRepository.GetByIdAsync(organizationId);
var organization = await GetOrgById(organizationId);
if(organization == null)
{
throw new NotFoundException();
@ -915,7 +920,7 @@ namespace Bit.Core.Services
private async Task SendInviteAsync(OrganizationUser orgUser)
{
var org = await _organizationRepository.GetByIdAsync(orgUser.OrganizationId);
var org = await GetOrgById(orgUser.OrganizationId);
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
var token = _dataProtector.Protect(
$"OrganizationUserInvite {orgUser.Id} {orgUser.Email} {nowMillis}");
@ -937,7 +942,7 @@ namespace Bit.Core.Services
if(orgUser.Type == OrganizationUserType.Owner || orgUser.Type == OrganizationUserType.Admin)
{
var org = await _organizationRepository.GetByIdAsync(orgUser.OrganizationId);
var org = await GetOrgById(orgUser.OrganizationId);
if(org.PlanType == PlanType.Free)
{
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(user.Id);
@ -999,7 +1004,7 @@ namespace Bit.Core.Services
throw new BadRequestException("User not valid.");
}
var org = await _organizationRepository.GetByIdAsync(organizationId);
var org = await GetOrgById(organizationId);
if(org.PlanType == PlanType.Free &&
(orgUser.Type == OrganizationUserType.Admin || orgUser.Type == OrganizationUserType.Owner))
{
@ -1140,7 +1145,7 @@ namespace Bit.Core.Services
public async Task<OrganizationLicense> GenerateLicenseAsync(Guid organizationId, Guid installationId)
{
var organization = await _organizationRepository.GetByIdAsync(organizationId);
var organization = await GetOrgById(organizationId);
return await GenerateLicenseAsync(organization, installationId);
}
@ -1168,7 +1173,7 @@ namespace Bit.Core.Services
IEnumerable<ImportedOrganizationUser> newUsers,
IEnumerable<string> removeUserExternalIds)
{
var organization = await _organizationRepository.GetByIdAsync(organizationId);
var organization = await GetOrgById(organizationId);
if(organization == null)
{
throw new NotFoundException();
@ -1340,5 +1345,21 @@ namespace Bit.Core.Services
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
return devices.Where(d => !string.IsNullOrWhiteSpace(d.PushToken)).Select(d => d.Id.ToString());
}
private async Task ReplaceAndUpdateCache(Organization org, EventType? orgEvent = null)
{
await _organizationRepository.ReplaceAsync(org);
await _applicationCacheService.UpsertOrganizationAbilityAsync(org);
if(orgEvent.HasValue)
{
await _eventService.LogOrganizationEventAsync(org, orgEvent.Value);
}
}
private async Task<Organization> GetOrgById(Guid id)
{
return await _organizationRepository.GetByIdAsync(id);
}
}
}

View File

@ -67,6 +67,7 @@ namespace Bit.Core.Utilities
{
services.AddSingleton<IMailService, BackupMailService>();
services.AddSingleton<ILicensingService, LicensingService>();
services.AddSingleton<IApplicationCacheService, InMemoryApplicationCacheService>();
if(CoreHelpers.SettingHasValue(globalSettings.Mail.SendGridApiKey))
{

View File

@ -223,5 +223,6 @@
<Build Include="dbo\Stored Procedures\Event_ReadPageByOrganizationId.sql" />
<Build Include="dbo\Stored Procedures\Event_ReadPageByCipherId.sql" />
<Build Include="dbo\Stored Procedures\Event_ReadPageByOrganizationIdActingUserId.sql" />
<Build Include="dbo\Stored Procedures\Organization_ReadAbilities.sql" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,12 @@
CREATE PROCEDURE [dbo].[Organization_ReadAbilities]
AS
BEGIN
SET NOCOUNT ON
SELECT
[Id],
[UseEvents],
[Enabled]
FROM
[dbo].[Organization]
END

View File

@ -202,6 +202,26 @@ BEGIN
END
GO
IF OBJECT_ID('[dbo].[Organization_ReadAbilities]') IS NULL
BEGIN
EXEC('CREATE PROCEDURE [dbo].[Organization_ReadAbilities] AS BEGIN SET NOCOUNT ON; END')
END
GO
ALTER PROCEDURE [dbo].[Organization_ReadAbilities]
AS
BEGIN
SET NOCOUNT ON
SELECT
[Id],
[UseEvents],
[Enabled]
FROM
[dbo].[Organization]
END
GO
IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'OrganizationView')
BEGIN
DROP VIEW [dbo].[OrganizationView]