Skip to content

Cosmos DB for Events #61

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions src/Core/Models/Data/EventItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System.Text.Json.Serialization;
using Bit.Core.Enums;

namespace Bit.Core.Models.Data;

public class EventItem : IEvent
{
public EventItem() {}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider removing the empty constructor if it's not explicitly needed


public EventItem(IEvent e)
{
Id = Guid.NewGuid().ToString();
Date = e.Date;
Type = e.Type;
UserId = e.UserId;
OrganizationId = e.OrganizationId;
InstallationId = e.InstallationId;
ProviderId = e.ProviderId;
CipherId = e.CipherId;
CollectionId = e.CollectionId;
PolicyId = e.PolicyId;
GroupId = e.GroupId;
OrganizationUserId = e.OrganizationUserId;
ProviderUserId = e.ProviderUserId;
ProviderOrganizationId = e.ProviderOrganizationId;
DeviceType = e.DeviceType;
IpAddress = e.IpAddress;
ActingUserId = e.ActingUserId;
SystemUser = e.SystemUser;
DomainName = e.DomainName;
SecretId = e.SecretId;
ServiceAccountId = e.ServiceAccountId;
}

[JsonPropertyName("id")]
public string Id { get; set; }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider making Id property init-only to prevent modification after creation


[JsonPropertyName("type")]
public EventType Type { get; set; }
[JsonPropertyName("ip")]
public string IpAddress { get; set; }
[JsonPropertyName("date")]
public DateTime Date { get; set; }
[JsonPropertyName("device")]
public DeviceType? DeviceType { get; set; }
[JsonPropertyName("sUser")]
public EventSystemUser? SystemUser { get; set; }
[JsonPropertyName("uId")]
public Guid? UserId { get; set; }
[JsonPropertyName("oId")]
public Guid? OrganizationId { get; set; }
[JsonPropertyName("inId")]
public Guid? InstallationId { get; set; }
[JsonPropertyName("prId")]
public Guid? ProviderId { get; set; }
[JsonPropertyName("cipId")]
public Guid? CipherId { get; set; }
[JsonPropertyName("colId")]
public Guid? CollectionId { get; set; }
[JsonPropertyName("grpId")]
public Guid? GroupId { get; set; }
[JsonPropertyName("polId")]
public Guid? PolicyId { get; set; }
[JsonPropertyName("ouId")]
public Guid? OrganizationUserId { get; set; }
[JsonPropertyName("pruId")]
public Guid? ProviderUserId { get; set; }
[JsonPropertyName("proId")]
public Guid? ProviderOrganizationId { get; set; }
[JsonPropertyName("auId")]
public Guid? ActingUserId { get; set; }
[JsonPropertyName("secId")]
public Guid? SecretId { get; set; }
[JsonPropertyName("saId")]
public Guid? ServiceAccountId { get; set; }
[JsonPropertyName("domain")]
public string DomainName { get; set; }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: DomainName should be nullable (string?) for consistency with other properties

}
152 changes: 152 additions & 0 deletions src/Core/Repositories/Cosmos/EventRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Bit.Core.Models.Data;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.Core.Vault.Entities;
using Microsoft.Azure.Cosmos;

namespace Bit.Core.Repositories.Cosmos;

public class EventRepository : IEventRepository
{
private readonly CosmosClient _client;
private readonly Database _database;
private readonly Container _container;

public EventRepository(GlobalSettings globalSettings)
: this("TODO")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Replace 'TODO' with actual connection string retrieval from globalSettings

{ }

public EventRepository(string cosmosConnectionString)
{
var options = new CosmosClientOptions
{
Serializer = new SystemTextJsonCosmosSerializer(new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false
}),
// ref: https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/tutorial-dotnet-bulk-import
AllowBulkExecution = true
};
// TODO: Perhaps we want to evaluate moving this to DI as a keyed service singleton in .NET 8
_client = new CosmosClient(cosmosConnectionString, options);
// TODO: Better naming here? Seems odd
_database = _client.GetDatabase("events");
_container = _database.GetContainer("events");
Comment on lines +36 to +37
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider using configuration for database and container names instead of hardcoding

}

public Task<PagedResult<IEvent>> GetManyByCipherAsync(Cipher cipher,
DateTime startDate, DateTime endDate, PageOptions pageOptions)
{
return PagedQueryAsync("e.cipId = @cipId",
q => q.WithParameter("@cipId", cipher.Id),
startDate, endDate, pageOptions);
}

public Task<PagedResult<IEvent>> GetManyByOrganizationActingUserAsync(Guid organizationId, Guid actingUserId,
DateTime startDate, DateTime endDate, PageOptions pageOptions)
{
return PagedQueryAsync("e.oId = @oId AND e.auId = @auId",
q => q.WithParameter("@oId", organizationId).WithParameter("@auId", actingUserId),
startDate, endDate, pageOptions);
}

public Task<PagedResult<IEvent>> GetManyByOrganizationAsync(Guid organizationId,
DateTime startDate, DateTime endDate, PageOptions pageOptions)
{
return PagedQueryAsync("e.oId = @oId",
q => q.WithParameter("@oId", organizationId),
startDate, endDate, pageOptions);
}

