-
Notifications
You must be signed in to change notification settings - Fork 0
Cosmos DB for Events #1
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: base-sha/91409a45f0adbaa8a5aee2d81060cd6369656c44
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; } | ||
|
|
||
| [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; } | ||
| } |
| 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") | ||
| { } | ||
|
|
||
| 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"); | ||
| } | ||
|
|
||
| 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 | ||
| 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); | ||
| } | ||
|
|
||
| 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; | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -90,7 +90,7 @@ public static SupportedDatabaseProviders AddDatabaseRepositories(this IServiceCo | |
| } | ||
| else | ||
| { | ||
| services.AddSingleton<IEventRepository, TableStorageRepos.EventRepository>(); | ||
|
Author
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. suggestion: Repository registration now always uses Cosmos implementation. Ensure the DI setup still supports conditional selection as in AzureQueueHostedService. If different parts need varied repository implementations, centralize this logic. Suggested implementation: }
else
{
if(configuration.GetValue<bool>("UseCosmosEventRepository"))
{
services.AddSingleton<IEventRepository, Core.Repositories.Cosmos.EventRepository>();
services.AddKeyedSingleton<IGrantRepository, Core.Auth.Repositories.Cosmos.GrantRepository>("cosmos");
}
else
{
services.AddSingleton<IEventRepository, TableStorageRepos.EventRepository>();
services.AddKeyedSingleton<IGrantRepository, TableStorageRepos.GrantRepository>("tableStorage");
}
services.AddSingleton<IInstallationDeviceRepository, TableStorageRepos.InstallationDeviceRepository>();
}• Ensure that a configuration instance (IConfiguration configuration) is available in this context and passed in to the extension method if needed. |
||
| services.AddSingleton<IEventRepository, Core.Repositories.Cosmos.EventRepository>(); | ||
| services.AddSingleton<IInstallationDeviceRepository, TableStorageRepos.InstallationDeviceRepository>(); | ||
| services.AddKeyedSingleton<IGrantRepository, Core.Auth.Repositories.Cosmos.GrantRepository>("cosmos"); | ||
| } | ||
|
|
||
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.
issue (bug_risk): Placeholder 'TODO' in the GlobalSettings constructor.
Using 'TODO' will cause runtime failures if not replaced. Either remove this constructor until you have a valid connection string or provide a sensible default.