-
Notifications
You must be signed in to change notification settings - Fork 1
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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() {} | ||
|
||
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; } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: DomainName should be nullable (string?) for consistency with other properties |
||
} |
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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
||
|
@@ -54,12 +55,15 @@ public void Dispose() | |
private async Task ExecuteAsync(CancellationToken cancellationToken) | ||
{ | ||
var storageConnectionString = _configuration["azureStorageConnectionString"]; | ||
var cosmosConnectionString = _configuration["cosmosConnectionString"]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"); | ||
|
||
|
There was a problem hiding this comment.
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