public Task<PagedResult<IEvent>> GetManyByOrganizationServiceAccountAsync(Guid organizationId, Guid serviceAccountId,
DateTime startDate, DateTime endDate, PageOptions pageOptions)
{
return PagedQueryAsync("e.oid = @oid AND e.saId = @saId",
q => q.WithParameter("@oid", organizationId).WithParameter("@saId", serviceAccountId),
startDate, endDate, pageOptions);
}

public Task<PagedResult<IEvent>> GetManyByProviderActingUserAsync(Guid providerId, Guid actingUserId,
DateTime startDate, DateTime endDate, PageOptions pageOptions)
{
return PagedQueryAsync("e.prId = @prId AND e.auId = @auId",
q => q.WithParameter("@prId", providerId).WithParameter("@auId", actingUserId),
startDate, endDate, pageOptions);
}

public Task<PagedResult<IEvent>> GetManyByProviderAsync(Guid providerId,
DateTime startDate, DateTime endDate, PageOptions pageOptions)
{
return PagedQueryAsync("e.prId = @prId",
q => q.WithParameter("@prId", providerId),
startDate, endDate, pageOptions);
}

public Task<PagedResult<IEvent>> GetManyByUserAsync(Guid userId,
DateTime startDate, DateTime endDate, PageOptions pageOptions)
{
return PagedQueryAsync("e.uId = @uId",
q => q.WithParameter("@uId", userId),
startDate, endDate, pageOptions);
}

public async Task CreateAsync(IEvent e)
{
if (e is not EventItem item)
{
item = new EventItem(e);
}
// TODO: How should we handle the partition yet? Perhaps something like table storage did with
// orgId, userId, providerId
Comment on lines +102 to +103
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Implement a proper partitioning strategy before production use

await _container.CreateItemAsync(item, new PartitionKey(item.Id), new ItemRequestOptions
{
// ref: https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/best-practice-dotnet#best-practices-for-write-heavy-workloads
EnableContentResponseOnWrite = false
});
}

public Task CreateManyAsync(IEnumerable<IEvent> events)
{
// ref: https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/tutorial-dotnet-bulk-import
var tasks = new List<Task>();
foreach (var e in events)
{
tasks.Add(CreateAsync(e));
}
return Task.WhenAll(tasks);
}
Comment on lines +111 to +120
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider using Cosmos DB bulk operations for better performance when creating multiple events


private async Task<PagedResult<IEvent>> PagedQueryAsync(string queryFilter,
Action<QueryDefinition> applyParameters, DateTime startDate, DateTime endDate,
PageOptions pageOptions)
{
var query = new QueryDefinition(
$"SELECT * FROM events e WHERE {queryFilter} AND e.date >= @startDate AND e.date <= @endDate")
.WithParameter("@startDate", startDate)
.WithParameter("@endDate", endDate);

applyParameters(query);

using var iterator = _container.GetItemQueryIterator<EventItem>(query, pageOptions.ContinuationToken,
new QueryRequestOptions
{
MaxItemCount = pageOptions.PageSize,
});

var result = new PagedResult<IEvent>();
while (iterator.HasMoreResults)
{
var response = await iterator.ReadNextAsync();
result.Data.AddRange(response);
if (response.Count > 0)
{
result.ContinuationToken = response.ContinuationToken;
break;
}
}
Comment on lines +139 to +149
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: This pagination implementation may not handle large result sets efficiently. Consider implementing server-side pagination

return result;
}
}
6 changes: 5 additions & 1 deletion src/EventsProcessor/AzureQueueHostedService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Azure.Storage.Queues;
using Bit.Core;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;

Expand Down Expand Up @@ -54,12 +55,15 @@ public void Dispose()
private async Task ExecuteAsync(CancellationToken cancellationToken)
{
var storageConnectionString = _configuration["azureStorageConnectionString"];
var cosmosConnectionString = _configuration["cosmosConnectionString"];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider using a more descriptive configuration key, such as 'cosmosDbConnectionString' for clarity

if (string.IsNullOrWhiteSpace(storageConnectionString))
{
return;
}

var repo = new Core.Repositories.TableStorage.EventRepository(storageConnectionString);
IEventRepository repo = string.IsNullOrWhiteSpace(cosmosConnectionString) ?
new Core.Repositories.TableStorage.EventRepository(storageConnectionString) :
new Core.Repositories.Cosmos.EventRepository(cosmosConnectionString);
Comment on lines +64 to +66
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: This logic might be better placed in a factory method or dependency injection setup

_eventWriteService = new RepositoryEventWriteService(repo);
_queueClient = new QueueClient(storageConnectionString, "event");

Expand Down
2 changes: 1 addition & 1 deletion src/SharedWeb/Utilities/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public static SupportedDatabaseProviders AddDatabaseRepositories(this IServiceCo
}
else
{
services.AddSingleton<IEventRepository, TableStorageRepos.EventRepository>();
services.AddSingleton<IEventRepository, Core.Repositories.Cosmos.EventRepository>();
services.AddSingleton<IInstallationDeviceRepository, TableStorageRepos.InstallationDeviceRepository>();
services.AddKeyedSingleton<IGrantRepository, Core.Auth.Repositories.Cosmos.GrantRepository>("cosmos");
}
Expand Down
Loading