diff --git a/bitwarden-server.slnx b/bitwarden-server.slnx index 04f7c6461766..c5003fbca99a 100644 --- a/bitwarden-server.slnx +++ b/bitwarden-server.slnx @@ -51,6 +51,7 @@ + diff --git a/bitwarden_license/src/Services/Pam/Api/Endpoints/Handlers/AccessRuleEndpointsHandler.cs b/bitwarden_license/src/Services/Pam/Api/Endpoints/Handlers/AccessRuleEndpointsHandler.cs index 2a7a558ce15a..948d5a155ca7 100644 --- a/bitwarden_license/src/Services/Pam/Api/Endpoints/Handlers/AccessRuleEndpointsHandler.cs +++ b/bitwarden_license/src/Services/Pam/Api/Endpoints/Handlers/AccessRuleEndpointsHandler.cs @@ -1,6 +1,10 @@ -using Bit.HttpExtensions; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.HttpExtensions; +using Bit.Pam.Repositories; using Bit.Services.Pam.Api.Models.Request; using Bit.Services.Pam.Api.Models.Response; +using Bit.Services.Pam.OrganizationFeatures.Commands.Interfaces; namespace Bit.Services.Pam.Api.Endpoints.Handlers; @@ -9,24 +13,53 @@ namespace Bit.Services.Pam.Api.Endpoints.Handlers; /// AccessRuleEndpoints) resolve this handler from DI. /// /// -/// Scaffold only: the method signatures define the wire contract (request/response models, status codes) that the -/// generated OpenAPI spec and client bindings are built from. The bodies are intentionally unimplemented — the -/// behavior lands with the rest of the PAM feature. +/// Access to the organization is already settled by the time a handler runs — AccessRuleEndpoints authorizes +/// the group and the write endpoints through the standard authorization middleware. What is left here is resource +/// scoping: confirming a rule reached by ID actually belongs to the organization on the route. /// -public class AccessRuleEndpointsHandler +public class AccessRuleEndpointsHandler( + ICurrentContext currentContext, + IAccessRuleRepository repository, + ICreateAccessRuleCommand createCommand, + IUpdateAccessRuleCommand updateCommand, + IDeleteAccessRuleCommand deleteCommand) { - public Task> GetAll(Guid orgId) - => throw new NotImplementedException(); + public async Task> GetAll(Guid orgId) + { + var rules = await repository.GetManyDetailsByOrganizationIdAsync(orgId); + return new ListResponseModel( + rules.Select(rule => new AccessRuleResponseModel(rule))); + } - public Task Get(Guid orgId, Guid id) - => throw new NotImplementedException(); + public async Task Get(Guid orgId, Guid id) + { + var rule = await repository.GetDetailsByIdAsync(id); + if (rule is null || rule.OrganizationId != orgId) + { + throw new NotFoundException(); + } - public Task Post(Guid orgId, AccessRuleRequestModel model) - => throw new NotImplementedException(); + return new AccessRuleResponseModel(rule); + } - public Task Put(Guid orgId, Guid id, AccessRuleRequestModel model) - => throw new NotImplementedException(); + public async Task Post(Guid orgId, AccessRuleRequestModel model) + { + var toCreate = model.ToAccessRule(orgId); + toCreate.LastEditedBy = currentContext.UserId; + var rule = await createCommand.CreateAsync(toCreate, model.Collections); + return new AccessRuleResponseModel(rule); + } - public Task Delete(Guid orgId, Guid id) - => throw new NotImplementedException(); + public async Task Put(Guid orgId, Guid id, AccessRuleRequestModel model) + { + var toUpdate = model.ToAccessRule(orgId); + toUpdate.LastEditedBy = currentContext.UserId; + var rule = await updateCommand.UpdateAsync(orgId, id, toUpdate, model.Collections); + return new AccessRuleResponseModel(rule); + } + + public async Task Delete(Guid orgId, Guid id) + { + await deleteCommand.DeleteAsync(orgId, id); + } } diff --git a/bitwarden_license/src/Services/Pam/Api/Models/Request/AccessRuleRequestModel.cs b/bitwarden_license/src/Services/Pam/Api/Models/Request/AccessRuleRequestModel.cs index 40e8458d0236..03388e7c62bb 100644 --- a/bitwarden_license/src/Services/Pam/Api/Models/Request/AccessRuleRequestModel.cs +++ b/bitwarden_license/src/Services/Pam/Api/Models/Request/AccessRuleRequestModel.cs @@ -1,4 +1,6 @@ using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using Bit.Pam.Entities; namespace Bit.Services.Pam.Api.Models.Request; @@ -23,9 +25,10 @@ public class AccessRuleRequestModel public bool Enabled { get; set; } = true; /// - /// The condition tree that decides how access is granted under this rule — for example requiring human - /// approval, or restricting to certain times of day or source IPs. Sent as a JSON object and stored verbatim; - /// an empty or null value means the rule imposes no conditions. + /// The conditions that decide how access is granted under this rule — for example requiring human approval, + /// or restricting to certain source IPs. Sent as a JSON array of condition objects and stored verbatim. + /// Required — a null or omitted value is rejected; an empty array means the rule imposes no conditions, so + /// requests under it resolve automatically. /// [Required] public object Conditions { get; set; } = null!; @@ -65,4 +68,24 @@ public class AccessRuleRequestModel /// [Required] public IEnumerable Collections { get; set; } = null!; + + public AccessRule ToAccessRule(Guid organizationId) => new() + { + OrganizationId = organizationId, + Name = Name, + Description = Description, + Conditions = SerializeConditions(Conditions), + SingleActiveLease = SingleActiveLease, + DefaultLeaseDurationSeconds = DefaultLeaseDurationSeconds, + MaxLeaseDurationSeconds = MaxLeaseDurationSeconds, + Enabled = Enabled, + AllowsExtensions = AllowsExtensions, + MaxExtensionDurationSeconds = MaxExtensionDurationSeconds, + }; + + private static string SerializeConditions(object conditions) => conditions switch + { + JsonElement je => je.GetRawText(), + _ => JsonSerializer.Serialize(conditions), + }; } diff --git a/bitwarden_license/src/Services/Pam/Api/Models/Response/AccessRuleResponseModel.cs b/bitwarden_license/src/Services/Pam/Api/Models/Response/AccessRuleResponseModel.cs index 9c978ebc57a2..5fe433246685 100644 --- a/bitwarden_license/src/Services/Pam/Api/Models/Response/AccessRuleResponseModel.cs +++ b/bitwarden_license/src/Services/Pam/Api/Models/Response/AccessRuleResponseModel.cs @@ -1,87 +1,120 @@ using System.Text.Json; using Bit.HttpExtensions; +using Bit.Pam.Models; namespace Bit.Services.Pam.Api.Models.Response; public class AccessRuleResponseModel : ResponseModel { - public AccessRuleResponseModel() + public AccessRuleResponseModel(AccessRuleDetails rule) : base("accessRule") { + ArgumentNullException.ThrowIfNull(rule); + + Id = rule.Id; + OrganizationId = rule.OrganizationId; + Name = rule.Name; + Description = rule.Description; + Enabled = rule.Enabled; + Conditions = TryParseConditions(rule.Conditions); + SingleActiveLease = rule.SingleActiveLease; + DefaultLeaseDurationSeconds = rule.DefaultLeaseDurationSeconds; + MaxLeaseDurationSeconds = rule.MaxLeaseDurationSeconds; + AllowsExtensions = rule.AllowsExtensions; + MaxExtensionDurationSeconds = rule.MaxExtensionDurationSeconds; + Collections = rule.CollectionIds.ToList(); + CreationDate = rule.CreationDate.AsUtc(); + RevisionDate = rule.RevisionDate.AsUtc(); } /// /// The rule's unique identifier. /// - public Guid Id { get; set; } + public Guid Id { get; } /// /// The organization this rule belongs to. /// - public Guid OrganizationId { get; set; } + public Guid OrganizationId { get; } /// /// The rule's display name, shown wherever rules are listed and managed. /// - public string Name { get; set; } = null!; + public string Name { get; } /// /// Optional free-text describing the rule's intent. Has no effect on evaluation; surfaced to admins only. /// - public string? Description { get; set; } + public string? Description { get; } /// /// When false, the rule is inactive and does not gate access for the collections it governs. /// - public bool Enabled { get; set; } + public bool Enabled { get; } /// - /// The condition tree that decides how access is granted under this rule — for example requiring human - /// approval, or restricting to certain times of day or source IPs. Returned as a JSON object; null when the - /// rule imposes no conditions. + /// The conditions that decide how access is granted under this rule — for example requiring human approval, + /// or restricting to certain source IPs. Returned as a JSON array of condition objects; an empty array (or + /// null) means the rule imposes no conditions. /// - public JsonElement? Conditions { get; set; } + public JsonElement? Conditions { get; } /// /// When true, the rule enforces a per-cipher singleton (at most one active lease per cipher across all users). /// - public bool SingleActiveLease { get; set; } + public bool SingleActiveLease { get; } /// /// Default lease duration in seconds, used to pre-fill a request opened under this rule. Null means the /// backend default applies. /// - public int? DefaultLeaseDurationSeconds { get; set; } + public int? DefaultLeaseDurationSeconds { get; } /// /// Hard ceiling on the duration of any single lease granted under this rule, in seconds. Null means no /// per-rule cap. /// - public int? MaxLeaseDurationSeconds { get; set; } + public int? MaxLeaseDurationSeconds { get; } /// /// When true, a member holding an active lease under this rule may extend it once (always auto-approved), by up /// to . /// - public bool AllowsExtensions { get; set; } + public bool AllowsExtensions { get; } /// /// The longest a single extension may run, in seconds. Set when is true. /// - public int? MaxExtensionDurationSeconds { get; set; } + public int? MaxExtensionDurationSeconds { get; } /// /// The complete set of collections this rule governs. /// - public IEnumerable Collections { get; set; } = null!; + public IEnumerable Collections { get; } /// /// When the rule was created (UTC). /// - public DateTime CreationDate { get; set; } + public DateTime CreationDate { get; } /// /// When the rule was last modified (UTC). /// - public DateTime RevisionDate { get; set; } + public DateTime RevisionDate { get; } + + private static JsonElement? TryParseConditions(string? conditionsJson) + { + if (string.IsNullOrEmpty(conditionsJson)) + { + return null; + } + try + { + return JsonDocument.Parse(conditionsJson).RootElement; + } + catch (JsonException) + { + return null; + } + } } diff --git a/bitwarden_license/src/Services/Pam/Api/Models/Response/PamDateTimeExtensions.cs b/bitwarden_license/src/Services/Pam/Api/Models/Response/PamDateTimeExtensions.cs new file mode 100644 index 000000000000..5ad29d61cefd --- /dev/null +++ b/bitwarden_license/src/Services/Pam/Api/Models/Response/PamDateTimeExtensions.cs @@ -0,0 +1,23 @@ +namespace Bit.Services.Pam.Api.Models.Response; + +/// +/// Marks PAM response timestamps as UTC for serialization. +/// +/// PAM entities and read models are materialised by Dapper, which leaves their as +/// . System.Text.Json then writes an unspecified-kind value with no timezone +/// designator (e.g. "2026-06-15T13:00:00"), which a JavaScript client parses as local time. For any +/// client east/west of UTC the instant shifts — and in the approver inbox that shift drops still-valid requests whose +/// requested window only appears to have lapsed. +/// +/// The stored values are already UTC instants (the commands stamp them from UtcNow), so we relabel the kind +/// with . We deliberately do not use ToUniversalTime(), which treats an +/// unspecified value as local and would shift the clock. This mirrors the convention in CipherRepository, which +/// specifies UTC on the dates it returns. +/// +internal static class PamDateTimeExtensions +{ + public static DateTime AsUtc(this DateTime value) => DateTime.SpecifyKind(value, DateTimeKind.Utc); + + public static DateTime? AsUtc(this DateTime? value) => + value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null; +} diff --git a/bitwarden_license/src/Services/Pam/Models/Conditions/AccessCondition.cs b/bitwarden_license/src/Services/Pam/Models/Conditions/AccessCondition.cs new file mode 100644 index 000000000000..cf61651a1a10 --- /dev/null +++ b/bitwarden_license/src/Services/Pam/Models/Conditions/AccessCondition.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Bit.Services.Pam.Models.Conditions; + +/// +/// Base type for a single leaf condition in an access rule's flat conditions list. Polymorphic deserialization is +/// keyed by the JSON kind property. +/// +[JsonPolymorphic(TypeDiscriminatorPropertyName = "kind", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] +[JsonDerivedType(typeof(HumanApprovalCondition), "human_approval")] +[JsonDerivedType(typeof(IpAllowlistCondition), "ip_allowlist")] +public abstract class AccessCondition; diff --git a/bitwarden_license/src/Services/Pam/Models/Conditions/HumanApprovalCondition.cs b/bitwarden_license/src/Services/Pam/Models/Conditions/HumanApprovalCondition.cs new file mode 100644 index 000000000000..d7a7b4f7e288 --- /dev/null +++ b/bitwarden_license/src/Services/Pam/Models/Conditions/HumanApprovalCondition.cs @@ -0,0 +1,6 @@ +namespace Bit.Services.Pam.Models.Conditions; + +/// +/// Always requires a human decision before a lease can be issued. +/// +public sealed class HumanApprovalCondition : AccessCondition; diff --git a/bitwarden_license/src/Services/Pam/Models/Conditions/IpAllowlistCondition.cs b/bitwarden_license/src/Services/Pam/Models/Conditions/IpAllowlistCondition.cs new file mode 100644 index 000000000000..fd63567945cc --- /dev/null +++ b/bitwarden_license/src/Services/Pam/Models/Conditions/IpAllowlistCondition.cs @@ -0,0 +1,9 @@ +namespace Bit.Services.Pam.Models.Conditions; + +/// +/// Auto-approves a lease when the requester's IP matches a listed CIDR; otherwise denies. +/// +public sealed class IpAllowlistCondition : AccessCondition +{ + public IReadOnlyList Cidrs { get; init; } = []; +} diff --git a/bitwarden_license/src/Services/Pam/OrganizationFeatures/Commands/CreateAccessRuleCommand.cs b/bitwarden_license/src/Services/Pam/OrganizationFeatures/Commands/CreateAccessRuleCommand.cs new file mode 100644 index 000000000000..4263db72bece --- /dev/null +++ b/bitwarden_license/src/Services/Pam/OrganizationFeatures/Commands/CreateAccessRuleCommand.cs @@ -0,0 +1,44 @@ +using Bit.Core.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Models; +using Bit.Pam.Repositories; +using Bit.Services.Pam.OrganizationFeatures.Commands.Interfaces; +using Bit.Services.Pam.Services; + +namespace Bit.Services.Pam.OrganizationFeatures.Commands; + +public class CreateAccessRuleCommand : ICreateAccessRuleCommand +{ + private readonly IAccessRuleRepository _repository; + private readonly ICollectionRepository _collectionRepository; + private readonly IAccessRuleWriteValidator _validator; + private readonly TimeProvider _timeProvider; + + public CreateAccessRuleCommand( + IAccessRuleRepository repository, + ICollectionRepository collectionRepository, + IAccessRuleWriteValidator validator, + TimeProvider timeProvider) + { + _repository = repository; + _collectionRepository = collectionRepository; + _validator = validator; + _timeProvider = timeProvider; + } + + public async Task CreateAsync(AccessRule rule, IEnumerable collectionIds) + { + var desiredCollectionIds = await _validator.ValidateAsync(rule.OrganizationId, rule, collectionIds); + + var now = _timeProvider.GetUtcNow().UtcDateTime; + rule.CreationDate = now; + rule.RevisionDate = now; + + var created = await _repository.CreateAsync(rule); + + await _collectionRepository.SetAccessRuleAssociationsAsync( + created.OrganizationId, created.Id, desiredCollectionIds, []); + + return AccessRuleDetails.From(created, desiredCollectionIds); + } +} diff --git a/bitwarden_license/src/Services/Pam/OrganizationFeatures/Commands/DeleteAccessRuleCommand.cs b/bitwarden_license/src/Services/Pam/OrganizationFeatures/Commands/DeleteAccessRuleCommand.cs new file mode 100644 index 000000000000..6196e6b0c944 --- /dev/null +++ b/bitwarden_license/src/Services/Pam/OrganizationFeatures/Commands/DeleteAccessRuleCommand.cs @@ -0,0 +1,27 @@ +using Bit.Core.Exceptions; +using Bit.Pam.Repositories; +using Bit.Services.Pam.OrganizationFeatures.Commands.Interfaces; + +namespace Bit.Services.Pam.OrganizationFeatures.Commands; + +public class DeleteAccessRuleCommand : IDeleteAccessRuleCommand +{ + private readonly IAccessRuleRepository _repository; + + public DeleteAccessRuleCommand(IAccessRuleRepository repository) + { + _repository = repository; + } + + public async Task DeleteAsync(Guid organizationId, Guid id) + { + var existing = await _repository.GetByIdAsync(id); + if (existing is null || existing.OrganizationId != organizationId) + { + throw new NotFoundException(); + } + + // Hard delete: remove the rule and clear its collection links (they become ungoverned). + await _repository.DeleteAsync(existing); + } +} diff --git a/bitwarden_license/src/Services/Pam/OrganizationFeatures/Commands/Interfaces/ICreateAccessRuleCommand.cs b/bitwarden_license/src/Services/Pam/OrganizationFeatures/Commands/Interfaces/ICreateAccessRuleCommand.cs new file mode 100644 index 000000000000..7de23490dc80 --- /dev/null +++ b/bitwarden_license/src/Services/Pam/OrganizationFeatures/Commands/Interfaces/ICreateAccessRuleCommand.cs @@ -0,0 +1,12 @@ +using Bit.Pam.Entities; +using Bit.Pam.Models; + +namespace Bit.Services.Pam.OrganizationFeatures.Commands.Interfaces; + +public interface ICreateAccessRuleCommand +{ + /// + /// Creates an access rule and associates exactly the given collections with it. + /// + Task CreateAsync(AccessRule rule, IEnumerable collectionIds); +} diff --git a/bitwarden_license/src/Services/Pam/OrganizationFeatures/Commands/Interfaces/IDeleteAccessRuleCommand.cs b/bitwarden_license/src/Services/Pam/OrganizationFeatures/Commands/Interfaces/IDeleteAccessRuleCommand.cs new file mode 100644 index 000000000000..89dc56bda016 --- /dev/null +++ b/bitwarden_license/src/Services/Pam/OrganizationFeatures/Commands/Interfaces/IDeleteAccessRuleCommand.cs @@ -0,0 +1,6 @@ +namespace Bit.Services.Pam.OrganizationFeatures.Commands.Interfaces; + +public interface IDeleteAccessRuleCommand +{ + Task DeleteAsync(Guid organizationId, Guid id); +} diff --git a/bitwarden_license/src/Services/Pam/OrganizationFeatures/Commands/Interfaces/IUpdateAccessRuleCommand.cs b/bitwarden_license/src/Services/Pam/OrganizationFeatures/Commands/Interfaces/IUpdateAccessRuleCommand.cs new file mode 100644 index 000000000000..3047a8ec3ae7 --- /dev/null +++ b/bitwarden_license/src/Services/Pam/OrganizationFeatures/Commands/Interfaces/IUpdateAccessRuleCommand.cs @@ -0,0 +1,12 @@ +using Bit.Pam.Entities; +using Bit.Pam.Models; + +namespace Bit.Services.Pam.OrganizationFeatures.Commands.Interfaces; + +public interface IUpdateAccessRuleCommand +{ + /// + /// Updates an access rule and replaces its collection associations with exactly the given collections. + /// + Task UpdateAsync(Guid organizationId, Guid id, AccessRule update, IEnumerable collectionIds); +} diff --git a/bitwarden_license/src/Services/Pam/OrganizationFeatures/Commands/UpdateAccessRuleCommand.cs b/bitwarden_license/src/Services/Pam/OrganizationFeatures/Commands/UpdateAccessRuleCommand.cs new file mode 100644 index 000000000000..eea728886fc9 --- /dev/null +++ b/bitwarden_license/src/Services/Pam/OrganizationFeatures/Commands/UpdateAccessRuleCommand.cs @@ -0,0 +1,68 @@ +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Models; +using Bit.Pam.Repositories; +using Bit.Services.Pam.OrganizationFeatures.Commands.Interfaces; +using Bit.Services.Pam.Services; + +namespace Bit.Services.Pam.OrganizationFeatures.Commands; + +public class UpdateAccessRuleCommand : IUpdateAccessRuleCommand +{ + private readonly IAccessRuleRepository _repository; + private readonly ICollectionRepository _collectionRepository; + private readonly IAccessRuleWriteValidator _validator; + private readonly TimeProvider _timeProvider; + + public UpdateAccessRuleCommand( + IAccessRuleRepository repository, + ICollectionRepository collectionRepository, + IAccessRuleWriteValidator validator, + TimeProvider timeProvider) + { + _repository = repository; + _collectionRepository = collectionRepository; + _validator = validator; + _timeProvider = timeProvider; + } + + public async Task UpdateAsync(Guid organizationId, Guid id, AccessRule update, + IEnumerable collectionIds) + { + var existing = await _repository.GetDetailsByIdAsync(id); + if (existing is null || existing.OrganizationId != organizationId) + { + throw new NotFoundException(); + } + + var desiredCollectionIds = await _validator.ValidateAsync(organizationId, update, collectionIds, id); + + // Persist a plain AccessRule: the AccessRuleDetails returned by GetDetailsByIdAsync carries an extra + // CollectionIds property that the base ReplaceAsync would otherwise forward to AccessRule_Update. + var toPersist = new AccessRule + { + Id = existing.Id, + OrganizationId = existing.OrganizationId, + Name = update.Name, + Description = update.Description, + Conditions = update.Conditions, + SingleActiveLease = update.SingleActiveLease, + DefaultLeaseDurationSeconds = update.DefaultLeaseDurationSeconds, + MaxLeaseDurationSeconds = update.MaxLeaseDurationSeconds, + Enabled = update.Enabled, + AllowsExtensions = update.AllowsExtensions, + MaxExtensionDurationSeconds = update.MaxExtensionDurationSeconds, + CreationDate = existing.CreationDate, + RevisionDate = _timeProvider.GetUtcNow().UtcDateTime, + LastEditedBy = update.LastEditedBy, + }; + + await _repository.ReplaceAsync(toPersist); + + var toClear = existing.CollectionIds.Except(desiredCollectionIds).ToList(); + await _collectionRepository.SetAccessRuleAssociationsAsync(organizationId, id, desiredCollectionIds, toClear); + + return AccessRuleDetails.From(toPersist, desiredCollectionIds); + } +} diff --git a/bitwarden_license/src/Services/Pam/Pam.csproj b/bitwarden_license/src/Services/Pam/Pam.csproj index e80fe52e7579..ddec143d0dc0 100644 --- a/bitwarden_license/src/Services/Pam/Pam.csproj +++ b/bitwarden_license/src/Services/Pam/Pam.csproj @@ -27,6 +27,7 @@ + diff --git a/bitwarden_license/src/Services/Pam/Services/AccessRuleValidator.cs b/bitwarden_license/src/Services/Pam/Services/AccessRuleValidator.cs new file mode 100644 index 000000000000..0d1c93d29ce0 --- /dev/null +++ b/bitwarden_license/src/Services/Pam/Services/AccessRuleValidator.cs @@ -0,0 +1,84 @@ +using System.Net; +using System.Text.Json; +using Bit.Services.Pam.Models.Conditions; + +namespace Bit.Services.Pam.Services; + +public sealed class AccessRuleValidator : IAccessRuleValidator +{ + private const int MaxConditions = 10; + + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + }; + + public AccessRuleValidationResult Validate(string? conditionsJson) + { + if (conditionsJson is null) + { + return AccessRuleValidationResult.Valid; + } + + if (string.IsNullOrWhiteSpace(conditionsJson)) + { + return AccessRuleValidationResult.Invalid("Conditions JSON cannot be empty."); + } + + List? conditions; + try + { + conditions = JsonSerializer.Deserialize>(conditionsJson, JsonOptions); + } + catch (JsonException ex) + { + return AccessRuleValidationResult.Invalid($"Conditions JSON is malformed: {ex.Message}"); + } + + if (conditions is null) + { + return AccessRuleValidationResult.Invalid("Conditions must be an array."); + } + + // An empty list is allowed: it is vacuously satisfied, so the rule governs its collections — routing access + // through the PAM flow for audit logging — without imposing any gating condition. The engine evaluates it + // to Allow. + if (conditions.Count > MaxConditions) + { + return AccessRuleValidationResult.Invalid($"Conditions cannot contain more than {MaxConditions} conditions."); + } + + return conditions.Select(ValidateCondition).FirstOrDefault(result => !result.IsValid) + ?? AccessRuleValidationResult.Valid; + } + + private static AccessRuleValidationResult ValidateCondition(AccessCondition? condition) + { + return condition switch + { + HumanApprovalCondition => AccessRuleValidationResult.Valid, + IpAllowlistCondition ip => ValidateIpAllowlist(ip), + null => AccessRuleValidationResult.Invalid("Conditions cannot contain a null entry."), + _ => AccessRuleValidationResult.Invalid($"Unsupported condition kind: {condition.GetType().Name}."), + }; + } + + private static AccessRuleValidationResult ValidateIpAllowlist(IpAllowlistCondition condition) + { + if (condition.Cidrs.Count == 0) + { + return AccessRuleValidationResult.Invalid("ip_allowlist requires at least one CIDR."); + } + + foreach (var cidr in condition.Cidrs) + { + if (string.IsNullOrWhiteSpace(cidr) || !IPNetwork.TryParse(cidr, out _)) + { + return AccessRuleValidationResult.Invalid($"Invalid CIDR: '{cidr}'."); + } + } + + return AccessRuleValidationResult.Valid; + } +} diff --git a/bitwarden_license/src/Services/Pam/Services/AccessRuleWriteValidator.cs b/bitwarden_license/src/Services/Pam/Services/AccessRuleWriteValidator.cs new file mode 100644 index 000000000000..905afbc9b811 --- /dev/null +++ b/bitwarden_license/src/Services/Pam/Services/AccessRuleWriteValidator.cs @@ -0,0 +1,92 @@ +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Repositories; + +namespace Bit.Services.Pam.Services; + +/// +/// The shared validation for the AccessRule create and update paths. Create and update differ only in whether the +/// rule already exists, which both the name-uniqueness and collection-conflict checks express by comparing against +/// existingRuleId — null for a create, so nothing is excluded from either check. +/// +public class AccessRuleWriteValidator : IAccessRuleWriteValidator +{ + private readonly IAccessRuleRepository _repository; + private readonly ICollectionRepository _collectionRepository; + private readonly IAccessRuleValidator _conditionsValidator; + + public AccessRuleWriteValidator( + IAccessRuleRepository repository, + ICollectionRepository collectionRepository, + IAccessRuleValidator conditionsValidator) + { + _repository = repository; + _collectionRepository = collectionRepository; + _conditionsValidator = conditionsValidator; + } + + public async Task> ValidateAsync(Guid organizationId, AccessRule rule, + IEnumerable collectionIds, Guid? existingRuleId = null) + { + if (string.IsNullOrWhiteSpace(rule.Name)) + { + throw new BadRequestException("Name is required."); + } + + if (rule.AllowsExtensions && rule.MaxExtensionDurationSeconds is not > 0) + { + throw new BadRequestException("A maximum extension length is required when extensions are allowed."); + } + + var conditions = _conditionsValidator.Validate(rule.Conditions); + if (!conditions.IsValid) + { + throw new BadRequestException(conditions.Error!); + } + + await ValidateNameIsUniqueAsync(organizationId, rule.Name, existingRuleId); + + return await ValidateCollectionsAsync(organizationId, collectionIds, existingRuleId); + } + + private async Task ValidateNameIsUniqueAsync(Guid organizationId, string name, Guid? existingRuleId) + { + var siblings = await _repository.GetManyByOrganizationIdAsync(organizationId); + if (siblings.Any(r => r.Id != existingRuleId && string.Equals(r.Name, name, StringComparison.OrdinalIgnoreCase))) + { + throw new BadRequestException("A rule with that name already exists."); + } + } + + private async Task> ValidateCollectionsAsync(Guid organizationId, IEnumerable collectionIds, + Guid? existingRuleId) + { + var distinctIds = collectionIds.Distinct().ToList(); + if (distinctIds.Count == 0) + { + return distinctIds; + } + + var collections = await _collectionRepository.GetManyByManyIdsAsync(distinctIds); + if (collections.Count != distinctIds.Count) + { + throw new BadRequestException("One or more collections could not be found."); + } + + if (collections.Any(c => c.OrganizationId != organizationId)) + { + throw new BadRequestException("One or more collections do not belong to this organization."); + } + + // Deletes clear Collection.AccessRuleId and the FK forbids dangling links, so any set link points at an + // existing rule; only a link to a different rule is a conflict. A rule being created has no id, so for it + // any link at all conflicts. + if (collections.Any(c => c.AccessRuleId.HasValue && c.AccessRuleId != existingRuleId)) + { + throw new BadRequestException("One or more collections are already governed by another access rule."); + } + + return distinctIds; + } +} diff --git a/bitwarden_license/src/Services/Pam/Services/IAccessRuleValidator.cs b/bitwarden_license/src/Services/Pam/Services/IAccessRuleValidator.cs new file mode 100644 index 000000000000..b706ccee6f8c --- /dev/null +++ b/bitwarden_license/src/Services/Pam/Services/IAccessRuleValidator.cs @@ -0,0 +1,16 @@ +namespace Bit.Services.Pam.Services; + +public interface IAccessRuleValidator +{ + /// + /// Validates a raw JSON conditions document. A null or empty document is treated as "no conditions + /// configured" and considered valid; callers decide how to treat that semantically. + /// + AccessRuleValidationResult Validate(string? conditionsJson); +} + +public sealed record AccessRuleValidationResult(bool IsValid, string? Error) +{ + public static AccessRuleValidationResult Valid { get; } = new(true, null); + public static AccessRuleValidationResult Invalid(string error) => new(false, error); +} diff --git a/bitwarden_license/src/Services/Pam/Services/IAccessRuleWriteValidator.cs b/bitwarden_license/src/Services/Pam/Services/IAccessRuleWriteValidator.cs new file mode 100644 index 000000000000..1823c36ba664 --- /dev/null +++ b/bitwarden_license/src/Services/Pam/Services/IAccessRuleWriteValidator.cs @@ -0,0 +1,23 @@ +using Bit.Core.Exceptions; +using Bit.Pam.Entities; + +namespace Bit.Services.Pam.Services; + +public interface IAccessRuleWriteValidator +{ + /// + /// Validates a rule that is about to be persisted — its own fields, its conditions document, its name's + /// uniqueness within the organization, and the collections it is to govern — and returns the deduplicated + /// collection ids to associate with it. + /// + /// The organization the rule belongs to, and the only one its collections may + /// belong to. + /// The rule as it will be persisted. + /// The complete set of collections the rule should govern. + /// The id of the rule being updated, or null when creating. An update is excluded + /// from its own name-uniqueness check and may keep the collections it already governs, whereas a create + /// conflicts with any already-governed collection. + /// Thrown on the first validation failure. + Task> ValidateAsync(Guid organizationId, AccessRule rule, IEnumerable collectionIds, + Guid? existingRuleId = null); +} diff --git a/bitwarden_license/src/Services/Pam/Utilities/ServiceCollectionExtensions.cs b/bitwarden_license/src/Services/Pam/Utilities/ServiceCollectionExtensions.cs index c3506ff35aa5..3e8d75435ad6 100644 --- a/bitwarden_license/src/Services/Pam/Utilities/ServiceCollectionExtensions.cs +++ b/bitwarden_license/src/Services/Pam/Utilities/ServiceCollectionExtensions.cs @@ -1,6 +1,10 @@ using Bit.HttpExtensions; using Bit.Services.Pam.Api.Endpoints; using Bit.Services.Pam.Api.Endpoints.Handlers; +using Bit.Services.Pam.OrganizationFeatures.Commands; +using Bit.Services.Pam.OrganizationFeatures.Commands.Interfaces; +using Bit.Services.Pam.Services; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace Bit.Services.Pam.Utilities; @@ -14,6 +18,14 @@ public static IServiceCollection AddPamServices(this IServiceCollection services services.AddScoped(); services.AddScoped(); + // AccessRule write path. + services.TryAddSingleton(TimeProvider.System); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddPamOpenApiEndpointDataSource(); return services; diff --git a/bitwarden_license/src/Services/Pam/packages.lock.json b/bitwarden_license/src/Services/Pam/packages.lock.json index 9b6ba7c35660..5e462472e4f9 100644 --- a/bitwarden_license/src/Services/Pam/packages.lock.json +++ b/bitwarden_license/src/Services/Pam/packages.lock.json @@ -916,6 +916,12 @@ "Core": "[2026.8.0, )" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[0.0.1, )" + } + }, "serilogfilelogging": { "type": "Project", "dependencies": { diff --git a/test/Api.IntegrationTest/Pam/AccessRuleAuthorizationTests.cs b/bitwarden_license/test/Services/Pam.IntegrationTest/AccessRuleAuthorizationTests.cs similarity index 51% rename from test/Api.IntegrationTest/Pam/AccessRuleAuthorizationTests.cs rename to bitwarden_license/test/Services/Pam.IntegrationTest/AccessRuleAuthorizationTests.cs index dc60c82b938b..f4cf20767465 100644 --- a/test/Api.IntegrationTest/Pam/AccessRuleAuthorizationTests.cs +++ b/bitwarden_license/test/Services/Pam.IntegrationTest/AccessRuleAuthorizationTests.cs @@ -1,8 +1,7 @@ using System.Net; +using System.Net.Http.Json; using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.Helpers; -using Bit.Core; -using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Providers.Interfaces; @@ -11,64 +10,30 @@ using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Repositories; -using Bitwarden.Server.Sdk.Features; -using NSubstitute; using Xunit; -namespace Bit.Api.IntegrationTest.Pam; +namespace Bit.Services.Pam.IntegrationTest; /// /// Authorization for organizations/{orgId}/access-rules, exercised over the real request pipeline. /// /// /// The endpoint-registration tests in Pam.Test assert which requirements are attached to which route, but they stop -/// before the pipeline runs — presence in metadata is not enforcement. These tests assert only denials, which stay -/// valid once the handler scaffolds are implemented; the allowed cases assert merely that the caller got past -/// authorization, since the handlers currently throw. +/// before the pipeline runs — presence in metadata is not enforcement. These tests deliberately assert only that a +/// caller was or was not denied, never what the handler returned; the round-trip behaviour is +/// 's subject. /// -public class AccessRuleAuthorizationTests : IClassFixture, IAsyncLifetime +public class AccessRuleAuthorizationTests(ApiApplicationFactory factory) + : AccessRuleIntegrationTestBase(factory, "pam-access-rule-authz") { - private readonly HttpClient _client; - private readonly ApiApplicationFactory _factory; - private readonly LoginHelper _loginHelper; - private readonly IFeatureService _featureService; - - private Organization _organization = null!; - private string _ownerEmail = null!; - - public AccessRuleAuthorizationTests(ApiApplicationFactory factory) - { - _factory = factory; - _factory.SubstituteService(_ => { }); - _client = _factory.CreateClient(); - _loginHelper = new LoginHelper(_factory, _client); - _featureService = _factory.GetService(); - } - - public async Task InitializeAsync() - { - _featureService.IsEnabled(FeatureFlagKeys.Pam).Returns(true); - - _ownerEmail = $"pam-access-rule-authz-{Guid.NewGuid()}@bitwarden.com"; - await _factory.LoginWithNewAccount(_ownerEmail); - (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually, - ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card); - } - - public Task DisposeAsync() - { - _client.Dispose(); - return Task.CompletedTask; - } - [Fact] public async Task Read_AsNonMember_ReturnsForbidden() { var outsiderEmail = $"outsider-{Guid.NewGuid()}@bitwarden.com"; - await _factory.LoginWithNewAccount(outsiderEmail); - await _loginHelper.LoginAsync(outsiderEmail); + await Factory.LoginWithNewAccount(outsiderEmail); + await LoginHelper.LoginAsync(outsiderEmail); - var response = await _client.GetAsync(AccessRules()); + var response = await Client.GetAsync(AccessRulesUrl); Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } @@ -80,8 +45,8 @@ public async Task Read_AsNonMember_ReturnsForbidden() public async Task Write_AsNonMember_ReturnsForbidden(string method) { var outsiderEmail = $"outsider-{Guid.NewGuid()}@bitwarden.com"; - await _factory.LoginWithNewAccount(outsiderEmail); - await _loginHelper.LoginAsync(outsiderEmail); + await Factory.LoginWithNewAccount(outsiderEmail); + await LoginHelper.LoginAsync(outsiderEmail); var response = await SendWriteAsync(method); @@ -94,9 +59,9 @@ public async Task Write_AsNonMember_ReturnsForbidden(string method) [InlineData("DELETE")] public async Task Write_AsPlainMember_ReturnsForbidden(string method) { - var (memberEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, - _organization.Id, OrganizationUserType.User); - await _loginHelper.LoginAsync(memberEmail); + var (memberEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(Factory, + Organization.Id, OrganizationUserType.User); + await LoginHelper.LoginAsync(memberEmail); var response = await SendWriteAsync(method); @@ -109,9 +74,9 @@ public async Task Write_AsPlainMember_ReturnsForbidden(string method) [InlineData("DELETE")] public async Task Write_AsCustomUserWithoutManageAccessRules_ReturnsForbidden(string method) { - var (customEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, - _organization.Id, OrganizationUserType.Custom, new Permissions { ManageAccessRules = false }); - await _loginHelper.LoginAsync(customEmail); + var (customEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(Factory, + Organization.Id, OrganizationUserType.Custom, new Permissions { ManageAccessRules = false }); + await LoginHelper.LoginAsync(customEmail); var response = await SendWriteAsync(method); @@ -125,7 +90,7 @@ public async Task Read_AsProviderUserForTheOrganization_ReturnsForbidden() // credentials out of it. The group deliberately uses MemberRequirement, not MemberOrProviderRequirement. await LoginAsProviderForOrganizationAsync(); - var response = await _client.GetAsync(AccessRules()); + var response = await Client.GetAsync(AccessRulesUrl); Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } @@ -143,13 +108,12 @@ public async Task Write_AsProviderUserForTheOrganization_ReturnsForbidden() [Fact] public async Task Read_AsMember_IsNotForbidden() { - // Guards against the group requirement over-denying. The handler is a scaffold that throws, so this - // asserts only that authorization let the caller through. - var (memberEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, - _organization.Id, OrganizationUserType.User); - await _loginHelper.LoginAsync(memberEmail); + // Guards against the group requirement over-denying: reading rules is available to any member. + var (memberEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(Factory, + Organization.Id, OrganizationUserType.User); + await LoginHelper.LoginAsync(memberEmail); - var response = await _client.GetAsync(AccessRules()); + var response = await Client.GetAsync(AccessRulesUrl); AssertReachedTheHandler(response); } @@ -157,7 +121,7 @@ public async Task Read_AsMember_IsNotForbidden() [Fact] public async Task Write_AsOwner_IsNotForbidden() { - await _loginHelper.LoginAsync(_ownerEmail); + await LoginHelper.LoginAsync(OwnerEmail); var response = await SendWriteAsync("POST"); @@ -165,10 +129,9 @@ public async Task Write_AsOwner_IsNotForbidden() } /// - /// Asserts a caller got past authorization without pinning what the scaffold handler does. NotFound is excluded - /// as well as Forbidden: without it these would still pass if the PAM feature gate silently swallowed the route, - /// which would in turn make every denial above pass for the wrong reason. Neither status is a legitimate result - /// for these two requests once the handlers are implemented. + /// Asserts a caller got past authorization without pinning what the handler did with a deliberately empty body. + /// NotFound is excluded as well as Forbidden: without it these would still pass if the PAM feature gate silently + /// swallowed the route, which would in turn make every denial above pass for the wrong reason. /// private static void AssertReachedTheHandler(HttpResponseMessage response) { @@ -176,40 +139,38 @@ private static void AssertReachedTheHandler(HttpResponseMessage response) Assert.NotEqual(HttpStatusCode.NotFound, response.StatusCode); } - private string AccessRules() => $"organizations/{_organization.Id}/access-rules"; - private Task SendWriteAsync(string method) => method switch { - "POST" => _client.PostAsJsonAsync(AccessRules(), new { }), - "PUT" => _client.PutAsJsonAsync($"{AccessRules()}/{Guid.NewGuid()}", new { }), - "DELETE" => _client.DeleteAsync($"{AccessRules()}/{Guid.NewGuid()}"), + "POST" => Client.PostAsJsonAsync(AccessRulesUrl, new { }), + "PUT" => Client.PutAsJsonAsync(AccessRuleUrl(Guid.NewGuid()), new { }), + "DELETE" => Client.DeleteAsync(AccessRuleUrl(Guid.NewGuid())), _ => throw new ArgumentOutOfRangeException(nameof(method)) }; private async Task LoginAsProviderForOrganizationAsync() { var providerEmail = $"provider-{Guid.NewGuid()}@bitwarden.com"; - await _factory.LoginWithNewAccount(providerEmail); + await Factory.LoginWithNewAccount(providerEmail); - await _factory.GetService() + await Factory.GetService() .CreateBusinessUnitAsync( new Provider { Name = "provider", Type = ProviderType.BusinessUnit }, providerEmail, PlanType.EnterpriseAnnually2023, 10); - var providerUserAccount = await _factory.GetService().GetByEmailAsync(providerEmail); - var providerUser = (await _factory.GetService() + var providerUserAccount = await Factory.GetService().GetByEmailAsync(providerEmail); + var providerUser = (await Factory.GetService() .GetManyByUserAsync(providerUserAccount!.Id)).First(); - await _factory.GetService().CreateAsync(new ProviderOrganization + await Factory.GetService().CreateAsync(new ProviderOrganization { ProviderId = providerUser.ProviderId, - OrganizationId = _organization.Id, + OrganizationId = Organization.Id, Key = null, Settings = null }); - await _loginHelper.LoginAsync(providerEmail); + await LoginHelper.LoginAsync(providerEmail); } } diff --git a/bitwarden_license/test/Services/Pam.IntegrationTest/AccessRuleCrudTests.cs b/bitwarden_license/test/Services/Pam.IntegrationTest/AccessRuleCrudTests.cs new file mode 100644 index 000000000000..28eb7a1f30df --- /dev/null +++ b/bitwarden_license/test/Services/Pam.IntegrationTest/AccessRuleCrudTests.cs @@ -0,0 +1,280 @@ +using System.Net; +using System.Net.Http.Json; +using System.Text.Json.Nodes; +using Bit.Api.IntegrationTest.Factories; +using Bit.Api.IntegrationTest.Helpers; +using Bit.Core; +using Bit.Core.Billing.Enums; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Repositories; +using Bit.Services.Pam.Api.Models.Request; +using NSubstitute; +using Xunit; + +namespace Bit.Services.Pam.IntegrationTest; + +/// +/// The access-rule CRUD contract over the real request pipeline: routing, the feature gate, model validation, the +/// exception → ErrorResponseModel translation, and the round trip through SQLite. +/// +/// +/// These tests cover the seams that the Pam.Test unit tests necessarily mock away — the conditions document surviving +/// HTTP binding and storage unchanged, the collection links actually being written, and validator failures arriving as +/// the documented 400 body. Authorization is 's subject; every test here +/// acts as the organization owner. +/// +public class AccessRuleCrudTests(ApiApplicationFactory factory) + : AccessRuleIntegrationTestBase(factory, "pam-access-rule-crud") +{ + private const string HumanApproval = """[{"kind":"human_approval","approverCount":1}]"""; + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + await LoginHelper.LoginAsync(OwnerEmail); + } + + [Fact] + public async Task AccessRule_CreateListGetUpdateDelete_RoundTripsOverTheApi() + { + var created = await PostRuleAsync(NewRule("Production database")); + var id = created["id"]!.GetValue(); + Assert.Equal("accessRule", created["object"]!.GetValue()); + Assert.Equal(Organization.Id, created["organizationId"]!.GetValue()); + + var list = await GetJsonAsync(AccessRulesUrl); + Assert.Equal("list", list["object"]!.GetValue()); + Assert.Contains(list["data"]!.AsArray(), rule => rule!["id"]!.GetValue() == id); + + var fetched = await GetJsonAsync(AccessRuleUrl(id)); + Assert.Equal("Production database", fetched["name"]!.GetValue()); + Assert.True(fetched["enabled"]!.GetValue()); + + var updated = await PutRuleAsync(id, NewRule("Production database (paused)", enabled: false)); + Assert.Equal(id, updated["id"]!.GetValue()); + Assert.Equal("Production database (paused)", updated["name"]!.GetValue()); + Assert.False(updated["enabled"]!.GetValue()); + + var deleteResponse = await Client.DeleteAsync(AccessRuleUrl(id)); + Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode); + + var afterDelete = await Client.GetAsync(AccessRuleUrl(id)); + Assert.Equal(HttpStatusCode.NotFound, afterDelete.StatusCode); + } + + /// + /// The conditions document is stored verbatim and handed back unparsed, so the engine that reads it later sees + /// exactly what the client sent — including properties this version does not model. + /// + [Fact] + public async Task Post_StoresTheConditionsDocumentVerbatim() + { + const string conditions = + """[{"kind":"ip_allowlist","cidrs":["10.0.0.0/8","192.168.1.0/24"],"unmodelled":"kept"}]"""; + + var created = await PostRuleAsync(NewRule("Office network only", conditions)); + + Assert.Equal(JsonNode.Parse(conditions)!.ToJsonString(), created["conditions"]!.ToJsonString()); + + var fetched = await GetJsonAsync(AccessRuleUrl(created["id"]!.GetValue())); + Assert.Equal(JsonNode.Parse(conditions)!.ToJsonString(), fetched["conditions"]!.ToJsonString()); + } + + [Fact] + public async Task Post_GovernsTheRequestedCollections_AndPutReplacesTheSet() + { + var governed = await OrganizationTestHelpers.CreateCollectionAsync(Factory, Organization.Id, "Governed"); + var other = await OrganizationTestHelpers.CreateCollectionAsync(Factory, Organization.Id, "Other"); + + var created = await PostRuleAsync(NewRule("Governs one collection", collections: [governed.Id])); + var id = created["id"]!.GetValue(); + + Assert.Equal(new[] { governed.Id }, created["collections"]!.AsArray().Select(c => c!.GetValue()).ToArray()); + Assert.Equal(id, await AccessRuleIdOfAsync(governed.Id)); + + await PutRuleAsync(id, NewRule("Governs the other collection", collections: [other.Id])); + + Assert.Null(await AccessRuleIdOfAsync(governed.Id)); + Assert.Equal(id, await AccessRuleIdOfAsync(other.Id)); + } + + /// + /// A validator failure has to surface as Bitwarden's ErrorResponseModel 400 rather than a 500, which is + /// what the exception filter being outermost in the PAM group's chain buys. + /// + [Fact] + public async Task Post_WithACidrThatDoesNotParse_ReturnsBadRequestWithTheValidatorMessage() + { + var response = await Client.PostAsJsonAsync(AccessRulesUrl, + NewRule("Bad allowlist", """[{"kind":"ip_allowlist","cidrs":["not-a-cidr"]}]""")); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + var body = await ReadJsonAsync(response); + Assert.Equal("error", body["object"]!.GetValue()); + Assert.Contains("not-a-cidr", body["message"]!.GetValue()); + } + + /// + /// Conditions is declared required, so an omitted value has to be rejected by the group's validation filter + /// before any handler runs — not dereferenced into a 500. + /// + [Fact] + public async Task Post_WithoutConditions_ReturnsBadRequestFromModelValidation() + { + var response = await Client.PostAsJsonAsync(AccessRulesUrl, + new { name = "No conditions", collections = Array.Empty() }); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + var body = await ReadJsonAsync(response); + Assert.Equal("The model state is invalid.", body["message"]!.GetValue()); + Assert.Contains(nameof(AccessRuleRequestModel.Conditions), + body["validationErrors"]!.AsObject().Select(error => error.Key)); + } + + /// + /// Authorization only proves the caller belongs to the organization on the route, so the handler is what stops a + /// rule ID from another organization being read through it. + /// + [Fact] + public async Task Get_ARuleBelongingToAnotherOrganization_ReturnsNotFound() + { + var foreignRule = await SeedRuleInAnotherOrganizationAsync(); + + var response = await Client.GetAsync(AccessRuleUrl(foreignRule.Id)); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Delete_ARuleBelongingToAnotherOrganization_ReturnsNotFound() + { + var foreignRule = await SeedRuleInAnotherOrganizationAsync(); + + var response = await Client.DeleteAsync(AccessRuleUrl(foreignRule.Id)); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + Assert.NotNull(await Factory.GetService().GetByIdAsync(foreignRule.Id)); + } + + /// + /// The whole surface is unreleased and reachable only behind the PAM flag. + /// + [Fact] + public async Task AccessRuleEndpoints_WithThePamFeatureFlagOff_AreNotRoutable() + { + FeatureService.IsEnabled(FeatureFlagKeys.Pam).Returns(false); + + Assert.Equal(HttpStatusCode.NotFound, (await Client.GetAsync(AccessRulesUrl)).StatusCode); + Assert.Equal(HttpStatusCode.NotFound, + (await Client.PostAsJsonAsync(AccessRulesUrl, NewRule("Should not be reachable"))).StatusCode); + } + + [Fact] + public async Task Post_RecordsTheCallingUserAsTheLastEditor() + { + var created = await PostRuleAsync(NewRule("Attributed to the owner")); + + var owner = await Factory.GetService().GetByEmailAsync(OwnerEmail); + var stored = await Factory.GetService() + .GetByIdAsync(created["id"]!.GetValue()); + + Assert.Equal(owner!.Id, stored!.LastEditedBy); + } + + /// + /// A kind-less DateTime serializes with no timezone designator, which a JavaScript client reads as local time — + /// shifting the instant for any client not sitting on UTC. + /// + [Fact] + public async Task Post_ReturnsTimestampsMarkedAsUtc() + { + var created = await PostRuleAsync(NewRule("Timestamped")); + + Assert.EndsWith("Z", created["creationDate"]!.GetValue(), StringComparison.Ordinal); + Assert.EndsWith("Z", created["revisionDate"]!.GetValue(), StringComparison.Ordinal); + } + + /// + /// Rules predating a conditions-format change still have to be readable, so a document that no longer parses + /// reads back as no conditions instead of failing the request. + /// + [Fact] + public async Task Get_WithStoredConditionsThatNoLongerParse_ReturnsNullConditions() + { + var rule = await Factory.GetService().CreateAsync(new AccessRule + { + OrganizationId = Organization.Id, + Name = "Unparseable conditions", + Conditions = "{ not json", + }); + + var fetched = await GetJsonAsync(AccessRuleUrl(rule.Id)); + + Assert.Null(fetched["conditions"]); + } + + private static object NewRule( + string name, + string conditions = HumanApproval, + bool enabled = true, + Guid[]? collections = null) => new + { + name, + enabled, + conditions = JsonNode.Parse(conditions), + collections = collections ?? [], + }; + + private async Task PostRuleAsync(object rule) + { + var response = await Client.PostAsJsonAsync(AccessRulesUrl, rule); + response.EnsureSuccessStatusCode(); + return await ReadJsonAsync(response); + } + + private async Task PutRuleAsync(Guid id, object rule) + { + var response = await Client.PutAsJsonAsync(AccessRuleUrl(id), rule); + response.EnsureSuccessStatusCode(); + return await ReadJsonAsync(response); + } + + private async Task GetJsonAsync(string url) + { + var response = await Client.GetAsync(url); + response.EnsureSuccessStatusCode(); + return await ReadJsonAsync(response); + } + + private static async Task ReadJsonAsync(HttpResponseMessage response) => + (await response.Content.ReadFromJsonAsync())!; + + private async Task AccessRuleIdOfAsync(Guid collectionId) => + (await Factory.GetService().GetByIdAsync(collectionId))!.AccessRuleId; + + private async Task SeedRuleInAnotherOrganizationAsync() + { + var otherOwnerEmail = $"pam-other-org-{Guid.NewGuid()}@bitwarden.com"; + await Factory.LoginWithNewAccount(otherOwnerEmail); + var (otherOrganization, _) = await OrganizationTestHelpers.SignUpAsync(Factory, + plan: PlanType.EnterpriseAnnually, ownerEmail: otherOwnerEmail, passwordManagerSeats: 10, + paymentMethod: PaymentMethodType.Card); + + // Seeded through the repository rather than the API: the point is a rule this caller can name but must not + // reach, and logging in as the other organization's owner would only get in the way. + var rule = await Factory.GetService().CreateAsync(new AccessRule + { + OrganizationId = otherOrganization.Id, + Name = "Another organization's rule", + Conditions = "[]", + }); + + await LoginHelper.LoginAsync(OwnerEmail); + return rule; + } +} diff --git a/bitwarden_license/test/Services/Pam.IntegrationTest/AccessRuleIntegrationTestBase.cs b/bitwarden_license/test/Services/Pam.IntegrationTest/AccessRuleIntegrationTestBase.cs new file mode 100644 index 000000000000..427472f77b0f --- /dev/null +++ b/bitwarden_license/test/Services/Pam.IntegrationTest/AccessRuleIntegrationTestBase.cs @@ -0,0 +1,75 @@ +using Bit.Api.IntegrationTest.Factories; +using Bit.Api.IntegrationTest.Helpers; +using Bit.Core; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Enums; +using Bit.Core.Enums; +using Bitwarden.Server.Sdk.Features; +using NSubstitute; +using Xunit; + +namespace Bit.Services.Pam.IntegrationTest; + +/// +/// Shared harness for the organizations/{orgId}/access-rules integration tests: the Api host over SQLite, the +/// PAM feature flag on, and an enterprise organization whose owner can be logged in. +/// +/// +/// PAM ships under bitwarden_license, so its integration tests live here. Api.IntegrationTest is referenced +/// only for the host fixture and the organization/login helpers, the same way Billing.IntegrationTest consumes them. +/// +/// Each test class gets its own — and so its own database — because xUnit scopes +/// per class. Setup runs per test, since xUnit constructs a fresh test-class instance +/// for every test, which is what keeps a test that flips the feature flag from leaking into its siblings. +/// +/// +public abstract class AccessRuleIntegrationTestBase : IClassFixture, IAsyncLifetime +{ + private readonly string _emailPrefix; + + protected AccessRuleIntegrationTestBase(ApiApplicationFactory factory, string emailPrefix) + { + Factory = factory; + // Every PAM group sits behind RequireFeature(FeatureFlagKeys.Pam), so without a substituted feature service + // the whole surface is unroutable and every assertion below would pass for the wrong reason. + Factory.SubstituteService(_ => { }); + Client = factory.CreateClient(); + LoginHelper = new LoginHelper(factory, Client); + FeatureService = factory.GetService(); + _emailPrefix = emailPrefix; + } + + protected ApiApplicationFactory Factory { get; } + + protected HttpClient Client { get; } + + protected LoginHelper LoginHelper { get; } + + protected IFeatureService FeatureService { get; } + + protected Organization Organization { get; private set; } = null!; + + protected string OwnerEmail { get; private set; } = null!; + + protected string AccessRulesUrl => AccessRulesUrlFor(Organization.Id); + + public virtual async Task InitializeAsync() + { + FeatureService.IsEnabled(FeatureFlagKeys.Pam).Returns(true); + + OwnerEmail = $"{_emailPrefix}-{Guid.NewGuid()}@bitwarden.com"; + await Factory.LoginWithNewAccount(OwnerEmail); + (Organization, _) = await OrganizationTestHelpers.SignUpAsync(Factory, plan: PlanType.EnterpriseAnnually, + ownerEmail: OwnerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card); + } + + public virtual Task DisposeAsync() + { + Client.Dispose(); + return Task.CompletedTask; + } + + protected static string AccessRulesUrlFor(Guid organizationId) => $"organizations/{organizationId}/access-rules"; + + protected string AccessRuleUrl(Guid id) => $"{AccessRulesUrl}/{id}"; +} diff --git a/bitwarden_license/test/Services/Pam.IntegrationTest/Pam.IntegrationTest.csproj b/bitwarden_license/test/Services/Pam.IntegrationTest/Pam.IntegrationTest.csproj new file mode 100644 index 000000000000..3619167a4f68 --- /dev/null +++ b/bitwarden_license/test/Services/Pam.IntegrationTest/Pam.IntegrationTest.csproj @@ -0,0 +1,37 @@ + + + + Bit.Services.Pam.IntegrationTest + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + %(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + + + + diff --git a/bitwarden_license/test/Services/Pam.IntegrationTest/packages.lock.json b/bitwarden_license/test/Services/Pam.IntegrationTest/packages.lock.json new file mode 100644 index 000000000000..89010f03412b --- /dev/null +++ b/bitwarden_license/test/Services/Pam.IntegrationTest/packages.lock.json @@ -0,0 +1,2096 @@ +{ + "version": 1, + "dependencies": { + "net10.0": { + "coverlet.collector": { + "type": "Direct", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "tW3lsNS+dAEII6YGUX/VMoJjBS1QvsxqJeqLaJXub08y1FSjasFPtQ4UBUsudE9PNrzLjooClMsPtY2cZLdXpQ==" + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[18.0.1, )", + "resolved": "18.0.1", + "contentHash": "WNpu6vI2rA0pXY4r7NKxCN16XRWl5uHu6qjuyVLoDo6oYEggIQefrMjkRuibQHm/NslIUNCcKftvoWAN80MSAg==", + "dependencies": { + "Microsoft.CodeCoverage": "18.0.1", + "Microsoft.TestPlatform.TestHost": "18.0.1" + } + }, + "NSubstitute": { + "type": "Direct", + "requested": "[5.1.0, )", + "resolved": "5.1.0", + "contentHash": "ZCqOP3Kpp2ea7QcLyjMU4wzE+0wmrMN35PQMsdPOHYc2IrvjmusG9hICOiqiOTPKN0gJon6wyCn6ZuGHdNs9hQ==", + "dependencies": { + "Castle.Core": "5.1.1" + } + }, + "xunit": { + "type": "Direct", + "requested": "[2.6.6, )", + "resolved": "2.6.6", + "contentHash": "MAbOOMtZIKyn2lrAmMlvhX0BhDOX/smyrTB+8WTXnSKkrmTGBS2fm8g1PZtHBPj91Dc5DJA7fY+/81TJ/yUFZw==", + "dependencies": { + "xunit.analyzers": "1.10.0", + "xunit.assert": "2.6.6", + "xunit.core": "[2.6.6]" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[2.5.6, )", + "resolved": "2.5.6", + "contentHash": "CW6uhMXNaQQNMSG1IWhHkBT+V5eqHqn7MP0zfNMhU9wS/sgKX7FGL3rzoaUgt26wkY3bpf7pDVw3IjXhwfiP4w==" + }, + "AdaptiveCards": { + "type": "Transitive", + "resolved": "3.1.0", + "contentHash": "b+sPwH0oyAflpgxCyNPMzH92xrQjWl6GuuEBv86/VhO6iHhiWv+PtwzqMS70nOXZQRzpl9YVHXAvn+dKot5IBQ==", + "dependencies": { + "Newtonsoft.Json": "13.0.3" + } + }, + "AspNetCore.HealthChecks.SqlServer": { + "type": "Transitive", + "resolved": "8.0.2", + "contentHash": "sTcVVq7/zhfUrSTs0WAktvPdpU1He/sj14gRTogq4eFhn0oImolxNNhJczkYMgFF92RMMW+O+rlcFO7HVOpfiQ==", + "dependencies": { + "Microsoft.Data.SqlClient": "5.2.0", + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.0" + } + }, + "AspNetCore.HealthChecks.Uris": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "A1ahRx4pdXjrSGlGFLoyoXOV4Lfp5sfs+OIGfvi14RwecIAac4xs6cP0Q8tw/rv4Ng+KAaYpzD4qhxXVwUcIyA==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.0", + "Microsoft.Extensions.Http": "8.0.0" + } + }, + "AspNetCoreRateLimit": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "6fq9+o1maGADUmpK/PvcF0DtXW2+7bSkIL7MDIo/agbIHKN8XkMQF4oze60DO731WaQmHmK260hB30FwPzCmEg==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.3", + "Microsoft.Extensions.Options": "6.0.0", + "Newtonsoft.Json": "13.0.2" + } + }, + "AspNetCoreRateLimit.Redis": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "3g6Mb4Y+rW14/oE7Qt8WFA9zS7XNdHx7TH3k/XQix7PtUWEZSSAK+VsqDhDIPkUysUHFBDUA7olNeTjytpzA/g==", + "dependencies": { + "AspNetCoreRateLimit": "5.0.0", + "StackExchange.Redis": "2.6.80" + } + }, + "AutoFixture": { + "type": "Transitive", + "resolved": "4.18.1", + "contentHash": "BmWZDY4fkrYOyd5/CTBOeXbzsNwV8kI4kDi/Ty1Y5F+WDHBVKxzfWlBE4RSicvZ+EOi2XDaN5uwdrHsItLW6Kw==", + "dependencies": { + "Fare": "[2.1.1, 3.0.0)" + } + }, + "AutoFixture.AutoNSubstitute": { + "type": "Transitive", + "resolved": "4.18.1", + "contentHash": "xJxIsShO/1Ceei7BDFCobFANiw5a+enpdklgX/Xic6vKavHo9gSJO7ZGkKlf2lh+TlblTEet9mjzf9wHWIqWGQ==", + "dependencies": { + "AutoFixture": "4.18.1", + "NSubstitute": "[2.0.3, 6.0.0)" + } + }, + "AutoFixture.Xunit2": { + "type": "Transitive", + "resolved": "4.18.1", + "contentHash": "I5Cwv1bvWb0lf2x2zO42bBQ2WaGudBh7tVBCzKIf8KmRJG+hmYY7ku3znnFZDVxbQaihNaqNkztLTwK4PwaoWg==", + "dependencies": { + "AutoFixture": "4.18.1", + "xunit.extensibility.core": "[2.2.0, 3.0.0)" + } + }, + "AutoMapper": { + "type": "Transitive", + "resolved": "14.0.0", + "contentHash": "OC+1neAPM4oCCqQj3g2GJ2shziNNhOkxmNB9cVS8jtx4JbgmRzLcUOxB9Tsz6cVPHugdkHgCaCrTjjSI0Z5sCQ==", + "dependencies": { + "Microsoft.Extensions.Options": "8.0.0" + } + }, + "AWSSDK.Core": { + "type": "Transitive", + "resolved": "4.0.3.3", + "contentHash": "YQv10JuxnciWh0QwnkarSbge4gXQV1qTURf5jkBjNUH/3jYS9QrbxopA4TK1qdjfOfP37tqiJkLSrRRNqX81aw==" + }, + "AWSSDK.SimpleEmail": { + "type": "Transitive", + "resolved": "4.0.2.5", + "contentHash": "LvV5mXlvpR3fTAJysO3KmUC6bR/KUZpdkcMJ5b6lYNpStlsFN+MXcaMh34TuwYaTCgIjF3bJb4oZifFkgh+Ccw==", + "dependencies": { + "AWSSDK.Core": "[4.0.3.3, 5.0.0)" + } + }, + "AWSSDK.SQS": { + "type": "Transitive", + "resolved": "4.0.2.5", + "contentHash": "bHA9m/2RZHNKt6NGvQ56rfEDj/pfUlcwPeCg2HmPJ3jZPyoerBuEsYvFkMP3YJNv3aoycNWZoiDk0/ULP0tEyA==", + "dependencies": { + "AWSSDK.Core": "[4.0.3.3, 5.0.0)" + } + }, + "Azure.Core": { + "type": "Transitive", + "resolved": "1.47.3", + "contentHash": "u/uCNtUWT+Q/Is7/PAMy3KP9kq5vY5klRnyAvRxO/kEa5OnV3/X5lHlCajNANC7vmej6jAqceqLBJNO/VyCKzg==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "8.0.0", + "System.ClientModel": "1.6.1", + "System.Memory.Data": "8.0.1" + } + }, + "Azure.Core.Amqp": { + "type": "Transitive", + "resolved": "1.3.1", + "contentHash": "AY1ZM4WwLBb9L2WwQoWs7wS2XKYg83tp3yVVdgySdebGN0FuIszuEqCy3Nhv6qHpbkjx/NGuOTsUbF/oNGBgwA==", + "dependencies": { + "Microsoft.Azure.Amqp": "2.6.7", + "System.Memory.Data": "1.0.2" + } + }, + "Azure.Data.Tables": { + "type": "Transitive", + "resolved": "12.11.0", + "contentHash": "MabH2HegMvZA1ocaMhEfW/idyTa3CoH64s43/V9/KFRGdVqEj0EETvd3ItDe6Bbs2teiR40KE9Kz9NLDc5DJJw==", + "dependencies": { + "Azure.Core": "1.44.1" + } + }, + "Azure.Extensions.AspNetCore.DataProtection.Blobs": { + "type": "Transitive", + "resolved": "1.3.4", + "contentHash": "zS+x0MpUMSbvZD598lwAoax+ohIeSAvGlXpT71iP7FFmMZ+Tjz/8hx+jZH/RbV2cJYTYbux8XFDll7LMPuz46g==", + "dependencies": { + "Azure.Core": "1.38.0", + "Azure.Storage.Blobs": "12.16.0", + "Microsoft.AspNetCore.DataProtection": "3.1.32" + } + }, + "Azure.Identity": { + "type": "Transitive", + "resolved": "1.11.4", + "contentHash": "Sf4BoE6Q3jTgFkgBkx7qztYOFELBCo+wQgpYDwal/qJ1unBH73ywPztIJKXBXORRzAeNijsuxhk94h0TIMvfYg==", + "dependencies": { + "Azure.Core": "1.38.0", + "Microsoft.Identity.Client": "4.61.3", + "Microsoft.Identity.Client.Extensions.Msal": "4.61.3", + "System.Security.Cryptography.ProtectedData": "4.7.0" + } + }, + "Azure.Messaging.EventGrid": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "/lc0X9Na9v8mb6cY8vRIaMuQEcerHK8msmwmc3nhkY9QX4x4R8w+sDxnHM0eXezzzmEumkfUPnDWIKCEzvsl9A==", + "dependencies": { + "Azure.Core": "1.46.2", + "Azure.Messaging.EventGrid.SystemEvents": "1.0.0" + } + }, + "Azure.Messaging.EventGrid.SystemEvents": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "sGAZL0Kw3ErcPPdN3+OcMM+fTCeyGtP/No0+D7PP4tCI5VqKB2DxOlu/tdF4UYuk6YXE3XAZ/yHDjmzEaqr59g==", + "dependencies": { + "Azure.Core": "1.46.2" + } + }, + "Azure.Messaging.ServiceBus": { + "type": "Transitive", + "resolved": "7.20.1", + "contentHash": "DxCkedWPQuiXrIyFcriOhsQcZmDZW+j9d55Ev4nnK3yjMUFjlVe4Hj37fuZTJlNhC3P+7EumqBTt33R6DfOxGA==", + "dependencies": { + "Azure.Core": "1.46.2", + "Azure.Core.Amqp": "1.3.1", + "Microsoft.Azure.Amqp": "2.7.0" + } + }, + "Azure.Storage.Blobs": { + "type": "Transitive", + "resolved": "12.26.0", + "contentHash": "EBRSHmI0eNzdufcIS1Rf7Ez9M8V1Jl7pMV4UWDERDMCv513KtAVsgz2ez2FQP9Qnwg7uEQrP+Uc7vBtumlr7sQ==", + "dependencies": { + "Azure.Core": "1.47.3", + "Azure.Storage.Common": "12.25.0" + } + }, + "Azure.Storage.Blobs.Batch": { + "type": "Transitive", + "resolved": "12.23.0", + "contentHash": "1Cj2/OEPoNpcwjQZ/vtng4ImrwuDlOZhYd3mKCxQXzUe50dl0lM5AWX8KE8GGKd5pLuRKYMNmn3mRvWpv/Me+A==", + "dependencies": { + "Azure.Core": "1.47.3", + "Azure.Storage.Blobs": "12.26.0", + "Azure.Storage.Common": "12.25.0" + } + }, + "Azure.Storage.Common": { + "type": "Transitive", + "resolved": "12.25.0", + "contentHash": "MHGWp4aLHRo0BdLj25U2qYdYK//Zz21k4bs3SVyNQEmJbBl3qZ8GuOmTSXJ+Zad93HnFXfvD8kyMr0gjA8Ftpw==", + "dependencies": { + "Azure.Core": "1.47.3", + "System.IO.Hashing": "8.0.0" + } + }, + "Azure.Storage.Queues": { + "type": "Transitive", + "resolved": "12.24.0", + "contentHash": "YSR051EMu421JZNCOyOB2JpVyA4bSW8CnbTYmYlwxsYIUJuwiMy2toSXIoq9RKG9PuBtnT5dS9M6QCYNGaswAw==", + "dependencies": { + "Azure.Core": "1.47.3", + "Azure.Storage.Common": "12.25.0" + } + }, + "BitPay.Light": { + "type": "Transitive", + "resolved": "1.0.1907", + "contentHash": "QTTIgXakHrRNQPxNyH7bZ7frm0bI8N6gRDtiqVyKG/QYQ+KfjN70xt0zQ0kO0zf8UBaKuwcV5B7vvpXtzR9ijg==", + "dependencies": { + "Newtonsoft.Json": "12.0.2" + } + }, + "Bitwarden.Server.Sdk.Environment": { + "type": "Transitive", + "resolved": "0.1.0", + "contentHash": "yLK3ik/+zQ4w41iDObvv4vq2QwWPaVMuyKFpzlgsukClOOuZUxJAivwnnZZDMXbp2wipWgaW4yeEY+yLdLVaVQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.9", + "Microsoft.Extensions.Logging.Abstractions": "10.0.9", + "Microsoft.Extensions.Options": "10.0.9" + } + }, + "Bitwarden.Server.Sdk.Features": { + "type": "Transitive", + "resolved": "1.4.0", + "contentHash": "74Ir68CSZSUvZvVkb2CaUNHSAZcFMtZY5pReHz+K1eQX6+Am29iWS1i8DbQWq9F6KmCAEbqcGDyfAo4gRCurbg==", + "dependencies": { + "Bitwarden.Server.Sdk.Environment": "0.1.0", + "LaunchDarkly.ServerSdk": "8.14.1" + } + }, + "Bitwarden.Server.Sdk.WebEssentials": { + "type": "Transitive", + "resolved": "0.5.0", + "contentHash": "pMKQCluxCGueZ8DK9NS8Xfa+/oxplS9NoSxd7JrI573JsKKl7THe2yeB772S7yogzmqt+0n3GrvA56b8fKOIsQ==", + "dependencies": { + "Bitwarden.Server.Sdk.Environment": "0.1.0" + } + }, + "Bogus": { + "type": "Transitive", + "resolved": "35.6.5", + "contentHash": "2FGZn+aAVHjmCgClgmGkTDBVZk0zkLvAKGaxEf5JL6b3i9JbHTE4wnuY4vHCuzlCmJdU6VZjgDfHwmYkQF8VAA==" + }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.6.2", + "contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w==" + }, + "Braintree": { + "type": "Transitive", + "resolved": "5.36.0", + "contentHash": "K43RjhEU5qXoYYxo0C0o54msQxbdRjVP+hDMZwSXsei4fLBHA2xwS1NCooZ9girCQNjoWOM8bygkHTVGgN+sag==", + "dependencies": { + "Newtonsoft.Json": "13.0.1", + "System.Xml.XPath.XmlDocument": "4.3.0" + } + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", + "dependencies": { + "System.Diagnostics.EventLog": "6.0.0" + } + }, + "CsvHelper": { + "type": "Transitive", + "resolved": "33.1.0", + "contentHash": "kqfTOZGrn7NarNeXgjh86JcpTHUoeQDMB8t9NVa/ZtlSYiV1rxfRnQ49WaJsob4AiGrbK0XDzpyKkBwai4F8eg==" + }, + "Dapper": { + "type": "Transitive", + "resolved": "2.1.66", + "contentHash": "/q77jUgDOS+bzkmk3Vy9SiWMaetTw+NOoPAV0xPBsGVAyljd5S6P+4RUW7R3ZUGGr9lDRyPKgAMj2UAOwvqZYw==" + }, + "dbup-core": { + "type": "Transitive", + "resolved": "6.1.1", + "contentHash": "kgpuyJVEFJHoIj/slnc994Go88aoeZqNDfGHDBr4sh7CsEWwJhOTCt/FJqO4ziUImL5L0NEY0kxxOiNgPKI2Fw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0" + } + }, + "dbup-sqlserver": { + "type": "Transitive", + "resolved": "7.2.0", + "contentHash": "1xhdu2ZoQEi2nNrirBfkkfn+AbHWQvy8CGilb+5dIjghFJrsMKFM17DiI5Nz+ofWg9N1lqz5WorujzuGvc/+fQ==", + "dependencies": { + "Microsoft.Data.SqlClient": "6.1.4", + "dbup-core": "6.1.1" + } + }, + "DnsClient": { + "type": "Transitive", + "resolved": "1.8.0", + "contentHash": "RRwtaCXkXWsx0mmsReGDqCbRLtItfUbkRJlet1FpdciVhyMGKcPd57T1+8Jki9ojHlq9fntVhXQroOOgRak8DQ==" + }, + "Duende.IdentityModel": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "8i+Tv4c38LgwoTRKbD0+MqtnNNDSVA83G6JkjGHgC4/7jH0nxZBP0RBhH8xTsvNQ5Pv9zrg+TR8rmtWK9HDOPg==" + }, + "Duende.IdentityServer": { + "type": "Transitive", + "resolved": "7.4.6", + "contentHash": "Zvri5e+SrOWLz0wmJry0ZaU8gVygv966jzP/CbMSCtV0K/nqK7abL9gQr9aKKmjt5DsONauUMT2E0cK/GbXJAg==", + "dependencies": { + "Duende.IdentityServer.Storage": "7.4.6", + "Microsoft.AspNetCore.Authentication.OpenIdConnect": "10.0.0" + } + }, + "Duende.IdentityServer.Storage": { + "type": "Transitive", + "resolved": "7.4.6", + "contentHash": "qPNsoj5H1TaT5gYptA/Z5LZE/UT4PFsgDen8K1DLj4W9O8i1PuEfFiba9CbmwLPs/TUS2xikCbxfiUgBUqn8GQ==", + "dependencies": { + "Duende.IdentityModel": "8.0.0", + "Microsoft.AspNetCore.DataProtection.Abstractions": "10.0.0" + } + }, + "DuoUniversal": { + "type": "Transitive", + "resolved": "1.3.1", + "contentHash": "BZUJplORCBO1PVDFT5v7HDYAlpgHAkay5N9vzRpJ/sBm+GU44pxNlo7v95Ym0tvUeKy0WWWv/iE4FjErYoZUHQ==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "6.34.0" + } + }, + "Fare": { + "type": "Transitive", + "resolved": "2.1.1", + "contentHash": "HaI8puqA66YU7/9cK4Sgbs1taUTP1Ssa4QT2PIzqJ7GvAbN1QgkjbRsjH+FSbMh1MJdvS0CIwQNLtFT+KF6KpA==", + "dependencies": { + "NETStandard.Library": "1.6.1" + } + }, + "Fido2": { + "type": "Transitive", + "resolved": "3.0.1", + "contentHash": "S0Bz1vfcKlO4Jase3AWp5XnQ746psf4oGx5kL+D2A10j1SsjoAOAIIpanSwfi0cEepDHgk1bClcOKY5TjOzGdA==", + "dependencies": { + "Fido2.Models": "3.0.1", + "Microsoft.Extensions.Http": "6.0.0", + "NSec.Cryptography": "22.4.0", + "System.Formats.Cbor": "6.0.0", + "System.IdentityModel.Tokens.Jwt": "6.17.0" + } + }, + "Fido2.AspNet": { + "type": "Transitive", + "resolved": "3.0.1", + "contentHash": "5n5shEXD7RFUyTesjUHGDjkpgES7j4KotQo1GwUcS08k+fx+1tl/zCFHJ9RFDuUwO+S681ZILT2PyA67IPYpaA==", + "dependencies": { + "Fido2": "3.0.1", + "Fido2.Models": "3.0.1" + } + }, + "Fido2.Models": { + "type": "Transitive", + "resolved": "3.0.1", + "contentHash": "mgjcuGETuYSCUEaZG+jQeeuuEMkDLc4GDJHBvKDdOz6oSOWp5adPdWP4btZx7Pi+9fu4szN3JIjJmby67MaILw==" + }, + "Handlebars.Net": { + "type": "Transitive", + "resolved": "2.1.6", + "contentHash": "WsYWCEXsIM6hEOSOSRHtIYLjC8BnbT5MVmqhNKRqUI7qiv0t8x3nJiBTEv0ZZfvUAMAFnadGIzSsS/U2anVG1Q==" + }, + "Kralizek.AutoFixture.Extensions.MockHttp": { + "type": "Transitive", + "resolved": "2.2.1", + "contentHash": "yNpYOT8k6L9PVS2YPoAe72IjILqGfPixKDzPsAFMz2aVyrmgGjirqORQa+bQNe+Qs5ytB+p41uzy4F9mjUuP9w==", + "dependencies": { + "AutoFixture": "4.18.1", + "RichardSzalay.MockHttp": "7.0.0" + } + }, + "LaunchDarkly.Cache": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "0bEnUVFVeW1TTDXb/bW6kS3FLQTLeGtw7Xh8yt6WNO56utVmtgcrMLvcnF6yeTn+N4FXrKfW09KkLNmK8YYQvw==" + }, + "LaunchDarkly.CommonSdk": { + "type": "Transitive", + "resolved": "7.2.0", + "contentHash": "YLT9rl0ooxHVNWtSsKBjtqdBt8Ysg99sXHAO1G8dcSH4B6yv3qR0lyCo6GEb9EQpUuGdneGqO7Bs7K2B8F+lTg==", + "dependencies": { + "LaunchDarkly.Logging": "2.0.0" + } + }, + "LaunchDarkly.EventSource": { + "type": "Transitive", + "resolved": "5.3.1", + "contentHash": "bVXethnjYnjC+hYSHYhn+1QFZhwtD0NYy1MJjtfOfC6CBW9h1pzR0hEAf16DQYl/LJtMi7fwOJbQhq8lFlFrSw==", + "dependencies": { + "LaunchDarkly.Logging": "[2.0.0, 3.0.0)" + } + }, + "LaunchDarkly.InternalSdk": { + "type": "Transitive", + "resolved": "3.6.1", + "contentHash": "qkbqHzz3AruAPW5PfbwQEM883p2ArHG3xun8IONmBT/QdiAhLFqh/5vxMCUsT1GfPyyly4S0HxYx3GP01Gc3Pg==", + "dependencies": { + "LaunchDarkly.CommonSdk": "[7.2.0, 8.0.0)", + "LaunchDarkly.Logging": "[2.0.0, 3.0.0)" + } + }, + "LaunchDarkly.Logging": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "lsLKNqAZ7HIlkdTIrf4FetfRA1SUDE3WlaZQn79aSVkLjYWEhUhkDDK7hORGh4JoA3V2gXN+cIvJQax2uR/ijA==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "6.0.0" + } + }, + "LaunchDarkly.ServerSdk": { + "type": "Transitive", + "resolved": "8.14.1", + "contentHash": "ufdAP+8OYpLEnDJanErS7wXj9xRYKCY1ZXmxaYuQwTB0qik3DviMRy5ml6Ehjd8kGgWZzOnur5EwqTKM+wW/AQ==", + "dependencies": { + "LaunchDarkly.Cache": "1.0.2", + "LaunchDarkly.CommonSdk": "7.2.0", + "LaunchDarkly.EventSource": "5.3.1", + "LaunchDarkly.InternalSdk": "3.6.1", + "LaunchDarkly.Logging": "2.0.0" + } + }, + "libsodium": { + "type": "Transitive", + "resolved": "1.0.18.2", + "contentHash": "flArHoVdscSzyV8ZdPV+bqqY2TTFlaN+xZf/vIqsmHI51KVcD/mOdUPaK3n/k/wGKz8dppiktXUqSmf3AXFgig==" + }, + "linq2db": { + "type": "Transitive", + "resolved": "5.4.1", + "contentHash": "qyH32MbFK6T55KsEcQYTbPFfkOa1Mo65lY/Zo8SFVMy0pwkQBCTnA/RUxyG5+l3D/mgfPz85PH3upDrtklSMrw==" + }, + "linq2db.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "8.1.0", + "contentHash": "wEUTdkWsrtwlE3aAb4qmxNkjrZOVp39KBM+wPvEnTNXoSym6Po3u9/PWRWAsbJAGjoljv5604ACcCOp/yMJ5XQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Relational": "8.0.0", + "linq2db": "5.4.0" + } + }, + "MailKit": { + "type": "Transitive", + "resolved": "4.17.0", + "contentHash": "1nUAVLxM9fhT/78we6/AGsCesnpn5dRNLLeRqOfr52Wnk87pzVwo5YMTMyqnmoXrYc7piGhmayiMA/OgDluIjg==", + "dependencies": { + "MimeKit": "4.17.0" + } + }, + "Microsoft.AspNetCore.Authentication.JwtBearer": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "oGnE+X/SN6jdqao9WOkOIfyZ5+a0AtluJWy1Mxndq+kcWG6sx5k6l6tucu8/wJ7o9fHfLgVCzm/c4v/KVgVk6w==", + "dependencies": { + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" + } + }, + "Microsoft.AspNetCore.Authentication.OpenIdConnect": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "6ATONu+5A2oh/vzmoFhf3cuQcclMaWGHrb1kvjVsYtml+gzuWD48MmbsItM4xAUQkJZ2t8XFmbGp8pZLPxKneA==", + "dependencies": { + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" + } + }, + "Microsoft.AspNetCore.Cryptography.Internal": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "R0TKX26vPlw4kfyaLECwxn5GUIUAv6B+5s8kiEku9cl9VVCrDQDslPuhUUhN6oI/TvLj1lMFkz6AhHIpbPC3Lg==" + }, + "Microsoft.AspNetCore.Cryptography.KeyDerivation": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "eKKLdOmdyr8TrzvD9eKdcvh8OlfomZiN6FwZNlK+F8fihZJBRgNVReqt3evYvZMHK3N/0Ui6P14cubllt1cFUg==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.Internal": "10.0.8" + } + }, + "Microsoft.AspNetCore.DataProtection": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "Rlbrr3XSGB4dwnUY/pA70TpVyrQhelDAUkIiGfJ2Tm32mscTYxrRnk9Ooy1rRhGZ1g7rliJPuNzhyMMKmRH5pw==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.Internal": "10.0.8", + "Microsoft.AspNetCore.DataProtection.Abstractions": "10.0.8", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.8", + "Microsoft.Extensions.Logging.Abstractions": "10.0.8", + "Microsoft.Extensions.Options": "10.0.8", + "System.Security.Cryptography.Xml": "10.0.8" + } + }, + "Microsoft.AspNetCore.DataProtection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "NYxEmhe2tDPwwGgl0kNraUOJdOqaIYJpnZ1Lf7OlqWRf3aLagniVxMl8JSVmy0jiRtElsPYq8lTvLlbvRSwCLA==" + }, + "Microsoft.AspNetCore.Mvc.Testing": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "C9kMpUciPgx7ObqoO6W+eXEf3zHFWb7XpQgFJBzdO8GsmmVYrgcErTLMuki6e3EihycGpHbcJECYHDgM7XRMkg==", + "dependencies": { + "Microsoft.AspNetCore.TestHost": "10.0.8", + "Microsoft.Extensions.DependencyModel": "10.0.8", + "Microsoft.Extensions.Hosting": "10.0.8" + } + }, + "Microsoft.AspNetCore.TestHost": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "HRH/XAke90wkHv9ykCsrvpVqvKOUt53jQzvHHIXrPIPZWAjyPq6B5/InCmPYWvme+WKMXD10rplMAitzNMtC3w==" + }, + "Microsoft.Azure.Amqp": { + "type": "Transitive", + "resolved": "2.7.0", + "contentHash": "gm/AEakujttMzrDhZ5QpRz3fICVkYDn/oDG9SmxDP+J7R8JDBXYU9WWG7hr6wQy40mY+wjUF0yUGXDPRDRNJwQ==" + }, + "Microsoft.Azure.Cosmos": { + "type": "Transitive", + "resolved": "3.52.0", + "contentHash": "NEjNpaO19gvJrXowqHFcYPSpro5+TNjHO/JpU4VXP37by2aU2RVnmgpZsWL1GUl5wCPZ4VIOUsP1lrHpfW8ADQ==", + "dependencies": { + "Azure.Core": "1.44.1", + "Microsoft.Bcl.AsyncInterfaces": "6.0.0", + "Microsoft.Bcl.HashCode": "1.1.0", + "System.Configuration.ConfigurationManager": "6.0.0" + } + }, + "Microsoft.Azure.NotificationHubs": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "LOCxFB/sB1frfuXjecdDRDKEHkH+I1qKRatS5NyWIgYhpKhIcAlPNIJajcwLgQBShckdc3hMG9E+75CnL3qDhQ==", + "dependencies": { + "Microsoft.Extensions.Caching.Memory": "6.0.1", + "Newtonsoft.Json": "13.0.1" + } + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==" + }, + "Microsoft.Bcl.Cryptography": { + "type": "Transitive", + "resolved": "9.0.13", + "contentHash": "5T+bH3Lb1nEe8Hf/ixMxLmhlrx5wRi53wv7OhVwG2F1ZviW1ejFRS1NHur3uqPpJRGtkQwUchtY6zhVK2R+v+w==" + }, + "Microsoft.Bcl.HashCode": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "J2G1k+u5unBV+aYcwxo94ip16Rkp65pgWFb0R6zwJipzWNMgvqlWeuI7/+R+e8bob66LnSG+llLJ+z8wI94cHg==" + }, + "Microsoft.Bot.Builder": { + "type": "Transitive", + "resolved": "4.23.0", + "contentHash": "bLTrp/tfSNWDAIJ90TqyZ8JtpjCvNO7kgvhW0R5eSu72tAifwEIi2uxhFS+c8alsa2DM4EW3JSemoDgn0r6Hog==", + "dependencies": { + "Microsoft.Bot.Connector": "4.23.0", + "Microsoft.Bot.Connector.Streaming": "4.23.0", + "Microsoft.Bot.Streaming": "4.23.0", + "Microsoft.Extensions.DependencyInjection": "8.0.0", + "Microsoft.Extensions.Logging": "8.0.0" + } + }, + "Microsoft.Bot.Builder.Integration.AspNet.Core": { + "type": "Transitive", + "resolved": "4.23.0", + "contentHash": "p6xghjJfg3Vh/q2NSd77TtuvCykuEakzMaELHctf3Cw4eTILsXWIgEJ0QxyelMnJGJjgfBwFS9ZeC2hWUFYzBA==", + "dependencies": { + "Microsoft.Bot.Builder": "4.23.0", + "Microsoft.Bot.Configuration": "4.23.0", + "Microsoft.Bot.Connector.Streaming": "4.23.0", + "Microsoft.Bot.Streaming": "4.23.0", + "Microsoft.Extensions.Configuration.Binder": "8.0.2", + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.Bot.Configuration": { + "type": "Transitive", + "resolved": "4.23.0", + "contentHash": "yCzxNU5QAEQ6zy7VBNuz3GwOY8OZcDkNYOmPw/QuVzViozxuJI200BMl+a5jhY9Nd7j6bGxO7Y3mmHy4Tu7Teg==", + "dependencies": { + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.Bot.Connector": { + "type": "Transitive", + "resolved": "4.23.0", + "contentHash": "/vgAQ8LonwAnyu6CzYYElU4k65k+9V2ncwztpZtBM+IuwkhDe3iAO2ycObqcyhMMWYUV81IOb0JZYcabjiZ4NQ==", + "dependencies": { + "Microsoft.Bot.Schema": "4.23.0", + "Microsoft.Extensions.Http": "8.0.0", + "Microsoft.Extensions.Logging": "8.0.0", + "Microsoft.Identity.Client": "4.66.1", + "Microsoft.Identity.Web.Certificateless": "3.3.0", + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.1.2", + "Microsoft.Rest.ClientRuntime": "2.3.24", + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.Bot.Connector.Streaming": { + "type": "Transitive", + "resolved": "4.23.0", + "contentHash": "Yz6PcySgtje88IGEJXj6agzya48pBL34/A5Zs3/xqmHvEQlI2ypMNc3OBOo2TRGxykklCSwc60PN2k95d4pbFw==", + "dependencies": { + "Microsoft.Bot.Schema": "4.23.0", + "Microsoft.Bot.Streaming": "4.23.0", + "Microsoft.Extensions.Logging": "8.0.0", + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.Bot.Schema": { + "type": "Transitive", + "resolved": "4.23.0", + "contentHash": "HZeEXg/PuniNRIUU8ioSx/LrOXcbTZCZ1hUIqDdc6akWwhjrC2sTv7TaBpD0FlY4hDyUvj3GLyurPI/YChGPzA==", + "dependencies": { + "AdaptiveCards": "3.1.0", + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.Bot.Streaming": { + "type": "Transitive", + "resolved": "4.23.0", + "contentHash": "gYudjsFAVjwZBRU5irJh7sdsD7gNRFfZEUwWTqkNp1eGaR1t+4sJY+YyqYL3PyCMSgLrKE46bt/JWuDi3CjRfA==", + "dependencies": { + "Microsoft.Extensions.Logging": "8.0.0", + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "O+utSr97NAJowIQT/OVp3Lh9QgW/wALVTP4RG1m2AfFP4IyJmJz0ZBmFJUsRQiAPgq6IRC0t8AAzsiPIsaUDEA==" + }, + "Microsoft.Data.SqlClient": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "/oolEwtHuDtpLKU8OItOTTxVJalgPtIkcNBwzXJ3YGyrkOAvLYqMtin9Z1jxqryJpds3PjuZBF5iKIYVVYVSvQ==", + "dependencies": { + "Microsoft.Bcl.Cryptography": "9.0.13", + "Microsoft.Data.SqlClient.Extensions.Abstractions": "1.0.0", + "Microsoft.Data.SqlClient.Internal.Logging": "1.0.0", + "Microsoft.Data.SqlClient.SNI.runtime": "6.0.2", + "Microsoft.Extensions.Caching.Memory": "9.0.13", + "Microsoft.IdentityModel.JsonWebTokens": "8.16.0", + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.16.0", + "Microsoft.SqlServer.Server": "1.0.0", + "System.Configuration.ConfigurationManager": "9.0.13", + "System.Security.Cryptography.Pkcs": "9.0.13" + } + }, + "Microsoft.Data.SqlClient.Extensions.Abstractions": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "rlnxc0KfwDSbE8ZHntFnl8SCgOa9QtJZblMv2zXLhRwl1Je7fsdsVzxSjzzC4JMsfAK+jXJWyezRB8SxUY4BdA==", + "dependencies": { + "Microsoft.Data.SqlClient.Internal.Logging": "1.0.0" + } + }, + "Microsoft.Data.SqlClient.Internal.Logging": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "Kue/7CF8KNT9zozfr30C94dMZVZml3atqWZvQemSXvTau76tRdypzeKiBKXadqgbOME0UiQIyVTNo5WxCRNVNg==" + }, + "Microsoft.Data.SqlClient.SNI.runtime": { + "type": "Transitive", + "resolved": "6.0.2", + "contentHash": "f+pRODTWX7Y67jXO3T5S2dIPZ9qMJNySjlZT/TKmWVNWe19N8jcWmHaqHnnchaq3gxEKv1SWVY5EFzOD06l41w==" + }, + "Microsoft.Data.Sqlite.Core": { + "type": "Transitive", + "resolved": "8.0.8", + "contentHash": "qHInO2EvOcPhjgboP0TGnXM7rASdvWXrw6jAH8Yuz5YP82VTje7d/NKiX1i+dVbE3+G3JuW1kqNVB8yLvsqgYA==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.6" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "8.0.8", + "contentHash": "iK+jrJzkfbIxutB7or808BPmJtjUEi5O+eSM7cLDwsyde6+3iOujCSfWnrHrLxY3u+EQrJD+aD8DJ6ogPA2Rtw==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "8.0.8", + "Microsoft.EntityFrameworkCore.Analyzers": "8.0.8", + "Microsoft.Extensions.Caching.Memory": "8.0.0", + "Microsoft.Extensions.Logging": "8.0.0" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "8.0.8", + "contentHash": "9mMQkZsfL1c2iifBD8MWRmwy59rvsVtR9NOezJj7+g1j4P7g49MJHd8k8faC/v7d5KuHkQ6KOQiSItvoRt9PXA==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "8.0.8", + "contentHash": "OlAXMU+VQgLz5y5/SBkLvAa9VeiR3dlJqgIebEEH2M2NGA3evm68/Tv7SLWmSxwnEAtA3nmDEZF2pacK6eXh4Q==" + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "8.0.8", + "contentHash": "3WnrwdXxKg4L98cDx0lNEEau8U2lsfuBJCs0Yzht+5XVTmahboM7MukKfQHAzVsHUPszm6ci929S7Qas0WfVHA==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "8.0.8", + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "type": "Transitive", + "resolved": "8.0.8", + "contentHash": "IDB7Xs16hN/3VkWFCCa4r3fqoJxMVezwq418gr8dBkRBO0pxH+BX/Kjk/U3PYXDvzVLkXqUgJsHv1XoFrJbZPQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "8.0.8", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.6" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite.Core": { + "type": "Transitive", + "resolved": "8.0.8", + "contentHash": "w5k/ENj3+BPbmggqh83RRuPhhKcJmW7CmdJuGwdX1eFrmptJwnzKiHfQCPkJAu9df16PSs5YFeWrDgepfqnltA==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "8.0.8", + "Microsoft.EntityFrameworkCore.Relational": "8.0.8", + "Microsoft.Extensions.DependencyModel": "8.0.1" + } + }, + "Microsoft.EntityFrameworkCore.SqlServer": { + "type": "Transitive", + "resolved": "8.0.8", + "contentHash": "A2F52W+hnGqvprx37HcAnYnJv4QoFFdc9cxd/QGNSd1vCu1I0eAEKRd0r9KS3E5I5RRj/m9XJfYCyTdy1cdn5Q==", + "dependencies": { + "Microsoft.Data.SqlClient": "5.1.5", + "Microsoft.EntityFrameworkCore.Relational": "8.0.8" + } + }, + "Microsoft.Extensions.ApiDescription.Server": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "NCWCGiwRwje8773yzPQhvucYnnfeR+ZoB1VRIrIMp4uaeUNw7jvEPHij3HIbwCDuNCrNcphA00KSAR9yD9qmbg==" + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "EoK2TwVR1daxmfXUPnvIYZSk5XQjHe45sGekox4kvMt88KQZQhDVzYW5Na5+oNwTuRpE48hipyGJg12F1Tm70w==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.8" + } + }, + "Microsoft.Extensions.Caching.Cosmos": { + "type": "Transitive", + "resolved": "1.8.0", + "contentHash": "8UI41/U5yla1z48klbtRdXxxloAChRzhAm592bitBNzTlXBq87zeO6Lbdvuh5d6oLNCU0I01kw6ovTD9Z6y05g==", + "dependencies": { + "Microsoft.Azure.Cosmos": "3.47.0", + "Microsoft.Extensions.Caching.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "9.0.13", + "contentHash": "OdQmN8LYcUEu20Fxii9mk68nHJGL+JPXF3w0+hxenf0oDDdDBA+ZV/S92FmIgAWAElowIiFA/g0x+8YB1g80Hg==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "9.0.13", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.13", + "Microsoft.Extensions.Logging.Abstractions": "9.0.13", + "Microsoft.Extensions.Options": "9.0.13", + "Microsoft.Extensions.Primitives": "9.0.13" + } + }, + "Microsoft.Extensions.Caching.SqlServer": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "GSP1UIw/VFiLOWBOCQYwLHsLnkqqqqEC9sc8IqCXpbSkwz03EZ9u4jcFORTE4TJ/gkKXKP6L3AO+ZFZ5MFX4Gg==", + "dependencies": { + "Azure.Identity": "1.11.4", + "Microsoft.Data.SqlClient": "5.2.2", + "Microsoft.Extensions.Caching.Abstractions": "10.0.8", + "Microsoft.Extensions.Options": "10.0.8" + } + }, + "Microsoft.Extensions.Caching.StackExchangeRedis": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "fle9ns3q2kk63vt/wHFtLw1U9kiEbM42vTk2Sar8VBjiGJFkXAgm0QEKFx15YMHkJIPRVdknWaovpvgGEgn10g==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.8", + "Microsoft.Extensions.Logging.Abstractions": "10.0.8", + "Microsoft.Extensions.Options": "10.0.8", + "StackExchange.Redis": "2.7.27" + } + }, + "Microsoft.Extensions.Compliance.Abstractions": { + "type": "Transitive", + "resolved": "9.10.0", + "contentHash": "Tgu40iIg2Kr8s+BoOhb8r8kQfcagwm1VnpnMZA9fd/sD8Hlj13cNpyCfLRrYEBP+VmfmaoficQvRNEUqH+F4mw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.10", + "Microsoft.Extensions.ObjectPool": "9.0.10" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "ehZcoPbjzWzS4XFvuz7R3V55SmpdkyMqFURLH3yXaN9NtXd9tR6CGB7pd49HYtCkenl+G7ctXSFLhNI08xLfRg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.8", + "Microsoft.Extensions.Primitives": "10.0.8" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "qGhRPd3VxfLV9UqatVOiD9mAeUbj2KiMwGFYC5uXlzExiZQoe4X/hdmzGIU7BQjNLTqCnnbTHVyBglG3668/HA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.9" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "R3NN1X+kVu14uoxLEW6sBSQyhogDSbaOQzILnCtuXxBN4hx22AgjWPwZX6v/suERFkEDgU1lk12AglHTrUxhlw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.8", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.8" + } + }, + "Microsoft.Extensions.Configuration.CommandLine": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "nQXq1a4MiInYh+0VF9fguxAl06q2ftmOyYQ+5e933s4rk57xjgkbTjUdFUySzjrcrvDeWsSqlZB+TE8+TbM2HA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.8", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.8" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "bVGqctAfPGfTxJvNp8pMshtvpsUj6r6JkeiCNVIGVYO5gBxuxdN0Lbr25kEvE/zXdctkEc44g8HssnPgDnFGVA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.8", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.8" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "1g9mzuu8gIHkjYb0jLxOTQVl/QDG5nn0b0JzgT/gbgNKr6gXZzxOHRAsdYRc1eDApB7LdHR8uK5vQrNjIQdRrQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.8", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.8", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.8", + "Microsoft.Extensions.FileProviders.Physical": "10.0.8", + "Microsoft.Extensions.Primitives": "10.0.8" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "KLtAZ6A38s1pIfCO2ns6aG14NNGMYNZ4PBYfFK4M+R4A+xuSc6oklhqDcpHZxvDpyBWeFtR5C8iQBw2ng8tUHQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.8", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.8", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.8", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.8" + } + }, + "Microsoft.Extensions.Configuration.UserSecrets": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "6XTfFOnf27WY8kEeZkTZ4YNn0t+imgvdQ0YaAdR4vgURKATo9bCaVJ1KB71IOJAQtJP7Elb53VHlTNXg2CtSsA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.8", + "Microsoft.Extensions.Configuration.Json": "10.0.8", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.8", + "Microsoft.Extensions.FileProviders.Physical": "10.0.8" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "daf62xHIrq8pnE709hgaZZN9tSam9TGGepWe1+bE6V3GEuVwJiMs6ib+38lfMCyAJAHiX0vapxBhsuMSV7U+cg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "g41l/30G3K4B/d/L8kjux0+30e27c8D0FVQ/PFCpbekgfDpj9mnDhieP67EqXWvl1EWNeZh2rpR4F5B/jcDOHA==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "vLyZVpxmduO2jx+76ggqnsA3m81kwMY3NkWciNTj5E+Nvqb0VihqCvQP89QsGONWp0AJwMZG+u9GzaCjDdFGNw==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "uduyw9d3Fi+sbredO5drA1S44AQS2FRNFyn72UmB2vmQIO1qaXprpp1U/2lYhYi8yFdVERfY9sy/pxw/qPOU9w==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.8", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.8", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.8" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "86RgyFsmVslW4Nu28IXgt8tLglynGQrwjk/xhGZaTe8j6YIeR1Ywoc42hSHsBSl920CQdfqq2dBohZiGm3AkUA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9", + "Microsoft.Extensions.Options": "10.0.9" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "P9SoBuVZhJPpALZmSq72aQEb9ryP67EdquaCZGXGrrcASTNHYdrUhnpgSwIipgM5oVC+dKpRXg5zxobmF9xr5g==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "8.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "AT2qqos3IgI09ok36Qag9T8bb6kHJ3uT9Q5ki6CySybFsK6/9JbvQAgAHf1pVEjST0/N4JaFaCbm40R5edffwg==" + }, + "Microsoft.Extensions.Diagnostics.Testing": { + "type": "Transitive", + "resolved": "9.10.0", + "contentHash": "p8XnKg4yZRRpORwm5VvoBZbHeEQUmJM6OMg4psJpClkyeVLP6sc+/bqR2rnfRUyP7yhIzJB/Tmulrg6mTt8dBw==", + "dependencies": { + "Microsoft.Extensions.Logging": "9.0.10", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.10", + "Microsoft.Extensions.Telemetry.Abstractions": "9.10.0" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "Oxn4vqDk+EwceTMpZxVm7L/UZEAM1qIQlNP1+7tBZckD+P4SKrm/5X4gMTPCTdpnau/xY8Sb4/0d6onomSg4ZA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.9" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "GkPvQe6IdidLu6Q3Lw6+B8NJpW8feW8czZ5mBKt5rXM/x8MvZfEp5WvAsjznzDGd23chIDrW0b2mmt+ScnEgiw==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.8", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.8", + "Microsoft.Extensions.Primitives": "10.0.8" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "IUQet3SY51xIFcFZKtAB6a54/Zdxs7T3SQ84kJtOD6yeXfZgiOMksACWD5qtTmXGQGFH4QYGBOT0KIO8Uy/dJw==" + }, + "Microsoft.Extensions.Hosting": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "VfEyM2BipThcSd0GG/FS2ZPCVCTiosVq2zLKEDsfeMIg78sOVZPEmS7CgWlb+dqTlgXvLSL4OG2q6sM4xRhHNg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.8", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.8", + "Microsoft.Extensions.Configuration.Binder": "10.0.8", + "Microsoft.Extensions.Configuration.CommandLine": "10.0.8", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.8", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.8", + "Microsoft.Extensions.Configuration.Json": "10.0.8", + "Microsoft.Extensions.Configuration.UserSecrets": "10.0.8", + "Microsoft.Extensions.DependencyInjection": "10.0.8", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8", + "Microsoft.Extensions.Diagnostics": "10.0.8", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.8", + "Microsoft.Extensions.FileProviders.Physical": "10.0.8", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.8", + "Microsoft.Extensions.Logging": "10.0.8", + "Microsoft.Extensions.Logging.Abstractions": "10.0.8", + "Microsoft.Extensions.Logging.Configuration": "10.0.8", + "Microsoft.Extensions.Logging.Console": "10.0.8", + "Microsoft.Extensions.Logging.Debug": "10.0.8", + "Microsoft.Extensions.Logging.EventLog": "10.0.8", + "Microsoft.Extensions.Logging.EventSource": "10.0.8", + "Microsoft.Extensions.Options": "10.0.8" + } + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "Xd/2F+uWblTiUp+ssaDZN2ea4vmnHmW6PXugmqBHumyhqVkyeh6RJ3S2Zo/F+1bXIL/KuGqe2pKv6UiGOc1KeQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.9", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.9", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.9", + "Microsoft.Extensions.Logging.Abstractions": "10.0.9" + } + }, + "Microsoft.Extensions.Http": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Diagnostics": "8.0.0", + "Microsoft.Extensions.Logging": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" + } + }, + "Microsoft.Extensions.Identity.Core": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "ZOuH3nlDslon9a0kJSRBTWnHNCKoiOoaurc3H1F4D6xLT+4UDvBNAqlLkEFyQdcxZFyUvYdwgc1+D/EjsD+RXA==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "10.0.8", + "Microsoft.Extensions.Diagnostics": "10.0.8", + "Microsoft.Extensions.Logging": "10.0.8", + "Microsoft.Extensions.Options": "10.0.8" + } + }, + "Microsoft.Extensions.Identity.Stores": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "xVbg4qLWyjKSJVxtL56PQPlHu/URpWPKufhfOj61+tkCmNs6DIgnGxG8BAO/fAfacoBDDYg+p1zBjFzzj/EQog==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.8", + "Microsoft.Extensions.Identity.Core": "10.0.8", + "Microsoft.Extensions.Logging": "10.0.8" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "K60JhWC2hN/Gi7TP68tBxSzk5ACWOs7lkmPzsfA8Bcf/IXTajujt2ORMf9rSMk1bsng6Lv4Y3fuxp3bm1+15ug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.8", + "Microsoft.Extensions.Logging.Abstractions": "10.0.8", + "Microsoft.Extensions.Options": "10.0.8" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "9S/DFt4cohlMPpzIxjG6kk0L8MuN2vDm9pbMCulxtJzzk82oJHVLBd8vuQxaPskaYQwKqmFmbannf5eoChgjYg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "rxSLTO7xTbcC3DuEJHNEijBr8g14Jj62zQ+DeFu68bsoTYoU8jLcMhc1735PV21bESXsATlL5LsfaWH71FOWAg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.8", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.8", + "Microsoft.Extensions.Configuration.Binder": "10.0.8", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8", + "Microsoft.Extensions.Logging": "10.0.8", + "Microsoft.Extensions.Logging.Abstractions": "10.0.8", + "Microsoft.Extensions.Options": "10.0.8", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.8" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "6cv53sHsPnFS56PJw8X4GbNcjeX1KGyFJRxJWvxOgK63cnqeSB1k1eRwjUdkse0tBhwlH6qc9EOYDlan+CYTuw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8", + "Microsoft.Extensions.Logging": "10.0.8", + "Microsoft.Extensions.Logging.Abstractions": "10.0.8", + "Microsoft.Extensions.Logging.Configuration": "10.0.8", + "Microsoft.Extensions.Options": "10.0.8" + } + }, + "Microsoft.Extensions.Logging.Debug": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "4HW3M1lGHHDwEYcDZHRNptBQ48LCI2yW+XV4vuxdfQUqafTpVT8j9RqAsez08krZKhIiaArWu8iQq5uRKZ9Ffg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8", + "Microsoft.Extensions.Logging": "10.0.8", + "Microsoft.Extensions.Logging.Abstractions": "10.0.8" + } + }, + "Microsoft.Extensions.Logging.EventLog": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "kK/C3SLIoGrcZvddYQw4eMm6YaROiSYBO7YgUR5Hdv5l+GIjBmbvQK5cST2FqjeubiAOPqFEimBT2N/8wVI+3A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8", + "Microsoft.Extensions.Logging": "10.0.8", + "Microsoft.Extensions.Logging.Abstractions": "10.0.8", + "Microsoft.Extensions.Options": "10.0.8", + "System.Diagnostics.EventLog": "10.0.8" + } + }, + "Microsoft.Extensions.Logging.EventSource": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "HX2M0MgzwQM8jpLe3AYAEMd0YsUfOP5RgGrDuk+Ki9n7HSuMbvLm9TEV3qRI3Pg9aqxc56GfgK/KdMRBhfWwKw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8", + "Microsoft.Extensions.Logging": "10.0.8", + "Microsoft.Extensions.Logging.Abstractions": "10.0.8", + "Microsoft.Extensions.Options": "10.0.8", + "Microsoft.Extensions.Primitives": "10.0.8" + } + }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "9.0.10", + "contentHash": "tw0jYoEdRp2AQMBYTkdCy0OKWcNaazaFQgo4KzdayTkX2N00g2hAacGd9mls4nBz6clP+87eeD0ucWyDrz+VKg==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "hyNdX4c2UwkRkzb9byw0H2DQkRzwBM3mzY2sCM9egwzTyg8dvQJmp5noQHGEaaCORQrNK3DD2gREBsc2DlXS4A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9", + "Microsoft.Extensions.Primitives": "10.0.9" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "VOapXeO3lhBH0zYoyAH7tjapuo4V5pTHlevPpiSHueEquAajqd5nF0mttm+h/uE/exwAEuM5s26SzOJtletE3w==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.8", + "Microsoft.Extensions.Configuration.Binder": "10.0.8", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8", + "Microsoft.Extensions.Options": "10.0.8", + "Microsoft.Extensions.Primitives": "10.0.8" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "fmEbAUFsaIKirgLt/lYhuFRBwhcSJN31jjHgCdbQxJiWOum6EdLjkbgGuukSP9z/a+9LibaxII/kF+GwOXgC4g==" + }, + "Microsoft.Extensions.Telemetry.Abstractions": { + "type": "Transitive", + "resolved": "9.10.0", + "contentHash": "hJflG5if8NqElmybxXDf38d4EPopOo9H+Qg6l5LKTsavqE4CFdA5DIPb9+jjAeL22FN+rs6KuuEIuBPS4PNXvw==", + "dependencies": { + "Microsoft.Extensions.Compliance.Abstractions": "9.10.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.10", + "Microsoft.Extensions.ObjectPool": "9.0.10", + "Microsoft.Extensions.Options": "9.0.10" + } + }, + "Microsoft.Extensions.TimeProvider.Testing": { + "type": "Transitive", + "resolved": "10.6.0", + "contentHash": "qQDiaYWpvIymGbu+kXaMDS8YdqfeQkv6DOxPF2GSwC+eSzIKqOOnSP34TYt7gKqvB7p8/aSptexnW6nF0CUdnw==" + }, + "Microsoft.Identity.Client": { + "type": "Transitive", + "resolved": "4.66.1", + "contentHash": "mE+m3pZ7zSKocSubKXxwZcUrCzLflC86IdLxrVjS8tialy0b1L+aECBqRBC/ykcPlB4y7skg49TaTiA+O2UfDw==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "6.35.0" + } + }, + "Microsoft.Identity.Client.Extensions.Msal": { + "type": "Transitive", + "resolved": "4.61.3", + "contentHash": "PWnJcznrSGr25MN8ajlc2XIDW4zCFu0U6FkpaNLEWLgd1NgFCp5uDY3mqLDgM8zCN8hqj8yo5wHYfLB2HjcdGw==", + "dependencies": { + "Microsoft.Identity.Client": "4.61.3", + "System.Security.Cryptography.ProtectedData": "4.5.0" + } + }, + "Microsoft.Identity.Web.Certificateless": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "ybEVPCLeJFuTCDVTtt3OlD5n+CYQgUAzmn0YZw+Z4NR5XwB4iGQ/zMqQ+ruJfgoKGWe6BTl0vCfsv1O4XPqCvg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "2.1.0", + "Microsoft.Identity.Client": "4.66.1", + "Microsoft.IdentityModel.JsonWebTokens": "8.1.2" + } + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "prBU72cIP4V8E9fhN+o/YdskTsLeIcnKPbhZf0X6mD7fdxoZqnS/NdEkSr+9Zp+2q7OZBOMfNBKGbTbhXODO4w==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.16.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.16.0" + } + }, + "Microsoft.IdentityModel.Protocols": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "UFrU7d46UTsPQTa2HIEIpB9H1uJe1BW9FLw5uhEJ2ZuKdur8bcUA/bO5caq5dlBt5gNJeRIB3QQXYNs5fCQCZA==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.16.0" + } + }, + "Microsoft.IdentityModel.Protocols.OpenIdConnect": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "h4yVXyJsEBBX5lg2G5ftMsi5JzcNEGAzrNphA6DQ6eOd8P0s+cDCOyPwVTYLePZvJL5unbPvYIvzrbTXzFjXnQ==", + "dependencies": { + "Microsoft.IdentityModel.Protocols": "8.16.0", + "System.IdentityModel.Tokens.Jwt": "8.16.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Microsoft.IdentityModel.Logging": "8.16.0" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "Microsoft.OpenApi": { + "type": "Transitive", + "resolved": "2.4.1", + "contentHash": "u7QhXCISMQuab3flasb1hoaiERmUqyWsW7tmQODyILoQ7mJV5IRGM+2KKZYo0QUfC13evEOcHAb6TPWgqEQtrw==" + }, + "Microsoft.Rest.ClientRuntime": { + "type": "Transitive", + "resolved": "2.3.24", + "contentHash": "hZH7XgM3eV2jFrnq7Yf0nBD4WVXQzDrer2gEY7HMNiwio2hwDsTHO6LWuueNQAfRpNp4W7mKxcXpwXUiuVIlYw==", + "dependencies": { + "Newtonsoft.Json": "10.0.3" + } + }, + "Microsoft.SqlServer.Server": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "N4KeF3cpcm1PUHym1RmakkzfkEv3GRMyofVv40uXsQhCQeglr2OHNcUk2WOG51AKpGO8ynGpo9M/kFXSzghwug==" + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==" + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "uDJKAEjFTaa2wHdWlfo6ektyoh+WD4/Eesrwb4FpBFKsLGehhACVnwwTI4qD3FrIlIEPlxdXg3SyrYRIcO+RRQ==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.0.1", + "Newtonsoft.Json": "13.0.3" + } + }, + "MimeKit": { + "type": "Transitive", + "resolved": "4.17.0", + "contentHash": "h/KXsCreJf8RpR/PSAtlbvYtZFndp9N8Wn1Bs248AL7ITyhn+AiIY5bLTvt60tbN2mu6g8KadtBNWygTji2lqg==", + "dependencies": { + "BouncyCastle.Cryptography": "2.6.2", + "System.Security.Cryptography.Pkcs": "10.0.0" + } + }, + "MySqlConnector": { + "type": "Transitive", + "resolved": "2.3.5", + "contentHash": "AmEfUPkFl+Ev6jJ8Dhns3CYHBfD12RHzGYWuLt6DfG6/af6YvOMyPz74ZPPjBYQGRJkumD2Z48Kqm8s5DJuhLA==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "7.0.1" + } + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "Npgsql": { + "type": "Transitive", + "resolved": "8.0.3", + "contentHash": "6WEmzsQJCZAlUG1pThKg/RmeF6V+I0DmBBBE/8YzpRtEzhyZzKcK7ulMANDm5CkxrALBEC8H+5plxHWtIL7xnA==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "Transitive", + "resolved": "8.0.4", + "contentHash": "/hHd9MqTRVDgIpsToCcxMDxZqla0HAQACiITkq1+L9J2hmHKV6lBAPlauF+dlNSfHpus7rrljWx4nAanKD6qAw==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "8.0.4", + "Microsoft.EntityFrameworkCore.Abstractions": "8.0.4", + "Microsoft.EntityFrameworkCore.Relational": "8.0.4", + "Npgsql": "8.0.3" + } + }, + "NSec.Cryptography": { + "type": "Transitive", + "resolved": "22.4.0", + "contentHash": "lEntcPYd7h3aZ8xxi/y/4TML7o8w0GEGqd+w4L1omqFLbdCBmhxJAeO2YBmv/fXbJKgKCQLm7+TD4bR605PEUQ==", + "dependencies": { + "libsodium": "[1.0.18.2, 1.0.19)" + } + }, + "OneOf": { + "type": "Transitive", + "resolved": "3.0.271", + "contentHash": "pqpqeK8xQGggExhr4tesVgJkjdn+9HQAO0QgrYV2hFjE3y90okzk1kQMntMiUOGfV7FrCUfKPaVvPBD4IANqKg==" + }, + "OpenTelemetry": { + "type": "Transitive", + "resolved": "1.15.3", + "contentHash": "N0i6WjPoHPbZyms1ugbDIFAJFuGlpeExJMU/+XSL0lQRUkg/D0utFkDoLXf8Z1km5B+xVZ2GyMXXiX8qdeNmPg==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.3" + } + }, + "OpenTelemetry.Api": { + "type": "Transitive", + "resolved": "1.15.3", + "contentHash": "fX+fkCysfPut+qCcT3bKqyX4QN9Saf4CgX8HLOHywEVD+Xr7sULtfuypITpoDysjx8R59dn/3mWhgimMH8cm/g==" + }, + "OpenTelemetry.Api.ProviderBuilderExtensions": { + "type": "Transitive", + "resolved": "1.15.3", + "contentHash": "SYn0lqYDwLMWhv/zlNGsQcl2yX++yTumanX46bmOZE/ZDOd1WjPBO2kZaZgKLEZTZk48pavIFGJ6vOvxXgWVFQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "OpenTelemetry.Api": "1.15.3" + } + }, + "OpenTelemetry.Exporter.OpenTelemetryProtocol": { + "type": "Transitive", + "resolved": "1.15.3", + "contentHash": "FEXJepcseTGbATiCkUfP7ipoFEYYfl/0UmmUwi0KxCPg9PaUA8ab2P1LGopK+/HExasJ1ZutFhZrN6WvUIR23g==", + "dependencies": { + "OpenTelemetry": "1.15.3" + } + }, + "OpenTelemetry.Extensions.Hosting": { + "type": "Transitive", + "resolved": "1.15.3", + "contentHash": "u8n/W8yIlqv0BXZmvId1iVaeWXG42tGKdTkuLYg5g57Y/r9CeUNzqtrSHNdG5IoO8iPX79w3v+WsbAHgUQbfeg==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "OpenTelemetry": "1.15.3" + } + }, + "OpenTelemetry.Instrumentation.AspNetCore": { + "type": "Transitive", + "resolved": "1.15.2", + "contentHash": "2nPd7r0ug/gd6/CNFL6Rlu+RSQ9WYGSGHAYQ1ssbSqyzKJpqTunfx2I/1O0WB5k+L0cyXbG4XVZpoSoUc3M7wg==", + "dependencies": { + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.3, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "1.12.0-beta.2", + "contentHash": "4D2PLiJWbBbQbauojkIflT11WGVXoRU+xgox1mvOkpfm7YXIfwTtROOlcdscS51sMh5fgwjGKJtLWpLKppe7dw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.12.0, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.Http": { + "type": "Transitive", + "resolved": "1.15.1", + "contentHash": "vFO4Fj/dXkoVNGo/nhoGpO2zYQmZwr4jTID7oRGo+XlQ8LqksyZjUXQ4p39RfUvTID7IzzL8Qe71tW7CcAFymA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.3, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.Runtime": { + "type": "Transitive", + "resolved": "1.15.1", + "contentHash": "cpPwlUT5HXcLGPaIgsbSy0W9eFYAPGVbTP1p8/uyQ4Osvf5BJuPpEXE7crL09SmEd44r0DGNKDtsqxaAz0HxQw==", + "dependencies": { + "OpenTelemetry.Api": "[1.15.3, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.SqlClient": { + "type": "Transitive", + "resolved": "1.15.2", + "contentHash": "pRB84YJEXD121kygc6CtcF46dtLuTKIjphA2SJye32G4g2Xl3epkCrOcPCOAg4WDagCdMGa16Ge6PPVzFJZsAQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.3, 2.0.0)" + } + }, + "Otp.NET": { + "type": "Transitive", + "resolved": "1.4.0", + "contentHash": "Fk1NKc0lWmlo6LAFYpFJInRgFKt72knRNEvxndDYoQHFwYOPXav+WEUBvQA0k4lxq5xt0SymrZ+oi0F/G40bPQ==" + }, + "Pipelines.Sockets.Unofficial": { + "type": "Transitive", + "resolved": "2.2.8", + "contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==" + }, + "Pomelo.EntityFrameworkCore.MySql": { + "type": "Transitive", + "resolved": "8.0.2", + "contentHash": "XjnlcxVBLnEMbyEc5cZzgZeDyLvAniACZQ04W1slWN0f4rmfNzl98gEMvHnFH0fMDF06z9MmgGi/Sr7hJ+BVnw==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Relational": "[8.0.2, 8.0.999]", + "MySqlConnector": "2.3.5" + } + }, + "Quartz": { + "type": "Transitive", + "resolved": "3.15.1", + "contentHash": "XIbhzUAKSm3xdl1ORLPnK7mc5XANP3cuvYQhCtuX/8888IN41e9OXJak4R9OlmAGRnyAMqHE40yojVa89NS1wg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "2.1.1" + } + }, + "Quartz.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "3.15.1", + "contentHash": "LinB9z54aPn49C/DGM1v3OflX2nosrEo4zNz10vfYqcCndFJ8MNU9k++Ap9T7vxeZc355WStPDggpX60TYj1Lg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0", + "Quartz": "3.15.1" + } + }, + "Quartz.Extensions.Hosting": { + "type": "Transitive", + "resolved": "3.15.1", + "contentHash": "svqLTEnVLb0VPUcNCd/khRqagwxM/yybUZ2sEOd7HFdPO+5dAOttL+ARtXSyBeaGWPWAaxY4VvU7pJTZzYhORw==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "9.0.0", + "Quartz.Extensions.DependencyInjection": "3.15.1" + } + }, + "RabbitMQ.Client": { + "type": "Transitive", + "resolved": "7.1.2", + "contentHash": "y3c6ulgULScWthHw5PLM1ShHRLhxg0vCtzX/hh61gRgNecL3ZC3WoBW2HYHoXOVRqTl99Br9E7CZEytGZEsCyQ==", + "dependencies": { + "System.Threading.RateLimiting": "8.0.0" + } + }, + "RichardSzalay.MockHttp": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "QwnauYiaywp65QKFnP+wvgiQ2D8Pv888qB2dyfd7MSVDF06sIvxqASenk+RxsWybyyt+Hu1Y251wQxpHTv3UYg==" + }, + "SendGrid": { + "type": "Transitive", + "resolved": "9.29.3", + "contentHash": "nb/zHePecN9U4/Bmct+O+lpgK994JklbCCNMIgGPOone/DngjQoMCHeTvkl+m0Nglvm0dqMEshmvB4fO8eF3dA==", + "dependencies": { + "Newtonsoft.Json": "13.0.1", + "starkbank-ecdsa": "[1.3.3, 2.0.0)" + } + }, + "Serilog": { + "type": "Transitive", + "resolved": "2.10.0", + "contentHash": "+QX0hmf37a0/OZLxM3wL7V6/ADvC1XihXN4Kq/p6d8lCPfgkRdiuhbWlMaFjR9Av0dy5F0+MBeDmDdRZN/YwQA==" + }, + "Serilog.Extensions.Logging": { + "type": "Transitive", + "resolved": "3.1.0", + "contentHash": "IWfem7wfrFbB3iw1OikqPFNPEzfayvDuN4WP7Ue1AVFskalMByeWk3QbtUXQR34SBkv1EbZ3AySHda/ErDgpcg==", + "dependencies": { + "Microsoft.Extensions.Logging": "2.0.0", + "Serilog": "2.9.0" + } + }, + "Serilog.Extensions.Logging.File": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "bUYjMHn7NhpK+/8HDftG7+G5hpWzD49XTSvLoUFZGgappDa6FoseqFOsLrjLRjwe1zM+igH5mySFJv3ntb+qcg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", + "Microsoft.Extensions.Configuration.Binder": "6.0.0", + "Serilog": "2.10.0", + "Serilog.Extensions.Logging": "3.1.0", + "Serilog.Formatting.Compact": "1.1.0", + "Serilog.Sinks.Async": "1.5.0", + "Serilog.Sinks.RollingFile": "3.3.0" + } + }, + "Serilog.Formatting.Compact": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "pNroKVjo+rDqlxNG5PXkRLpfSCuDOBY0ri6jp9PLe505ljqwhwZz8ospy2vWhQlFu5GkIesh3FcDs4n7sWZODA==", + "dependencies": { + "Serilog": "2.8.0" + } + }, + "Serilog.Sinks.Async": { + "type": "Transitive", + "resolved": "1.5.0", + "contentHash": "csHYIqAwI4Gy9oAhXYRwxGrQEAtBg3Ep7WaCzsnA1cZuBZjVAU0n7hWaJhItjO7hbLHh/9gRVxALCUB4Dv+gZw==", + "dependencies": { + "Serilog": "2.9.0" + } + }, + "Serilog.Sinks.File": { + "type": "Transitive", + "resolved": "3.2.0", + "contentHash": "VHbo68pMg5hwSWrzLEdZv5b/rYmIgHIRhd4d5rl8GnC5/a8Fr+RShT5kWyeJOXax1el6mNJ+dmHDOVgnNUQxaw==", + "dependencies": { + "Serilog": "2.3.0" + } + }, + "Serilog.Sinks.RollingFile": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "2lT5X1r3GH4P0bRWJfhA7etGl8Q2Ipw9AACvtAHWRUSpYZ42NGVyHoVs2ALBZ/cAkkS+tA4jl80Zie144eLQPg==", + "dependencies": { + "Serilog.Sinks.File": "3.2.0" + } + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.6", + "contentHash": "BmAf6XWt4TqtowmiWe4/5rRot6GerAeklmOPfviOvwLoF5WwgxcJHAxZtySuyW9r9w+HLILnm8VfJFLCUJYW8A==", + "dependencies": { + "SQLitePCLRaw.lib.e_sqlite3": "2.1.6", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.6" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.1.6", + "contentHash": "wO6v9GeMx9CUngAet8hbO7xdm+M42p1XeJq47ogyRoYSvNSp0NGLI+MgC0bhrMk9C17MTVFlLiN6ylyExLCc5w==" + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.6", + "contentHash": "2ObJJLkIUIxRpOUlZNGuD4rICpBnrBR5anjyfUFQep4hMOIeqW+XGQYzrNmHSVz5xSWZ3klSbh7sFR6UyDj68Q==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.6", + "contentHash": "PQ2Oq3yepLY4P7ll145P3xtx2bX8xF4PzaKPRpw9jZlKvfe4LE/saAV82inND9usn1XRpmxXk7Lal3MTI+6CNg==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.6" + } + }, + "StackExchange.Redis": { + "type": "Transitive", + "resolved": "2.8.31", + "contentHash": "RCHVQa9Zke8k0oBgJn1Yl6BuYy8i6kv+sdMObiH60nOwD6QvWAjxdDwOm+LO78E8WsGiPqgOuItkz98fPS6haQ==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Pipelines.Sockets.Unofficial": "2.2.8" + } + }, + "starkbank-ecdsa": { + "type": "Transitive", + "resolved": "1.3.3", + "contentHash": "OblOaKb1enXn+dSp7tsx9yjwV+/BEKM9jFhshIkZTwCk7LuTFTp+wSon6rFzuPiIiTGtvVWQNUw2slHjGktJog==" + }, + "Stripe.net": { + "type": "Transitive", + "resolved": "48.5.0", + "contentHash": "wOAZYR0EnrLMok/ScfVOpTxjci+n3vFP0A7w/BE63yJdkRSDwZVCJIhlOjeJvgyQnMX8ZbwDAHMaxaiDa0Z5TA==", + "dependencies": { + "Newtonsoft.Json": "13.0.3", + "System.Configuration.ConfigurationManager": "8.0.0" + } + }, + "Swashbuckle.AspNetCore": { + "type": "Transitive", + "resolved": "10.1.7", + "contentHash": "vgef8DPT411JU5JjHiDbr0WOxsIVuAvegPGtqmm4Na4JRl/264dfBJcGkiPHsAr5P+Vda+qN1rZKRtBl1rF9aA==", + "dependencies": { + "Microsoft.Extensions.ApiDescription.Server": "10.0.0", + "Swashbuckle.AspNetCore.Swagger": "10.1.7", + "Swashbuckle.AspNetCore.SwaggerGen": "10.1.7", + "Swashbuckle.AspNetCore.SwaggerUI": "10.1.7" + } + }, + "Swashbuckle.AspNetCore.Swagger": { + "type": "Transitive", + "resolved": "10.1.7", + "contentHash": "EjLibt/d/QuRv170GoihTbcPUpgzSFm2WKHhnGJFZQ03JYzfuitsM79azaAR8NBwRunU7yScSX6HRE5JUlrEMQ==", + "dependencies": { + "Microsoft.OpenApi": "2.4.1" + } + }, + "Swashbuckle.AspNetCore.SwaggerGen": { + "type": "Transitive", + "resolved": "10.1.7", + "contentHash": "PuubO9BjvNn6U3D9kLpuWKY1JtziWw7SsGBq0age1E50uQjQ8Fzl8s0EwzrLfANqYJNgDnJi9l7N1QxcGVB2Zw==", + "dependencies": { + "Swashbuckle.AspNetCore.Swagger": "10.1.7" + } + }, + "Swashbuckle.AspNetCore.SwaggerUI": { + "type": "Transitive", + "resolved": "10.1.7", + "contentHash": "iJo3ODyUb/M8Vm8AH1r9y9iAba0w95xsCn3zFVl96ISRHbTDWxi+l7oFVCZqUEdjd97B8VMDPnMliWAdomR8uw==" + }, + "System.ClientModel": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "xcHHhDqB5MnOOY8yIn64Vzp6gtBEs6k5J1hluG04CrShSvQNXOx4PSDs7wJiXLDidlY/FZJmxJdKTKskyJwjvw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.3", + "System.Memory.Data": "8.0.1" + } + }, + "System.Configuration.ConfigurationManager": { + "type": "Transitive", + "resolved": "9.0.13", + "contentHash": "GbBrJq9S/gYpHzm7Pxx6Y5tDyfSfyxW6tlP5oiKJV38uf19Wp+GIIAnWfyL1zmNiz1+EjwVapw2WkBFvvqKQzg==", + "dependencies": { + "System.Diagnostics.EventLog": "9.0.13", + "System.Security.Cryptography.ProtectedData": "9.0.13" + } + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "+Ro7WgIom+BDNH+YhTuZKL6QJ0ctfOpTyfUG/h3aU5KwXt3OaNf0wYWrTvoBUj+34Dy5V8dN9yCco1hAJQ4txw==" + }, + "System.Formats.Cbor": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "mGaLOoiw7KurJagOOcIsWUoCT5ACIiGxKlCcbYQASefBGXjnCcKTq5Hdjb94eEAKg38zXKlHw4c6EjzgBl9dIw==" + }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "rrs2u7DRMXQG2yh0oVyF/vLwosfRv20Ld2iEpYcKwQWXHjfV+gFXNQsQ9p008kR9Ou4pxBs68Q6/9zC8Gi1wjg==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "8.16.0", + "Microsoft.IdentityModel.Tokens": "8.16.0" + } + }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "ne1843evDugl0md7Fjzy6QjJrzsjh46ZKbhf8GwBXb5f/gw97J4bxMs0NQKifDuThh/f0bZ0e62NPl1jzTuRqA==" + }, + "System.Memory.Data": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "BVYuec3jV23EMRDeR7Dr1/qhx7369dZzJ9IWy2xylvb4YfXsrUxspWc4UWYid/tj4zZK58uGZqn2WQiaDMhmAg==" + }, + "System.Security.Cryptography.Pkcs": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "2wOycCqMyg9Tu+SDP03FFwDEWBni/3xOKgt0bRXplOvyeIcUJmWO7m3gTCF2mIdtQLROLtOP5VwWRT8YBwP/bA==" + }, + "System.Security.Cryptography.ProtectedData": { + "type": "Transitive", + "resolved": "9.0.13", + "contentHash": "t8S9IDpjJKsLpLkeBdW8cWtcPyYqrGu93Dej1RO6WwuL/lkFSqWlan3rMJfortqz1mRIh+sys2AFsSA6jWJ3Jg==" + }, + "System.Security.Cryptography.Xml": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "Fb+L55vEJaf8RCOzrzN564sfyCL8SZEfce9z6XkuhHg+294SBfyS4fIoLU3EljofDCkp+EKDeleI8ug5WO2NtA==", + "dependencies": { + "System.Security.Cryptography.Pkcs": "10.0.8" + } + }, + "System.Threading.RateLimiting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" + }, + "System.Xml.XPath.XmlDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "A/uxsWi/Ifzkmd4ArTLISMbfFs6XpRPsXZonrIqyTY70xi8t+mDtvSM5Os0RqyRDobjMBwIDHDL4NOIbkDwf7A==" + }, + "xunit.abstractions": { + "type": "Transitive", + "resolved": "2.0.3", + "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "1.10.0", + "contentHash": "Lw8CiDy5NaAWcO6keqD7iZHYUTIuCOcoFrUHw5Sv84ITZ9gFeDybdkVdH0Y2maSlP9fUjtENyiykT44zwFQIHA==" + }, + "xunit.assert": { + "type": "Transitive", + "resolved": "2.6.6", + "contentHash": "74Cm9lAZOk5TKCz2MvCBCByKsS23yryOKDIMxH3XRDHXmfGM02jKZWzRA7g4mGB41GnBnv/pcWP3vUYkrCtEcg==" + }, + "xunit.core": { + "type": "Transitive", + "resolved": "2.6.6", + "contentHash": "tqi7RfaNBqM7t8zx6QHryuBPzmotsZXKGaWnopQG2Ez5UV7JoWuyoNdT6gLpDIcKdGYey6YTXJdSr9IXDMKwjg==", + "dependencies": { + "xunit.extensibility.core": "[2.6.6]", + "xunit.extensibility.execution": "[2.6.6]" + } + }, + "xunit.extensibility.core": { + "type": "Transitive", + "resolved": "2.6.6", + "contentHash": "ty6VKByzbx4Toj4/VGJLEnlmOawqZiMv0in/tLju+ftA+lbWuAWDERM+E52Jfhj4ZYHrAYVa14KHK5T+dq0XxA==", + "dependencies": { + "xunit.abstractions": "2.0.3" + } + }, + "xunit.extensibility.execution": { + "type": "Transitive", + "resolved": "2.6.6", + "contentHash": "UDjIVGj2TepVKN3n32/qXIdb3U6STwTb9L6YEwoQO2A8OxiJS5QAVv2l1aT6tDwwv/9WBmm8Khh/LyHALipcng==", + "dependencies": { + "xunit.extensibility.core": "[2.6.6]" + } + }, + "YubicoDotNetClient": { + "type": "Transitive", + "resolved": "1.2.0", + "contentHash": "uP5F3Ko1gqZi3lwS2R/jAAwhBxXs/6PKDpS6FdQjsBA5qmF0hQmbtfxM6QHTXOMoWbUtfetG7+LtgmG8T5zDIg==", + "dependencies": { + "NETStandard.Library": "1.6.1" + } + }, + "ZiggyCreatures.FusionCache": { + "type": "Transitive", + "resolved": "2.0.2", + "contentHash": "nO6ysiVP/1S1zVZMzsK0xASeSUay27iIlK8GjeyTpIAmq5P4/0KOzV9AqlabZYFgzeQDAu5IcB39ela2w/HCwQ==", + "dependencies": { + "Microsoft.Extensions.Caching.Memory": "8.0.1" + } + }, + "ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis": { + "type": "Transitive", + "resolved": "2.0.2", + "contentHash": "E9KfGnhY+xcy8bmxoB/jJbfYwBlQwDD6c0v0P/Qr3IxGyJXyncza/45vFo5nI+5CggqczQ1GwQeEJqk0Lg8Q5g==", + "dependencies": { + "StackExchange.Redis": "2.8.31", + "ZiggyCreatures.FusionCache": "2.0.2" + } + }, + "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": { + "type": "Transitive", + "resolved": "2.0.2", + "contentHash": "gt5ia5PHpCxhnI0hr51Y6L/acrrU17OuBqiU3vJPFXZxS3R1pIj2hr6WGB5fzW64VZDVdHTYsD5gTr2Pu8I+QQ==", + "dependencies": { + "ZiggyCreatures.FusionCache": "2.0.2" + } + }, + "api": { + "type": "Project", + "dependencies": { + "AspNetCore.HealthChecks.SqlServer": "[8.0.2, 8.0.2]", + "AspNetCore.HealthChecks.Uris": "[8.0.1, 8.0.1]", + "Azure.Messaging.EventGrid": "[5.0.0, 5.0.0]", + "Bitwarden.Server.Sdk.Environment": "[0.1.0, )", + "Bitwarden.Server.Sdk.Features": "[1.4.0, )", + "Bitwarden.Server.Sdk.WebEssentials": "[0.5.0, )", + "Commercial.Core": "[2026.8.0, )", + "Commercial.Infrastructure.EntityFramework": "[2026.8.0, )", + "Core": "[2026.8.0, )", + "HttpExtensions": "[2026.8.0, )", + "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", + "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", + "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.2, )", + "OpenTelemetry.Instrumentation.EntityFrameworkCore": "[1.12.0-beta.2, )", + "OpenTelemetry.Instrumentation.Http": "[1.15.1, )", + "OpenTelemetry.Instrumentation.Runtime": "[1.15.1, )", + "OpenTelemetry.Instrumentation.SqlClient": "[1.15.2, )", + "OrganizationAuthorization": "[0.0.1, )", + "Pam": "[2026.8.0, )", + "SharedWeb": "[2026.8.0, )", + "Swashbuckle.AspNetCore": "[10.1.7, 10.1.7]" + } + }, + "api.integrationtest": { + "type": "Project", + "dependencies": { + "Api": "[2026.8.0, )", + "IntegrationTestCommon": "[2026.8.0, )", + "Microsoft.Extensions.Diagnostics.Testing": "[9.10.0, 9.10.0]", + "Microsoft.NET.Test.Sdk": "[18.0.1, )", + "Seeder": "[2026.8.0, )", + "xunit": "[2.6.6, )" + } + }, + "commercial.core": { + "type": "Project", + "dependencies": { + "Core": "[2026.8.0, )", + "CsvHelper": "[33.1.0, 33.1.0]" + } + }, + "commercial.infrastructure.entityframework": { + "type": "Project", + "dependencies": { + "AutoMapper": "[14.0.0, 14.0.0]", + "Core": "[2026.8.0, )", + "Infrastructure.EntityFramework": "[2026.8.0, )" + } + }, + "common": { + "type": "Project", + "dependencies": { + "AutoFixture.AutoNSubstitute": "[4.18.1, )", + "AutoFixture.Xunit2": "[4.18.1, )", + "Core": "[2026.8.0, )", + "Kralizek.AutoFixture.Extensions.MockHttp": "[2.2.1, 2.2.1]", + "Microsoft.Extensions.TimeProvider.Testing": "[10.6.0, 10.6.0]", + "Microsoft.NET.Test.Sdk": "[18.0.1, )", + "NSubstitute": "[5.1.0, )", + "xunit": "[2.6.6, )" + } + }, + "core": { + "type": "Project", + "dependencies": { + "AWSSDK.SQS": "[4.0.2.5, 4.0.2.5]", + "AWSSDK.SimpleEmail": "[4.0.2.5, 4.0.2.5]", + "AspNetCoreRateLimit": "[5.0.0, 5.0.0]", + "AspNetCoreRateLimit.Redis": "[2.0.0, 2.0.0]", + "Azure.Data.Tables": "[12.11.0, 12.11.0]", + "Azure.Extensions.AspNetCore.DataProtection.Blobs": "[1.3.4, 1.3.4]", + "Azure.Messaging.ServiceBus": "[7.20.1, 7.20.1]", + "Azure.Storage.Blobs": "[12.26.0, 12.26.0]", + "Azure.Storage.Blobs.Batch": "[12.23.0, 12.23.0]", + "Azure.Storage.Queues": "[12.24.0, 12.24.0]", + "BitPay.Light": "[1.0.1907, 1.0.1907]", + "Bitwarden.Server.Sdk.Environment": "[0.1.0, )", + "Bitwarden.Server.Sdk.Features": "[1.4.0, )", + "Braintree": "[5.36.0, 5.36.0]", + "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[0.0.1, )", + "DnsClient": "[1.8.0, 1.8.0]", + "Duende.IdentityServer": "[7.4.6, 7.4.6]", + "DuoUniversal": "[1.3.1, 1.3.1]", + "ExceptionHandling": "[0.0.1, )", + "Fido2.AspNet": "[3.0.1, 3.0.1]", + "Handlebars.Net": "[2.1.6, 2.1.6]", + "MailKit": "[4.17.0, 4.17.0]", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[10.0.8, 10.0.8]", + "Microsoft.AspNetCore.DataProtection": "[10.0.8, 10.0.8]", + "Microsoft.Azure.Cosmos": "[3.52.0, 3.52.0]", + "Microsoft.Azure.NotificationHubs": "[4.2.0, 4.2.0]", + "Microsoft.Bot.Builder": "[4.23.0, 4.23.0]", + "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", + "Microsoft.Bot.Connector": "[4.23.0, 4.23.0]", + "Microsoft.Data.SqlClient": "[7.0.0, 7.0.0]", + "Microsoft.Extensions.Caching.Cosmos": "[1.8.0, 1.8.0]", + "Microsoft.Extensions.Caching.SqlServer": "[10.0.8, 10.0.8]", + "Microsoft.Extensions.Caching.StackExchangeRedis": "[10.0.8, 10.0.8]", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "[10.0.8, 10.0.8]", + "Microsoft.Extensions.Configuration.UserSecrets": "[10.0.8, 10.0.8]", + "Microsoft.Extensions.Identity.Stores": "[10.0.8, 10.0.8]", + "Newtonsoft.Json": "[13.0.3, 13.0.3]", + "OneOf": "[3.0.271, 3.0.271]", + "Otp.NET": "[1.4.0, 1.4.0]", + "Quartz": "[3.15.1, 3.15.1]", + "Quartz.Extensions.DependencyInjection": "[3.15.1, 3.15.1]", + "Quartz.Extensions.Hosting": "[3.15.1, 3.15.1]", + "RabbitMQ.Client": "[7.1.2, 7.1.2]", + "SendGrid": "[9.29.3, 9.29.3]", + "SerilogFileLogging": "[0.0.1, )", + "SsrfProtection": "[0.0.1, )", + "Stripe.net": "[48.5.0, 48.5.0]", + "YubicoDotNetClient": "[1.2.0, 1.2.0]", + "ZiggyCreatures.FusionCache": "[2.0.2, 2.0.2]", + "ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis": "[2.0.2, 2.0.2]", + "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" + } + }, + "data": { + "type": "Project" + }, + "exceptionhandling": { + "type": "Project", + "dependencies": { + "HttpExtensions": "[2026.8.0, )" + } + }, + "httpextensions": { + "type": "Project" + }, + "identity": { + "type": "Project", + "dependencies": { + "Bitwarden.Server.Sdk.Environment": "[0.1.0, )", + "Bitwarden.Server.Sdk.Features": "[1.4.0, )", + "Bitwarden.Server.Sdk.WebEssentials": "[0.5.0, )", + "Core": "[2026.8.0, )", + "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", + "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", + "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.2, )", + "OpenTelemetry.Instrumentation.EntityFrameworkCore": "[1.12.0-beta.2, )", + "OpenTelemetry.Instrumentation.Http": "[1.15.1, )", + "OpenTelemetry.Instrumentation.Runtime": "[1.15.1, )", + "OpenTelemetry.Instrumentation.SqlClient": "[1.15.2, )", + "SharedWeb": "[2026.8.0, )" + } + }, + "infrastructure.dapper": { + "type": "Project", + "dependencies": { + "Core": "[2026.8.0, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.8.0, )" + } + }, + "infrastructure.entityframework": { + "type": "Project", + "dependencies": { + "AutoMapper": "[14.0.0, 14.0.0]", + "Core": "[2026.8.0, )", + "Microsoft.EntityFrameworkCore.Relational": "[8.0.8, 8.0.8]", + "Microsoft.EntityFrameworkCore.SqlServer": "[8.0.8, 8.0.8]", + "Microsoft.EntityFrameworkCore.Sqlite": "[8.0.8, 8.0.8]", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[8.0.4, 8.0.4]", + "Pam.Domain": "[2026.8.0, )", + "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", + "linq2db": "[5.4.1, 5.4.1]", + "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" + } + }, + "integrationtestcommon": { + "type": "Project", + "dependencies": { + "Common": "[2026.8.0, )", + "Identity": "[2026.8.0, )", + "Microsoft.AspNetCore.Mvc.Testing": "[10.0.8, 10.0.8]", + "Migrator": "[2026.8.0, )", + "Seeder": "[2026.8.0, )" + } + }, + "migrator": { + "type": "Project", + "dependencies": { + "Core": "[2026.8.0, )", + "Microsoft.Extensions.Logging": "[10.0.8, 10.0.8]", + "dbup-sqlserver": "[7.2.0, 7.2.0]" + } + }, + "organizationauthorization": { + "type": "Project", + "dependencies": { + "Core": "[2026.8.0, )" + } + }, + "pam": { + "type": "Project", + "dependencies": { + "Core": "[2026.8.0, )", + "HttpExtensions": "[2026.8.0, )", + "OrganizationAuthorization": "[0.0.1, )", + "Pam.Domain": "[2026.8.0, )" + } + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[0.0.1, )" + } + }, + "rustsdk": { + "type": "Project" + }, + "seeder": { + "type": "Project", + "dependencies": { + "Bogus": "[35.6.5, 35.6.5]", + "Core": "[2026.8.0, )", + "Infrastructure.EntityFramework": "[2026.8.0, )", + "RustSdk": "[2026.8.0, )", + "SharedWeb": "[2026.8.0, )" + } + }, + "serilogfilelogging": { + "type": "Project", + "dependencies": { + "Serilog.Extensions.Logging.File": "[3.0.0, 3.0.0]" + } + }, + "sharedweb": { + "type": "Project", + "dependencies": { + "Bitwarden.Server.Sdk.Environment": "[0.1.0, )", + "Bitwarden.Server.Sdk.Features": "[1.4.0, )", + "Core": "[2026.8.0, )", + "Infrastructure.Dapper": "[2026.8.0, )", + "Infrastructure.EntityFramework": "[2026.8.0, )", + "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", + "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" + } + }, + "ssrfprotection": { + "type": "Project" + } + } + } +} \ No newline at end of file diff --git a/bitwarden_license/test/Services/Pam.Test/Api/Endpoints/Handlers/AccessRuleEndpointsHandlerTests.cs b/bitwarden_license/test/Services/Pam.Test/Api/Endpoints/Handlers/AccessRuleEndpointsHandlerTests.cs new file mode 100644 index 000000000000..62c4203cbabf --- /dev/null +++ b/bitwarden_license/test/Services/Pam.Test/Api/Endpoints/Handlers/AccessRuleEndpointsHandlerTests.cs @@ -0,0 +1,145 @@ +using System.Text.Json; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Pam.Entities; +using Bit.Pam.Models; +using Bit.Pam.Repositories; +using Bit.Services.Pam.Api.Endpoints.Handlers; +using Bit.Services.Pam.Api.Models.Request; +using Bit.Services.Pam.OrganizationFeatures.Commands.Interfaces; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Services.Pam.Test.Api.Endpoints.Handlers; + +/// +/// Whether the caller may touch the organization at all is settled by the authorization middleware before a handler +/// runs (see AccessRuleEndpoints). What is left to the handler — and so what these tests pin — is resource +/// scoping, that a rule reached by ID belongs to the organization on the route, and the edit attribution handed to +/// the commands. +/// +[SutProviderCustomize] +public class AccessRuleEndpointsHandlerTests +{ + [Theory, BitAutoData] + public async Task GetAll_ReturnsTheOrganizationsRules( + Guid organizationId, + AccessRuleDetails first, + AccessRuleDetails second, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetManyDetailsByOrganizationIdAsync(organizationId) + .Returns(new List { first, second }); + + var result = await sutProvider.Sut.GetAll(organizationId); + + Assert.Equal(new[] { first.Id, second.Id }, result.Data.Select(rule => rule.Id).ToArray()); + } + + [Theory, BitAutoData] + public async Task Get_ReturnsTheRule( + AccessRuleDetails rule, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetDetailsByIdAsync(rule.Id) + .Returns(rule); + + var result = await sutProvider.Sut.Get(rule.OrganizationId, rule.Id); + + Assert.Equal(rule.Id, result.Id); + } + + /// + /// Membership in the route's organization is all the middleware establishes, so nothing but this check stops a + /// rule ID from one organization being read through another organization's route. + /// + [Theory, BitAutoData] + public async Task Get_ARuleBelongingToAnotherOrganization_ThrowsNotFound( + AccessRuleDetails rule, Guid otherOrganizationId, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetDetailsByIdAsync(rule.Id) + .Returns(rule); + + await Assert.ThrowsAsync(() => sutProvider.Sut.Get(otherOrganizationId, rule.Id)); + } + + [Theory, BitAutoData] + public async Task Get_AMissingRule_ThrowsNotFound( + Guid organizationId, Guid id, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetDetailsByIdAsync(id) + .Returns((AccessRuleDetails?)null); + + await Assert.ThrowsAsync(() => sutProvider.Sut.Get(organizationId, id)); + } + + [Theory, BitAutoData] + public async Task Post_CreatesTheRuleForTheRouteOrganization_StampedWithTheCallingUser( + Guid organizationId, + Guid userId, + AccessRuleDetails created, + SutProvider sutProvider) + { + var model = RequestModel(); + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency() + .CreateAsync(Arg.Any(), Arg.Any>()) + .Returns(created); + + await sutProvider.Sut.Post(organizationId, model); + + await sutProvider.GetDependency().Received(1) + .CreateAsync( + Arg.Is(rule => rule.OrganizationId == organizationId && rule.LastEditedBy == userId), + model.Collections); + } + + [Theory, BitAutoData] + public async Task Put_UpdatesTheRule_StampedWithTheCallingUser( + Guid organizationId, + Guid id, + Guid userId, + AccessRuleDetails updated, + SutProvider sutProvider) + { + var model = RequestModel(); + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency() + .UpdateAsync(organizationId, id, Arg.Any(), Arg.Any>()) + .Returns(updated); + + await sutProvider.Sut.Put(organizationId, id, model); + + await sutProvider.GetDependency().Received(1) + .UpdateAsync( + organizationId, + id, + Arg.Is(rule => rule.OrganizationId == organizationId && rule.LastEditedBy == userId), + model.Collections); + } + + /// + /// The route's organization is what scopes the delete — the command rejects an ID belonging to any other. + /// + [Theory, BitAutoData] + public async Task Delete_DeletesWithinTheRouteOrganization( + Guid organizationId, Guid id, SutProvider sutProvider) + { + await sutProvider.Sut.Delete(organizationId, id); + + await sutProvider.GetDependency().Received(1) + .DeleteAsync(organizationId, id); + } + + private static AccessRuleRequestModel RequestModel() => new() + { + Name = "Production database", + Conditions = JsonDocument.Parse("[]").RootElement, + Collections = new List { Guid.NewGuid() }, + }; +} diff --git a/bitwarden_license/test/Services/Pam.Test/Api/Models/AccessRuleRequestModelTests.cs b/bitwarden_license/test/Services/Pam.Test/Api/Models/AccessRuleRequestModelTests.cs new file mode 100644 index 000000000000..9eaf5cce312c --- /dev/null +++ b/bitwarden_license/test/Services/Pam.Test/Api/Models/AccessRuleRequestModelTests.cs @@ -0,0 +1,78 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Bit.Services.Pam.Api.Models.Request; +using Xunit; + +namespace Bit.Services.Pam.Test.Api.Models; + +public class AccessRuleRequestModelTests +{ + [Fact] + public void ToAccessRule_CopiesTheEditableFieldsOntoTheRouteOrganization() + { + var organizationId = Guid.NewGuid(); + var model = new AccessRuleRequestModel + { + Name = "Production database", + Description = "Requires an approver", + Enabled = false, + Conditions = Parse("[]"), + SingleActiveLease = true, + DefaultLeaseDurationSeconds = 900, + MaxLeaseDurationSeconds = 3600, + AllowsExtensions = true, + MaxExtensionDurationSeconds = 300, + Collections = [Guid.NewGuid()], + }; + + var rule = model.ToAccessRule(organizationId); + + Assert.Equal(organizationId, rule.OrganizationId); + Assert.Equal("Production database", rule.Name); + Assert.Equal("Requires an approver", rule.Description); + Assert.False(rule.Enabled); + Assert.True(rule.SingleActiveLease); + Assert.Equal(900, rule.DefaultLeaseDurationSeconds); + Assert.Equal(3600, rule.MaxLeaseDurationSeconds); + Assert.True(rule.AllowsExtensions); + Assert.Equal(300, rule.MaxExtensionDurationSeconds); + } + + /// + /// The conditions document is persisted as the client sent it rather than round-tripped through the condition + /// types, so anything this version does not model still reaches whoever reads the rule back. + /// + [Fact] + public void ToAccessRule_StoresTheConditionsDocumentVerbatim() + { + const string conditions = + """[{"kind":"ip_allowlist","cidrs":["10.0.0.0/8"],"unmodelled":"kept"}]"""; + + var rule = NewModel(Parse(conditions)).ToAccessRule(Guid.NewGuid()); + + Assert.Equal(JsonNode.Parse(conditions)!.ToJsonString(), JsonNode.Parse(rule.Conditions)!.ToJsonString()); + Assert.Contains("unmodelled", rule.Conditions, StringComparison.Ordinal); + } + + /// + /// Conditions is bound as object, which is a over the wire but an ordinary CLR + /// value for anything constructing the model in process. The fallback has to serialize that value rather than + /// store its ToString(). + /// + [Fact] + public void ToAccessRule_SerializesConditionsThatAreNotAJsonElement() + { + var rule = NewModel(new[] { new { kind = "human_approval" } }).ToAccessRule(Guid.NewGuid()); + + Assert.Equal("""[{"kind":"human_approval"}]""", rule.Conditions); + } + + private static AccessRuleRequestModel NewModel(object conditions) => new() + { + Name = "Production database", + Conditions = conditions, + Collections = [], + }; + + private static JsonElement Parse(string json) => JsonDocument.Parse(json).RootElement; +} diff --git a/bitwarden_license/test/Services/Pam.Test/Api/Models/AccessRuleResponseModelTests.cs b/bitwarden_license/test/Services/Pam.Test/Api/Models/AccessRuleResponseModelTests.cs new file mode 100644 index 000000000000..e2da2c4b048e --- /dev/null +++ b/bitwarden_license/test/Services/Pam.Test/Api/Models/AccessRuleResponseModelTests.cs @@ -0,0 +1,84 @@ +using System.Text.Json; +using Bit.Pam.Models; +using Bit.Services.Pam.Api.Models.Response; +using Xunit; + +namespace Bit.Services.Pam.Test.Api.Models; + +public class AccessRuleResponseModelTests +{ + [Fact] + public void Constructor_ReturnsTheStoredConditionsAsJson() + { + const string conditions = """[{"kind":"human_approval","approverCount":1}]"""; + + var model = new AccessRuleResponseModel(Details(conditions)); + + Assert.Equal(JsonValueKind.Array, model.Conditions!.Value.ValueKind); + Assert.Equal("human_approval", model.Conditions.Value[0].GetProperty("kind").GetString()); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void Constructor_WithoutStoredConditions_ReturnsNullConditions(string? conditions) + { + var model = new AccessRuleResponseModel(Details(conditions!)); + + Assert.Null(model.Conditions); + } + + /// + /// A rule stored before a conditions-format change has to stay readable, so an unparseable document reads back as + /// no conditions rather than failing the request. Note the engine treats a rule with no conditions as satisfied, + /// which makes this a fail-open read — deliberate, and worth knowing about when the format next changes. + /// + [Fact] + public void Constructor_WithStoredConditionsThatDoNotParse_ReturnsNullConditions() + { + var model = new AccessRuleResponseModel(Details("{ not json")); + + Assert.Null(model.Conditions); + } + + /// + /// Dapper materializes these timestamps with , which serializes without a + /// timezone designator and is then read as local time by a JavaScript client. The stored values are already UTC + /// instants, so the kind is relabelled — the clock must not move. + /// + [Fact] + public void Constructor_MarksTheTimestampsAsUtcWithoutShiftingThem() + { + var stored = new DateTime(2026, 6, 15, 13, 0, 0, DateTimeKind.Unspecified); + var details = Details("[]"); + details.CreationDate = stored; + details.RevisionDate = stored; + + var model = new AccessRuleResponseModel(details); + + Assert.Equal(DateTimeKind.Utc, model.CreationDate.Kind); + Assert.Equal(DateTimeKind.Utc, model.RevisionDate.Kind); + Assert.Equal(stored.TimeOfDay, model.CreationDate.TimeOfDay); + Assert.Equal(stored.TimeOfDay, model.RevisionDate.TimeOfDay); + } + + [Fact] + public void Constructor_ReturnsTheGovernedCollections() + { + var collectionId = Guid.NewGuid(); + var details = Details("[]"); + details.CollectionIds = [collectionId]; + + var model = new AccessRuleResponseModel(details); + + Assert.Equal(new[] { collectionId }, model.Collections.ToArray()); + } + + private static AccessRuleDetails Details(string conditions) => new() + { + Id = Guid.NewGuid(), + OrganizationId = Guid.NewGuid(), + Name = "Production database", + Conditions = conditions, + }; +} diff --git a/bitwarden_license/test/Services/Pam.Test/Commands/CreateAccessRuleCommandTests.cs b/bitwarden_license/test/Services/Pam.Test/Commands/CreateAccessRuleCommandTests.cs new file mode 100644 index 000000000000..0a169019db61 --- /dev/null +++ b/bitwarden_license/test/Services/Pam.Test/Commands/CreateAccessRuleCommandTests.cs @@ -0,0 +1,125 @@ +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Repositories; +using Bit.Services.Pam.OrganizationFeatures.Commands; +using Bit.Services.Pam.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Bit.Services.Pam.Test.Commands; + +/// +/// The validation these commands share lives in and is covered by +/// AccessRuleWriteValidatorTests; these tests cover persistence, timestamps, and collection association wiring. +/// +[SutProviderCustomize] +public class CreateAccessRuleCommandTests +{ + private static readonly DateTime _now = new(2026, 5, 21, 12, 0, 0, DateTimeKind.Utc); + + [Theory, BitAutoData] + public async Task CreateAsync_HappyPath_PersistsWithTimestampsAndValidates(AccessRule rule) + { + var sutProvider = SetupSutProvider(); + rule.Name = "VPN + business hours"; + rule.Conditions = """[{"kind":"human_approval"}]"""; + rule.DefaultLeaseDurationSeconds = 3600; + rule.MaxLeaseDurationSeconds = 28800; + SetupValidator(sutProvider, rule.OrganizationId, []); + sutProvider.GetDependency() + .CreateAsync(rule) + .Returns(rule); + + var result = await sutProvider.Sut.CreateAsync(rule, []); + + Assert.Equal(_now, result.CreationDate); + Assert.Equal(_now, result.RevisionDate); + Assert.Equal(3600, result.DefaultLeaseDurationSeconds); + Assert.Equal(28800, result.MaxLeaseDurationSeconds); + await sutProvider.GetDependency().Received(1) + .CreateAsync(Arg.Is(r => + r.DefaultLeaseDurationSeconds == 3600 && r.MaxLeaseDurationSeconds == 28800)); + // A create has no existing rule to exclude from the validator's uniqueness and conflict checks. + await sutProvider.GetDependency().Received(1) + .ValidateAsync(rule.OrganizationId, rule, Arg.Any>(), null); + } + + [Theory, BitAutoData] + public async Task CreateAsync_WithCollections_AssociatesAndReturnsThem(AccessRule rule, Collection collectionA, + Collection collectionB) + { + var sutProvider = SetupSutProvider(); + rule.Name = "VPN + business hours"; + rule.Conditions = """[{"kind":"human_approval"}]"""; + var collectionIds = new[] { collectionA.Id, collectionB.Id }; + SetupValidator(sutProvider, rule.OrganizationId, [.. collectionIds]); + sutProvider.GetDependency() + .CreateAsync(rule) + .Returns(rule); + + var result = await sutProvider.Sut.CreateAsync(rule, collectionIds); + + Assert.Equal(collectionIds, result.CollectionIds); + await sutProvider.GetDependency().Received(1) + .SetAccessRuleAssociationsAsync(rule.OrganizationId, rule.Id, + Arg.Is>(ids => ids.OrderBy(x => x).SequenceEqual(collectionIds.OrderBy(x => x))), + Arg.Is>(ids => !ids.Any())); + } + + [Theory, BitAutoData] + public async Task CreateAsync_AllowsExtensionsWithPositiveMax_Persists(AccessRule rule) + { + var sutProvider = SetupSutProvider(); + rule.Name = "extendable"; + rule.Conditions = """[{"kind":"human_approval"}]"""; + rule.AllowsExtensions = true; + rule.MaxExtensionDurationSeconds = 3600; + SetupValidator(sutProvider, rule.OrganizationId, []); + sutProvider.GetDependency() + .CreateAsync(rule) + .Returns(rule); + + var result = await sutProvider.Sut.CreateAsync(rule, []); + + Assert.True(result.AllowsExtensions); + Assert.Equal(3600, result.MaxExtensionDurationSeconds); + await sutProvider.GetDependency().Received(1) + .CreateAsync(Arg.Is(r => r.AllowsExtensions && r.MaxExtensionDurationSeconds == 3600)); + } + + [Theory, BitAutoData] + public async Task CreateAsync_ValidationFails_DoesNotPersist(AccessRule rule) + { + var sutProvider = SetupSutProvider(); + sutProvider.GetDependency() + .ValidateAsync(Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()) + .ThrowsAsync(new BadRequestException("Name is required.")); + + var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(rule, [])); + Assert.Equal("Name is required.", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default!); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .SetAccessRuleAssociationsAsync(default, default, default!, default!); + } + + private static void SetupValidator(SutProvider sutProvider, Guid organizationId, + List validatedCollectionIds) + => sutProvider.GetDependency() + .ValidateAsync(organizationId, Arg.Any(), Arg.Any>(), Arg.Any()) + .Returns(validatedCollectionIds); + + private static SutProvider SetupSutProvider() + { + var sutProvider = new SutProvider() + .WithFakeTimeProvider() + .Create(); + sutProvider.GetDependency().SetUtcNow(_now); + return sutProvider; + } +} diff --git a/bitwarden_license/test/Services/Pam.Test/Commands/DeleteAccessRuleCommandTests.cs b/bitwarden_license/test/Services/Pam.Test/Commands/DeleteAccessRuleCommandTests.cs new file mode 100644 index 000000000000..3307488a42ba --- /dev/null +++ b/bitwarden_license/test/Services/Pam.Test/Commands/DeleteAccessRuleCommandTests.cs @@ -0,0 +1,56 @@ +using Bit.Core.Exceptions; +using Bit.Pam.Entities; +using Bit.Pam.Repositories; +using Bit.Services.Pam.OrganizationFeatures.Commands; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Services.Pam.Test.Commands; + +[SutProviderCustomize] +public class DeleteAccessRuleCommandTests +{ + [Theory, BitAutoData] + public async Task DeleteAsync_HappyPath_HardDeletes( + AccessRule existing, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByIdAsync(existing.Id) + .Returns(existing); + + await sutProvider.Sut.DeleteAsync(existing.OrganizationId, existing.Id); + + await sutProvider.GetDependency().Received(1) + .DeleteAsync(existing); + } + + [Theory, BitAutoData] + public async Task DeleteAsync_MissingExisting_ThrowsNotFound( + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((AccessRule?)null); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.DeleteAsync(Guid.NewGuid(), Guid.NewGuid())); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs().DeleteAsync(default!); + } + + [Theory, BitAutoData] + public async Task DeleteAsync_WrongOrg_ThrowsNotFound( + AccessRule existing, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByIdAsync(existing.Id) + .Returns(existing); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.DeleteAsync(Guid.NewGuid(), existing.Id)); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs().DeleteAsync(default!); + } +} diff --git a/bitwarden_license/test/Services/Pam.Test/Commands/UpdateAccessRuleCommandTests.cs b/bitwarden_license/test/Services/Pam.Test/Commands/UpdateAccessRuleCommandTests.cs new file mode 100644 index 000000000000..769d2cc4138d --- /dev/null +++ b/bitwarden_license/test/Services/Pam.Test/Commands/UpdateAccessRuleCommandTests.cs @@ -0,0 +1,176 @@ +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Models; +using Bit.Pam.Repositories; +using Bit.Services.Pam.OrganizationFeatures.Commands; +using Bit.Services.Pam.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Bit.Services.Pam.Test.Commands; + +/// +/// The validation these commands share lives in and is covered by +/// AccessRuleWriteValidatorTests; these tests cover persistence, timestamps, and collection association wiring. +/// +[SutProviderCustomize] +public class UpdateAccessRuleCommandTests +{ + private static readonly DateTime _now = new(2026, 5, 21, 12, 0, 0, DateTimeKind.Utc); + + [Theory, BitAutoData] + public async Task UpdateAsync_HappyPath_UpdatesFieldsAndBumpsRevision(AccessRuleDetails existing, AccessRule update) + { + var sutProvider = SetupSutProvider(); + var orgId = existing.OrganizationId; + existing.CollectionIds = []; + update.Name = "renamed"; + update.Description = "new description"; + update.Conditions = """[{"kind":"human_approval"}]"""; + update.SingleActiveLease = true; + update.DefaultLeaseDurationSeconds = 3600; + update.MaxLeaseDurationSeconds = 28800; + update.AllowsExtensions = true; + update.MaxExtensionDurationSeconds = 7200; + sutProvider.GetDependency() + .GetDetailsByIdAsync(existing.Id) + .Returns(existing); + SetupValidator(sutProvider, orgId, existing.Id, []); + + var result = await sutProvider.Sut.UpdateAsync(orgId, existing.Id, update, []); + + Assert.Equal("renamed", result.Name); + Assert.Equal("new description", result.Description); + Assert.Equal(update.Conditions, result.Conditions); + Assert.True(result.SingleActiveLease); + Assert.Equal(3600, result.DefaultLeaseDurationSeconds); + Assert.Equal(28800, result.MaxLeaseDurationSeconds); + Assert.True(result.AllowsExtensions); + Assert.Equal(7200, result.MaxExtensionDurationSeconds); + Assert.Equal(_now, result.RevisionDate); + await sutProvider.GetDependency().Received(1) + .ReplaceAsync(Arg.Is(r => + r.Id == existing.Id && r.Name == "renamed" && r.Description == "new description" + && r.SingleActiveLease + && r.DefaultLeaseDurationSeconds == 3600 && r.MaxLeaseDurationSeconds == 28800 + && r.AllowsExtensions && r.MaxExtensionDurationSeconds == 7200)); + // The rule under update is excluded from the validator's uniqueness and conflict checks by its own id. + await sutProvider.GetDependency().Received(1) + .ValidateAsync(orgId, update, Arg.Any>(), existing.Id); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_ReplacesCollections_AssignsNewAndClearsRemoved(AccessRuleDetails existing, + AccessRule update, Guid keptId, Guid addedId) + { + var sutProvider = SetupSutProvider(); + var orgId = existing.OrganizationId; + update.Name = "renamed"; + var desired = new[] { keptId, addedId }; + var removedId = Guid.NewGuid(); + existing.CollectionIds = [keptId, removedId]; + sutProvider.GetDependency() + .GetDetailsByIdAsync(existing.Id) + .Returns(existing); + SetupValidator(sutProvider, orgId, existing.Id, [.. desired]); + + var result = await sutProvider.Sut.UpdateAsync(orgId, existing.Id, update, desired); + + Assert.Equal(desired, result.CollectionIds); + await sutProvider.GetDependency().Received(1) + .SetAccessRuleAssociationsAsync(orgId, existing.Id, + Arg.Is>(ids => ids.OrderBy(x => x).SequenceEqual(desired.OrderBy(x => x))), + Arg.Is>(ids => ids.SequenceEqual(new[] { removedId }))); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_EmptyCollections_ClearsAll(AccessRuleDetails existing, AccessRule update) + { + var sutProvider = SetupSutProvider(); + var orgId = existing.OrganizationId; + update.Name = "renamed"; + var currentId = Guid.NewGuid(); + existing.CollectionIds = [currentId]; + sutProvider.GetDependency() + .GetDetailsByIdAsync(existing.Id) + .Returns(existing); + SetupValidator(sutProvider, orgId, existing.Id, []); + + var result = await sutProvider.Sut.UpdateAsync(orgId, existing.Id, update, []); + + Assert.Empty(result.CollectionIds); + await sutProvider.GetDependency().Received(1) + .SetAccessRuleAssociationsAsync(orgId, existing.Id, + Arg.Is>(ids => !ids.Any()), + Arg.Is>(ids => ids.SequenceEqual(new[] { currentId }))); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_MissingExisting_ThrowsNotFoundWithoutValidating(AccessRule update) + { + var sutProvider = SetupSutProvider(); + sutProvider.GetDependency() + .GetDetailsByIdAsync(Arg.Any()) + .Returns((AccessRuleDetails?)null); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.UpdateAsync(Guid.NewGuid(), Guid.NewGuid(), update, [])); + // A rule the caller cannot see is a 404 before anything about the payload is judged. + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ValidateAsync(default, default!, default!, default); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_WrongOrg_ThrowsNotFound(AccessRuleDetails existing, AccessRule update) + { + var sutProvider = SetupSutProvider(); + var differentOrg = Guid.NewGuid(); + sutProvider.GetDependency() + .GetDetailsByIdAsync(existing.Id) + .Returns(existing); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.UpdateAsync(differentOrg, existing.Id, update, [])); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ValidateAsync(default, default!, default!, default); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_ValidationFails_DoesNotPersist(AccessRuleDetails existing, AccessRule update) + { + var sutProvider = SetupSutProvider(); + sutProvider.GetDependency() + .GetDetailsByIdAsync(existing.Id) + .Returns(existing); + sutProvider.GetDependency() + .ValidateAsync(Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()) + .ThrowsAsync(new BadRequestException("A rule with that name already exists.")); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.UpdateAsync(existing.OrganizationId, existing.Id, update, [])); + Assert.Equal("A rule with that name already exists.", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default!); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .SetAccessRuleAssociationsAsync(default, default, default!, default!); + } + + private static void SetupValidator(SutProvider sutProvider, Guid organizationId, + Guid existingRuleId, List validatedCollectionIds) + => sutProvider.GetDependency() + .ValidateAsync(organizationId, Arg.Any(), Arg.Any>(), existingRuleId) + .Returns(validatedCollectionIds); + + private static SutProvider SetupSutProvider() + { + var sutProvider = new SutProvider() + .WithFakeTimeProvider() + .Create(); + sutProvider.GetDependency().SetUtcNow(_now); + return sutProvider; + } +} diff --git a/bitwarden_license/test/Services/Pam.Test/Pam.Test.csproj b/bitwarden_license/test/Services/Pam.Test/Pam.Test.csproj index 776467840305..d7d3725365ec 100644 --- a/bitwarden_license/test/Services/Pam.Test/Pam.Test.csproj +++ b/bitwarden_license/test/Services/Pam.Test/Pam.Test.csproj @@ -1,6 +1,7 @@ + Bit.Services.Pam.Test false @@ -19,6 +20,7 @@ + diff --git a/bitwarden_license/test/Services/Pam.Test/Services/AccessRuleValidatorTests.cs b/bitwarden_license/test/Services/Pam.Test/Services/AccessRuleValidatorTests.cs new file mode 100644 index 000000000000..22c9cdc13afc --- /dev/null +++ b/bitwarden_license/test/Services/Pam.Test/Services/AccessRuleValidatorTests.cs @@ -0,0 +1,140 @@ +using Bit.Services.Pam.Services; +using Xunit; + +namespace Bit.Services.Pam.Test.Services; + +public class AccessRuleValidatorTests +{ + private readonly AccessRuleValidator _sut = new(); + + [Fact] + public void Validate_NullConditions_IsValid() + { + var result = _sut.Validate(null); + + Assert.True(result.IsValid); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void Validate_EmptyOrWhitespaceConditions_IsInvalid(string conditionsJson) + { + var result = _sut.Validate(conditionsJson); + + Assert.False(result.IsValid); + } + + [Fact] + public void Validate_MalformedJson_IsInvalid() + { + var result = _sut.Validate("{not json"); + + Assert.False(result.IsValid); + Assert.Contains("malformed", result.Error); + } + + [Fact] + public void Validate_NonArrayDocument_IsInvalid() + { + // The conditions document is a flat array; a bare object is rejected. + var result = _sut.Validate("""{"kind":"human_approval"}"""); + + Assert.False(result.IsValid); + } + + [Fact] + public void Validate_UnknownKind_IsInvalid() + { + var result = _sut.Validate("""[{"kind":"bogus"}]"""); + + Assert.False(result.IsValid); + } + + [Fact] + public void Validate_LegacyAllOfKind_IsInvalid() + { + // The flattened model dropped the all_of composite; a document that still nests one is rejected rather than + // silently accepted. + var result = _sut.Validate("""[{"kind":"all_of","conditions":[]}]"""); + + Assert.False(result.IsValid); + } + + [Fact] + public void Validate_HumanApproval_IsValid() + { + var result = _sut.Validate("""[{"kind":"human_approval"}]"""); + + Assert.True(result.IsValid); + } + + [Theory] + [InlineData("""[{"kind":"ip_allowlist","cidrs":["10.0.0.0/8"]}]""")] + [InlineData("""[{"kind":"ip_allowlist","cidrs":["10.0.0.0/8","192.168.0.0/16","2001:db8::/32"]}]""")] + public void Validate_IpAllowlist_ValidCidrs_IsValid(string conditionsJson) + { + var result = _sut.Validate(conditionsJson); + + Assert.True(result.IsValid); + } + + [Theory] + [InlineData("""[{"kind":"ip_allowlist","cidrs":[]}]""", "at least one CIDR")] + [InlineData("""[{"kind":"ip_allowlist","cidrs":["not-a-cidr"]}]""", "Invalid CIDR")] + [InlineData("""[{"kind":"ip_allowlist","cidrs":["10.0.0.0/99"]}]""", "Invalid CIDR")] + public void Validate_IpAllowlist_InvalidCidrs_IsInvalid(string conditionsJson, string expectedMessageFragment) + { + var result = _sut.Validate(conditionsJson); + + Assert.False(result.IsValid); + Assert.Contains(expectedMessageFragment, result.Error); + } + + [Fact] + public void Validate_MultipleConditions_IsValid() + { + var result = _sut.Validate(""" + [ + { "kind": "human_approval" }, + { "kind": "ip_allowlist", "cidrs": ["10.0.0.0/8"] } + ] + """); + + Assert.True(result.IsValid); + } + + [Fact] + public void Validate_EmptyConditions_IsValid() + { + // A rule with no conditions is allowed: it gates nothing and exists to route access through the PAM flow + // for audit logging. + var result = _sut.Validate("[]"); + + Assert.True(result.IsValid); + } + + [Fact] + public void Validate_ExceedsMaxConditions_IsInvalid() + { + var conditions = string.Join(",", Enumerable.Repeat("""{"kind":"human_approval"}""", 11)); + var result = _sut.Validate($$"""[{{conditions}}]"""); + + Assert.False(result.IsValid); + Assert.Contains("more than", result.Error); + } + + [Fact] + public void Validate_InvalidCondition_IsInvalid() + { + var result = _sut.Validate(""" + [ + { "kind": "human_approval" }, + { "kind": "ip_allowlist", "cidrs": ["bogus"] } + ] + """); + + Assert.False(result.IsValid); + Assert.Contains("CIDR", result.Error); + } +} diff --git a/bitwarden_license/test/Services/Pam.Test/Services/AccessRuleWriteValidatorTests.cs b/bitwarden_license/test/Services/Pam.Test/Services/AccessRuleWriteValidatorTests.cs new file mode 100644 index 000000000000..650b4e79ca3e --- /dev/null +++ b/bitwarden_license/test/Services/Pam.Test/Services/AccessRuleWriteValidatorTests.cs @@ -0,0 +1,240 @@ +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Repositories; +using Bit.Services.Pam.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Services.Pam.Test.Services; + +[SutProviderCustomize] +public class AccessRuleWriteValidatorTests +{ + [Theory] + [BitAutoData("")] + [BitAutoData(" ")] + public async Task ValidateAsync_EmptyName_ThrowsBadRequest(string name, AccessRule rule) + { + var sutProvider = new SutProvider().Create(); + rule.Name = name; + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.ValidateAsync(rule.OrganizationId, rule, [])); + Assert.Contains("Name is required", ex.Message); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_AllowsExtensionsWithoutMax_ThrowsBadRequest(AccessRule rule) + { + var sutProvider = new SutProvider().Create(); + rule.Name = "extendable"; + rule.AllowsExtensions = true; + rule.MaxExtensionDurationSeconds = null; + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.ValidateAsync(rule.OrganizationId, rule, [])); + Assert.Contains("maximum extension length", ex.Message); + } + + [Theory] + [BitAutoData(0)] + [BitAutoData(-1)] + public async Task ValidateAsync_AllowsExtensionsWithNonPositiveMax_ThrowsBadRequest( + int maxExtensionDurationSeconds, AccessRule rule) + { + var sutProvider = new SutProvider().Create(); + rule.Name = "extendable"; + rule.AllowsExtensions = true; + rule.MaxExtensionDurationSeconds = maxExtensionDurationSeconds; + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.ValidateAsync(rule.OrganizationId, rule, [])); + Assert.Contains("maximum extension length", ex.Message); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_InvalidConditions_ThrowsBadRequestWithValidatorError(AccessRule rule) + { + var sutProvider = SetupSutProvider(rule); + sutProvider.GetDependency() + .Validate(rule.Conditions) + .Returns(AccessRuleValidationResult.Invalid("Unsupported condition kind")); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.ValidateAsync(rule.OrganizationId, rule, [])); + Assert.Equal("Unsupported condition kind", ex.Message); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_DuplicateName_ThrowsBadRequest(AccessRule rule, AccessRule sibling) + { + var sutProvider = SetupSutProvider(rule); + rule.Name = "duplicate"; + sibling.OrganizationId = rule.OrganizationId; + sibling.Name = "Duplicate"; // case-insensitive collision + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(rule.OrganizationId) + .Returns(new List { sibling }); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.ValidateAsync(rule.OrganizationId, rule, [])); + Assert.Contains("already exists", ex.Message); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_UpdateKeepingItsOwnName_IsValid(AccessRule rule) + { + var sutProvider = SetupSutProvider(rule); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(rule.OrganizationId) + .Returns(new List { rule }); + + var result = await sutProvider.Sut.ValidateAsync(rule.OrganizationId, rule, [], rule.Id); + + Assert.Empty(result); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_UpdateTakingAnotherRulesName_ThrowsBadRequest(AccessRule rule, AccessRule sibling) + { + var sutProvider = SetupSutProvider(rule); + rule.Name = "taken"; + sibling.OrganizationId = rule.OrganizationId; + sibling.Name = "taken"; + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(rule.OrganizationId) + .Returns(new List { rule, sibling }); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.ValidateAsync(rule.OrganizationId, rule, [], rule.Id)); + Assert.Contains("already exists", ex.Message); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_NoCollections_SkipsCollectionLookup(AccessRule rule) + { + var sutProvider = SetupSutProvider(rule); + + var result = await sutProvider.Sut.ValidateAsync(rule.OrganizationId, rule, []); + + Assert.Empty(result); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .GetManyByManyIdsAsync(default!); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_DuplicateCollectionIds_ReturnsThemDeduplicated(AccessRule rule, + Collection collection) + { + var sutProvider = SetupSutProvider(rule); + collection.OrganizationId = rule.OrganizationId; + collection.AccessRuleId = null; + sutProvider.GetDependency() + .GetManyByManyIdsAsync(Arg.Any>()) + .Returns(new List { collection }); + + var result = await sutProvider.Sut.ValidateAsync(rule.OrganizationId, rule, + [collection.Id, collection.Id]); + + Assert.Equal([collection.Id], result); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_CollectionNotFound_ThrowsBadRequest(AccessRule rule, Guid missingCollectionId) + { + var sutProvider = SetupSutProvider(rule); + sutProvider.GetDependency() + .GetManyByManyIdsAsync(Arg.Any>()) + .Returns(new List()); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.ValidateAsync(rule.OrganizationId, rule, [missingCollectionId])); + Assert.Contains("could not be found", ex.Message); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_CollectionInDifferentOrg_ThrowsBadRequest(AccessRule rule, Collection collection) + { + var sutProvider = SetupSutProvider(rule); + collection.OrganizationId = Guid.NewGuid(); + sutProvider.GetDependency() + .GetManyByManyIdsAsync(Arg.Any>()) + .Returns(new List { collection }); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.ValidateAsync(rule.OrganizationId, rule, [collection.Id])); + Assert.Contains("do not belong to this organization", ex.Message); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_CreateWithGovernedCollection_ThrowsBadRequest(AccessRule rule, + AccessRule otherRule, Collection collection) + { + var sutProvider = SetupSutProvider(rule); + otherRule.OrganizationId = rule.OrganizationId; + collection.OrganizationId = rule.OrganizationId; + collection.AccessRuleId = otherRule.Id; + sutProvider.GetDependency() + .GetManyByManyIdsAsync(Arg.Any>()) + .Returns(new List { collection }); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.ValidateAsync(rule.OrganizationId, rule, [collection.Id])); + Assert.Contains("already governed by another access rule", ex.Message); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_UpdateWithCollectionGovernedByAnotherRule_ThrowsBadRequest(AccessRule rule, + AccessRule otherRule, Collection collection) + { + var sutProvider = SetupSutProvider(rule); + otherRule.OrganizationId = rule.OrganizationId; + collection.OrganizationId = rule.OrganizationId; + collection.AccessRuleId = otherRule.Id; + sutProvider.GetDependency() + .GetManyByManyIdsAsync(Arg.Any>()) + .Returns(new List { collection }); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.ValidateAsync(rule.OrganizationId, rule, [collection.Id], rule.Id)); + Assert.Contains("already governed by another access rule", ex.Message); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_UpdateWithCollectionItAlreadyGoverns_IsValid(AccessRule rule, + Collection collection) + { + var sutProvider = SetupSutProvider(rule); + collection.OrganizationId = rule.OrganizationId; + collection.AccessRuleId = rule.Id; // already governed by the rule under update + sutProvider.GetDependency() + .GetManyByManyIdsAsync(Arg.Any>()) + .Returns(new List { collection }); + + var result = await sutProvider.Sut.ValidateAsync(rule.OrganizationId, rule, [collection.Id], rule.Id); + + Assert.Equal([collection.Id], result); + } + + /// + /// Sets up a rule that passes the field-level checks, with the conditions validator and the sibling lookup + /// stubbed to succeed, so each test only has to arrange the check it is exercising. + /// + private static SutProvider SetupSutProvider(AccessRule rule) + { + var sutProvider = new SutProvider().Create(); + rule.Name = "rule"; + rule.Conditions = """[{"kind":"human_approval"}]"""; + sutProvider.GetDependency() + .Validate(rule.Conditions) + .Returns(AccessRuleValidationResult.Valid); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(rule.OrganizationId) + .Returns(new List()); + return sutProvider; + } +} diff --git a/bitwarden_license/test/Services/Pam.Test/packages.lock.json b/bitwarden_license/test/Services/Pam.Test/packages.lock.json index 2b0f966ca7d8..6dd6dc7c509a 100644 --- a/bitwarden_license/test/Services/Pam.Test/packages.lock.json +++ b/bitwarden_license/test/Services/Pam.Test/packages.lock.json @@ -63,6 +63,32 @@ "StackExchange.Redis": "2.6.80" } }, + "AutoFixture": { + "type": "Transitive", + "resolved": "4.18.1", + "contentHash": "BmWZDY4fkrYOyd5/CTBOeXbzsNwV8kI4kDi/Ty1Y5F+WDHBVKxzfWlBE4RSicvZ+EOi2XDaN5uwdrHsItLW6Kw==", + "dependencies": { + "Fare": "[2.1.1, 3.0.0)" + } + }, + "AutoFixture.AutoNSubstitute": { + "type": "Transitive", + "resolved": "4.18.1", + "contentHash": "xJxIsShO/1Ceei7BDFCobFANiw5a+enpdklgX/Xic6vKavHo9gSJO7ZGkKlf2lh+TlblTEet9mjzf9wHWIqWGQ==", + "dependencies": { + "AutoFixture": "4.18.1", + "NSubstitute": "[2.0.3, 6.0.0)" + } + }, + "AutoFixture.Xunit2": { + "type": "Transitive", + "resolved": "4.18.1", + "contentHash": "I5Cwv1bvWb0lf2x2zO42bBQ2WaGudBh7tVBCzKIf8KmRJG+hmYY7ku3znnFZDVxbQaihNaqNkztLTwK4PwaoWg==", + "dependencies": { + "AutoFixture": "4.18.1", + "xunit.extensibility.core": "[2.2.0, 3.0.0)" + } + }, "AWSSDK.Core": { "type": "Transitive", "resolved": "4.0.3.3", @@ -221,6 +247,14 @@ "System.Xml.XPath.XmlDocument": "4.3.0" } }, + "Castle.Core": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", + "dependencies": { + "System.Diagnostics.EventLog": "6.0.0" + } + }, "CsvHelper": { "type": "Transitive", "resolved": "33.1.0", @@ -262,6 +296,14 @@ "Microsoft.IdentityModel.JsonWebTokens": "6.34.0" } }, + "Fare": { + "type": "Transitive", + "resolved": "2.1.1", + "contentHash": "HaI8puqA66YU7/9cK4Sgbs1taUTP1Ssa4QT2PIzqJ7GvAbN1QgkjbRsjH+FSbMh1MJdvS0CIwQNLtFT+KF6KpA==", + "dependencies": { + "NETStandard.Library": "1.6.1" + } + }, "Fido2": { "type": "Transitive", "resolved": "3.0.1", @@ -293,6 +335,15 @@ "resolved": "2.1.6", "contentHash": "WsYWCEXsIM6hEOSOSRHtIYLjC8BnbT5MVmqhNKRqUI7qiv0t8x3nJiBTEv0ZZfvUAMAFnadGIzSsS/U2anVG1Q==" }, + "Kralizek.AutoFixture.Extensions.MockHttp": { + "type": "Transitive", + "resolved": "2.2.1", + "contentHash": "yNpYOT8k6L9PVS2YPoAe72IjILqGfPixKDzPsAFMz2aVyrmgGjirqORQa+bQNe+Qs5ytB+p41uzy4F9mjUuP9w==", + "dependencies": { + "AutoFixture": "4.18.1", + "RichardSzalay.MockHttp": "7.0.0" + } + }, "LaunchDarkly.Cache": { "type": "Transitive", "resolved": "1.0.2", @@ -614,6 +665,15 @@ "StackExchange.Redis": "2.7.27" } }, + "Microsoft.Extensions.Compliance.Abstractions": { + "type": "Transitive", + "resolved": "10.6.0", + "contentHash": "L8zTKn8e2LCQbsDFLWFm6fZQ54F/1FisLx43nkEof4HmmsO2HaZHshV85+qF8HXO48MlGJdrWUg+uVBj/WDmmw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8", + "Microsoft.Extensions.ObjectPool": "10.0.8" + } + }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "10.0.8", @@ -715,6 +775,16 @@ "Microsoft.Extensions.Options": "10.0.9" } }, + "Microsoft.Extensions.Diagnostics.Testing": { + "type": "Transitive", + "resolved": "10.6.0", + "contentHash": "WFgkep0Nxz0aht9k/OKwXdBOZ/uIB8VULY35ou91BiK4k0gj9CJz985T8GN0Q7XjCOMjNgjVFnv/9FmqcDEivg==", + "dependencies": { + "Microsoft.Extensions.Logging": "10.0.8", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.8", + "Microsoft.Extensions.Telemetry.Abstractions": "10.6.0" + } + }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "10.0.9", @@ -802,6 +872,11 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9" } }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "aQBFbY8i/dacE0fP+ZJ8Lhx/unYRnGHhtM+tHb46GLkeNjBdOzgFk88sX6BVZBhoa6JrYIOBGYTc5K4WItBsag==" + }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "10.0.9", @@ -828,6 +903,22 @@ "resolved": "10.0.9", "contentHash": "fmEbAUFsaIKirgLt/lYhuFRBwhcSJN31jjHgCdbQxJiWOum6EdLjkbgGuukSP9z/a+9LibaxII/kF+GwOXgC4g==" }, + "Microsoft.Extensions.Telemetry.Abstractions": { + "type": "Transitive", + "resolved": "10.6.0", + "contentHash": "aNQEJu5DD2YVQEWWmC/ALEiV1Qt400BaDO+SExtfAaGqYaNu/r2sW9xGLuc71fcjbrmzqX8LzNgK5mzjjMW9RQ==", + "dependencies": { + "Microsoft.Extensions.Compliance.Abstractions": "10.6.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.8", + "Microsoft.Extensions.ObjectPool": "10.0.8", + "Microsoft.Extensions.Options": "10.0.8" + } + }, + "Microsoft.Extensions.TimeProvider.Testing": { + "type": "Transitive", + "resolved": "10.6.0", + "contentHash": "qQDiaYWpvIymGbu+kXaMDS8YdqfeQkv6DOxPF2GSwC+eSzIKqOOnSP34TYt7gKqvB7p8/aSptexnW6nF0CUdnw==" + }, "Microsoft.Identity.Client": { "type": "Transitive", "resolved": "4.66.1", @@ -964,6 +1055,14 @@ "libsodium": "[1.0.18.2, 1.0.19)" } }, + "NSubstitute": { + "type": "Transitive", + "resolved": "5.1.0", + "contentHash": "ZCqOP3Kpp2ea7QcLyjMU4wzE+0wmrMN35PQMsdPOHYc2IrvjmusG9hICOiqiOTPKN0gJon6wyCn6ZuGHdNs9hQ==", + "dependencies": { + "Castle.Core": "5.1.1" + } + }, "OneOf": { "type": "Transitive", "resolved": "3.0.271", @@ -1014,6 +1113,11 @@ "System.Threading.RateLimiting": "8.0.0" } }, + "RichardSzalay.MockHttp": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "QwnauYiaywp65QKFnP+wvgiQ2D8Pv888qB2dyfd7MSVDF06sIvxqASenk+RxsWybyyt+Hu1Y251wQxpHTv3UYg==" + }, "SendGrid": { "type": "Transitive", "resolved": "9.29.3", @@ -1254,6 +1358,19 @@ "ZiggyCreatures.FusionCache": "2.0.2" } }, + "common": { + "type": "Project", + "dependencies": { + "AutoFixture.AutoNSubstitute": "[4.18.1, )", + "AutoFixture.Xunit2": "[4.18.1, )", + "Core": "[2026.8.0, )", + "Kralizek.AutoFixture.Extensions.MockHttp": "[2.2.1, 2.2.1]", + "Microsoft.Extensions.TimeProvider.Testing": "[10.6.0, 10.6.0]", + "Microsoft.NET.Test.Sdk": "[18.0.1, )", + "NSubstitute": "[5.1.0, )", + "xunit": "[2.6.6, )" + } + }, "core": { "type": "Project", "dependencies": { @@ -1311,6 +1428,20 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "core.test": { + "type": "Project", + "dependencies": { + "AutoFixture.AutoNSubstitute": "[4.18.1, )", + "AutoFixture.Xunit2": "[4.18.1, )", + "Common": "[2026.8.0, )", + "Core": "[2026.8.0, )", + "Kralizek.AutoFixture.Extensions.MockHttp": "[2.2.1, 2.2.1]", + "Microsoft.Extensions.Diagnostics.Testing": "[10.6.0, 10.6.0]", + "Microsoft.NET.Test.Sdk": "[18.0.1, )", + "NSubstitute": "[5.1.0, )", + "xunit": "[2.6.6, )" + } + }, "data": { "type": "Project" }, @@ -1334,7 +1465,14 @@ "dependencies": { "Core": "[2026.8.0, )", "HttpExtensions": "[2026.8.0, )", - "OrganizationAuthorization": "[0.0.1, )" + "OrganizationAuthorization": "[0.0.1, )", + "Pam.Domain": "[2026.8.0, )" + } + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[0.0.1, )" } }, "serilogfilelogging": { diff --git a/src/Api/packages.lock.json b/src/Api/packages.lock.json index 5e8f3d406ce2..965fade6ff7f 100644 --- a/src/Api/packages.lock.json +++ b/src/Api/packages.lock.json @@ -1279,7 +1279,8 @@ "dependencies": { "Core": "[2026.8.0, )", "HttpExtensions": "[2026.8.0, )", - "OrganizationAuthorization": "[0.0.1, )" + "OrganizationAuthorization": "[0.0.1, )", + "Pam.Domain": "[2026.8.0, )" } }, "pam.domain": { diff --git a/test/Api.IntegrationTest/packages.lock.json b/test/Api.IntegrationTest/packages.lock.json index fdc94ab8fd12..049ca87f7250 100644 --- a/test/Api.IntegrationTest/packages.lock.json +++ b/test/Api.IntegrationTest/packages.lock.json @@ -1557,7 +1557,8 @@ "dependencies": { "Core": "[2026.8.0, )", "HttpExtensions": "[2026.8.0, )", - "OrganizationAuthorization": "[0.0.1, )" + "OrganizationAuthorization": "[0.0.1, )", + "Pam.Domain": "[2026.8.0, )" } }, "pam.domain": { diff --git a/test/Api.Test/packages.lock.json b/test/Api.Test/packages.lock.json index 9b60931b94cd..9b73ac0b431f 100644 --- a/test/Api.Test/packages.lock.json +++ b/test/Api.Test/packages.lock.json @@ -1895,7 +1895,8 @@ "dependencies": { "Core": "[2026.8.0, )", "HttpExtensions": "[2026.8.0, )", - "OrganizationAuthorization": "[0.0.1, )" + "OrganizationAuthorization": "[0.0.1, )", + "Pam.Domain": "[2026.8.0, )" } }, "pam.domain": { diff --git a/test/Billing.IntegrationTest/packages.lock.json b/test/Billing.IntegrationTest/packages.lock.json index b6343e52d589..d1bbb9ccd48b 100644 --- a/test/Billing.IntegrationTest/packages.lock.json +++ b/test/Billing.IntegrationTest/packages.lock.json @@ -2102,7 +2102,8 @@ "dependencies": { "Core": "[2026.8.0, )", "HttpExtensions": "[2026.8.0, )", - "OrganizationAuthorization": "[0.0.1, )" + "OrganizationAuthorization": "[0.0.1, )", + "Pam.Domain": "[2026.8.0, )" } }, "pam.domain": { diff --git a/util/SqlServerEFScaffold/packages.lock.json b/util/SqlServerEFScaffold/packages.lock.json index 9f990b997173..35f980fae7be 100644 --- a/util/SqlServerEFScaffold/packages.lock.json +++ b/util/SqlServerEFScaffold/packages.lock.json @@ -1792,7 +1792,8 @@ "dependencies": { "Core": "[2026.8.0, )", "HttpExtensions": "[2026.8.0, )", - "OrganizationAuthorization": "[0.0.1, )" + "OrganizationAuthorization": "[0.0.1, )", + "Pam.Domain": "[2026.8.0, )" } }, "pam.domain": {