diff --git a/bitwarden-server.slnx b/bitwarden-server.slnx index 28f4406b31fc..835e4b2b57f8 100644 --- a/bitwarden-server.slnx +++ b/bitwarden-server.slnx @@ -19,6 +19,7 @@ + @@ -26,6 +27,7 @@ + @@ -37,11 +39,15 @@ + + + + diff --git a/bitwarden_license/src/Commercial.Core/packages.lock.json b/bitwarden_license/src/Commercial.Core/packages.lock.json index 5f90fe68bef3..67418c84e1d9 100644 --- a/bitwarden_license/src/Commercial.Core/packages.lock.json +++ b/bitwarden_license/src/Commercial.Core/packages.lock.json @@ -1159,6 +1159,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1183,6 +1184,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1195,6 +1197,15 @@ "ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis": "[2.0.2, 2.0.2]", "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } + }, + "data": { + "type": "Project" + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } } } } diff --git a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/packages.lock.json b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/packages.lock.json index 1423baf938d1..f7e8b634d243 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/packages.lock.json +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/packages.lock.json @@ -1315,6 +1315,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1339,6 +1340,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1352,19 +1354,29 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } } } } diff --git a/bitwarden_license/src/Commercial.Pam/Api/Endpoints/AccessRequestEndpoints.cs b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/AccessRequestEndpoints.cs new file mode 100644 index 000000000000..7f6cd2a210dd --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/AccessRequestEndpoints.cs @@ -0,0 +1,43 @@ +using System.Security.Claims; +using Bit.Commercial.Pam.Api.Endpoints.Handlers; +using Bit.Commercial.Pam.Api.Models.Request; + +namespace Bit.Commercial.Pam.Api.Endpoints; + +/// +/// The access-requests resource: lease requests through their lifecycle (the requester's own queue plus the +/// approver's queue and decision). Mirrors the routes the former AccessRequestsController served. +/// +internal static class AccessRequestEndpoints +{ + public static RouteGroupBuilder MapAccessRequestEndpoints(this RouteGroupBuilder group) + { + group.MapGet("inbox", (AccessRequestEndpointsHandler handler, ClaimsPrincipal user) => handler.GetInbox(user)) + .WithName("Pam_AccessRequests_GetInbox"); + + group.MapGet("history", (AccessRequestEndpointsHandler handler, ClaimsPrincipal user) => handler.GetHistory(user)) + .WithName("Pam_AccessRequests_GetHistory"); + + group.MapGet("mine", (AccessRequestEndpointsHandler handler, ClaimsPrincipal user) => handler.GetMine(user)) + .WithName("Pam_AccessRequests_GetMine"); + + group.MapPost("{id:guid}/decision", + (Guid id, AccessDecisionRequestModel model, AccessRequestEndpointsHandler handler, ClaimsPrincipal user) => + handler.Decide(user, id, model)) + .WithName("Pam_AccessRequests_Decide"); + + group.MapPost("{id:guid}/activate", + (Guid id, AccessRequestEndpointsHandler handler, ClaimsPrincipal user) => handler.Activate(user, id)) + .WithName("Pam_AccessRequests_Activate"); + + group.MapPost("{id:guid}/revoke", + async (Guid id, AccessRequestEndpointsHandler handler, ClaimsPrincipal user) => + { + await handler.Revoke(user, id); + return TypedResults.NoContent(); + }) + .WithName("Pam_AccessRequests_Revoke"); + + return group; + } +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Endpoints/AccessRuleEndpoints.cs b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/AccessRuleEndpoints.cs new file mode 100644 index 000000000000..05247c97f21f --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/AccessRuleEndpoints.cs @@ -0,0 +1,36 @@ +using Bit.Commercial.Pam.Api.Endpoints.Handlers; +using Bit.Commercial.Pam.Api.Models.Request; + +namespace Bit.Commercial.Pam.Api.Endpoints; + +/// +/// The organizations/{orgId}/access-rules resource: rule CRUD scoped to an organization. Mirrors the routes +/// the former AccessRulesController served. orgId is bound from the group's route prefix. +/// +internal static class AccessRuleEndpoints +{ + public static RouteGroupBuilder MapAccessRuleEndpoints(this RouteGroupBuilder group) + { + group.MapGet("", (Guid orgId, AccessRuleEndpointsHandler handler) => handler.GetAll(orgId)) + .WithName("Pam_AccessRules_GetAll"); + + group.MapGet("{id:guid}", (Guid orgId, Guid id, AccessRuleEndpointsHandler handler) => handler.Get(orgId, id)) + .WithName("Pam_AccessRules_Get"); + + group.MapPost("", (Guid orgId, AccessRuleRequestModel model, AccessRuleEndpointsHandler handler) => handler.Post(orgId, model)) + .WithName("Pam_AccessRules_Post"); + + group.MapPut("{id:guid}", (Guid orgId, Guid id, AccessRuleRequestModel model, AccessRuleEndpointsHandler handler) => handler.Put(orgId, id, model)) + .WithName("Pam_AccessRules_Put"); + + group.MapDelete("{id:guid}", + async (Guid orgId, Guid id, AccessRuleEndpointsHandler handler) => + { + await handler.Delete(orgId, id); + return TypedResults.NoContent(); + }) + .WithName("Pam_AccessRules_Delete"); + + return group; + } +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Endpoints/CipherLeaseEndpoints.cs b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/CipherLeaseEndpoints.cs new file mode 100644 index 000000000000..0e2c84b38f49 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/CipherLeaseEndpoints.cs @@ -0,0 +1,30 @@ +using System.Security.Claims; +using Bit.Commercial.Pam.Api.Endpoints.Handlers; +using Bit.Commercial.Pam.Api.Models.Request; + +namespace Bit.Commercial.Pam.Api.Endpoints; + +/// +/// The ciphers/{id}/lease resource: the per-cipher leasing entry points (pre-check, state, submit). +/// Mirrors the routes the former CipherLeaseController served. id is bound from the group's route +/// prefix. The deprecated GET …/lease/cipher read-back is hosted by a small MVC controller in the Api +/// project (it depends on the Api Vault response models). +/// +internal static class CipherLeaseEndpoints +{ + public static RouteGroupBuilder MapCipherLeaseEndpoints(this RouteGroupBuilder group) + { + group.MapGet("pre-check", (Guid id, CipherLeaseEndpointsHandler handler, ClaimsPrincipal user) => handler.PreCheck(user, id)) + .WithName("Pam_CipherLease_PreCheck"); + + group.MapGet("state", (Guid id, CipherLeaseEndpointsHandler handler, ClaimsPrincipal user) => handler.State(user, id)) + .WithName("Pam_CipherLease_State"); + + group.MapPost("", + (Guid id, AccessRequestCreateRequestModel model, CipherLeaseEndpointsHandler handler, ClaimsPrincipal user) => + handler.Post(user, id, model)) + .WithName("Pam_CipherLease_Post"); + + return group; + } +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Filters/PamExceptionHandlerEndpointFilter.cs b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Filters/PamExceptionHandlerEndpointFilter.cs new file mode 100644 index 000000000000..e3a110a9d257 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Filters/PamExceptionHandlerEndpointFilter.cs @@ -0,0 +1,92 @@ +using Bit.Core.Exceptions; +using Bit.Core.Models.Api; +using Microsoft.IdentityModel.Tokens; + +namespace Bit.Commercial.Pam.Api.Endpoints.Filters; + +/// +/// Minimal API equivalent of the internal-API branch of Bit.Api.Utilities.ExceptionHandlerFilterAttribute. +/// Minimal API endpoints do not run MVC exception filters and src/Api has no exception-handling middleware, +/// so this filter translates thrown exceptions into Bitwarden's with the same +/// status codes the controllers produced (e.g. → 404 "Resource not found."). +/// +public class PamExceptionHandlerEndpointFilter : IEndpointFilter +{ + public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + { + try + { + return await next(context); + } + catch (Exception exception) + { + return Handle(exception, context.HttpContext); + } + } + + private static IResult Handle(Exception exception, HttpContext httpContext) + { + var message = "An error has occurred."; + int statusCode; + ErrorResponseModel? validationModel = null; + + switch (exception) + { + case BadRequestException badRequestException: + statusCode = StatusCodes.Status400BadRequest; + if (badRequestException.ModelState != null) + { + validationModel = new ErrorResponseModel(badRequestException.ModelState); + } + else + { + message = badRequestException.Message; + } + break; + case NotSupportedException when !string.IsNullOrWhiteSpace(exception.Message): + message = exception.Message; + statusCode = StatusCodes.Status400BadRequest; + break; + case ApplicationException: + statusCode = StatusCodes.Status402PaymentRequired; + break; + case NotFoundException: + message = "Resource not found."; + statusCode = StatusCodes.Status404NotFound; + break; + case SecurityTokenValidationException: + message = "Invalid token."; + statusCode = StatusCodes.Status403Forbidden; + break; + case UnauthorizedAccessException: + message = "Unauthorized."; + statusCode = StatusCodes.Status401Unauthorized; + break; + case ConflictException: + message = exception.Message; + statusCode = StatusCodes.Status409Conflict; + break; + case AggregateException aggregateException: + statusCode = StatusCodes.Status400BadRequest; + validationModel = new ErrorResponseModel(message, aggregateException.InnerExceptions.Select(e => e.Message)); + break; + default: + httpContext.RequestServices.GetRequiredService>() + .LogError(0, exception, "Unhandled exception"); + message = "An unhandled server error has occurred."; + statusCode = StatusCodes.Status500InternalServerError; + break; + } + + var errorModel = validationModel ?? new ErrorResponseModel(message); + var environment = httpContext.RequestServices.GetRequiredService(); + if (environment.IsDevelopment()) + { + errorModel.ExceptionMessage = exception.Message; + errorModel.ExceptionStackTrace = exception.StackTrace; + errorModel.InnerExceptionMessage = exception.InnerException?.Message; + } + + return Results.Json(errorModel, statusCode: statusCode); + } +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Filters/PamValidationEndpointFilter.cs b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Filters/PamValidationEndpointFilter.cs new file mode 100644 index 000000000000..f2155c40714c --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Filters/PamValidationEndpointFilter.cs @@ -0,0 +1,44 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Models.Api; + +namespace Bit.Commercial.Pam.Api.Endpoints.Filters; + +/// +/// Minimal API equivalent of the MVC ModelStateValidationFilterAttribute: runs DataAnnotations validation +/// (including ) over the request-model arguments and, on failure, short-circuits +/// with Bitwarden's internal 400 — the same body the controllers produced. +/// +public class PamValidationEndpointFilter : IEndpointFilter +{ + private const string RequestModelNamespace = "Bit.Commercial.Pam.Api.Models.Request"; + + public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + { + foreach (var argument in context.Arguments) + { + if (argument is null || argument.GetType().Namespace != RequestModelNamespace) + { + continue; + } + + var results = new List(); + if (Validator.TryValidateObject(argument, new ValidationContext(argument), results, validateAllProperties: true)) + { + continue; + } + + var validationErrors = results + .SelectMany( + result => result.MemberNames.Any() ? result.MemberNames : [string.Empty], + (result, member) => (member, message: result.ErrorMessage ?? string.Empty)) + .GroupBy(error => error.member) + .ToDictionary(group => group.Key, group => (IEnumerable)group.Select(error => error.message).ToArray()); + + return Results.Json( + new ErrorResponseModel("The model state is invalid.", validationErrors), + statusCode: StatusCodes.Status400BadRequest); + } + + return await next(context); + } +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Filters/RequireFeatureEndpointFilter.cs b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Filters/RequireFeatureEndpointFilter.cs new file mode 100644 index 000000000000..95448d9c43ca --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Filters/RequireFeatureEndpointFilter.cs @@ -0,0 +1,23 @@ +using Bit.Core.Exceptions; +using Bit.Core.Services; + +namespace Bit.Commercial.Pam.Api.Endpoints.Filters; + +/// +/// Minimal API equivalent of : gates an endpoint group +/// behind a boolean feature flag. When the flag is disabled a (a +/// ) is thrown, which renders as a 404. +/// +public class RequireFeatureEndpointFilter(string featureFlagKey) : IEndpointFilter +{ + public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + { + var featureService = context.HttpContext.RequestServices.GetRequiredService(); + if (!featureService.IsEnabled(featureFlagKey)) + { + throw new FeatureUnavailableException(); + } + + return await next(context); + } +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Handlers/AccessRequestEndpointsHandler.cs b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Handlers/AccessRequestEndpointsHandler.cs new file mode 100644 index 000000000000..215f92cccb87 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Handlers/AccessRequestEndpointsHandler.cs @@ -0,0 +1,67 @@ +using System.Security.Claims; +using Bit.Commercial.Pam.Api.Models.Request; +using Bit.Commercial.Pam.Api.Models.Response; +using Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; +using Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; +using Bit.Core.Services; +using Bit.SharedWeb.Models.Response; + +namespace Bit.Commercial.Pam.Api.Endpoints.Handlers; + +/// +/// Handler for the access-requests resource. Holds the logic the AccessRequestsController previously +/// hosted; the Minimal API endpoints (see AccessRequestEndpoints) resolve this handler from DI. +/// +public class AccessRequestEndpointsHandler( + IUserService userService, + IListInboxRequestsQuery listInboxRequestsQuery, + IListInboxHistoryQuery listInboxHistoryQuery, + IDecideAccessRequestCommand decideAccessRequestCommand, + IListMyAccessRequestsQuery listMyAccessRequestsQuery, + IActivateAccessRequestCommand activateAccessRequestCommand, + ICancelAccessRequestCommand cancelAccessRequestCommand) +{ + public async Task> GetInbox(ClaimsPrincipal user) + { + var userId = userService.GetProperUserId(user)!.Value; + var requests = await listInboxRequestsQuery.GetPendingAsync(userId); + return new ListResponseModel( + requests.Select(r => new AccessRequestDetailsResponseModel(r))); + } + + public async Task> GetHistory(ClaimsPrincipal user) + { + var userId = userService.GetProperUserId(user)!.Value; + var history = await listInboxHistoryQuery.GetHistoryAsync(userId); + return new ListResponseModel( + history.Select(r => new AccessRequestDetailsResponseModel(r))); + } + + public async Task> GetMine(ClaimsPrincipal user) + { + var userId = userService.GetProperUserId(user)!.Value; + var requests = await listMyAccessRequestsQuery.GetMineAsync(userId); + return new ListResponseModel( + requests.Select(r => new AccessRequestDetailsResponseModel(r))); + } + + public async Task Decide(ClaimsPrincipal user, Guid id, AccessDecisionRequestModel model) + { + var userId = userService.GetProperUserId(user)!.Value; + var result = await decideAccessRequestCommand.DecideAsync(userId, id, model.ToSubmission()); + return new AccessRequestDetailsResponseModel(result); + } + + public async Task Activate(ClaimsPrincipal user, Guid id) + { + var userId = userService.GetProperUserId(user)!.Value; + var lease = await activateAccessRequestCommand.ActivateAsync(userId, id); + return new AccessLeaseResponseModel(lease); + } + + public async Task Revoke(ClaimsPrincipal user, Guid id) + { + var userId = userService.GetProperUserId(user)!.Value; + await cancelAccessRequestCommand.CancelAsync(userId, id); + } +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Handlers/AccessRuleEndpointsHandler.cs b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Handlers/AccessRuleEndpointsHandler.cs new file mode 100644 index 000000000000..9783d2b20401 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Handlers/AccessRuleEndpointsHandler.cs @@ -0,0 +1,83 @@ +using Bit.SharedWeb.Models.Response; +using Bit.Commercial.Pam.Api.Models.Request; +using Bit.Commercial.Pam.Api.Models.Response; +using Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Pam.Repositories; + +namespace Bit.Commercial.Pam.Api.Endpoints.Handlers; + +/// +/// Handler for the organizations/{orgId}/access-rules resource. Holds the logic the +/// AccessRulesController previously hosted; the Minimal API endpoints (see AccessRuleEndpoints) +/// resolve this handler from DI. +/// +public class AccessRuleEndpointsHandler( + ICurrentContext currentContext, + IAccessRuleRepository repository, + ICreateAccessRuleCommand createCommand, + IUpdateAccessRuleCommand updateCommand, + IDeleteAccessRuleCommand deleteCommand) +{ + public async Task> GetAll(Guid orgId) + { + await EnsureMemberAsync(orgId); + + var rules = await repository.GetManyDetailsByOrganizationIdAsync(orgId); + return new ListResponseModel( + rules.Select(rule => new AccessRuleResponseModel(rule))); + } + + public async Task Get(Guid orgId, Guid id) + { + await EnsureMemberAsync(orgId); + + var rule = await repository.GetDetailsByIdAsync(id); + if (rule is null || rule.OrganizationId != orgId) + { + throw new NotFoundException(); + } + + return new AccessRuleResponseModel(rule); + } + + public async Task Post(Guid orgId, AccessRuleRequestModel model) + { + await EnsureAdminAsync(orgId); + + var rule = await createCommand.CreateAsync(model.ToAccessRule(orgId), model.Collections); + return new AccessRuleResponseModel(rule); + } + + public async Task Put(Guid orgId, Guid id, AccessRuleRequestModel model) + { + await EnsureAdminAsync(orgId); + + var rule = await updateCommand.UpdateAsync(orgId, id, model.ToAccessRule(orgId), model.Collections); + return new AccessRuleResponseModel(rule); + } + + public async Task Delete(Guid orgId, Guid id) + { + await EnsureAdminAsync(orgId); + + await deleteCommand.DeleteAsync(orgId, id); + } + + private async Task EnsureMemberAsync(Guid orgId) + { + if (!await currentContext.OrganizationUser(orgId)) + { + throw new NotFoundException(); + } + } + + private async Task EnsureAdminAsync(Guid orgId) + { + if (!await currentContext.OrganizationAdmin(orgId) && !await currentContext.OrganizationOwner(orgId)) + { + throw new NotFoundException(); + } + } +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Handlers/CipherLeaseEndpointsHandler.cs b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Handlers/CipherLeaseEndpointsHandler.cs new file mode 100644 index 000000000000..59ae5eafd769 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Handlers/CipherLeaseEndpointsHandler.cs @@ -0,0 +1,41 @@ +using System.Security.Claims; +using Bit.Commercial.Pam.Api.Models.Request; +using Bit.Commercial.Pam.Api.Models.Response; +using Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; +using Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; +using Bit.Core.Services; + +namespace Bit.Commercial.Pam.Api.Endpoints.Handlers; + +/// +/// Handler for the ciphers/{id}/lease resource: the per-cipher leasing entry points (pre-check, state, +/// submit). The deprecated full-cipher read-back (GET …/lease/cipher) is hosted by a small MVC controller +/// in the Api project instead, since it depends on the Api Vault response models. +/// +public class CipherLeaseEndpointsHandler( + IUserService userService, + IAccessPreCheckQuery preCheckQuery, + IGetCipherAccessStateQuery cipherAccessStateQuery, + ISubmitAccessRequestCommand submitAccessRequestCommand) +{ + public async Task PreCheck(ClaimsPrincipal user, Guid id) + { + var userId = userService.GetProperUserId(user)!.Value; + var result = await preCheckQuery.PreCheckAsync(userId, id); + return new AccessPreCheckResponseModel(id, result); + } + + public async Task State(ClaimsPrincipal user, Guid id) + { + var userId = userService.GetProperUserId(user)!.Value; + var result = await cipherAccessStateQuery.GetStateAsync(userId, id); + return new CipherAccessStateResponseModel(result); + } + + public async Task Post(ClaimsPrincipal user, Guid id, AccessRequestCreateRequestModel model) + { + var userId = userService.GetProperUserId(user)!.Value; + var result = await submitAccessRequestCommand.SubmitAsync(userId, id, model.ToSubmission()); + return new AccessRequestResultResponseModel(result); + } +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Handlers/LeaseEndpointsHandler.cs b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Handlers/LeaseEndpointsHandler.cs new file mode 100644 index 000000000000..78c396b7aab0 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/Handlers/LeaseEndpointsHandler.cs @@ -0,0 +1,59 @@ +using System.Security.Claims; +using Bit.Commercial.Pam.Api.Models.Request; +using Bit.Commercial.Pam.Api.Models.Response; +using Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; +using Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; +using Bit.Core.Services; +using Bit.SharedWeb.Models.Response; + +namespace Bit.Commercial.Pam.Api.Endpoints.Handlers; + +/// +/// Handler for the leases resource. Holds the logic the LeasesController previously hosted; the +/// Minimal API endpoints (see LeaseEndpoints) are thin lambdas that resolve this handler from DI. +/// +public class LeaseEndpointsHandler( + IUserService userService, + IListActiveLeasesQuery listActiveLeasesQuery, + IListLeaseHistoryQuery listLeaseHistoryQuery, + IListMyActiveAccessLeasesQuery listMyActiveAccessLeasesQuery, + IRevokeAccessLeaseCommand revokeAccessLeaseCommand, + IRequestLeaseExtensionCommand requestLeaseExtensionCommand) +{ + public async Task> GetActive(ClaimsPrincipal user) + { + var userId = userService.GetProperUserId(user)!.Value; + var leases = await listActiveLeasesQuery.GetActiveAsync(userId); + return new ListResponseModel( + leases.Select(l => new AccessLeaseResponseModel(l))); + } + + public async Task> GetHistory(ClaimsPrincipal user) + { + var userId = userService.GetProperUserId(user)!.Value; + var leases = await listLeaseHistoryQuery.GetHistoryAsync(userId); + return new ListResponseModel( + leases.Select(l => new AccessLeaseResponseModel(l))); + } + + public async Task> GetMine(ClaimsPrincipal user) + { + var userId = userService.GetProperUserId(user)!.Value; + var leases = await listMyActiveAccessLeasesQuery.GetMineActiveAsync(userId); + return new ListResponseModel( + leases.Select(l => new AccessLeaseResponseModel(l))); + } + + public async Task Revoke(ClaimsPrincipal user, Guid id, AccessLeaseRevokeRequestModel model) + { + var userId = userService.GetProperUserId(user)!.Value; + await revokeAccessLeaseCommand.RevokeAsync(userId, id, model.Reason); + } + + public async Task Extend(ClaimsPrincipal user, Guid id, AccessLeaseExtensionRequestModel model) + { + var userId = userService.GetProperUserId(user)!.Value; + var details = await requestLeaseExtensionCommand.ExtendAsync(userId, model.ToSubmission(id)); + return new AccessRequestDetailsResponseModel(details); + } +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Endpoints/LeaseEndpoints.cs b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/LeaseEndpoints.cs new file mode 100644 index 000000000000..c81cd70d8f23 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/LeaseEndpoints.cs @@ -0,0 +1,39 @@ +using System.Security.Claims; +using Bit.Commercial.Pam.Api.Endpoints.Handlers; +using Bit.Commercial.Pam.Api.Models.Request; + +namespace Bit.Commercial.Pam.Api.Endpoints; + +/// +/// The leases resource: the caller's own leases, the governance surface over manageable collections, and the +/// per-lease actions (revoke, extend). Mirrors the routes the former LeasesController served. +/// +internal static class LeaseEndpoints +{ + public static RouteGroupBuilder MapLeaseEndpoints(this RouteGroupBuilder group) + { + group.MapGet("active", (LeaseEndpointsHandler handler, ClaimsPrincipal user) => handler.GetActive(user)) + .WithName("Pam_Leases_GetActive"); + + group.MapGet("history", (LeaseEndpointsHandler handler, ClaimsPrincipal user) => handler.GetHistory(user)) + .WithName("Pam_Leases_GetHistory"); + + group.MapGet("mine", (LeaseEndpointsHandler handler, ClaimsPrincipal user) => handler.GetMine(user)) + .WithName("Pam_Leases_GetMine"); + + group.MapPost("{id:guid}/revoke", + async (Guid id, AccessLeaseRevokeRequestModel model, LeaseEndpointsHandler handler, ClaimsPrincipal user) => + { + await handler.Revoke(user, id, model); + return TypedResults.NoContent(); + }) + .WithName("Pam_Leases_Revoke"); + + group.MapPost("{id:guid}/extend", + (Guid id, AccessLeaseExtensionRequestModel model, LeaseEndpointsHandler handler, ClaimsPrincipal user) => + handler.Extend(user, id, model)) + .WithName("Pam_Leases_Extend"); + + return group; + } +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Endpoints/PamEndpointsExtensions.cs b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/PamEndpointsExtensions.cs new file mode 100644 index 000000000000..c18eedf73cc5 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Endpoints/PamEndpointsExtensions.cs @@ -0,0 +1,46 @@ +using Bit.Commercial.Pam.Api.Endpoints.Filters; +using Bit.Core; +using Bit.Core.Auth.Identity; + +namespace Bit.Commercial.Pam.Api.Endpoints; + +/// +/// Maps the PAM HTTP surface as Minimal API endpoint groups. Each resource group shares the same cross-cutting +/// chain — authorization, exception → ErrorResponseModel translation, the PAM feature gate, and request-model +/// validation — reproducing what the MVC controllers received from attributes and conventions. Routes are identical +/// to the controllers they replace. +/// +public static class PamEndpointsExtensions +{ + public static void MapPamEndpoints(this IEndpointRouteBuilder endpoints) + { + endpoints.MapGroup("/leases").WithPamDefaults().MapLeaseEndpoints(); + endpoints.MapGroup("/access-requests").WithPamDefaults().MapAccessRequestEndpoints(); + endpoints.MapGroup("/organizations/{orgId:guid}/access-rules").WithPamDefaults().MapAccessRuleEndpoints(); + endpoints.MapGroup("/ciphers/{id:guid}/lease").WithPamDefaults().MapCipherLeaseEndpoints(); + } + + /// + /// Applies the shared PAM endpoint chain to a group. Order matters: the exception filter is outermost so it + /// translates throws from the feature filter (), + /// the validation filter, and the handlers into the ErrorResponseModel contract. + /// + private static RouteGroupBuilder WithPamDefaults(this RouteGroupBuilder group) + { + group.RequireAuthorization(Policies.Application); + group.AddEndpointFilter(); + group.RequireFeature(FeatureFlagKeys.Pam); + group.AddEndpointFilter(); + group.WithGroupName("internal"); + return group; + } + + /// + /// Minimal API equivalent of [RequireFeature(key)]: gates every endpoint in the group behind the flag. + /// + public static RouteGroupBuilder RequireFeature(this RouteGroupBuilder group, string featureFlagKey) + { + group.AddEndpointFilter(new RequireFeatureEndpointFilter(featureFlagKey)); + return group; + } +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Models/Request/AccessDecisionRequestModel.cs b/bitwarden_license/src/Commercial.Pam/Api/Models/Request/AccessDecisionRequestModel.cs new file mode 100644 index 000000000000..d6b64b3b74ee --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Models/Request/AccessDecisionRequestModel.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Commercial.Pam.Models; +using Bit.Pam.Enums; + +namespace Bit.Commercial.Pam.Api.Models.Request; + +/// +/// An approver's decision on a pending access request. is the +/// value on the wire (0 = deny, 1 = approve); +/// is optional. +/// +public class AccessDecisionRequestModel +{ + [Required] + [EnumDataType(typeof(AccessDecisionVerdict))] + public AccessDecisionVerdict? Verdict { get; set; } + + public string? Comment { get; set; } + + public AccessDecisionSubmission ToSubmission() => new() + { + Verdict = Verdict!.Value, + Comment = Comment, + }; +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Models/Request/AccessLeaseExtensionRequestModel.cs b/bitwarden_license/src/Commercial.Pam/Api/Models/Request/AccessLeaseExtensionRequestModel.cs new file mode 100644 index 000000000000..7839824cddd6 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Models/Request/AccessLeaseExtensionRequestModel.cs @@ -0,0 +1,21 @@ +using Bit.Commercial.Pam.Models; +namespace Bit.Commercial.Pam.Api.Models.Request; + +/// +/// A request to extend an active lease, identified by the route's lease id. The lease's end is pushed out by +/// ; a justifying is required. Extensions are always auto-approved, +/// subject to the governing rule allowing extensions and the per-lease maximum not being reached. +/// +public class AccessLeaseExtensionRequestModel +{ + public int DurationSeconds { get; set; } + + public string? Reason { get; set; } + + public AccessLeaseExtensionSubmission ToSubmission(Guid leaseId) => new() + { + LeaseId = leaseId, + DurationSeconds = DurationSeconds, + Reason = Reason, + }; +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Models/Request/AccessLeaseRevokeRequestModel.cs b/bitwarden_license/src/Commercial.Pam/Api/Models/Request/AccessLeaseRevokeRequestModel.cs new file mode 100644 index 000000000000..1bf078f34c89 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Models/Request/AccessLeaseRevokeRequestModel.cs @@ -0,0 +1,9 @@ +namespace Bit.Commercial.Pam.Api.Models.Request; + +/// +/// A request to revoke an active lease early. is optional and retained for the audit trail. +/// +public class AccessLeaseRevokeRequestModel +{ + public string? Reason { get; set; } +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Models/Request/AccessRequestCreateRequestModel.cs b/bitwarden_license/src/Commercial.Pam/Api/Models/Request/AccessRequestCreateRequestModel.cs new file mode 100644 index 000000000000..8af9108c7cc1 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Models/Request/AccessRequestCreateRequestModel.cs @@ -0,0 +1,26 @@ +using Bit.Commercial.Pam.Models; +namespace Bit.Commercial.Pam.Api.Models.Request; + +/// +/// A request to lease a cipher. Supply for the automatic path, or +/// / + for the human path. The server validates the shape +/// against the cipher's resolved approval outcome (run a pre-check first). The cipher is identified by the route. +/// +public class AccessRequestCreateRequestModel +{ + public int? DurationSeconds { get; set; } + + public DateTime? Start { get; set; } + + public DateTime? End { get; set; } + + public string? Reason { get; set; } + + public AccessRequestSubmission ToSubmission() => new() + { + DurationSeconds = DurationSeconds, + Start = Start, + End = End, + Reason = Reason, + }; +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Models/Request/AccessRuleRequestModel.cs b/bitwarden_license/src/Commercial.Pam/Api/Models/Request/AccessRuleRequestModel.cs new file mode 100644 index 000000000000..2c5fd18fd521 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Models/Request/AccessRuleRequestModel.cs @@ -0,0 +1,80 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using Bit.Pam.Entities; + +namespace Bit.Commercial.Pam.Api.Models.Request; + +public class AccessRuleRequestModel +{ + [Required] + [StringLength(256)] + public string Name { get; set; } = null!; + + public string? Description { get; set; } + + [Required] + public object Conditions { get; set; } = null!; + + /// + /// When true, the rule enforces a per-cipher singleton (at most one active lease per cipher across all users). + /// + public bool SingleActiveLease { get; set; } + + /// + /// 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; } + + /// + /// 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; } + + /// + /// When false, the rule is inactive and does not gate access for the collections it governs. Defaults to + /// true so a request that omits the field creates an active rule. + /// + public bool Enabled { get; set; } = true; + + /// + /// 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; } + + /// + /// The longest a single extension may run, in seconds. Required to be positive when + /// is true. + /// + public int? MaxExtensionDurationSeconds { get; set; } + + /// + /// The complete set of collections this rule governs. The rule's associations are replaced to match + /// exactly this set; an empty array clears all associations. + /// + [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 when je.ValueKind == JsonValueKind.Null => string.Empty, + JsonElement je => je.GetRawText(), + _ => JsonSerializer.Serialize(conditions), + }; +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessDeciderKindNames.cs b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessDeciderKindNames.cs new file mode 100644 index 000000000000..33b2163a99dc --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessDeciderKindNames.cs @@ -0,0 +1,20 @@ +using Bit.Pam.Enums; + +namespace Bit.Commercial.Pam.Api.Models.Response; + +/// +/// Maps the backend to the vocabulary the client expects on a decision: +/// human | automatic. Mirrors / . +/// +public static class AccessDeciderKindNames +{ + public const string Human = "human"; + public const string Automatic = "automatic"; + + public static string From(AccessDeciderKind kind) => kind switch + { + AccessDeciderKind.Human => Human, + AccessDeciderKind.Automatic => Automatic, + _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null), + }; +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessLeaseResponseModel.cs b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessLeaseResponseModel.cs new file mode 100644 index 000000000000..2efddaab6e29 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessLeaseResponseModel.cs @@ -0,0 +1,58 @@ +using Bit.Core.Models.Api; +using Bit.Pam.Entities; + +namespace Bit.Commercial.Pam.Api.Models.Response; + +/// +/// An access lease as its requester sees it: the originating request, string status vocabulary, and revocation +/// fields. Powers the request-submission envelope, the caller-scoped "my active leases" surface, and the cipher +/// access-state snapshot. Fields without a backing store in v1 (, +/// ) are null. +/// +public class AccessLeaseResponseModel : ResponseModel +{ + public AccessLeaseResponseModel(AccessLease lease) + : base("accessLease") + { + ArgumentNullException.ThrowIfNull(lease); + + Id = lease.Id; + RequestId = lease.AccessRequestId; + CipherId = lease.CipherId; + CollectionId = lease.CollectionId; + OrganizationId = lease.OrganizationId; + RequesterId = lease.RequesterId; + Status = AccessLeaseStatusNames.From(lease.Status); + NotBefore = lease.NotBefore.AsUtc(); + NotAfter = lease.NotAfter.AsUtc(); + RevokedAt = lease.RevokedDate.AsUtc(); + RevokedByUserId = lease.RevokedBy; + } + + public Guid Id { get; } + + /// The request this lease was born from. + public Guid RequestId { get; } + + public Guid CipherId { get; } + public Guid CollectionId { get; } + + /// The access rule that gated the cipher at grant time. Not tracked in v1. + public string? RuleId => null; + + public Guid OrganizationId { get; } + + /// The user the lease was granted to (the original requester). + public Guid RequesterId { get; } + + /// active | expired | revoked. + public string Status { get; } + + public DateTime NotBefore { get; } + public DateTime NotAfter { get; } + public DateTime? RevokedAt { get; } + public Guid? RevokedByUserId { get; } + + /// The reason captured on early revocation. Recorded on the audit decision, not surfaced here in v1. + public string? RevocationReason => null; +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessLeaseStatusNames.cs b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessLeaseStatusNames.cs new file mode 100644 index 000000000000..7d4ff8423cf3 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessLeaseStatusNames.cs @@ -0,0 +1,22 @@ +using Bit.Pam.Enums; + +namespace Bit.Commercial.Pam.Api.Models.Response; + +/// +/// Maps the backend to the status vocabulary the leasing client expects: +/// active | expired | revoked. Mirrors for the request side. +/// +public static class AccessLeaseStatusNames +{ + public const string Active = "active"; + public const string Expired = "expired"; + public const string Revoked = "revoked"; + + public static string From(AccessLeaseStatus status) => status switch + { + AccessLeaseStatus.Active => Active, + AccessLeaseStatus.Expired => Expired, + AccessLeaseStatus.Revoked => Revoked, + _ => throw new ArgumentOutOfRangeException(nameof(status), status, null), + }; +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessPreCheckResponseModel.cs b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessPreCheckResponseModel.cs new file mode 100644 index 000000000000..c15f5b0ad26f --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessPreCheckResponseModel.cs @@ -0,0 +1,29 @@ +using Bit.Commercial.Pam.Enums; +using Bit.Commercial.Pam.Models; +using Bit.Core.Models.Api; + +namespace Bit.Commercial.Pam.Api.Models.Response; + +public class AccessPreCheckResponseModel : ResponseModel +{ + public AccessPreCheckResponseModel(Guid cipherId, AccessPreCheckResult result) + : base("accessPreCheck") + { + CipherId = cipherId; + ApprovalMode = result.ApprovalMode; + HasActiveLease = result.HasActiveLease; + } + + public Guid CipherId { get; } + + /// + /// when a request would be approved immediately, + /// when it needs an approver. + /// + public AccessApprovalMode ApprovalMode { get; } + + /// + /// True when the caller already holds an active lease: reveal the credential, no request needed. + /// + public bool HasActiveLease { get; } +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessRequestDecisionResponseModel.cs b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessRequestDecisionResponseModel.cs new file mode 100644 index 000000000000..f60b2de27386 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessRequestDecisionResponseModel.cs @@ -0,0 +1,32 @@ +using Bit.Pam.Enums; + +namespace Bit.Commercial.Pam.Api.Models.Response; + +/// +/// One decision on an access request: who decided (), their identity for a human decision, +/// the verdict, an optional comment, and when. An element of +/// — the request's full decision log, oldest first. +/// +/// For an automatic (access-rule) decision // are null; for a +/// human decision they carry the approver (name/email denormalized by the server, null only when the user could not be +/// resolved). +/// +public class AccessRequestDecisionResponseModel +{ + /// human | automatic. + public string DeciderKind { get; init; } = AccessDeciderKindNames.Human; + + /// The human approver, or null for an automatic decision. + public Guid? Id { get; init; } + + public string? Name { get; init; } + + public string? Email { get; init; } + + public string? Comment { get; init; } + + /// The verdict reached (0 = deny, 1 = approve). + public AccessDecisionVerdict Verdict { get; init; } + + public DateTime DecidedAt { get; init; } +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessRequestDetailsResponseModel.cs b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessRequestDetailsResponseModel.cs new file mode 100644 index 000000000000..c0e1aa1cd8c7 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessRequestDetailsResponseModel.cs @@ -0,0 +1,110 @@ +using Bit.Core.Models.Api; +using Bit.Pam.Models; + +namespace Bit.Commercial.Pam.Api.Models.Response; + +/// +/// An access request with its denormalized display fields (cipher/collection names, requester identity), serving the +/// approver inbox, the caller's own request list, and the cipher access-state snapshot. Fields without a backing +/// store in v1 (, ) are always null. +/// +public class AccessRequestDetailsResponseModel : ResponseModel +{ + public AccessRequestDetailsResponseModel(AccessRequestDetails details) + : base("accessRequestDetails") + { + ArgumentNullException.ThrowIfNull(details); + + Id = details.Id; + CipherId = details.CipherId; + CollectionId = details.CollectionId; + OrganizationId = details.OrganizationId; + RequesterId = details.RequesterId; + Status = AccessRequestStatusNames.From(details.Status, details.ProducedLeaseId.HasValue); + RequestedNotBefore = details.NotBefore.AsUtc(); + RequestedNotAfter = details.NotAfter.AsUtc(); + RequestedTtlSeconds = (int)(details.NotAfter - details.NotBefore).TotalSeconds; + Reason = details.Reason; + SubmittedAt = details.CreationDate.AsUtc(); + ResolvedAt = details.ResolvedDate.AsUtc(); + // The request's full decision log, oldest first: one element per recorded decision (human or automatic). + // Empty only while pending (no decision recorded yet). + Decisions = details.Decisions + .Select(d => new AccessRequestDecisionResponseModel + { + DeciderKind = AccessDeciderKindNames.From(d.DeciderKind), + Id = d.Id, + Name = d.Name, + Email = d.Email, + Comment = d.Comment, + Verdict = d.Verdict, + DecidedAt = d.DecidedAt.AsUtc(), + }) + .ToList(); + ProducedLeaseId = details.ProducedLeaseId; + ProducedLeaseStatus = details.ProducedLeaseStatus.HasValue + ? AccessLeaseStatusNames.From(details.ProducedLeaseStatus.Value) + : null; + ExtensionOfLeaseId = details.ExtensionOfLeaseId; + CipherName = details.CipherName; + CollectionName = details.CollectionName; + RequesterName = details.RequesterName; + RequesterEmail = details.RequesterEmail; + } + + public Guid Id { get; } + public Guid CipherId { get; } + public Guid CollectionId { get; } + + /// The access rule that gated the cipher at submit time. Not tracked in v1. + public string? RuleId => null; + + public Guid OrganizationId { get; } + public Guid RequesterId { get; } + + /// pending | approved | activated | denied | canceled | expired. + public string Status { get; } + + public DateTime RequestedNotBefore { get; } + public DateTime RequestedNotAfter { get; } + public int RequestedTtlSeconds { get; } + public string? Reason { get; } + public DateTime SubmittedAt { get; } + public DateTime? ResolvedAt { get; } + + /// Distinct from ; set when an approved request lapses unactivated. Not tracked in v1. + public DateTime? ExpiredAt => null; + + /// + /// The request's decision log, oldest first — one element per decision (human or automatic). Each carries who + /// decided (deciderKind), the verdict, and (for a human decision) the approver's identity and comment. + /// Empty only while pending. An array so multi-party approval lands without breaking the contract. + /// + public IEnumerable Decisions { get; } + + /// Set once an approved request has produced a lease. + public Guid? ProducedLeaseId { get; } + + /// + /// The produced lease's status (active | expired | revoked), or null when no lease exists. The inbox uses + /// this to keep an ended lease out of the "active" group so it is not offered for revocation. + /// + public string? ProducedLeaseStatus { get; } + + /// The parent lease if this is an extension request. + public Guid? ExtensionOfLeaseId { get; } + + /// + /// Always null in v0: human requests are window-bound, so itself bounds when an + /// approved request can be activated. A separate deadline only becomes meaningful with on-demand (duration-only) + /// human requests, which don't exist yet. + /// + public DateTime? ActivationDeadline => null; + + /// The cipher's client-encrypted name. The only cipher attribute exposed by the inbox. + public string? CipherName { get; } + + public string? CollectionName { get; } + public string? RequesterName { get; } + public string? RequesterEmail { get; } +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessRequestResponseModel.cs b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessRequestResponseModel.cs new file mode 100644 index 000000000000..fcb34799d55a --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessRequestResponseModel.cs @@ -0,0 +1,36 @@ +using Bit.Core.Models.Api; +using Bit.Pam.Entities; + +namespace Bit.Commercial.Pam.Api.Models.Response; + +public class AccessRequestResponseModel : ResponseModel +{ + public AccessRequestResponseModel(AccessRequest request) + : base("accessRequest") + { + ArgumentNullException.ThrowIfNull(request); + + Id = request.Id; + CipherId = request.CipherId; + CollectionId = request.CollectionId; + OrganizationId = request.OrganizationId; + Status = AccessRequestStatusNames.From(request.Status, hasLease: false); + NotBefore = request.NotBefore.AsUtc(); + NotAfter = request.NotAfter.AsUtc(); + Reason = request.Reason; + CreationDate = request.CreationDate.AsUtc(); + } + + public Guid Id { get; } + public Guid CipherId { get; } + public Guid CollectionId { get; } + public Guid OrganizationId { get; } + + /// pending | approved | activated | denied | cancelled | expired. + public string Status { get; } + + public DateTime NotBefore { get; } + public DateTime NotAfter { get; } + public string? Reason { get; } + public DateTime CreationDate { get; } +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessRequestResultResponseModel.cs b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessRequestResultResponseModel.cs new file mode 100644 index 000000000000..5e398e84f11d --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessRequestResultResponseModel.cs @@ -0,0 +1,26 @@ +using Bit.Commercial.Pam.Enums; +using Bit.Commercial.Pam.Models; +using Bit.Core.Models.Api; + +namespace Bit.Commercial.Pam.Api.Models.Response; + +public class AccessRequestResultResponseModel : ResponseModel +{ + public AccessRequestResultResponseModel(AccessRequestResult result) + : base("accessRequestResult") + { + ArgumentNullException.ThrowIfNull(result); + + ApprovalMode = result.ApprovalMode; + Request = new AccessRequestResponseModel(result.Request); + } + + /// + /// when the was approved on submit and is ready + /// to activate (the client shows "Start lease"), when it is pending an + /// approver. No lease is minted at submit on either path; the requester activates the request to start the lease. + /// + public AccessApprovalMode ApprovalMode { get; } + + public AccessRequestResponseModel Request { get; } +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessRequestStatusNames.cs b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessRequestStatusNames.cs new file mode 100644 index 000000000000..a5872777809a --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessRequestStatusNames.cs @@ -0,0 +1,28 @@ +using Bit.Pam.Enums; + +namespace Bit.Commercial.Pam.Api.Models.Response; + +/// +/// Maps the backend (plus whether the request has produced a lease) to the status +/// vocabulary the approver-inbox client expects: pending | approved | activated | denied | cancelled | expired. +/// An approved request that has produced a lease is reported as activated. +/// +public static class AccessRequestStatusNames +{ + public const string Pending = "pending"; + public const string Approved = "approved"; + public const string Activated = "activated"; + public const string Denied = "denied"; + public const string Cancelled = "cancelled"; + public const string Expired = "expired"; + + public static string From(AccessRequestStatus status, bool hasLease) => status switch + { + AccessRequestStatus.Pending => Pending, + AccessRequestStatus.Approved => hasLease ? Activated : Approved, + AccessRequestStatus.Denied => Denied, + AccessRequestStatus.Cancelled => Cancelled, + AccessRequestStatus.ExpiredUnanswered => Expired, + _ => throw new ArgumentOutOfRangeException(nameof(status), status, null), + }; +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessRuleResponseModel.cs b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessRuleResponseModel.cs new file mode 100644 index 000000000000..778aaddd1732 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/AccessRuleResponseModel.cs @@ -0,0 +1,60 @@ +using System.Text.Json; +using Bit.Core.Models.Api; +using Bit.Pam.Models; + +namespace Bit.Commercial.Pam.Api.Models.Response; + +public class AccessRuleResponseModel : ResponseModel +{ + public AccessRuleResponseModel(AccessRuleDetails rule) + : base("accessRule") + { + ArgumentNullException.ThrowIfNull(rule); + + Id = rule.Id; + OrganizationId = rule.OrganizationId; + Name = rule.Name; + Description = rule.Description; + Conditions = TryParseConditions(rule.Conditions); + SingleActiveLease = rule.SingleActiveLease; + DefaultLeaseDurationSeconds = rule.DefaultLeaseDurationSeconds; + MaxLeaseDurationSeconds = rule.MaxLeaseDurationSeconds; + Enabled = rule.Enabled; + AllowsExtensions = rule.AllowsExtensions; + MaxExtensionDurationSeconds = rule.MaxExtensionDurationSeconds; + CreationDate = rule.CreationDate.AsUtc(); + RevisionDate = rule.RevisionDate.AsUtc(); + Collections = rule.CollectionIds.ToList(); + } + + public Guid Id { get; } + public Guid OrganizationId { get; } + public string Name { get; } + public string? Description { get; } + public JsonElement? Conditions { get; } + public bool SingleActiveLease { get; } + public int? DefaultLeaseDurationSeconds { get; } + public int? MaxLeaseDurationSeconds { get; } + public bool Enabled { get; } + public bool AllowsExtensions { get; } + public int? MaxExtensionDurationSeconds { get; } + public DateTime CreationDate { get; } + public DateTime RevisionDate { get; } + public IEnumerable Collections { 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/Commercial.Pam/Api/Models/Response/CipherAccessStateResponseModel.cs b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/CipherAccessStateResponseModel.cs new file mode 100644 index 000000000000..8e3e124b2bae --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/CipherAccessStateResponseModel.cs @@ -0,0 +1,42 @@ +using Bit.Commercial.Pam.Models; +using Bit.Core.Models.Api; + +namespace Bit.Commercial.Pam.Api.Models.Response; + +/// +/// A single-snapshot read of the caller's access state for one cipher, powering the cipher-view banner and the +/// vault-row badge. At most one of the three branches is meaningfully "next": an active lease authorizes access, a +/// pending request awaits a decision, and an approved request awaits activation by the caller. +/// +public class CipherAccessStateResponseModel : ResponseModel +{ + public CipherAccessStateResponseModel(CipherAccessState state) + : base("cipherAccessState") + { + ArgumentNullException.ThrowIfNull(state); + + CipherId = state.CipherId; + ActiveLease = state.ActiveLease is null ? null : new AccessLeaseResponseModel(state.ActiveLease); + PendingRequest = state.PendingRequest is null ? null : new AccessRequestDetailsResponseModel(state.PendingRequest); + ApprovedRequest = state.ApprovedRequest is null ? null : new AccessRequestDetailsResponseModel(state.ApprovedRequest); + ExtensionsAllowed = state.ExtensionsAllowed; + MaxExtensionDurationSeconds = state.MaxExtensionDurationSeconds; + } + + public Guid CipherId { get; } + + public AccessLeaseResponseModel? ActiveLease { get; } + public AccessRequestDetailsResponseModel? PendingRequest { get; } + + /// + /// An approved request awaiting activation, with a window that can still produce access. The caller activates it + /// to mint the lease; lapsed approvals are never surfaced here. + /// + public AccessRequestDetailsResponseModel? ApprovedRequest { get; } + + /// Whether the active lease can still be extended (the rule opts in and it has not been extended yet). + public bool ExtensionsAllowed { get; } + + /// The longest a single extension of the active lease may run, in seconds; null when there is no cap or no active lease. + public int? MaxExtensionDurationSeconds { get; } +} diff --git a/bitwarden_license/src/Commercial.Pam/Api/Models/Response/PamDateTimeExtensions.cs b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/PamDateTimeExtensions.cs new file mode 100644 index 000000000000..d170b50902cd --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/Models/Response/PamDateTimeExtensions.cs @@ -0,0 +1,23 @@ +namespace Bit.Commercial.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/Commercial.Pam/Api/PamApiServiceCollectionExtensions.cs b/bitwarden_license/src/Commercial.Pam/Api/PamApiServiceCollectionExtensions.cs new file mode 100644 index 000000000000..fd4376aa8dc1 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Api/PamApiServiceCollectionExtensions.cs @@ -0,0 +1,19 @@ +using Bit.Commercial.Pam.Api.Endpoints.Handlers; + +namespace Bit.Commercial.Pam.Api; + +public static class PamApiServiceCollectionExtensions +{ + /// + /// Registers the PAM Minimal API endpoint handlers. The endpoints (see PamEndpointsExtensions) resolve + /// these from DI. PAM is a commercial feature, so this is only wired in non-OSS builds. + /// + public static IServiceCollection AddPamApiServices(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + return services; + } +} diff --git a/bitwarden_license/src/Commercial.Pam/Commercial.Pam.csproj b/bitwarden_license/src/Commercial.Pam/Commercial.Pam.csproj new file mode 100644 index 000000000000..53fe2115f558 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Commercial.Pam.csproj @@ -0,0 +1,29 @@ + + + + Bit.Commercial.Pam + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bitwarden_license/src/Commercial.Pam/Engine/AccessEvaluation.cs b/bitwarden_license/src/Commercial.Pam/Engine/AccessEvaluation.cs new file mode 100644 index 000000000000..d21c67772673 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Engine/AccessEvaluation.cs @@ -0,0 +1,49 @@ +namespace Bit.Commercial.Pam.Engine; + +public enum AccessEvaluationOutcome +{ + Allow, + RequiresApproval, + Deny, +} + +public enum DenyReason +{ + None = 0, + NotWithinIpRange, + NotWithinTimeWindow, + UnsupportedCondition, +} + +public sealed record AccessEvaluation +{ + public required AccessEvaluationOutcome Outcome { get; init; } + public DenyReason Reason { get; init; } = DenyReason.None; + + public static AccessEvaluation Allow { get; } = new() { Outcome = AccessEvaluationOutcome.Allow }; + public static AccessEvaluation RequiresApproval { get; } = new() { Outcome = AccessEvaluationOutcome.RequiresApproval }; + + public static AccessEvaluation Deny(DenyReason reason) => new() + { + Outcome = AccessEvaluationOutcome.Deny, + Reason = reason + }; + + public static AccessEvaluation Combine(IEnumerable evaluations) + { + var requiresApproval = false; + foreach (var evaluation in evaluations) + { + switch (evaluation.Outcome) + { + case AccessEvaluationOutcome.Deny: + return evaluation; + case AccessEvaluationOutcome.RequiresApproval: + requiresApproval = true; + break; + } + } + + return requiresApproval ? RequiresApproval : Allow; + } +} diff --git a/bitwarden_license/src/Commercial.Pam/Engine/AccessRuleEngine.cs b/bitwarden_license/src/Commercial.Pam/Engine/AccessRuleEngine.cs new file mode 100644 index 000000000000..453c054bbb3b --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Engine/AccessRuleEngine.cs @@ -0,0 +1,82 @@ +using System.Globalization; +using System.Net; +using Bit.Commercial.Pam.Models.Conditions; + +namespace Bit.Commercial.Pam.Engine; + +/// +/// Evaluates the access rule's flat list of s against the caller's signals. Each +/// condition yields an ; the results combine with deny taking precedence over a +/// pending approval, which in turn takes precedence over allow. An empty list is vacuously satisfied (allow). +/// Unparseable inputs fail closed before they reach the engine. +/// +public sealed class AccessRuleEngine : IAccessRuleEngine +{ + public AccessEvaluation Evaluate(IReadOnlyList conditions, AccessSignals signals) => + AccessEvaluation.Combine(conditions.Select(condition => EvaluateCondition(condition, signals))); + + private static AccessEvaluation EvaluateCondition(AccessCondition condition, AccessSignals signals) => condition switch + { + HumanApprovalCondition => AccessEvaluation.RequiresApproval, + IpAllowlistCondition ip => EvaluateIpAllowlist(ip, signals), + TimeOfDayCondition time => EvaluateTimeOfDay(time, signals), + // A condition kind the engine does not understand cannot be shown to be satisfied, so deny. + _ => AccessEvaluation.Deny(DenyReason.UnsupportedCondition), + }; + + private static AccessEvaluation EvaluateIpAllowlist(IpAllowlistCondition condition, AccessSignals signals) + { + // An allowlist with no entries permits no address; combined with an unknown caller IP, both fail closed. + if (condition.Cidrs.Count == 0 || signals.IpAddress is null) + { + return AccessEvaluation.Deny(DenyReason.NotWithinIpRange); + } + + foreach (var cidr in condition.Cidrs) + { + if (IPNetwork.TryParse(cidr, out var network) && network.Contains(signals.IpAddress)) + { + return AccessEvaluation.Allow; + } + } + + return AccessEvaluation.Deny(DenyReason.NotWithinIpRange); + } + + private static AccessEvaluation EvaluateTimeOfDay(TimeOfDayCondition condition, AccessSignals signals) + { + if (!TimeZoneInfo.TryFindSystemTimeZoneById(condition.Tz, out var timeZone)) + { + // The window cannot be evaluated without a valid timezone, so fail closed. + return AccessEvaluation.Deny(DenyReason.NotWithinTimeWindow); + } + + var local = TimeZoneInfo.ConvertTime(signals.Timestamp, timeZone); + var day = local.DayOfWeek; + var time = TimeOnly.FromTimeSpan(local.TimeOfDay); + + foreach (var window in condition.Windows) + { + if (WindowContains(window, day, time)) + { + return AccessEvaluation.Allow; + } + } + + return AccessEvaluation.Deny(DenyReason.NotWithinTimeWindow); + } + + private static bool WindowContains(TimeWindow window, DayOfWeek day, TimeOnly time) + { + // AccessWeekday values align with System.DayOfWeek (Sunday = 0), so a direct cast compares correctly. + var dayMatches = window.Days.Any(d => (DayOfWeek)d == day); + if (!dayMatches) + { + return false; + } + + return TimeOnly.TryParseExact(window.From, "HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out var from) + && TimeOnly.TryParseExact(window.To, "HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out var to) + && time >= from && time <= to; + } +} diff --git a/bitwarden_license/src/Commercial.Pam/Engine/AccessSignals.cs b/bitwarden_license/src/Commercial.Pam/Engine/AccessSignals.cs new file mode 100644 index 000000000000..6594abd104c0 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Engine/AccessSignals.cs @@ -0,0 +1,25 @@ +using System.Net; + +namespace Bit.Commercial.Pam.Engine; + +/// +/// The request-time inputs an access rule is evaluated against: the caller's source IP and the instant the +/// evaluation is performed. is null when the caller's address cannot be determined, which +/// IP-restricted rules treat as a denial so access never opens up on a missing signal. +/// +public sealed record AccessSignals +{ + public required IPAddress? IpAddress { get; init; } + public required DateTimeOffset Timestamp { get; init; } + + /// + /// Builds the signals for the current request: the caller's source IP (parsed, or null when it is absent or + /// unparseable) and the supplied evaluation . Callers typically pass the request's + /// source address (e.g. ICurrentContext.IpAddress). + /// + public static AccessSignals From(string? ipAddress, DateTimeOffset timestamp) => new() + { + IpAddress = IPAddress.TryParse(ipAddress, out var ip) ? ip : null, + Timestamp = timestamp, + }; +} diff --git a/bitwarden_license/src/Commercial.Pam/Engine/IAccessRuleEngine.cs b/bitwarden_license/src/Commercial.Pam/Engine/IAccessRuleEngine.cs new file mode 100644 index 000000000000..b3cd1c003525 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Engine/IAccessRuleEngine.cs @@ -0,0 +1,14 @@ +using Bit.Commercial.Pam.Models.Conditions; + +namespace Bit.Commercial.Pam.Engine; + +/// +/// Evaluates an access rule's conditions — a flat list of ANDed together — against +/// the request-time , deciding whether access is allowed, denied, or gated on human +/// approval. The engine is pure: it reads no state and issues no leases. Lease lifecycle is owned by the lease +/// commands and queries, which call the engine to decide whether a lease may be issued or its data handed over. +/// +public interface IAccessRuleEngine +{ + AccessEvaluation Evaluate(IReadOnlyList conditions, AccessSignals signals); +} diff --git a/bitwarden_license/src/Commercial.Pam/Enums/AccessApprovalMode.cs b/bitwarden_license/src/Commercial.Pam/Enums/AccessApprovalMode.cs new file mode 100644 index 000000000000..4ba9a6867356 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Enums/AccessApprovalMode.cs @@ -0,0 +1,11 @@ +namespace Bit.Commercial.Pam.Enums; + +/// +/// The approval path a lease request will take, surfaced by the pre-check so the client can present the right +/// workflow: (pick a duration) or (pick a window + justify). +/// +public enum AccessApprovalMode : byte +{ + Automatic = 0, + Human = 1, +} diff --git a/bitwarden_license/src/Commercial.Pam/Enums/AccessWeekday.cs b/bitwarden_license/src/Commercial.Pam/Enums/AccessWeekday.cs new file mode 100644 index 000000000000..b8693bb06d6c --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Enums/AccessWeekday.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; +using Bit.Commercial.Pam.Models.Conditions; + +namespace Bit.Commercial.Pam.Enums; + +/// +/// A day of the week used in a window. Values align with +/// (Sunday = 0) so the engine can compare directly. Serialized as the lowercase +/// three-letter tokens ("sun".."sat") via . +/// +[JsonConverter(typeof(AccessWeekdayJsonConverter))] +public enum AccessWeekday : byte +{ + Sun = 0, + Mon = 1, + Tue = 2, + Wed = 3, + Thu = 4, + Fri = 5, + Sat = 6, +} diff --git a/bitwarden_license/src/Commercial.Pam/Models/AccessDecisionSubmission.cs b/bitwarden_license/src/Commercial.Pam/Models/AccessDecisionSubmission.cs new file mode 100644 index 000000000000..28f3ce21c814 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Models/AccessDecisionSubmission.cs @@ -0,0 +1,12 @@ +using Bit.Pam.Enums; + +namespace Bit.Commercial.Pam.Models; + +/// +/// An approver's decision on a pending lease request: approve or deny, with an optional comment. +/// +public sealed class AccessDecisionSubmission +{ + public required AccessDecisionVerdict Verdict { get; init; } + public string? Comment { get; init; } +} diff --git a/bitwarden_license/src/Commercial.Pam/Models/AccessLeaseExtensionSubmission.cs b/bitwarden_license/src/Commercial.Pam/Models/AccessLeaseExtensionSubmission.cs new file mode 100644 index 000000000000..bb3b975ed5d4 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Models/AccessLeaseExtensionSubmission.cs @@ -0,0 +1,13 @@ +namespace Bit.Commercial.Pam.Models; + +/// +/// A request to extend an active lease. Extensions are always auto-approved, subject to the governing rule's +/// AllowsExtensions / MaxExtensionDurationSeconds settings: the lease's end is pushed out by +/// in place (no new lease is minted), and a justifying is required. +/// +public sealed class AccessLeaseExtensionSubmission +{ + public Guid LeaseId { get; init; } + public int DurationSeconds { get; init; } + public string? Reason { get; init; } +} diff --git a/bitwarden_license/src/Commercial.Pam/Models/AccessPreCheckResult.cs b/bitwarden_license/src/Commercial.Pam/Models/AccessPreCheckResult.cs new file mode 100644 index 000000000000..1400384739d0 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Models/AccessPreCheckResult.cs @@ -0,0 +1,10 @@ +using Bit.Commercial.Pam.Enums; +namespace Bit.Commercial.Pam.Models; + +/// +/// The result of a pre-check. When is true the caller already holds an active lease for +/// the cipher, so the client should reveal the credential rather than prompt for a new request; otherwise +/// describes whether a fresh request would be approved automatically or require human +/// approval. +/// +public sealed record AccessPreCheckResult(AccessApprovalMode ApprovalMode, bool HasActiveLease = false); diff --git a/bitwarden_license/src/Commercial.Pam/Models/AccessRequestResult.cs b/bitwarden_license/src/Commercial.Pam/Models/AccessRequestResult.cs new file mode 100644 index 000000000000..005602cd4267 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Models/AccessRequestResult.cs @@ -0,0 +1,23 @@ +using Bit.Commercial.Pam.Enums; +using Bit.Pam.Entities; +using Bit.Pam.Enums; + +namespace Bit.Commercial.Pam.Models; + +/// +/// The result of submitting an access request. Neither path mints a lease at submit: the +/// path creates an already- +/// the requester then activates to start the lease, while the +/// path creates a request to await +/// an approver. tells the client which workflow to present. +/// +public sealed record AccessRequestResult( + AccessApprovalMode ApprovalMode, + AccessRequest Request) +{ + public static AccessRequestResult Automatic(AccessRequest request) => + new(AccessApprovalMode.Automatic, request); + + public static AccessRequestResult Human(AccessRequest request) => + new(AccessApprovalMode.Human, request); +} diff --git a/bitwarden_license/src/Commercial.Pam/Models/AccessRequestSubmission.cs b/bitwarden_license/src/Commercial.Pam/Models/AccessRequestSubmission.cs new file mode 100644 index 000000000000..cd473ddfddc9 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Models/AccessRequestSubmission.cs @@ -0,0 +1,14 @@ +namespace Bit.Commercial.Pam.Models; + +/// +/// A request to lease a cipher. The automatic path supplies (and an optional +/// ); the human path supplies a / window and a required +/// . The command validates the shape against the cipher's resolved approval outcome. +/// +public sealed class AccessRequestSubmission +{ + public int? DurationSeconds { get; init; } + public DateTime? Start { get; init; } + public DateTime? End { get; init; } + public string? Reason { get; init; } +} diff --git a/bitwarden_license/src/Commercial.Pam/Models/CipherAccessState.cs b/bitwarden_license/src/Commercial.Pam/Models/CipherAccessState.cs new file mode 100644 index 000000000000..0cfd604abd06 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Models/CipherAccessState.cs @@ -0,0 +1,20 @@ +using Bit.Pam.Entities; + +using Bit.Pam.Models; +namespace Bit.Commercial.Pam.Models; + +/// +/// The caller's access state for a single cipher: the active lease they hold (if any), their pending request (if +/// any), and their approved-but-not-yet-activated request (if any). Approval no longer mints the lease, so the +/// approved request is the startable state in between — the caller activates it to produce the active lease. +/// ExtensionsAllowed says whether the active lease can still be extended (the rule opts in and it has not been +/// extended yet — a lease may be extended once); MaxExtensionDurationSeconds is the longest a single extension may +/// run, so the banner can gate its "Extend" control and cap the duration picker. +/// +public record CipherAccessState( + Guid CipherId, + AccessLease? ActiveLease, + AccessRequestDetails? PendingRequest, + AccessRequestDetails? ApprovedRequest, + bool ExtensionsAllowed = false, + int? MaxExtensionDurationSeconds = null); diff --git a/bitwarden_license/src/Commercial.Pam/Models/Conditions/AccessCondition.cs b/bitwarden_license/src/Commercial.Pam/Models/Conditions/AccessCondition.cs new file mode 100644 index 000000000000..cf68f6e0a7aa --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Models/Conditions/AccessCondition.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace Bit.Commercial.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")] +[JsonDerivedType(typeof(TimeOfDayCondition), "time_of_day")] +public abstract class AccessCondition; diff --git a/bitwarden_license/src/Commercial.Pam/Models/Conditions/AccessWeekdayJsonConverter.cs b/bitwarden_license/src/Commercial.Pam/Models/Conditions/AccessWeekdayJsonConverter.cs new file mode 100644 index 000000000000..cb463cacf51b --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Models/Conditions/AccessWeekdayJsonConverter.cs @@ -0,0 +1,52 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Bit.Commercial.Pam.Enums; + +namespace Bit.Commercial.Pam.Models.Conditions; + +/// +/// (De)serializes as the lowercase three-letter tokens the conditions JSON uses +/// ("sun".."sat"), keeping the wire format stable while the value is strongly typed in C#. This is the +/// single source of truth for the accepted day vocabulary; an unknown token fails closed with a +/// . +/// +public sealed class AccessWeekdayJsonConverter : JsonConverter +{ + private static readonly IReadOnlyDictionary _fromToken = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["sun"] = AccessWeekday.Sun, + ["mon"] = AccessWeekday.Mon, + ["tue"] = AccessWeekday.Tue, + ["wed"] = AccessWeekday.Wed, + ["thu"] = AccessWeekday.Thu, + ["fri"] = AccessWeekday.Fri, + ["sat"] = AccessWeekday.Sat, + }; + + public override AccessWeekday Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String && + _fromToken.TryGetValue(reader.GetString()!, out var day)) + { + return day; + } + + throw new JsonException("Invalid day. Expected one of: sun, mon, tue, wed, thu, fri, sat."); + } + + public override void Write(Utf8JsonWriter writer, AccessWeekday value, JsonSerializerOptions options) => + writer.WriteStringValue(ToToken(value)); + + private static string ToToken(AccessWeekday day) => day switch + { + AccessWeekday.Sun => "sun", + AccessWeekday.Mon => "mon", + AccessWeekday.Tue => "tue", + AccessWeekday.Wed => "wed", + AccessWeekday.Thu => "thu", + AccessWeekday.Fri => "fri", + AccessWeekday.Sat => "sat", + _ => throw new ArgumentOutOfRangeException(nameof(day), day, null), + }; +} diff --git a/bitwarden_license/src/Commercial.Pam/Models/Conditions/HumanApprovalCondition.cs b/bitwarden_license/src/Commercial.Pam/Models/Conditions/HumanApprovalCondition.cs new file mode 100644 index 000000000000..73bbf53a15b1 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Models/Conditions/HumanApprovalCondition.cs @@ -0,0 +1,6 @@ +namespace Bit.Commercial.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/Commercial.Pam/Models/Conditions/IpAllowlistCondition.cs b/bitwarden_license/src/Commercial.Pam/Models/Conditions/IpAllowlistCondition.cs new file mode 100644 index 000000000000..9f095b778311 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Models/Conditions/IpAllowlistCondition.cs @@ -0,0 +1,9 @@ +namespace Bit.Commercial.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/Commercial.Pam/Models/Conditions/TimeOfDayCondition.cs b/bitwarden_license/src/Commercial.Pam/Models/Conditions/TimeOfDayCondition.cs new file mode 100644 index 000000000000..ee1be2a44eed --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Models/Conditions/TimeOfDayCondition.cs @@ -0,0 +1,19 @@ +using Bit.Commercial.Pam.Enums; +namespace Bit.Commercial.Pam.Models.Conditions; + +/// +/// Auto-approves a lease when the request falls inside one of the configured windows, evaluated in +/// the named IANA timezone; otherwise denies. +/// +public sealed class TimeOfDayCondition : AccessCondition +{ + public string Tz { get; init; } = string.Empty; + public IReadOnlyList Windows { get; init; } = []; +} + +public sealed class TimeWindow +{ + public IReadOnlyList Days { get; init; } = []; + public string From { get; init; } = string.Empty; + public string To { get; init; } = string.Empty; +} diff --git a/bitwarden_license/src/Commercial.Pam/Models/GoverningRule.cs b/bitwarden_license/src/Commercial.Pam/Models/GoverningRule.cs new file mode 100644 index 000000000000..05caab465854 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Models/GoverningRule.cs @@ -0,0 +1,28 @@ +using Bit.Commercial.Pam.Models.Conditions; + +namespace Bit.Commercial.Pam.Models; + +/// +/// The access rule that governs a cipher for a particular caller: which collection's rule applies, the owning +/// organization, whether the rule requires human approval, and the parsed flat list of s +/// so the rule engine can evaluate them against the caller's signals. A null governing rule means the cipher is not +/// leasing-gated for the caller. +/// +public sealed record GoverningRule( + Guid OrganizationId, + Guid CollectionId, + bool RequiresHumanApproval, + IReadOnlyList Conditions) +{ + /// + /// 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; init; } + + /// + /// The longest a single extension under this rule may run, in seconds; meaningful only when + /// is true. + /// + public int? MaxExtensionDurationSeconds { get; init; } +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/ActivateAccessRequestCommand.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/ActivateAccessRequestCommand.cs new file mode 100644 index 000000000000..2069e61e9445 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/ActivateAccessRequestCommand.cs @@ -0,0 +1,127 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; +using Bit.Commercial.Pam.Services; +using Bit.Core.Exceptions; +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Pam.Repositories; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Commands; + +public class ActivateAccessRequestCommand : IActivateAccessRequestCommand +{ + private readonly IAccessRequestRepository _accessRequestRepository; + private readonly IAccessLeaseRepository _accessLeaseRepository; + private readonly IApproverInboxNotifier _approverInboxNotifier; + private readonly IRequesterNotifier _requesterNotifier; + private readonly ISingleActiveLeaseEvaluator _singleActiveLeaseEvaluator; + private readonly TimeProvider _timeProvider; + + public ActivateAccessRequestCommand( + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository, + IApproverInboxNotifier approverInboxNotifier, + IRequesterNotifier requesterNotifier, + ISingleActiveLeaseEvaluator singleActiveLeaseEvaluator, + TimeProvider timeProvider) + { + _accessRequestRepository = accessRequestRepository; + _accessLeaseRepository = accessLeaseRepository; + _approverInboxNotifier = approverInboxNotifier; + _requesterNotifier = requesterNotifier; + _singleActiveLeaseEvaluator = singleActiveLeaseEvaluator; + _timeProvider = timeProvider; + } + + public async Task ActivateAsync(Guid userId, Guid requestId) + { + var request = await _accessRequestRepository.GetByIdAsync(requestId); + + // 404 for both missing and someone else's request, so the caller can't probe for requests they don't own. + if (request is null || request.RequesterId != userId) + { + throw new NotFoundException(); + } + + var now = _timeProvider.GetUtcNow().UtcDateTime; + + // Activation is idempotent while the produced lease is live (double-click, a second tab racing the + // auto-activating open flow); a revoked or lapsed lease is final — a request authorizes access at most once. + var existing = await _accessLeaseRepository.GetByAccessRequestIdAsync(request.Id); + if (existing is not null) + { + if (existing.Status == AccessLeaseStatus.Active && existing.NotAfter > now) + { + return existing; + } + throw new ConflictException("This request's access has already been used and is no longer active."); + } + + if (request.Status != AccessRequestStatus.Approved) + { + throw new ConflictException(request.Status == AccessRequestStatus.Pending + ? "This request has not been approved yet." + : "This request can no longer be activated."); + } + + if (request.NotBefore > now) + { + throw new BadRequestException("The approved access window has not started yet."); + } + + if (request.NotAfter <= now) + { + throw new BadRequestException("The approved access window has already ended."); + } + + var lease = new AccessLease + { + AccessRequestId = request.Id, + OrganizationId = request.OrganizationId, + CollectionId = request.CollectionId, + CipherId = request.CipherId, + RequesterId = request.RequesterId, + Status = AccessLeaseStatus.Active, + // Activation mints the window the approver approved, exactly as the old approval-time path did; the + // creation date is the activation audit timestamp (no decision row is written — approval was the + // decision). + NotBefore = request.NotBefore, + NotAfter = request.NotAfter, + CreationDate = now, + }; + lease.SetNewId(); + + // The per-cipher singleton binds only when every path the caller reaches the cipher through is governed by a + // singleton rule; an escape path leaves them unconstrained. The mint proc enforces it under a range lock. + var enforceSingleActiveLease = await _singleActiveLeaseEvaluator.AppliesAsync(userId, request.CipherId); + + var outcome = await _accessLeaseRepository.CreateFromApprovedRequestAsync(lease, now, enforceSingleActiveLease); + + if (outcome == AccessLeaseMintOutcome.SingleActiveLeaseConflict) + { + throw new ConflictException("Another active lease exists for this item. Try again once it ends."); + } + + if (outcome == AccessLeaseMintOutcome.PreconditionFailed) + { + // Lost a race: the guarded insert re-checks every precondition, so a miss means another activation won + // or the request changed underneath us. If the winner's lease is live, activation still succeeded from + // this caller's point of view. + var winner = await _accessLeaseRepository.GetByAccessRequestIdAsync(request.Id); + if (winner is { Status: AccessLeaseStatus.Active } && winner.NotAfter > now) + { + return winner; + } + throw new ConflictException("This request can no longer be activated."); + } + + // The approver's history row just flipped approved -> activated and gained a revocable lease; tell every + // approver of this collection to re-fetch, mirroring decide and revoke. + await _approverInboxNotifier.NotifyCollectionApproversAsync(request.CollectionId); + + // Tell the requester's other devices the approved request just minted a lease, so their "My requests" view + // and any open partial cipher pick up the live lease without a manual refresh. + await _requesterNotifier.NotifyRequesterAsync(request.RequesterId); + + return lease; + } +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/CancelAccessRequestCommand.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/CancelAccessRequestCommand.cs new file mode 100644 index 000000000000..a934caf6efb9 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/CancelAccessRequestCommand.cs @@ -0,0 +1,105 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; +using Bit.Commercial.Pam.Services; +using Bit.Core.Exceptions; +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Pam.Repositories; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Commands; + +public class CancelAccessRequestCommand : ICancelAccessRequestCommand +{ + private readonly IAccessRequestRepository _accessRequestRepository; + private readonly IAccessLeaseRepository _accessLeaseRepository; + private readonly IApproverCollectionAccessQuery _approverCollectionAccessQuery; + private readonly IApproverInboxNotifier _approverInboxNotifier; + private readonly IRequesterNotifier _requesterNotifier; + private readonly TimeProvider _timeProvider; + + public CancelAccessRequestCommand( + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository, + IApproverCollectionAccessQuery approverCollectionAccessQuery, + IApproverInboxNotifier approverInboxNotifier, + IRequesterNotifier requesterNotifier, + TimeProvider timeProvider) + { + _accessRequestRepository = accessRequestRepository; + _accessLeaseRepository = accessLeaseRepository; + _approverCollectionAccessQuery = approverCollectionAccessQuery; + _approverInboxNotifier = approverInboxNotifier; + _requesterNotifier = requesterNotifier; + _timeProvider = timeProvider; + } + + public async Task CancelAsync(Guid userId, Guid requestId) + { + var request = await _accessRequestRepository.GetByIdAsync(requestId); + + // 404 when the request is missing or the caller is neither its requester nor a managing approver, so the + // caller can't probe for requests they have no business seeing. Mirrors the inbox/decide surfaces. + if (request is null) + { + throw new NotFoundException(); + } + + var isRequester = request.RequesterId == userId; + var isManager = !isRequester + && await _approverCollectionAccessQuery.CanManageCollectionAsync(userId, request.CollectionId); + if (!isRequester && !isManager) + { + throw new NotFoundException(); + } + + // Only a request that has not produced a lease can be cancelled: still Pending, or Approved that the requester + // has not yet activated. Anything else (denied/cancelled/expired) is terminal; surfaced as a conflict so the + // client refreshes. The stored procs additionally guard the transition to stay race-safe. + if (request.Status is not (AccessRequestStatus.Pending or AccessRequestStatus.Approved)) + { + throw new ConflictException("This request has already been resolved."); + } + + // An approved request that has minted a lease is governed by that lease, not the request: end it via lease + // revoke while active, and once the lease has ended the request is terminal history. + var lease = await _accessLeaseRepository.GetByAccessRequestIdAsync(requestId); + if (lease is not null) + { + throw lease.Status == AccessLeaseStatus.Active + ? new ConflictException("This request has an active lease; revoke the lease instead.") + : new ConflictException("This request has already been resolved."); + } + + var now = _timeProvider.GetUtcNow().UtcDateTime; + + if (isRequester) + { + // The requester withdraws their own request: Cancelled, no decision recorded. A user who is both the + // requester and a manager takes this branch when cancelling their own request. + await _accessRequestRepository.CancelAsync(request.Id, now); + } + else + { + // A managing approver retracts the request: Denied, recorded as a human Deny decision so the audit trail + // names the approver — mirrors RevokeAccessLeaseCommand. + var decision = new AccessDecision + { + AccessRequestId = request.Id, + DeciderKind = AccessDeciderKind.Human, + ApproverId = userId, + Verdict = AccessDecisionVerdict.Deny, + Comment = null, + CreationDate = now, + }; + decision.SetNewId(); + await _accessRequestRepository.CancelWithDecisionAsync(request, decision, now); + } + + // The request just left the pending/approved set; tell every approver of this collection to re-fetch so it + // drops out of their inbox. Mirrors decide. + await _approverInboxNotifier.NotifyCollectionApproversAsync(request.CollectionId); + + // Tell the requester their request is gone, so a manager's retraction reaches them and their other devices + // drop the request from "My requests" without a manual refresh. + await _requesterNotifier.NotifyRequesterAsync(request.RequesterId); + } +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/CreateAccessRuleCommand.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/CreateAccessRuleCommand.cs new file mode 100644 index 000000000000..3ee74a581d0b --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/CreateAccessRuleCommand.cs @@ -0,0 +1,98 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; +using Bit.Commercial.Pam.Services; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Models; +using Bit.Pam.Repositories; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Commands; + +public class CreateAccessRuleCommand : ICreateAccessRuleCommand +{ + private readonly IAccessRuleRepository _repository; + private readonly ICollectionRepository _collectionRepository; + private readonly IAccessRuleValidator _validator; + private readonly TimeProvider _timeProvider; + + public CreateAccessRuleCommand( + IAccessRuleRepository repository, + ICollectionRepository collectionRepository, + IAccessRuleValidator validator, + TimeProvider timeProvider) + { + _repository = repository; + _collectionRepository = collectionRepository; + _validator = validator; + _timeProvider = timeProvider; + } + + public async Task CreateAsync(AccessRule rule, IEnumerable collectionIds) + { + 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 validation = _validator.Validate(rule.Conditions); + if (!validation.IsValid) + { + throw new BadRequestException(validation.Error!); + } + + var existing = await _repository.GetManyByOrganizationIdAsync(rule.OrganizationId); + if (existing.Any(p => string.Equals(p.Name, rule.Name, StringComparison.OrdinalIgnoreCase))) + { + throw new BadRequestException("A rule with that name already exists."); + } + + var desiredCollectionIds = await ValidateCollectionsAsync(rule.OrganizationId, collectionIds); + + var now = _timeProvider.GetUtcNow().UtcDateTime; + rule.CreationDate = now; + rule.RevisionDate = now; + + var created = await _repository.CreateAsync(rule); + + await _repository.SetCollectionAssociationsAsync( + created.OrganizationId, created.Id, desiredCollectionIds, []); + + return AccessRuleDetails.From(created, desiredCollectionIds); + } + + private async Task> ValidateCollectionsAsync(Guid organizationId, IEnumerable collectionIds) + { + 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."); + } + + if (collections.Any(IsGovernedByAnotherRule)) + { + throw new BadRequestException("One or more collections are already governed by another access rule."); + } + + return distinctIds; + } + + // A new rule has no Id yet, so any existing association is a conflict. + private static bool IsGovernedByAnotherRule(Collection collection) => collection.AccessRuleId.HasValue; +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/DecideAccessRequestCommand.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/DecideAccessRequestCommand.cs new file mode 100644 index 000000000000..7ff819b74189 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/DecideAccessRequestCommand.cs @@ -0,0 +1,120 @@ +using Bit.Commercial.Pam.Models; +using Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; +using Bit.Commercial.Pam.Services; +using Bit.Core.Exceptions; +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Pam.Models; +using Bit.Pam.Repositories; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Commands; + +public class DecideAccessRequestCommand : IDecideAccessRequestCommand +{ + private readonly IAccessRequestRepository _accessRequestRepository; + private readonly IApproverCollectionAccessQuery _approverCollectionAccessQuery; + private readonly IApproverInboxNotifier _approverInboxNotifier; + private readonly IRequesterNotifier _requesterNotifier; + private readonly TimeProvider _timeProvider; + + public DecideAccessRequestCommand( + IAccessRequestRepository accessRequestRepository, + IApproverCollectionAccessQuery approverCollectionAccessQuery, + IApproverInboxNotifier approverInboxNotifier, + IRequesterNotifier requesterNotifier, + TimeProvider timeProvider) + { + _accessRequestRepository = accessRequestRepository; + _approverCollectionAccessQuery = approverCollectionAccessQuery; + _approverInboxNotifier = approverInboxNotifier; + _requesterNotifier = requesterNotifier; + _timeProvider = timeProvider; + } + + public async Task DecideAsync(Guid userId, Guid requestId, AccessDecisionSubmission submission) + { + var request = await _accessRequestRepository.GetByIdAsync(requestId); + + // 404 for both missing and not-visible, so the caller can't probe for requests they don't manage. + if (request is null || !await _approverCollectionAccessQuery.CanManageCollectionAsync(userId, request.CollectionId)) + { + throw new NotFoundException(); + } + + if (request.Status != AccessRequestStatus.Pending) + { + throw new ConflictException("This request has already been resolved."); + } + + // Self-approval is blocked server-side even though the client disables the buttons. Surfaced as 400 rather + // than 403 because Bitwarden clients treat 403 as a forced logout. + if (request.RequesterId == userId) + { + throw new BadRequestException("You cannot decide your own request."); + } + + var now = _timeProvider.GetUtcNow().UtcDateTime; + var approved = submission.Verdict == AccessDecisionVerdict.Approve; + var status = approved ? AccessRequestStatus.Approved : AccessRequestStatus.Denied; + + // An approval the requester can never activate (the requested window already ended) would only mint a dead + // "approved" state, so reject it. Denial is still allowed so the audit trail can close the request out. + if (approved && request.NotAfter <= now) + { + throw new BadRequestException("The requested access window has already ended."); + } + + var decision = new AccessDecision + { + AccessRequestId = request.Id, + DeciderKind = AccessDeciderKind.Human, + ApproverId = userId, + Verdict = submission.Verdict, + Comment = string.IsNullOrWhiteSpace(submission.Comment) ? null : submission.Comment, + CreationDate = now, + }; + decision.SetNewId(); + + // Approval records the verdict only. The lease that actually authorizes access is minted when the requester + // activates the approved request (ActivateAccessRequestCommand) — until then they hold a startable approval, + // not access. The automatic path still mints instantly at submit, where the requester is present and asking. + await _accessRequestRepository.ResolveWithDecisionAsync(request, decision, status, now); + + // The request just left the pending queue; tell every approver of this collection to re-fetch. + await _approverInboxNotifier.NotifyCollectionApproversAsync(request.CollectionId); + + // Tell the requester their request was resolved, so their "My requests" view flips to approved/denied and + // an approval becomes activatable without a manual refresh. + await _requesterNotifier.NotifyRequesterAsync(request.RequesterId); + + // The client repaints the row from Status, ResolvedAt, and the single Decisions element (verdict + comment), + // so those must be accurate; the approver's denormalized name/email is resolved on the next read. Project from + // what we just wrote rather than re-reading. + return new AccessRequestDetails + { + Id = request.Id, + ExtensionOfLeaseId = request.ExtensionOfLeaseId, + OrganizationId = request.OrganizationId, + CollectionId = request.CollectionId, + CipherId = request.CipherId, + RequesterId = request.RequesterId, + NotBefore = request.NotBefore, + NotAfter = request.NotAfter, + Reason = request.Reason, + Status = status, + CreationDate = request.CreationDate, + ResolvedDate = now, + Decisions = + [ + new AccessRequestDecision + { + DeciderKind = AccessDeciderKind.Human, + Id = userId, + Comment = decision.Comment, + Verdict = decision.Verdict, + DecidedAt = now, + }, + ], + }; + } +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/DeleteAccessRuleCommand.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/DeleteAccessRuleCommand.cs new file mode 100644 index 000000000000..afd166ff3b9e --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/DeleteAccessRuleCommand.cs @@ -0,0 +1,26 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; +using Bit.Core.Exceptions; +using Bit.Pam.Repositories; + +namespace Bit.Commercial.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(); + } + + await _repository.DeleteAsync(existing); + } +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/IActivateAccessRequestCommand.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/IActivateAccessRequestCommand.cs new file mode 100644 index 000000000000..2410fc632931 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/IActivateAccessRequestCommand.cs @@ -0,0 +1,23 @@ +using Bit.Pam.Entities; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; + +public interface IActivateAccessRequestCommand +{ + /// + /// Activates the caller's approved access request: mints the active lease that authorizes access, spanning the + /// request's approved window. Only the requester may activate, and only while the window is open. Activation is + /// idempotent while the produced lease is live — a repeat call returns the existing lease. + /// + /// + /// The request does not exist or the caller is not its requester. + /// + /// + /// The request is not approved (still pending, or denied/cancelled/expired), or it already produced a lease that + /// has since been revoked or has lapsed — a request authorizes access at most once. + /// + /// + /// The approved window has not started yet or has already ended. + /// + Task ActivateAsync(Guid userId, Guid requestId); +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/ICancelAccessRequestCommand.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/ICancelAccessRequestCommand.cs new file mode 100644 index 000000000000..086198bd206f --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/ICancelAccessRequestCommand.cs @@ -0,0 +1,17 @@ +namespace Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; + +public interface ICancelAccessRequestCommand +{ + /// + /// Withdraws the caller's own pending access request: transitions it to + /// and drops it from any approver's inbox. Only the requester + /// may cancel, and only while the request is still pending. + /// + /// + /// The request does not exist or the caller is not its requester. + /// + /// + /// The request is no longer pending (already approved, denied, cancelled, or expired) and cannot be withdrawn. + /// + Task CancelAsync(Guid userId, Guid requestId); +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/ICreateAccessRuleCommand.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/ICreateAccessRuleCommand.cs new file mode 100644 index 000000000000..2ed1a09a8893 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/ICreateAccessRuleCommand.cs @@ -0,0 +1,12 @@ +using Bit.Pam.Entities; +using Bit.Pam.Models; + +namespace Bit.Commercial.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/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/IDecideAccessRequestCommand.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/IDecideAccessRequestCommand.cs new file mode 100644 index 000000000000..476820e38c19 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/IDecideAccessRequestCommand.cs @@ -0,0 +1,23 @@ +using Bit.Commercial.Pam.Models; +using Bit.Pam.Models; +namespace Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; + +public interface IDecideAccessRequestCommand +{ + /// + /// Approves or denies a pending lease request on behalf of an approver. The caller must be able to Manage the + /// request's collection and must not be the requester. An approval does not mint the lease — the requester + /// activates the approved request when they access the item. Returns the updated inbox row. + /// + /// + /// The request does not exist or the caller cannot Manage its collection. + /// + /// The request is no longer pending. + /// + /// The caller is the requester (self-approval), or the verdict is an approval but the requested window has + /// already ended (the requester could never activate it). Self-approval per the spec calls for 403, but + /// Bitwarden clients treat 403 as a forced logout, so this is surfaced as 400 — matching the existing convention + /// in the Admin Console controllers. + /// + Task DecideAsync(Guid userId, Guid requestId, AccessDecisionSubmission submission); +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/IDeleteAccessRuleCommand.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/IDeleteAccessRuleCommand.cs new file mode 100644 index 000000000000..f16f77cb32fd --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/IDeleteAccessRuleCommand.cs @@ -0,0 +1,6 @@ +namespace Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; + +public interface IDeleteAccessRuleCommand +{ + Task DeleteAsync(Guid organizationId, Guid id); +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/IRequestLeaseExtensionCommand.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/IRequestLeaseExtensionCommand.cs new file mode 100644 index 000000000000..2f61987bcfeb --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/IRequestLeaseExtensionCommand.cs @@ -0,0 +1,24 @@ +using Bit.Commercial.Pam.Models; +using Bit.Pam.Models; +namespace Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; + +public interface IRequestLeaseExtensionCommand +{ + /// + /// Extends the caller's active lease by the requested duration. Extensions are always auto-approved, subject to + /// the governing rule's AllowsExtensions / MaxExtensionDurationSeconds settings: the lease's end is + /// pushed out in place (no new lease is minted) and an auto-approved extension request is recorded. Only the + /// lease's requester may extend it. + /// + /// + /// The lease does not exist or the caller is not its requester. + /// + /// + /// The lease is no longer active (revoked or expired). + /// + /// + /// The item is not lease-gated or does not allow extensions, the lease has already been extended, the duration is + /// non-positive or exceeds the maximum extension length, or no justification was supplied. + /// + Task ExtendAsync(Guid userId, AccessLeaseExtensionSubmission submission); +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/IRevokeAccessLeaseCommand.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/IRevokeAccessLeaseCommand.cs new file mode 100644 index 000000000000..232d64e7c5da --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/IRevokeAccessLeaseCommand.cs @@ -0,0 +1,15 @@ +namespace Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; + +public interface IRevokeAccessLeaseCommand +{ + /// + /// Ends an active lease early, settling it to revoked. The caller must be either the lease's holder (ending their + /// own access) or able to Manage the lease's collection (a managing approver or org admin); the actor is recorded + /// as the revoker. The optional reason is retained for the audit trail. + /// + /// + /// The lease does not exist, or the caller is neither its holder nor able to Manage its collection. + /// + /// The lease is not active. + Task RevokeAsync(Guid userId, Guid leaseId, string? reason); +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/ISubmitAccessRequestCommand.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/ISubmitAccessRequestCommand.cs new file mode 100644 index 000000000000..24b28f71fbb7 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/ISubmitAccessRequestCommand.cs @@ -0,0 +1,11 @@ +using Bit.Commercial.Pam.Models; +namespace Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; + +public interface ISubmitAccessRequestCommand +{ + /// + /// Submits a request to lease a cipher. On the automatic path a lease is issued immediately; on the human path a + /// pending request is created. The submission's shape is validated against the cipher's resolved approval outcome. + /// + Task SubmitAsync(Guid userId, Guid cipherId, AccessRequestSubmission submission); +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/IUpdateAccessRuleCommand.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/IUpdateAccessRuleCommand.cs new file mode 100644 index 000000000000..93b563d6d3d2 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/Interfaces/IUpdateAccessRuleCommand.cs @@ -0,0 +1,12 @@ +using Bit.Pam.Entities; +using Bit.Pam.Models; + +namespace Bit.Commercial.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/Commercial.Pam/OrganizationFeatures/Commands/RequestLeaseExtensionCommand.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/RequestLeaseExtensionCommand.cs new file mode 100644 index 000000000000..20c9c4533a10 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/RequestLeaseExtensionCommand.cs @@ -0,0 +1,158 @@ +using Bit.Commercial.Pam.Engine; +using Bit.Commercial.Pam.Models; +using Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; +using Bit.Commercial.Pam.Services; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Pam.Models; +using Bit.Pam.Repositories; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Commands; + +public class RequestLeaseExtensionCommand : IRequestLeaseExtensionCommand +{ + private readonly IAccessLeaseRepository _accessLeaseRepository; + private readonly IGoverningRuleResolver _resolver; + private readonly IAccessRequestRepository _accessRequestRepository; + private readonly IApproverInboxNotifier _approverInboxNotifier; + private readonly IRequesterNotifier _requesterNotifier; + private readonly ICurrentContext _currentContext; + private readonly TimeProvider _timeProvider; + + public RequestLeaseExtensionCommand( + IAccessLeaseRepository accessLeaseRepository, + IGoverningRuleResolver resolver, + IAccessRequestRepository accessRequestRepository, + IApproverInboxNotifier approverInboxNotifier, + IRequesterNotifier requesterNotifier, + ICurrentContext currentContext, + TimeProvider timeProvider) + { + _accessLeaseRepository = accessLeaseRepository; + _resolver = resolver; + _accessRequestRepository = accessRequestRepository; + _approverInboxNotifier = approverInboxNotifier; + _requesterNotifier = requesterNotifier; + _currentContext = currentContext; + _timeProvider = timeProvider; + } + + public async Task ExtendAsync(Guid userId, AccessLeaseExtensionSubmission submission) + { + var lease = await _accessLeaseRepository.GetByIdAsync(submission.LeaseId); + + // 404 for both missing and someone else's lease, so the caller can't probe for leases they don't own. + if (lease is null || lease.RequesterId != userId) + { + throw new NotFoundException(); + } + + var now = _timeProvider.GetUtcNow().UtcDateTime; + + if (lease.Status != AccessLeaseStatus.Active || lease.NotAfter <= now) + { + throw new ConflictException("This lease is no longer active."); + } + + // Extensions reuse the cipher's governing rule, but never its approval gate: they are always auto-approved, + // gated only by the rule opting in and the per-lease maximum. + var signals = AccessSignals.From(_currentContext.IpAddress, new DateTimeOffset(now, TimeSpan.Zero)); + var governingRule = await _resolver.ResolveAsync(userId, lease.CipherId, signals); + if (governingRule is null) + { + throw new BadRequestException("This item does not require a lease."); + } + + if (!governingRule.AllowsExtensions) + { + throw new BadRequestException("This item does not allow extending a lease."); + } + + if (submission.DurationSeconds <= 0) + { + throw new BadRequestException("A positive duration is required."); + } + + // The rule's max extension length is the cap (the admin picks it from presets); it is always set when + // AllowsExtensions is true. A missing cap is treated as zero so a misconfigured rule denies. + if (submission.DurationSeconds > (governingRule.MaxExtensionDurationSeconds ?? 0)) + { + throw new BadRequestException("The requested duration exceeds the maximum extension length for this item."); + } + + if (string.IsNullOrWhiteSpace(submission.Reason)) + { + throw new BadRequestException("A justification is required to extend a lease."); + } + + // A lease may be extended exactly once. Friendly early check; the mint proc re-counts under a per-lease lock + // and is the race-safe authority. + if (await _accessRequestRepository.CountExtensionsByLeaseIdAsync(lease.Id) >= 1) + { + throw new BadRequestException("This lease has already been extended."); + } + + // The extension window spans from the lease's current end to its new end; NotAfter is the lease's new end. + var request = new AccessRequest + { + ExtensionOfLeaseId = lease.Id, + OrganizationId = lease.OrganizationId, + CollectionId = lease.CollectionId, + CipherId = lease.CipherId, + RequesterId = userId, + NotBefore = lease.NotAfter, + NotAfter = lease.NotAfter.AddSeconds(submission.DurationSeconds), + Reason = submission.Reason, + Status = AccessRequestStatus.Approved, + CreationDate = now, + ResolvedDate = now, + }; + request.SetNewId(); + + var decision = new AccessDecision + { + AccessRequestId = request.Id, + DeciderKind = AccessDeciderKind.Automatic, + Verdict = AccessDecisionVerdict.Approve, + CreationDate = now, + }; + decision.SetNewId(); + + var outcome = await _accessRequestRepository.CreateApprovedExtensionAsync(request, decision, now); + + switch (outcome) + { + case AccessLeaseExtendOutcome.LeaseNotActive: + throw new ConflictException("This lease is no longer active."); + case AccessLeaseExtendOutcome.AlreadyExtended: + throw new BadRequestException("This lease has already been extended."); + } + + // The parent lease's window just grew. Tell every approver of the collection to re-fetch (their active-leases + // and history views show the new end), and tell the requester's other devices so the banner/badge countdown + // reflects the longer window without a manual refresh. + await _approverInboxNotifier.NotifyCollectionApproversAsync(lease.CollectionId); + await _requesterNotifier.NotifyRequesterAsync(lease.RequesterId); + + // Project the approved-extension state the client renders (Status approved + ExtensionOfLeaseId set) from + // what we just wrote. The parent lease's end has already been pushed out, so the next access-state snapshot + // re-emits the longer countdown. + return new AccessRequestDetails + { + Id = request.Id, + ExtensionOfLeaseId = request.ExtensionOfLeaseId, + OrganizationId = request.OrganizationId, + CollectionId = request.CollectionId, + CipherId = request.CipherId, + RequesterId = request.RequesterId, + NotBefore = request.NotBefore, + NotAfter = request.NotAfter, + Reason = request.Reason, + Status = AccessRequestStatus.Approved, + CreationDate = request.CreationDate, + ResolvedDate = request.ResolvedDate, + }; + } +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/RevokeAccessLeaseCommand.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/RevokeAccessLeaseCommand.cs new file mode 100644 index 000000000000..cf0a2b8aa514 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/RevokeAccessLeaseCommand.cs @@ -0,0 +1,75 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; +using Bit.Commercial.Pam.Services; +using Bit.Core.Exceptions; +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Pam.Repositories; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Commands; + +public class RevokeAccessLeaseCommand : IRevokeAccessLeaseCommand +{ + private readonly IAccessLeaseRepository _accessLeaseRepository; + private readonly IApproverCollectionAccessQuery _approverCollectionAccessQuery; + private readonly IApproverInboxNotifier _approverInboxNotifier; + private readonly IRequesterNotifier _requesterNotifier; + private readonly TimeProvider _timeProvider; + + public RevokeAccessLeaseCommand( + IAccessLeaseRepository accessLeaseRepository, + IApproverCollectionAccessQuery approverCollectionAccessQuery, + IApproverInboxNotifier approverInboxNotifier, + IRequesterNotifier requesterNotifier, + TimeProvider timeProvider) + { + _accessLeaseRepository = accessLeaseRepository; + _approverCollectionAccessQuery = approverCollectionAccessQuery; + _approverInboxNotifier = approverInboxNotifier; + _requesterNotifier = requesterNotifier; + _timeProvider = timeProvider; + } + + public async Task RevokeAsync(Guid userId, Guid leaseId, string? reason) + { + var lease = await _accessLeaseRepository.GetByIdAsync(leaseId); + + // Who may end a lease early: the lease's own holder (ending their own access), or anyone who can Manage its + // collection (a managing approver or org admin). Either way the lease settles to revoked, recording the actor + // as RevokedBy — RevokedBy == RequesterId distinguishes a self-end from an operator revoke. 404 covers both + // missing and not-authorized, so a caller can't probe for leases they can't touch. + var isHolder = lease is not null && lease.RequesterId == userId; + if (lease is null || + (!isHolder && !await _approverCollectionAccessQuery.CanManageCollectionAsync(userId, lease.CollectionId))) + { + throw new NotFoundException(); + } + + if (lease.Status != AccessLeaseStatus.Active) + { + throw new ConflictException("This lease is not active."); + } + + var now = _timeProvider.GetUtcNow().UtcDateTime; + + // The reason has no dedicated column, so it is preserved as a human decision against the originating request. + var auditDecision = new AccessDecision + { + AccessRequestId = lease.AccessRequestId, + DeciderKind = AccessDeciderKind.Human, + ApproverId = userId, + Verdict = AccessDecisionVerdict.Deny, + Comment = string.IsNullOrWhiteSpace(reason) ? null : reason, + CreationDate = now, + }; + auditDecision.SetNewId(); + + await _accessLeaseRepository.RevokeAsync(lease, auditDecision, now); + + // The active lease just drained; tell every approver of this collection to re-fetch. + await _approverInboxNotifier.NotifyCollectionApproversAsync(lease.CollectionId); + + // Tell the lease holder their access ended, so an open cipher re-locks and the banner/badges drop the lease + // — whether an operator revoked it or the holder ended it from another device. + await _requesterNotifier.NotifyRequesterAsync(lease.RequesterId); + } +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/SubmitAccessRequestCommand.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/SubmitAccessRequestCommand.cs new file mode 100644 index 000000000000..bf4a6bca63b1 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/SubmitAccessRequestCommand.cs @@ -0,0 +1,305 @@ +using Bit.Commercial.Pam.Engine; +using Bit.Commercial.Pam.Models; +using Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; +using Bit.Commercial.Pam.Services; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Vault.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Pam.Repositories; +using Microsoft.Extensions.Logging; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Commands; + +public class SubmitAccessRequestCommand : ISubmitAccessRequestCommand +{ + /// + /// The maximum lease window length, applied to both the automatic duration and the human-requested window. + /// Global for v0; per-rule configuration is a later concern. + /// + public const int MaxDurationSeconds = 24 * 60 * 60; + + private readonly ICipherRepository _cipherRepository; + private readonly IGoverningRuleResolver _resolver; + private readonly IAccessRuleEngine _ruleEngine; + private readonly ICurrentContext _currentContext; + private readonly IAccessLeaseRepository _accessLeaseRepository; + private readonly IAccessRequestRepository _accessRequestRepository; + private readonly IApproverInboxNotifier _approverInboxNotifier; + private readonly IRequesterNotifier _requesterNotifier; + private readonly ICollectionRepository _collectionRepository; + private readonly IUserRepository _userRepository; + private readonly IOrganizationRepository _organizationRepository; + private readonly IMailService _mailService; + private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; + + public SubmitAccessRequestCommand( + ICipherRepository cipherRepository, + IGoverningRuleResolver resolver, + IAccessRuleEngine ruleEngine, + ICurrentContext currentContext, + IAccessLeaseRepository accessLeaseRepository, + IAccessRequestRepository accessRequestRepository, + IApproverInboxNotifier approverInboxNotifier, + IRequesterNotifier requesterNotifier, + ICollectionRepository collectionRepository, + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IMailService mailService, + ILogger logger, + TimeProvider timeProvider) + { + _cipherRepository = cipherRepository; + _resolver = resolver; + _ruleEngine = ruleEngine; + _currentContext = currentContext; + _accessLeaseRepository = accessLeaseRepository; + _accessRequestRepository = accessRequestRepository; + _approverInboxNotifier = approverInboxNotifier; + _requesterNotifier = requesterNotifier; + _collectionRepository = collectionRepository; + _userRepository = userRepository; + _organizationRepository = organizationRepository; + _mailService = mailService; + _logger = logger; + _timeProvider = timeProvider; + } + + public async Task SubmitAsync(Guid userId, Guid cipherId, AccessRequestSubmission submission) + { + var cipher = await _cipherRepository.GetByIdAsync(cipherId, userId); + if (cipher is null) + { + throw new NotFoundException(); + } + + var now = _timeProvider.GetUtcNow().UtcDateTime; + var signals = AccessSignals.From(_currentContext.IpAddress, new DateTimeOffset(now, TimeSpan.Zero)); + + var governingRule = await _resolver.ResolveAsync(userId, cipherId, signals); + if (governingRule is null) + { + throw new BadRequestException("This item does not require a lease."); + } + + if (await _accessLeaseRepository.GetActiveByRequesterIdCipherIdAsync(userId, cipherId, now) is not null) + { + throw new BadRequestException("You already have active access to this item."); + } + + if (await _accessRequestRepository.GetActivePendingByRequesterIdCipherIdAsync(userId, cipherId) is not null) + { + throw new BadRequestException("You already have a pending request for this item."); + } + + // An approved-but-not-yet-activated request already grants startable access; a second request would let the + // caller stack grants. Lapsed approvals don't match here, so they correctly don't block a fresh request. + if (await _accessRequestRepository.GetActiveApprovedByRequesterIdCipherIdAsync(userId, cipherId, now) is not null) + { + throw new BadRequestException("You already have an approved request for this item."); + } + + return governingRule.RequiresHumanApproval + ? await RequestHumanApprovalAsync(userId, cipherId, governingRule, submission) + : await ApproveAutomaticallyAsync(userId, cipherId, governingRule, submission, now, signals); + } + + private async Task ApproveAutomaticallyAsync( + Guid userId, Guid cipherId, GoverningRule governingRule, AccessRequestSubmission submission, DateTime now, + AccessSignals signals) + { + if (submission.Start.HasValue || submission.End.HasValue) + { + throw new BadRequestException("This item is approved automatically; provide a duration, not a window."); + } + + if (submission.DurationSeconds is not { } durationSeconds || durationSeconds <= 0) + { + throw new BadRequestException("A positive duration is required."); + } + + if (durationSeconds > MaxDurationSeconds) + { + throw new BadRequestException($"The requested duration exceeds the maximum of {MaxDurationSeconds} seconds."); + } + + // The cipher must satisfy its access rule's conditions (source IP, time of day, ...) before the request is + // auto-approved. The resolver only routes a rule here when it carries no human-approval gate, so the engine + // never asks for approval on this path; any non-allow outcome is a denial we surface to the caller. + var evaluation = _ruleEngine.Evaluate(governingRule.Conditions, signals); + if (evaluation.Outcome != AccessEvaluationOutcome.Allow) + { + throw new BadRequestException(DenyMessage(evaluation)); + } + + var notAfter = now.AddSeconds(durationSeconds); + + var request = new AccessRequest + { + OrganizationId = governingRule.OrganizationId, + CollectionId = governingRule.CollectionId, + CipherId = cipherId, + RequesterId = userId, + NotBefore = now, + NotAfter = notAfter, + Reason = string.IsNullOrWhiteSpace(submission.Reason) ? null : submission.Reason, + Status = AccessRequestStatus.Approved, + CreationDate = now, + ResolvedDate = now, + }; + request.SetNewId(); + + var decision = new AccessDecision + { + AccessRequestId = request.Id, + DeciderKind = AccessDeciderKind.Automatic, + Verdict = AccessDecisionVerdict.Approve, + CreationDate = now, + }; + decision.SetNewId(); + + // Auto-approval records only the request and its automatic verdict — no lease. The requester explicitly + // activates the approved request (ActivateAccessRequestCommand) to start the lease, exactly like the human + // path after approval. Deferring the mint means the per-cipher single-active-lease guard runs at activation, + // the one place a lease is now minted, rather than here. + await _accessRequestRepository.CreateAutoApprovedAsync(request, decision); + + // Tell the requester's other devices a new approved request exists, so "My requests" can offer to activate it + // without a manual refresh. + await _requesterNotifier.NotifyRequesterAsync(userId); + + return AccessRequestResult.Automatic(request); + } + + private async Task RequestHumanApprovalAsync( + Guid userId, Guid cipherId, GoverningRule governingRule, AccessRequestSubmission submission) + { + if (submission.DurationSeconds.HasValue) + { + throw new BadRequestException("This item requires human approval; provide a start and end date, not a duration."); + } + + if (string.IsNullOrWhiteSpace(submission.Reason)) + { + throw new BadRequestException("A reason is required for items that need human approval."); + } + + if (submission.Start is not { } start || submission.End is not { } end) + { + throw new BadRequestException("A start and end date are required."); + } + + if (start >= end) + { + throw new BadRequestException("The start date must be before the end date."); + } + + if ((end - start).TotalSeconds > MaxDurationSeconds) + { + throw new BadRequestException($"The requested window exceeds the maximum of {MaxDurationSeconds} seconds."); + } + + var now = _timeProvider.GetUtcNow().UtcDateTime; + var request = new AccessRequest + { + OrganizationId = governingRule.OrganizationId, + CollectionId = governingRule.CollectionId, + CipherId = cipherId, + RequesterId = userId, + NotBefore = start, + NotAfter = end, + Reason = submission.Reason, + Status = AccessRequestStatus.Pending, + CreationDate = now, + }; + + var created = await _accessRequestRepository.CreateAsync(request); + + // A new request just entered the pending queue; tell every approver of this collection to re-fetch. + await _approverInboxNotifier.NotifyCollectionApproversAsync(created.CollectionId); + + // Tell the requester's other devices a new pending request exists, so "My requests" reflects it without a + // manual refresh. + await _requesterNotifier.NotifyRequesterAsync(userId); + + // Also email the collection's approvers so they learn about the request outside of an open session. + await NotifyApproversByEmailAsync(created); + + return AccessRequestResult.Human(created); + } + + /// + /// Emails the managers (approvers) of the request's collection that a new request is pending their review. + /// Best-effort: failures are logged and swallowed so they never fail the request submission. + /// + private async Task NotifyApproversByEmailAsync(AccessRequest request) + { + try + { + var managerIds = await _collectionRepository.GetManagingUserIdsAsync(request.CollectionId); + + // The requester may manage the collection themselves; never notify them of their own request. + var recipientIds = managerIds.Where(id => id != request.RequesterId).ToList(); + if (recipientIds.Count == 0) + { + return; + } + + var managers = await _userRepository.GetManyAsync(recipientIds); + var managerEmails = managers + .Select(u => u.Email) + .Where(email => !string.IsNullOrWhiteSpace(email)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + if (managerEmails.Count == 0) + { + return; + } + + var requester = await _userRepository.GetByIdAsync(request.RequesterId); + if (requester is null || string.IsNullOrWhiteSpace(requester.Email)) + { + _logger.LogWarning( + "Skipping PAM approver email for access request {AccessRequestId}; requester not found or has no email.", + request.Id); + return; + } + + var organization = await _organizationRepository.GetByIdAsync(request.OrganizationId); + if (organization is null) + { + _logger.LogWarning( + "Skipping PAM approver email for access request {AccessRequestId}; organization not found.", + request.Id); + return; + } + + await _mailService.SendPamPendingAccessRequestEmailsAsync( + managerEmails, + organization.Name, + requester.Name, + requester.Email, + request.NotBefore, + request.NotAfter, + request.Reason); + } + catch (Exception ex) + { + // Best effort: the request is already persisted and the inbox push already fired. An email failure + // must never fail the submission, so log and move on. + _logger.LogError(ex, + "Failed to send PAM approver emails for access request {AccessRequestId}.", request.Id); + } + } + + private static string DenyMessage(AccessEvaluation evaluation) => evaluation.Reason switch + { + DenyReason.NotWithinIpRange => "Access to this item is not permitted from your current network.", + DenyReason.NotWithinTimeWindow => "Access to this item is not permitted at this time.", + _ => "Access to this item is not permitted right now.", + }; +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/UpdateAccessRuleCommand.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/UpdateAccessRuleCommand.cs new file mode 100644 index 000000000000..ecc32e72430f --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Commands/UpdateAccessRuleCommand.cs @@ -0,0 +1,116 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; +using Bit.Commercial.Pam.Services; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Models; +using Bit.Pam.Repositories; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Commands; + +public class UpdateAccessRuleCommand : IUpdateAccessRuleCommand +{ + private readonly IAccessRuleRepository _repository; + private readonly ICollectionRepository _collectionRepository; + private readonly IAccessRuleValidator _validator; + private readonly TimeProvider _timeProvider; + + public UpdateAccessRuleCommand( + IAccessRuleRepository repository, + ICollectionRepository collectionRepository, + IAccessRuleValidator validator, + TimeProvider timeProvider) + { + _repository = repository; + _collectionRepository = collectionRepository; + _validator = validator; + _timeProvider = timeProvider; + } + + public async Task UpdateAsync(Guid organizationId, Guid id, AccessRule update, + IEnumerable collectionIds) + { + if (string.IsNullOrWhiteSpace(update.Name)) + { + throw new BadRequestException("Name is required."); + } + + if (update.AllowsExtensions && update.MaxExtensionDurationSeconds is not > 0) + { + throw new BadRequestException("A maximum extension length is required when extensions are allowed."); + } + + var existing = await _repository.GetDetailsByIdAsync(id); + if (existing is null || existing.OrganizationId != organizationId) + { + throw new NotFoundException(); + } + + var validation = _validator.Validate(update.Conditions); + if (!validation.IsValid) + { + throw new BadRequestException(validation.Error!); + } + + var siblings = await _repository.GetManyByOrganizationIdAsync(organizationId); + if (siblings.Any(p => p.Id != id && string.Equals(p.Name, update.Name, StringComparison.OrdinalIgnoreCase))) + { + throw new BadRequestException("A rule with that name already exists."); + } + + var desiredCollectionIds = await ValidateCollectionsAsync(organizationId, id, collectionIds); + + // 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, + }; + await _repository.ReplaceAsync(toPersist); + + var toClear = existing.CollectionIds.Except(desiredCollectionIds).ToList(); + await _repository.SetCollectionAssociationsAsync(organizationId, id, desiredCollectionIds, toClear); + + return AccessRuleDetails.From(toPersist, desiredCollectionIds); + } + + private async Task> ValidateCollectionsAsync(Guid organizationId, Guid accessRuleId, + IEnumerable collectionIds) + { + 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."); + } + + if (collections.Any(c => c.AccessRuleId.HasValue && c.AccessRuleId != accessRuleId)) + { + throw new BadRequestException("One or more collections are already governed by another access rule."); + } + + return distinctIds; + } +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/AccessPreCheckQuery.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/AccessPreCheckQuery.cs new file mode 100644 index 000000000000..2f6acd757b5f --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/AccessPreCheckQuery.cs @@ -0,0 +1,61 @@ +using Bit.Commercial.Pam.Engine; +using Bit.Commercial.Pam.Enums; +using Bit.Commercial.Pam.Models; +using Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; +using Bit.Commercial.Pam.Services; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Vault.Repositories; +using Bit.Pam.Repositories; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Queries; + +public class AccessPreCheckQuery : IAccessPreCheckQuery +{ + private readonly ICipherRepository _cipherRepository; + private readonly IGoverningRuleResolver _resolver; + private readonly IAccessLeaseRepository _accessLeaseRepository; + private readonly ICurrentContext _currentContext; + private readonly TimeProvider _timeProvider; + + public AccessPreCheckQuery( + ICipherRepository cipherRepository, + IGoverningRuleResolver resolver, + IAccessLeaseRepository accessLeaseRepository, + ICurrentContext currentContext, + TimeProvider timeProvider) + { + _cipherRepository = cipherRepository; + _resolver = resolver; + _accessLeaseRepository = accessLeaseRepository; + _currentContext = currentContext; + _timeProvider = timeProvider; + } + + public async Task PreCheckAsync(Guid userId, Guid cipherId) + { + // GetByIdAsync filters by access, so a null result means the caller cannot see the cipher. + var cipher = await _cipherRepository.GetByIdAsync(cipherId, userId); + if (cipher is null) + { + throw new NotFoundException(); + } + + var now = _timeProvider.GetUtcNow().UtcDateTime; + + // A caller who already holds an active lease should be sent straight to the credential, not prompted to make + // a request that SubmitAccessRequestCommand would reject. This mirrors the active-lease guard there. + if (await _accessLeaseRepository.GetActiveByRequesterIdCipherIdAsync(userId, cipherId, now) is not null) + { + return new AccessPreCheckResult(AccessApprovalMode.Automatic, HasActiveLease: true); + } + + var signals = AccessSignals.From(_currentContext.IpAddress, new DateTimeOffset(now, TimeSpan.Zero)); + var governingRule = await _resolver.ResolveAsync(userId, cipherId, signals); + var approvalMode = governingRule?.RequiresHumanApproval == true + ? AccessApprovalMode.Human + : AccessApprovalMode.Automatic; + + return new AccessPreCheckResult(approvalMode); + } +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/GetCipherAccessStateQuery.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/GetCipherAccessStateQuery.cs new file mode 100644 index 000000000000..afaf6d4e48c7 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/GetCipherAccessStateQuery.cs @@ -0,0 +1,103 @@ +using Bit.Commercial.Pam.Engine; +using Bit.Commercial.Pam.Models; +using Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; +using Bit.Commercial.Pam.Services; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Vault.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Models; +using Bit.Pam.Repositories; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Queries; + +public class GetCipherAccessStateQuery : IGetCipherAccessStateQuery +{ + private readonly ICipherRepository _cipherRepository; + private readonly IGoverningRuleResolver _resolver; + private readonly IAccessLeaseRepository _accessLeaseRepository; + private readonly IAccessRequestRepository _accessRequestRepository; + private readonly ICurrentContext _currentContext; + private readonly TimeProvider _timeProvider; + + public GetCipherAccessStateQuery( + ICipherRepository cipherRepository, + IGoverningRuleResolver resolver, + IAccessLeaseRepository accessLeaseRepository, + IAccessRequestRepository accessRequestRepository, + ICurrentContext currentContext, + TimeProvider timeProvider) + { + _cipherRepository = cipherRepository; + _resolver = resolver; + _accessLeaseRepository = accessLeaseRepository; + _accessRequestRepository = accessRequestRepository; + _currentContext = currentContext; + _timeProvider = timeProvider; + } + + public async Task GetStateAsync(Guid userId, Guid cipherId) + { + // GetByIdAsync filters by access, so a null result means the caller cannot see the cipher. + var cipher = await _cipherRepository.GetByIdAsync(cipherId, userId); + if (cipher is null) + { + throw new NotFoundException(); + } + + var now = _timeProvider.GetUtcNow().UtcDateTime; + var signals = AccessSignals.From(_currentContext.IpAddress, new DateTimeOffset(now, TimeSpan.Zero)); + var activeLease = await _accessLeaseRepository.GetActiveByRequesterIdCipherIdAsync(userId, cipherId, now); + var pending = await _accessRequestRepository.GetActivePendingByRequesterIdCipherIdAsync(userId, cipherId); + var approved = await _accessRequestRepository.GetActiveApprovedByRequesterIdCipherIdAsync(userId, cipherId, now); + + var extensionsAllowed = false; + int? maxExtensionDurationSeconds = null; + if (activeLease is not null) + { + // Extension eligibility drives the banner's "Extend" control. A lease may be extended once, so it is + // extendable only while the rule opts in and no extension has been recorded yet; surface the rule's max + // length so the client can cap its duration picker. + var rule = await _resolver.ResolveAsync(userId, cipherId, signals); + if (rule?.AllowsExtensions == true) + { + var used = await _accessRequestRepository.CountExtensionsByLeaseIdAsync(activeLease.Id); + extensionsAllowed = used == 0; + maxExtensionDurationSeconds = rule.MaxExtensionDurationSeconds; + } + } + else if (pending is null && approved is null && await _resolver.ResolveAsync(userId, cipherId, signals) is null) + { + // Nothing to report and the cipher isn't leasing-gated. (When a lease or request exists we still return a + // snapshot even if the rule was since removed, so the caller's state isn't hidden.) + throw new NotFoundException(); + } + + return new CipherAccessState( + cipherId, + activeLease, + pending is null ? null : ToDetails(pending), + approved is null ? null : ToDetails(approved), + extensionsAllowed, + maxExtensionDurationSeconds); + } + + // Neither a pending nor an approved-unactivated request has produced a lease (the approved read excludes + // activated rows), and the approver identity/comment and inbox display-name fields aren't needed for this + // caller-scoped snapshot, so they stay null. + private static AccessRequestDetails ToDetails(AccessRequest request) => new() + { + Id = request.Id, + ExtensionOfLeaseId = request.ExtensionOfLeaseId, + OrganizationId = request.OrganizationId, + CollectionId = request.CollectionId, + CipherId = request.CipherId, + RequesterId = request.RequesterId, + NotBefore = request.NotBefore, + NotAfter = request.NotAfter, + Reason = request.Reason, + Status = request.Status, + CreationDate = request.CreationDate, + ResolvedDate = request.ResolvedDate, + }; +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/GetLeasedCipherQuery.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/GetLeasedCipherQuery.cs new file mode 100644 index 000000000000..16110361f770 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/GetLeasedCipherQuery.cs @@ -0,0 +1,62 @@ +using Bit.Commercial.Pam.Engine; +using Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; +using Bit.Commercial.Pam.Services; +using Bit.Core.Context; +using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Repositories; +using Bit.Pam.Repositories; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Queries; + +public class GetLeasedCipherQuery : IGetLeasedCipherQuery +{ + private readonly ICipherRepository _cipherRepository; + private readonly IAccessLeaseRepository _accessLeaseRepository; + private readonly IGoverningRuleResolver _resolver; + private readonly IAccessRuleEngine _ruleEngine; + private readonly ICurrentContext _currentContext; + private readonly TimeProvider _timeProvider; + + public GetLeasedCipherQuery( + ICipherRepository cipherRepository, + IAccessLeaseRepository accessLeaseRepository, + IGoverningRuleResolver resolver, + IAccessRuleEngine ruleEngine, + ICurrentContext currentContext, + TimeProvider timeProvider) + { + _cipherRepository = cipherRepository; + _accessLeaseRepository = accessLeaseRepository; + _resolver = resolver; + _ruleEngine = ruleEngine; + _currentContext = currentContext; + _timeProvider = timeProvider; + } + + public async Task GetLeasedCipherAsync(Guid userId, Guid cipherId) + { + var now = _timeProvider.GetUtcNow(); + + // Without an active lease whose window contains now, the caller is not entitled to the full data right now. + var lease = await _accessLeaseRepository.GetActiveByRequesterIdCipherIdAsync(userId, cipherId, now.UtcDateTime); + if (lease is null) + { + return null; + } + + var signals = AccessSignals.From(_currentContext.IpAddress, now); + + // A lease grants a window, but the access rule's environmental conditions (source IP, time of day) must + // still hold at the moment the data is handed over. Approval is not re-checked here: holding the lease is + // proof it was already granted, so only an outright denial withholds the data. + var governingRule = await _resolver.ResolveAsync(userId, cipherId, signals); + if (governingRule is not null + && _ruleEngine.Evaluate(governingRule.Conditions, signals).Outcome == AccessEvaluationOutcome.Deny) + { + return null; + } + + // GetByIdAsync filters by access, so a null result means the caller cannot see the cipher. + return await _cipherRepository.GetByIdAsync(cipherId, userId); + } +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IAccessPreCheckQuery.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IAccessPreCheckQuery.cs new file mode 100644 index 000000000000..b4fb773493ec --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IAccessPreCheckQuery.cs @@ -0,0 +1,11 @@ +using Bit.Commercial.Pam.Models; +namespace Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; + +public interface IAccessPreCheckQuery +{ + /// + /// Determines, without any side effects, whether the caller requesting access to the cipher would be approved + /// automatically or would require human approval. + /// + Task PreCheckAsync(Guid userId, Guid cipherId); +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IGetCipherAccessStateQuery.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IGetCipherAccessStateQuery.cs new file mode 100644 index 000000000000..566d23aa6763 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IGetCipherAccessStateQuery.cs @@ -0,0 +1,12 @@ +using Bit.Commercial.Pam.Models; +namespace Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; + +public interface IGetCipherAccessStateQuery +{ + /// + /// Returns the caller's lease state for a single leasing-gated cipher — their active lease and pending request, if + /// any. Throws when the caller cannot see the cipher, or when + /// the cipher is not leasing-gated and the caller holds nothing to report. + /// + Task GetStateAsync(Guid userId, Guid cipherId); +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IGetLeasedCipherQuery.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IGetLeasedCipherQuery.cs new file mode 100644 index 000000000000..882c4b194893 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IGetLeasedCipherQuery.cs @@ -0,0 +1,13 @@ +using Bit.Core.Vault.Models.Data; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; + +public interface IGetLeasedCipherQuery +{ + /// + /// Returns the cipher with its complete data, but only if the caller currently holds an active lease for it and + /// can otherwise access it. Returns null when there is no active lease or the caller cannot access the cipher, so + /// callers cannot distinguish "no lease" from "no such cipher". + /// + Task GetLeasedCipherAsync(Guid userId, Guid cipherId); +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IListActiveLeasesQuery.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IListActiveLeasesQuery.cs new file mode 100644 index 000000000000..8ff16ec418d6 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IListActiveLeasesQuery.cs @@ -0,0 +1,14 @@ +using Bit.Pam.Entities; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; + +public interface IListActiveLeasesQuery +{ + /// + /// Returns every currently-active lease on the collections the caller can Manage — the governance view of all + /// active access in the caller's scope, not just their own leases. Scope is resolved the same way as the approver + /// inbox (): the caller's manageable collections across every organization. + /// Returns an empty collection when the caller manages none. + /// + Task> GetActiveAsync(Guid userId); +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IListInboxHistoryQuery.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IListInboxHistoryQuery.cs new file mode 100644 index 000000000000..6e1664767a7c --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IListInboxHistoryQuery.cs @@ -0,0 +1,12 @@ +using Bit.Pam.Models; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; + +public interface IListInboxHistoryQuery +{ + /// + /// Returns the resolved lease requests (no longer pending) the user can approve, within the history retention + /// window, for collections the user can Manage. Returns an empty collection when the user manages none. + /// + Task> GetHistoryAsync(Guid userId); +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IListInboxRequestsQuery.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IListInboxRequestsQuery.cs new file mode 100644 index 000000000000..01221b30c72c --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IListInboxRequestsQuery.cs @@ -0,0 +1,12 @@ +using Bit.Pam.Models; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; + +public interface IListInboxRequestsQuery +{ + /// + /// Returns the pending lease requests the user can approve — those on collections the user can Manage. Returns an + /// empty collection when the user manages none. + /// + Task> GetPendingAsync(Guid userId); +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IListLeaseHistoryQuery.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IListLeaseHistoryQuery.cs new file mode 100644 index 000000000000..e6af8614d296 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IListLeaseHistoryQuery.cs @@ -0,0 +1,14 @@ +using Bit.Pam.Entities; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; + +public interface IListLeaseHistoryQuery +{ + /// + /// Returns the ended leases (expired or revoked) on the collections the caller can Manage, within the shared + /// history window — the governance history view of recently-ended access in the caller's scope. Scope is resolved + /// the same way as the approver inbox (): the caller's manageable collections + /// across every organization. Returns an empty collection when the caller manages none. + /// + Task> GetHistoryAsync(Guid userId); +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IListMyAccessRequestsQuery.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IListMyAccessRequestsQuery.cs new file mode 100644 index 000000000000..7c45c55c6cdb --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IListMyAccessRequestsQuery.cs @@ -0,0 +1,12 @@ +using Bit.Pam.Models; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; + +public interface IListMyAccessRequestsQuery +{ + /// + /// Returns the caller's own lease requests across every organization they belong to, regardless of status. Returns + /// an empty collection when they have none. + /// + Task> GetMineAsync(Guid userId); +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IListMyActiveAccessLeasesQuery.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IListMyActiveAccessLeasesQuery.cs new file mode 100644 index 000000000000..2fe1fcc95a7f --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/Interfaces/IListMyActiveAccessLeasesQuery.cs @@ -0,0 +1,12 @@ +using Bit.Pam.Entities; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; + +public interface IListMyActiveAccessLeasesQuery +{ + /// + /// Returns the caller's currently-active leases across every organization they belong to. Returns an empty + /// collection when none are active. + /// + Task> GetMineActiveAsync(Guid userId); +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/ListActiveLeasesQuery.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/ListActiveLeasesQuery.cs new file mode 100644 index 000000000000..093fa2d6842d --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/ListActiveLeasesQuery.cs @@ -0,0 +1,35 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; +using Bit.Commercial.Pam.Services; +using Bit.Pam.Entities; +using Bit.Pam.Repositories; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Queries; + +public class ListActiveLeasesQuery : IListActiveLeasesQuery +{ + private readonly IApproverCollectionAccessQuery _approverCollectionAccessQuery; + private readonly IAccessLeaseRepository _accessLeaseRepository; + private readonly TimeProvider _timeProvider; + + public ListActiveLeasesQuery( + IApproverCollectionAccessQuery approverCollectionAccessQuery, + IAccessLeaseRepository accessLeaseRepository, + TimeProvider timeProvider) + { + _approverCollectionAccessQuery = approverCollectionAccessQuery; + _accessLeaseRepository = accessLeaseRepository; + _timeProvider = timeProvider; + } + + public async Task> GetActiveAsync(Guid userId) + { + var manageableCollectionIds = await _approverCollectionAccessQuery.GetManageableCollectionIdsAsync(userId); + if (manageableCollectionIds.Count == 0) + { + return new List(); + } + + return await _accessLeaseRepository.GetManyActiveByCollectionIdsAsync( + manageableCollectionIds, _timeProvider.GetUtcNow().UtcDateTime); + } +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/ListInboxHistoryQuery.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/ListInboxHistoryQuery.cs new file mode 100644 index 000000000000..ffcd904606ad --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/ListInboxHistoryQuery.cs @@ -0,0 +1,40 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; +using Bit.Commercial.Pam.Services; +using Bit.Pam.Models; +using Bit.Pam.Repositories; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Queries; + +public class ListInboxHistoryQuery : IListInboxHistoryQuery +{ + /// + /// How far back the resolved history reaches. Older activity may be omitted. v1 has no pagination. + /// + public const int HistoryRetentionDays = 90; + + private readonly IApproverCollectionAccessQuery _approverCollectionAccessQuery; + private readonly IAccessRequestRepository _accessRequestRepository; + private readonly TimeProvider _timeProvider; + + public ListInboxHistoryQuery( + IApproverCollectionAccessQuery approverCollectionAccessQuery, + IAccessRequestRepository accessRequestRepository, + TimeProvider timeProvider) + { + _approverCollectionAccessQuery = approverCollectionAccessQuery; + _accessRequestRepository = accessRequestRepository; + _timeProvider = timeProvider; + } + + public async Task> GetHistoryAsync(Guid userId) + { + var manageableCollectionIds = await _approverCollectionAccessQuery.GetManageableCollectionIdsAsync(userId); + if (manageableCollectionIds.Count == 0) + { + return new List(); + } + + var since = _timeProvider.GetUtcNow().UtcDateTime.AddDays(-HistoryRetentionDays); + return await _accessRequestRepository.GetManyInboxHistoryByCollectionIdsAsync(manageableCollectionIds, since); + } +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/ListInboxRequestsQuery.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/ListInboxRequestsQuery.cs new file mode 100644 index 000000000000..b5a6ecb69769 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/ListInboxRequestsQuery.cs @@ -0,0 +1,31 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; +using Bit.Commercial.Pam.Services; +using Bit.Pam.Models; +using Bit.Pam.Repositories; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Queries; + +public class ListInboxRequestsQuery : IListInboxRequestsQuery +{ + private readonly IApproverCollectionAccessQuery _approverCollectionAccessQuery; + private readonly IAccessRequestRepository _accessRequestRepository; + + public ListInboxRequestsQuery( + IApproverCollectionAccessQuery approverCollectionAccessQuery, + IAccessRequestRepository accessRequestRepository) + { + _approverCollectionAccessQuery = approverCollectionAccessQuery; + _accessRequestRepository = accessRequestRepository; + } + + public async Task> GetPendingAsync(Guid userId) + { + var manageableCollectionIds = await _approverCollectionAccessQuery.GetManageableCollectionIdsAsync(userId); + if (manageableCollectionIds.Count == 0) + { + return new List(); + } + + return await _accessRequestRepository.GetManyInboxPendingByCollectionIdsAsync(manageableCollectionIds); + } +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/ListLeaseHistoryQuery.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/ListLeaseHistoryQuery.cs new file mode 100644 index 000000000000..2244c697aef6 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/ListLeaseHistoryQuery.cs @@ -0,0 +1,36 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; +using Bit.Commercial.Pam.Services; +using Bit.Pam.Entities; +using Bit.Pam.Repositories; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Queries; + +public class ListLeaseHistoryQuery : IListLeaseHistoryQuery +{ + private readonly IApproverCollectionAccessQuery _approverCollectionAccessQuery; + private readonly IAccessLeaseRepository _accessLeaseRepository; + private readonly TimeProvider _timeProvider; + + public ListLeaseHistoryQuery( + IApproverCollectionAccessQuery approverCollectionAccessQuery, + IAccessLeaseRepository accessLeaseRepository, + TimeProvider timeProvider) + { + _approverCollectionAccessQuery = approverCollectionAccessQuery; + _accessLeaseRepository = accessLeaseRepository; + _timeProvider = timeProvider; + } + + public async Task> GetHistoryAsync(Guid userId) + { + var manageableCollectionIds = await _approverCollectionAccessQuery.GetManageableCollectionIdsAsync(userId); + if (manageableCollectionIds.Count == 0) + { + return new List(); + } + + // Shares the approver inbox's history window so request history and lease history reach equally far back. + var since = _timeProvider.GetUtcNow().UtcDateTime.AddDays(-ListInboxHistoryQuery.HistoryRetentionDays); + return await _accessLeaseRepository.GetManyEndedByCollectionIdsAsync(manageableCollectionIds, since); + } +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/ListMyAccessRequestsQuery.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/ListMyAccessRequestsQuery.cs new file mode 100644 index 000000000000..dcb0ace0bbe1 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/ListMyAccessRequestsQuery.cs @@ -0,0 +1,18 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; +using Bit.Pam.Models; +using Bit.Pam.Repositories; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Queries; + +public class ListMyAccessRequestsQuery : IListMyAccessRequestsQuery +{ + private readonly IAccessRequestRepository _accessRequestRepository; + + public ListMyAccessRequestsQuery(IAccessRequestRepository accessRequestRepository) + { + _accessRequestRepository = accessRequestRepository; + } + + public Task> GetMineAsync(Guid userId) => + _accessRequestRepository.GetManyByRequesterIdAsync(userId); +} diff --git a/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/ListMyActiveAccessLeasesQuery.cs b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/ListMyActiveAccessLeasesQuery.cs new file mode 100644 index 000000000000..6a7bca15fcbb --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/OrganizationFeatures/Queries/ListMyActiveAccessLeasesQuery.cs @@ -0,0 +1,20 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; +using Bit.Pam.Entities; +using Bit.Pam.Repositories; + +namespace Bit.Commercial.Pam.OrganizationFeatures.Queries; + +public class ListMyActiveAccessLeasesQuery : IListMyActiveAccessLeasesQuery +{ + private readonly IAccessLeaseRepository _accessLeaseRepository; + private readonly TimeProvider _timeProvider; + + public ListMyActiveAccessLeasesQuery(IAccessLeaseRepository accessLeaseRepository, TimeProvider timeProvider) + { + _accessLeaseRepository = accessLeaseRepository; + _timeProvider = timeProvider; + } + + public Task> GetMineActiveAsync(Guid userId) => + _accessLeaseRepository.GetManyActiveByRequesterIdAsync(userId, _timeProvider.GetUtcNow().UtcDateTime); +} diff --git a/bitwarden_license/src/Commercial.Pam/Services/AccessRuleValidator.cs b/bitwarden_license/src/Commercial.Pam/Services/AccessRuleValidator.cs new file mode 100644 index 000000000000..9ebc5c33b2aa --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Services/AccessRuleValidator.cs @@ -0,0 +1,145 @@ +using System.Net; +using System.Text.Json; +using System.Text.RegularExpressions; +using Bit.Commercial.Pam.Models.Conditions; + +namespace Bit.Commercial.Pam.Services; + +public sealed partial class AccessRuleValidator : IAccessRuleValidator +{ + private const int MaxConditions = 10; + + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + }; + + [GeneratedRegex(@"^([01][0-9]|2[0-3]):[0-5][0-9]$")] + private static partial Regex TimeOfDayRegex(); + + 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."); + } + + foreach (var condition in conditions) + { + var result = ValidateCondition(condition); + if (!result.IsValid) + { + return result; + } + } + + return AccessRuleValidationResult.Valid; + } + + private static AccessRuleValidationResult ValidateCondition(AccessCondition? condition) + { + return condition switch + { + HumanApprovalCondition => AccessRuleValidationResult.Valid, + IpAllowlistCondition ip => ValidateIpAllowlist(ip), + TimeOfDayCondition tod => ValidateTimeOfDay(tod), + 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; + } + + private static AccessRuleValidationResult ValidateTimeOfDay(TimeOfDayCondition condition) + { + if (string.IsNullOrWhiteSpace(condition.Tz)) + { + return AccessRuleValidationResult.Invalid("time_of_day requires a tz."); + } + + try + { + TimeZoneInfo.FindSystemTimeZoneById(condition.Tz); + } + catch (TimeZoneNotFoundException) + { + return AccessRuleValidationResult.Invalid($"Unknown timezone: '{condition.Tz}'."); + } + catch (InvalidTimeZoneException) + { + return AccessRuleValidationResult.Invalid($"Invalid timezone: '{condition.Tz}'."); + } + + if (condition.Windows.Count == 0) + { + return AccessRuleValidationResult.Invalid("time_of_day requires at least one window."); + } + + foreach (var window in condition.Windows) + { + if (window.Days.Count == 0) + { + return AccessRuleValidationResult.Invalid("time_of_day window requires at least one day."); + } + + // Day tokens are validated during deserialization by AccessWeekdayJsonConverter; an unknown token fails + // the JSON parse above and is reported as malformed. + if (!TimeOfDayRegex().IsMatch(window.From)) + { + return AccessRuleValidationResult.Invalid($"Invalid 'from' time: '{window.From}'. Expected HH:mm."); + } + + if (!TimeOfDayRegex().IsMatch(window.To)) + { + return AccessRuleValidationResult.Invalid($"Invalid 'to' time: '{window.To}'. Expected HH:mm."); + } + } + + return AccessRuleValidationResult.Valid; + } +} diff --git a/bitwarden_license/src/Commercial.Pam/Services/ApproverCollectionAccessQuery.cs b/bitwarden_license/src/Commercial.Pam/Services/ApproverCollectionAccessQuery.cs new file mode 100644 index 000000000000..80a49f40912b --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Services/ApproverCollectionAccessQuery.cs @@ -0,0 +1,62 @@ +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Core.Services; + +namespace Bit.Commercial.Pam.Services; + +public class ApproverCollectionAccessQuery : IApproverCollectionAccessQuery +{ + private readonly ICollectionRepository _collectionRepository; + private readonly ICurrentContext _currentContext; + private readonly IApplicationCacheService _applicationCacheService; + + public ApproverCollectionAccessQuery( + ICollectionRepository collectionRepository, + ICurrentContext currentContext, + IApplicationCacheService applicationCacheService) + { + _collectionRepository = collectionRepository; + _currentContext = currentContext; + _applicationCacheService = applicationCacheService; + } + + public async Task> GetManageableCollectionIdsAsync(Guid userId) + { + // Collections the user is assigned with Manage (the repository aggregates Manage across direct and group + // access), mirroring BulkCollectionAuthorizationHandler. + var assigned = await _collectionRepository.GetManyByUserIdAsync(userId); + var manageable = assigned.Where(c => c.Manage).Select(c => c.Id).ToHashSet(); + + // Owners/Admins (when the org permits) and EditAnyCollection custom users can manage every collection in the + // organization, so fold those collections in too. + foreach (var org in _currentContext.Organizations) + { + var canManageAll = org.Permissions.EditAnyCollection; + if (!canManageAll && org.Type is OrganizationUserType.Owner or OrganizationUserType.Admin) + { + var ability = await _applicationCacheService.GetOrganizationAbilityAsync(org.Id); + canManageAll = ability?.AllowAdminAccessToAllCollectionItems ?? false; + } + + if (!canManageAll) + { + continue; + } + + var orgCollections = await _collectionRepository.GetManyByOrganizationIdAsync(org.Id); + foreach (var collection in orgCollections) + { + manageable.Add(collection.Id); + } + } + + return manageable; + } + + public async Task CanManageCollectionAsync(Guid userId, Guid collectionId) + { + var manageable = await GetManageableCollectionIdsAsync(userId); + return manageable.Contains(collectionId); + } +} diff --git a/bitwarden_license/src/Commercial.Pam/Services/ApproverInboxNotifier.cs b/bitwarden_license/src/Commercial.Pam/Services/ApproverInboxNotifier.cs new file mode 100644 index 000000000000..f6f0cb3134ab --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Services/ApproverInboxNotifier.cs @@ -0,0 +1,27 @@ +using Bit.Core.Platform.Push; +using Bit.Core.Repositories; + +namespace Bit.Commercial.Pam.Services; + +public class ApproverInboxNotifier : IApproverInboxNotifier +{ + private readonly ICollectionRepository _collectionRepository; + private readonly IPushNotificationService _pushNotificationService; + + public ApproverInboxNotifier( + ICollectionRepository collectionRepository, + IPushNotificationService pushNotificationService) + { + _collectionRepository = collectionRepository; + _pushNotificationService = pushNotificationService; + } + + public async Task NotifyCollectionApproversAsync(Guid collectionId) + { + var userIds = await _collectionRepository.GetManagingUserIdsAsync(collectionId); + foreach (var userId in userIds) + { + await _pushNotificationService.PushRefreshApproverInboxAsync(userId); + } + } +} diff --git a/bitwarden_license/src/Commercial.Pam/Services/CipherLeaseGate.cs b/bitwarden_license/src/Commercial.Pam/Services/CipherLeaseGate.cs new file mode 100644 index 000000000000..a2e83bda99a4 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Services/CipherLeaseGate.cs @@ -0,0 +1,203 @@ +using Bit.Commercial.Pam.Engine; +using Bit.Core; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Vault.Authorization; +using Bit.Core.Vault.Entities; +using Bit.Pam.Repositories; +using Bit.Pam.Services; + +namespace Bit.Commercial.Pam.Services; + +/// +public class CipherLeaseGate : ICipherLeaseGate +{ + private readonly IFeatureService _featureService; + private readonly IGoverningRuleResolver _resolver; + private readonly IAccessLeaseRepository _accessLeaseRepository; + private readonly ICollectionRepository _collectionRepository; + private readonly ICollectionCipherRepository _collectionCipherRepository; + private readonly ICurrentContext _currentContext; + private readonly TimeProvider _timeProvider; + + public CipherLeaseGate( + IFeatureService featureService, + IGoverningRuleResolver resolver, + IAccessLeaseRepository accessLeaseRepository, + ICollectionRepository collectionRepository, + ICollectionCipherRepository collectionCipherRepository, + ICurrentContext currentContext, + TimeProvider timeProvider) + { + _featureService = featureService; + _resolver = resolver; + _accessLeaseRepository = accessLeaseRepository; + _collectionRepository = collectionRepository; + _collectionCipherRepository = collectionCipherRepository; + _currentContext = currentContext; + _timeProvider = timeProvider; + } + + private bool Enabled => _featureService.IsEnabled(FeatureFlagKeys.Pam); + + public async Task AuthorizeReadAsync(Guid userId, Cipher cipher) + { + if (!Enabled) + { + return FullCipherAccess.Unrestricted(); + } + + return await IsBlockedAsync(userId, cipher.Id) + ? null + : FullCipherAccess.ForCipher(cipher.Id); + } + + public Task AuthorizeReadManyAsync( + Guid userId, + IEnumerable ciphers, + IEnumerable? collections, + IDictionary>? collectionCiphersByCipher) + { + if (!Enabled) + { + return Task.FromResult(FullCipherAccess.Unrestricted()); + } + + return Task.FromResult(BuildBulkWitness(ciphers, collections, collectionCiphersByCipher)); + } + + public async Task AuthorizeReadManyAsync(Guid userId, IEnumerable ciphers) + { + if (!Enabled) + { + return FullCipherAccess.Unrestricted(); + } + + var collections = await _collectionRepository.GetManyByUserIdAsync(userId); + var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdAsync(userId); + var collectionCiphersByCipher = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(g => g.Key); + return BuildBulkWitness(ciphers, collections, collectionCiphersByCipher); + } + + // Bulk reads never release a gated cipher's secrets, regardless of lease state, so the witness + // authorizes only the non-gated subset and the gated ones fall through to the partial shape. + private FullCipherAccess BuildBulkWitness( + IEnumerable ciphers, + IEnumerable? collections, + IDictionary>? collectionCiphersByCipher) + { + var gated = GetGatedCipherIds(collections, collectionCiphersByCipher); + var authorized = ciphers.Select(c => c.Id).Where(id => !gated.Contains(id)); + return FullCipherAccess.ForCiphers(authorized); + } + + public ISet GetGatedCipherIds( + IEnumerable? collections, + IDictionary>? collectionCiphersByCipher) + { + var gated = new HashSet(); + if (!Enabled || collections == null || collectionCiphersByCipher == null) + { + return gated; + } + + var leasingCollectionIds = collections + .Where(c => c.AccessRuleId.HasValue) + .Select(c => c.Id) + .ToHashSet(); + if (leasingCollectionIds.Count == 0) + { + return gated; + } + + foreach (var (cipherId, collectionCiphers) in collectionCiphersByCipher) + { + if (collectionCiphers.Any() && collectionCiphers.All(cc => leasingCollectionIds.Contains(cc.CollectionId))) + { + gated.Add(cipherId); + } + } + + return gated; + } + + public async Task EnsureCanMutateAsync(Guid userId, Cipher cipher) + { + if (!Enabled) + { + return FullCipherAccess.Unrestricted(); + } + + if (await IsBlockedAsync(userId, cipher.Id)) + { + throw new NotFoundException(); + } + + return FullCipherAccess.ForCipher(cipher.Id); + } + + public async Task EnsureCanMutateManyAsync(Guid userId, IEnumerable ciphers) + { + if (!Enabled) + { + return FullCipherAccess.Unrestricted(); + } + + var cipherList = ciphers as ICollection ?? ciphers.ToList(); + if (cipherList.Count == 0) + { + return FullCipherAccess.ForCiphers([]); + } + + var now = _timeProvider.GetUtcNow().UtcDateTime; + var leasedCipherIds = (await _accessLeaseRepository.GetManyActiveByRequesterIdAsync(userId, now)) + .Select(l => l.CipherId) + .ToHashSet(); + var signals = AccessSignals.From(_currentContext.IpAddress, new DateTimeOffset(now, TimeSpan.Zero)); + + foreach (var cipher in cipherList) + { + if (leasedCipherIds.Contains(cipher.Id)) + { + // A valid lease overrides gating, so no resolve is needed. + continue; + } + + if (await _resolver.ResolveAsync(userId, cipher.Id, signals) is not null) + { + // Gated with no lease: refuse the whole batch, mirroring how the service hides + // inaccessible ciphers. + throw new NotFoundException(); + } + } + + return FullCipherAccess.ForCiphers(cipherList.Select(c => c.Id)); + } + + public FullCipherAccess Unrestricted() => + // The flag-off path is unrestricted anyway; gating only ever narrows access, never widens it. + FullCipherAccess.Unrestricted(); + + /// + /// True when the cipher is leasing-gated for the caller and they hold no valid active lease — the + /// shared "withhold full data" condition behind both read and mutation decisions. Resolves the + /// governing rule first so non-gated ciphers (the common case) cost no lease query. + /// + private async Task IsBlockedAsync(Guid userId, Guid cipherId) + { + var now = _timeProvider.GetUtcNow().UtcDateTime; + var signals = AccessSignals.From(_currentContext.IpAddress, new DateTimeOffset(now, TimeSpan.Zero)); + + if (await _resolver.ResolveAsync(userId, cipherId, signals) is null) + { + return false; + } + + var activeLease = await _accessLeaseRepository.GetActiveByRequesterIdCipherIdAsync(userId, cipherId, now); + return activeLease is null; + } +} diff --git a/bitwarden_license/src/Commercial.Pam/Services/GoverningRuleResolver.cs b/bitwarden_license/src/Commercial.Pam/Services/GoverningRuleResolver.cs new file mode 100644 index 000000000000..8eccc33ddca6 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Services/GoverningRuleResolver.cs @@ -0,0 +1,112 @@ +using System.Text.Json; +using Bit.Commercial.Pam.Engine; +using Bit.Commercial.Pam.Models; +using Bit.Commercial.Pam.Models.Conditions; +using Bit.Core.Repositories; +using Bit.Pam.Repositories; + +namespace Bit.Commercial.Pam.Services; + +public class GoverningRuleResolver : IGoverningRuleResolver +{ + private static readonly JsonSerializerOptions _jsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + }; + + private readonly ICollectionCipherRepository _collectionCipherRepository; + private readonly ICollectionRepository _collectionRepository; + private readonly IAccessRuleRepository _accessRuleRepository; + private readonly IAccessRuleEngine _ruleEngine; + + public GoverningRuleResolver( + ICollectionCipherRepository collectionCipherRepository, + ICollectionRepository collectionRepository, + IAccessRuleRepository accessRuleRepository, + IAccessRuleEngine ruleEngine) + { + _collectionCipherRepository = collectionCipherRepository; + _collectionRepository = collectionRepository; + _accessRuleRepository = accessRuleRepository; + _ruleEngine = ruleEngine; + } + + public async Task ResolveAsync(Guid userId, Guid cipherId, AccessSignals signals) + { + var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, cipherId); + if (collectionCiphers.Count == 0) + { + return null; + } + + var collectionIds = collectionCiphers.Select(cc => cc.CollectionId).ToHashSet(); + var collections = await _collectionRepository.GetManyByManyIdsAsync(collectionIds); + + // Deterministic order so ties between equally-favourable rules resolve to a stable governing collection. + var governed = collections + .Where(c => collectionIds.Contains(c.Id) && c.AccessRuleId.HasValue) + .OrderBy(c => c.Id); + + // Least-restrictive wins: among the rules on the collections through which the caller reaches the cipher, + // an automatic grant (Allow) is favoured over one needing human approval (RequiresApproval), which is + // favoured over a denial (Deny). Evaluating each rule against the request signals means a failing automatic + // rule (e.g. an out-of-range IP) never pre-empts a different path that would grant or route to a human. + GoverningRule? best = null; + var bestOutcome = AccessEvaluationOutcome.Deny; + foreach (var collection in governed) + { + var accessRule = await _accessRuleRepository.GetByIdAsync(collection.AccessRuleId!.Value); + if (accessRule is null) + { + continue; + } + + var conditions = Parse(accessRule.Conditions); + var outcome = _ruleEngine.Evaluate(conditions, signals).Outcome; + if (best is not null && outcome >= bestOutcome) + { + continue; + } + + bestOutcome = outcome; + best = new GoverningRule( + collection.OrganizationId, + collection.Id, + outcome == AccessEvaluationOutcome.RequiresApproval, + conditions) + { + AllowsExtensions = accessRule.AllowsExtensions, + MaxExtensionDurationSeconds = accessRule.MaxExtensionDurationSeconds, + }; + + if (outcome == AccessEvaluationOutcome.Allow) + { + // Nothing beats an automatic grant; stop scanning the remaining collections. + break; + } + } + + return best; + } + + /// + /// Parses the stored conditions JSON into a flat list of . A malformed or + /// unparseable document fails safe to a single human-approval condition so access is never silently auto-approved + /// on conditions the server could not understand; the human-approval path then routes it to an approver rather + /// than issuing an automatic lease. + /// + private static IReadOnlyList Parse(string conditionsJson) + { + try + { + return JsonSerializer.Deserialize>(conditionsJson, _jsonOptions) ?? FailSafe(); + } + catch (JsonException) + { + return FailSafe(); + } + } + + private static IReadOnlyList FailSafe() => [new HumanApprovalCondition()]; +} diff --git a/bitwarden_license/src/Commercial.Pam/Services/IAccessRuleValidator.cs b/bitwarden_license/src/Commercial.Pam/Services/IAccessRuleValidator.cs new file mode 100644 index 000000000000..12afdd5e13f7 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Services/IAccessRuleValidator.cs @@ -0,0 +1,16 @@ +namespace Bit.Commercial.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/Commercial.Pam/Services/IApproverCollectionAccessQuery.cs b/bitwarden_license/src/Commercial.Pam/Services/IApproverCollectionAccessQuery.cs new file mode 100644 index 000000000000..202e179c0462 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Services/IApproverCollectionAccessQuery.cs @@ -0,0 +1,19 @@ +namespace Bit.Commercial.Pam.Services; + +/// +/// Resolves which collections the current user can Manage — the single authorization predicate for the approver +/// inbox. A user "approves" a request iff they can Manage the collection that holds the request's cipher. The list +/// endpoints use the full set as a filter; the decision/revoke endpoints check a single collection. +/// +public interface IApproverCollectionAccessQuery +{ + /// + /// The ids of every collection the user can Manage: collections they are assigned with Manage (directly or via + /// group), plus all collections in any organization where they are an Owner/Admin (when the org allows admin + /// access to all collection items) or hold the EditAnyCollection permission. + /// + Task> GetManageableCollectionIdsAsync(Guid userId); + + /// Whether the user can Manage the given collection. + Task CanManageCollectionAsync(Guid userId, Guid collectionId); +} diff --git a/bitwarden_license/src/Commercial.Pam/Services/IApproverInboxNotifier.cs b/bitwarden_license/src/Commercial.Pam/Services/IApproverInboxNotifier.cs new file mode 100644 index 000000000000..d52ce58b8e3e --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Services/IApproverInboxNotifier.cs @@ -0,0 +1,11 @@ +namespace Bit.Commercial.Pam.Services; + +/// +/// Pushes the RefreshApproverInbox signal to every user who can Manage a collection, telling their clients to +/// re-fetch the approver inbox. Fired whenever something the inbox renders changes (a new pending request, a request +/// leaving pending, a lease being revoked). +/// +public interface IApproverInboxNotifier +{ + Task NotifyCollectionApproversAsync(Guid collectionId); +} diff --git a/bitwarden_license/src/Commercial.Pam/Services/IGoverningRuleResolver.cs b/bitwarden_license/src/Commercial.Pam/Services/IGoverningRuleResolver.cs new file mode 100644 index 000000000000..9e31d9d98b8f --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Services/IGoverningRuleResolver.cs @@ -0,0 +1,17 @@ +using Bit.Commercial.Pam.Engine; +using Bit.Commercial.Pam.Models; + +namespace Bit.Commercial.Pam.Services; + +public interface IGoverningRuleResolver +{ + /// + /// Resolves the leasing context that governs for the caller, evaluating each + /// reachable collection's rule against the request , or null when the cipher is not + /// leasing-gated for them (no reachable collection carries an access rule). When more than one governing + /// collection applies, the least-restrictive applicable rule wins: an automatic grant is favoured over one that + /// needs human approval, which is favoured over a denial — so the caller is never routed to an approver when some + /// path would auto-grant, and a failing automatic rule never pre-empts a path that would grant. + /// + Task ResolveAsync(Guid userId, Guid cipherId, AccessSignals signals); +} diff --git a/bitwarden_license/src/Commercial.Pam/Services/IRequesterNotifier.cs b/bitwarden_license/src/Commercial.Pam/Services/IRequesterNotifier.cs new file mode 100644 index 000000000000..d0dbf5082990 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Services/IRequesterNotifier.cs @@ -0,0 +1,13 @@ +namespace Bit.Commercial.Pam.Services; + +/// +/// Pushes the RefreshAccessRequest signal to a single requester, telling their clients to re-fetch their own +/// access requests and active leases. Fired whenever something the requester's view renders changes for a reason +/// other than their own local action — a pending request being decided, a held lease being revoked or extended, a +/// request being cancelled — so their "My requests" list, lease banner, and row badges stay live, and an open cipher +/// re-locks the moment its lease ends. +/// +public interface IRequesterNotifier +{ + Task NotifyRequesterAsync(Guid requesterId); +} diff --git a/bitwarden_license/src/Commercial.Pam/Services/ISingleActiveLeaseEvaluator.cs b/bitwarden_license/src/Commercial.Pam/Services/ISingleActiveLeaseEvaluator.cs new file mode 100644 index 000000000000..d28b4c515a2c --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Services/ISingleActiveLeaseEvaluator.cs @@ -0,0 +1,13 @@ +namespace Bit.Commercial.Pam.Services; + +public interface ISingleActiveLeaseEvaluator +{ + /// + /// Decides whether the per-cipher single-active-lease constraint binds for the caller, using the same + /// union-over-paths logic as cipher gating. Returns true only when the caller reaches the cipher through at + /// least one collection and every such collection is governed by a rule whose + /// SingleActiveLease is true. Any ungated path (no access rule) or a path governed by a non-singleton + /// rule is an escape that leaves the caller unconstrained, so the method returns false. + /// + Task AppliesAsync(Guid userId, Guid cipherId); +} diff --git a/bitwarden_license/src/Commercial.Pam/Services/RequesterNotifier.cs b/bitwarden_license/src/Commercial.Pam/Services/RequesterNotifier.cs new file mode 100644 index 000000000000..f4e439260fb0 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Services/RequesterNotifier.cs @@ -0,0 +1,18 @@ +using Bit.Core.Platform.Push; + +namespace Bit.Commercial.Pam.Services; + +public class RequesterNotifier : IRequesterNotifier +{ + private readonly IPushNotificationService _pushNotificationService; + + public RequesterNotifier(IPushNotificationService pushNotificationService) + { + _pushNotificationService = pushNotificationService; + } + + public Task NotifyRequesterAsync(Guid requesterId) + { + return _pushNotificationService.PushRefreshAccessRequestAsync(requesterId); + } +} diff --git a/bitwarden_license/src/Commercial.Pam/Services/SingleActiveLeaseEvaluator.cs b/bitwarden_license/src/Commercial.Pam/Services/SingleActiveLeaseEvaluator.cs new file mode 100644 index 000000000000..87653bf65daa --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Services/SingleActiveLeaseEvaluator.cs @@ -0,0 +1,61 @@ +using Bit.Core.Repositories; +using Bit.Pam.Repositories; + +namespace Bit.Commercial.Pam.Services; + +public class SingleActiveLeaseEvaluator : ISingleActiveLeaseEvaluator +{ + private readonly ICollectionCipherRepository _collectionCipherRepository; + private readonly ICollectionRepository _collectionRepository; + private readonly IAccessRuleRepository _accessRuleRepository; + + public SingleActiveLeaseEvaluator( + ICollectionCipherRepository collectionCipherRepository, + ICollectionRepository collectionRepository, + IAccessRuleRepository accessRuleRepository) + { + _collectionCipherRepository = collectionCipherRepository; + _collectionRepository = collectionRepository; + _accessRuleRepository = accessRuleRepository; + } + + public async Task AppliesAsync(Guid userId, Guid cipherId) + { + var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, cipherId); + if (collectionCiphers.Count == 0) + { + // No reachable collection: there is no path, so the constraint does not bind. + return false; + } + + var collectionIds = collectionCiphers.Select(cc => cc.CollectionId).ToHashSet(); + var collections = await _collectionRepository.GetManyByManyIdsAsync(collectionIds); + + var paths = collections.Where(c => collectionIds.Contains(c.Id)).ToList(); + if (paths.Count == 0) + { + return false; + } + + foreach (var collection in paths) + { + // An ungated path is an escape: the caller can reach the cipher without any singleton rule, so the + // constraint does not bind for them. + if (!collection.AccessRuleId.HasValue) + { + return false; + } + + var accessRule = await _accessRuleRepository.GetByIdAsync(collection.AccessRuleId.Value); + + // A missing rule, or a rule that does not ask for a singleton, is likewise an escape path. + if (accessRule is null || !accessRule.SingleActiveLease) + { + return false; + } + } + + // Every path is governed by a singleton rule: the constraint binds. + return true; + } +} diff --git a/bitwarden_license/src/Commercial.Pam/Utilities/ServiceCollectionExtensions.cs b/bitwarden_license/src/Commercial.Pam/Utilities/ServiceCollectionExtensions.cs new file mode 100644 index 000000000000..fd77eaca772b --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/Utilities/ServiceCollectionExtensions.cs @@ -0,0 +1,45 @@ +using Bit.Commercial.Pam.Engine; +using Bit.Commercial.Pam.OrganizationFeatures.Commands; +using Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; +using Bit.Commercial.Pam.OrganizationFeatures.Queries; +using Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; +using Bit.Commercial.Pam.Services; +using Bit.Pam.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Bit.Commercial.Pam.Utilities; + +public static class ServiceCollectionExtensions +{ + public static void AddCommercialPamServices(this IServiceCollection services) + { + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.TryAddSingleton(TimeProvider.System); + services.AddScoped(); + } +} diff --git a/bitwarden_license/src/Commercial.Pam/packages.lock.json b/bitwarden_license/src/Commercial.Pam/packages.lock.json new file mode 100644 index 000000000000..db3a2021f277 --- /dev/null +++ b/bitwarden_license/src/Commercial.Pam/packages.lock.json @@ -0,0 +1,1099 @@ +{ + "version": 1, + "dependencies": { + "net10.0": { + "AdaptiveCards": { + "type": "Transitive", + "resolved": "3.1.0", + "contentHash": "b+sPwH0oyAflpgxCyNPMzH92xrQjWl6GuuEBv86/VhO6iHhiWv+PtwzqMS70nOXZQRzpl9YVHXAvn+dKot5IBQ==", + "dependencies": { + "Newtonsoft.Json": "13.0.3" + } + }, + "AspNetCoreRateLimit": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "6fq9+o1maGADUmpK/PvcF0DtXW2+7bSkIL7MDIo/agbIHKN8XkMQF4oze60DO731WaQmHmK260hB30FwPzCmEg==", + "dependencies": { + "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" + } + }, + "AutoMapper": { + "type": "Transitive", + "resolved": "14.0.0", + "contentHash": "OC+1neAPM4oCCqQj3g2GJ2shziNNhOkxmNB9cVS8jtx4JbgmRzLcUOxB9Tsz6cVPHugdkHgCaCrTjjSI0Z5sCQ==" + }, + "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" + } + }, + "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.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" + } + }, + "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" + } + }, + "CsvHelper": { + "type": "Transitive", + "resolved": "33.1.0", + "contentHash": "kqfTOZGrn7NarNeXgjh86JcpTHUoeQDMB8t9NVa/ZtlSYiV1rxfRnQ49WaJsob4AiGrbK0XDzpyKkBwai4F8eg==" + }, + "Dapper": { + "type": "Transitive", + "resolved": "2.1.66", + "contentHash": "/q77jUgDOS+bzkmk3Vy9SiWMaetTw+NOoPAV0xPBsGVAyljd5S6P+4RUW7R3ZUGGr9lDRyPKgAMj2UAOwvqZYw==" + }, + "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" + } + }, + "DuoUniversal": { + "type": "Transitive", + "resolved": "1.3.1", + "contentHash": "BZUJplORCBO1PVDFT5v7HDYAlpgHAkay5N9vzRpJ/sBm+GU44pxNlo7v95Ym0tvUeKy0WWWv/iE4FjErYoZUHQ==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "6.34.0" + } + }, + "Fido2": { + "type": "Transitive", + "resolved": "3.0.1", + "contentHash": "S0Bz1vfcKlO4Jase3AWp5XnQ746psf4oGx5kL+D2A10j1SsjoAOAIIpanSwfi0cEepDHgk1bClcOKY5TjOzGdA==", + "dependencies": { + "Fido2.Models": "3.0.1", + "NSec.Cryptography": "22.4.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==" + }, + "LaunchDarkly.Cache": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "0bEnUVFVeW1TTDXb/bW6kS3FLQTLeGtw7Xh8yt6WNO56utVmtgcrMLvcnF6yeTn+N4FXrKfW09KkLNmK8YYQvw==" + }, + "LaunchDarkly.CommonSdk": { + "type": "Transitive", + "resolved": "7.1.1", + "contentHash": "J557Na/XeYV8JjgWbGRMzi5eOgXrEMqcuPBtkZP/y7EgFLiWgvaPBGDm+hq766Kp0Kxs2MaZaNTQn4Fog7Hebw==", + "dependencies": { + "LaunchDarkly.Logging": "2.0.0" + } + }, + "LaunchDarkly.EventSource": { + "type": "Transitive", + "resolved": "5.3.0", + "contentHash": "i++YvdrzvTc1tOxfVreU2yjo/E+iTFcVtwiB4PZ/3uTouNdhABLbJfz1jmGN3jmnvJKWhsMeEZLZM0dzkI+PXQ==", + "dependencies": { + "LaunchDarkly.Logging": "[2.0.0, 3.0.0)" + } + }, + "LaunchDarkly.InternalSdk": { + "type": "Transitive", + "resolved": "3.6.0", + "contentHash": "Drf1rL+sZ/4UZDUovDLgRN9qQPf8J4QTQjJR2A8nCuxkFU7QjVFXqqW9+KL8RfX3DmJkhpgC3QVA1B9B6xEYJQ==", + "dependencies": { + "LaunchDarkly.CommonSdk": "[7.1.1, 8.0.0)", + "LaunchDarkly.Logging": "[2.0.0, 3.0.0)" + } + }, + "LaunchDarkly.Logging": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "lsLKNqAZ7HIlkdTIrf4FetfRA1SUDE3WlaZQn79aSVkLjYWEhUhkDDK7hORGh4JoA3V2gXN+cIvJQax2uR/ijA==" + }, + "LaunchDarkly.ServerSdk": { + "type": "Transitive", + "resolved": "8.11.0", + "contentHash": "MMXfyGMDtul9UhEleHXdGNe2amLY1gjTZ9K2R18XoZp5OOlSGu4CmBqE4MXMsXhLYoEtYg2GIibmoTSPaU2Y+A==", + "dependencies": { + "LaunchDarkly.Cache": "1.0.2", + "LaunchDarkly.CommonSdk": "7.1.1", + "LaunchDarkly.EventSource": "5.3.0", + "LaunchDarkly.InternalSdk": "3.6.0", + "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.16.0", + "contentHash": "trJ82DOpAmo8i1jO1vNE+dGn4mPRyeYfy4swRcAGgMJhPoI1Kohf4OFJJf0+YIj4iUxgxPn8W+ht7e7KiYzSjg==", + "dependencies": { + "MimeKit": "4.16.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.DataProtection": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "Rlbrr3XSGB4dwnUY/pA70TpVyrQhelDAUkIiGfJ2Tm32mscTYxrRnk9Ooy1rRhGZ1g7rliJPuNzhyMMKmRH5pw==" + }, + "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": { + "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.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", + "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.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", + "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": { + "Newtonsoft.Json": "13.0.3" + } + }, + "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.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.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.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.Caching.Cosmos": { + "type": "Transitive", + "resolved": "1.8.0", + "contentHash": "8UI41/U5yla1z48klbtRdXxxloAChRzhAm592bitBNzTlXBq87zeO6Lbdvuh5d6oLNCU0I01kw6ovTD9Z6y05g==", + "dependencies": { + "Microsoft.Azure.Cosmos": "3.47.0", + "Newtonsoft.Json": "13.0.3" + } + }, + "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.StackExchangeRedis": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "fle9ns3q2kk63vt/wHFtLw1U9kiEbM42vTk2Sar8VBjiGJFkXAgm0QEKFx15YMHkJIPRVdknWaovpvgGEgn10g==", + "dependencies": { + "StackExchange.Redis": "2.7.27" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "bVGqctAfPGfTxJvNp8pMshtvpsUj6r6JkeiCNVIGVYO5gBxuxdN0Lbr25kEvE/zXdctkEc44g8HssnPgDnFGVA==" + }, + "Microsoft.Extensions.Configuration.UserSecrets": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "6XTfFOnf27WY8kEeZkTZ4YNn0t+imgvdQ0YaAdR4vgURKATo9bCaVJ1KB71IOJAQtJP7Elb53VHlTNXg2CtSsA==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "5Ou6varcxLBzQ+Agfm0k0pnH7vrEITYlXMDuE6s7ZHlZHz6/G8XJ3iISZDr5rfwfge6RnXJ1+Wc479mMn52vjA==" + }, + "Microsoft.Extensions.Identity.Stores": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "xVbg4qLWyjKSJVxtL56PQPlHu/URpWPKufhfOj61+tkCmNs6DIgnGxG8BAO/fAfacoBDDYg+p1zBjFzzj/EQog==" + }, + "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.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.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==" + }, + "MimeKit": { + "type": "Transitive", + "resolved": "4.16.0", + "contentHash": "X0LFxeM4gPRIhODyY/HYS9b+zRZ7y//v59rFzgS6wLxcPuZThnMtNZHtrr0fjLyRRkg3gqJBtvW36XfUzZ7Djw==", + "dependencies": { + "BouncyCastle.Cryptography": "2.6.2", + "System.Security.Cryptography.Pkcs": "10.0.0" + } + }, + "MySqlConnector": { + "type": "Transitive", + "resolved": "2.3.5", + "contentHash": "AmEfUPkFl+Ev6jJ8Dhns3CYHBfD12RHzGYWuLt6DfG6/af6YvOMyPz74ZPPjBYQGRJkumD2Z48Kqm8s5DJuhLA==" + }, + "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==" + }, + "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==" + }, + "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==" + }, + "Quartz.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "3.15.1", + "contentHash": "LinB9z54aPn49C/DGM1v3OflX2nosrEo4zNz10vfYqcCndFJ8MNU9k++Ap9T7vxeZc355WStPDggpX60TYj1Lg==", + "dependencies": { + "Quartz": "3.15.1" + } + }, + "Quartz.Extensions.Hosting": { + "type": "Transitive", + "resolved": "3.15.1", + "contentHash": "svqLTEnVLb0VPUcNCd/khRqagwxM/yybUZ2sEOd7HFdPO+5dAOttL+ARtXSyBeaGWPWAaxY4VvU7pJTZzYhORw==", + "dependencies": { + "Quartz.Extensions.DependencyInjection": "3.15.1" + } + }, + "RabbitMQ.Client": { + "type": "Transitive", + "resolved": "7.1.2", + "contentHash": "y3c6ulgULScWthHw5PLM1ShHRLhxg0vCtzX/hh61gRgNecL3ZC3WoBW2HYHoXOVRqTl99Br9E7CZEytGZEsCyQ==" + }, + "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": { + "Serilog": "2.9.0" + } + }, + "Serilog.Extensions.Logging.File": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "bUYjMHn7NhpK+/8HDftG7+G5hpWzD49XTSvLoUFZGgappDa6FoseqFOsLrjLRjwe1zM+igH5mySFJv3ntb+qcg==", + "dependencies": { + "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": { + "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.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" + } + }, + "System.ClientModel": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "xcHHhDqB5MnOOY8yIn64Vzp6gtBEs6k5J1hluG04CrShSvQNXOx4PSDs7wJiXLDidlY/FZJmxJdKTKskyJwjvw==", + "dependencies": { + "System.Memory.Data": "8.0.1" + } + }, + "System.Configuration.ConfigurationManager": { + "type": "Transitive", + "resolved": "9.0.13", + "contentHash": "GbBrJq9S/gYpHzm7Pxx6Y5tDyfSfyxW6tlP5oiKJV38uf19Wp+GIIAnWfyL1zmNiz1+EjwVapw2WkBFvvqKQzg==", + "dependencies": { + "System.Security.Cryptography.ProtectedData": "9.0.13" + } + }, + "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.0", + "contentHash": "UPWqLSygJlFerRi9XNIuM0a1VC8gHUIufyP24xQ0sc+XimqUAEcjpOz9DhKpyDjH+5B/wO3RpC0KpkEeDj/ddg==" + }, + "System.Security.Cryptography.ProtectedData": { + "type": "Transitive", + "resolved": "9.0.13", + "contentHash": "t8S9IDpjJKsLpLkeBdW8cWtcPyYqrGu93Dej1RO6WwuL/lkFSqWlan3rMJfortqz1mRIh+sys2AFsSA6jWJ3Jg==" + }, + "System.Xml.XPath.XmlDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "A/uxsWi/Ifzkmd4ArTLISMbfFs6XpRPsXZonrIqyTY70xi8t+mDtvSM5Os0RqyRDobjMBwIDHDL4NOIbkDwf7A==" + }, + "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==" + }, + "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" + } + }, + "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]", + "Braintree": "[5.36.0, 5.36.0]", + "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", + "DnsClient": "[1.8.0, 1.8.0]", + "Duende.IdentityServer": "[7.4.6, 7.4.6]", + "DuoUniversal": "[1.3.1, 1.3.1]", + "Fido2.AspNet": "[3.0.1, 3.0.1]", + "Handlebars.Net": "[2.1.6, 2.1.6]", + "LaunchDarkly.ServerSdk": "[8.11.0, 8.11.0]", + "MailKit": "[4.16.0, 4.16.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]", + "Pam.Domain": "[2026.6.1, )", + "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]", + "Serilog.Extensions.Logging.File": "[3.0.0, 3.0.0]", + "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" + }, + "infrastructure.dapper": { + "type": "Project", + "dependencies": { + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" + } + }, + "infrastructure.entityframework": { + "type": "Project", + "dependencies": { + "AutoMapper": "[14.0.0, 14.0.0]", + "Core": "[2026.6.1, )", + "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.6.1, )", + "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", + "linq2db": "[5.4.1, 5.4.1]", + "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" + } + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, + "sharedweb": { + "type": "Project", + "dependencies": { + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", + "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", + "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" + } + } + } + } +} \ No newline at end of file diff --git a/bitwarden_license/src/Scim/packages.lock.json b/bitwarden_license/src/Scim/packages.lock.json index 4337d161981f..903bc87d916f 100644 --- a/bitwarden_license/src/Scim/packages.lock.json +++ b/bitwarden_license/src/Scim/packages.lock.json @@ -1097,6 +1097,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1121,6 +1122,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1134,33 +1136,44 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/bitwarden_license/src/Sso/packages.lock.json b/bitwarden_license/src/Sso/packages.lock.json index 29906d5f12f2..31acb935fb48 100644 --- a/bitwarden_license/src/Sso/packages.lock.json +++ b/bitwarden_license/src/Sso/packages.lock.json @@ -1136,6 +1136,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1160,6 +1161,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1173,33 +1175,44 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/bitwarden_license/test/Commercial.Core.Test/packages.lock.json b/bitwarden_license/test/Commercial.Core.Test/packages.lock.json index bb35f0deb41b..2011a1ab1bc7 100644 --- a/bitwarden_license/test/Commercial.Core.Test/packages.lock.json +++ b/bitwarden_license/test/Commercial.Core.Test/packages.lock.json @@ -1341,7 +1341,7 @@ "commercial.core": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "CsvHelper": "[33.1.0, 33.1.0]" } }, @@ -1350,7 +1350,7 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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, )", @@ -1374,6 +1374,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1398,6 +1399,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1416,14 +1418,24 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Common": "[2026.6.0, )", - "Core": "[2026.6.0, )", + "Common": "[2026.6.1, )", + "Core": "[2026.6.1, )", "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, )", + "Pam.Domain": "[2026.6.1, )", "xunit": "[2.6.6, )" } + }, + "data": { + "type": "Project" + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } } } } diff --git a/bitwarden_license/test/Commercial.Pam.Test/Api/Controllers/CipherLeaseControllerTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Api/Controllers/CipherLeaseControllerTests.cs new file mode 100644 index 000000000000..d8f6de5dffb8 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Api/Controllers/CipherLeaseControllerTests.cs @@ -0,0 +1,77 @@ +using System.Security.Claims; +using Bit.Api.Pam.Controllers; +using Bit.Api.Vault.Models.Response; +using Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Vault.Models.Data; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using Xunit; +using CipherType = Bit.Core.Vault.Enums.CipherType; + +namespace Bit.Commercial.Pam.Test.Api.Controllers; + +// The deprecated GET /ciphers/{id}/lease/cipher endpoint stays in the Api project (it depends on the Vault +// response models), so it is tested here as an MVC controller. The rest of the cipher-lease resource lives in +// the Commercial.Pam Minimal API handler (see CipherLeaseEndpointsHandlerTests). +[ControllerCustomize(typeof(CipherLeaseController))] +[SutProviderCustomize] +[Bit.Api.Test.Vault.AutoFixture.CipherLeaseGateBypassCustomize] +public class CipherLeaseControllerTests +{ + // GET /ciphers/{id}/lease/cipher is [Obsolete] (deprecated, scheduled for removal) but still fully functional, so + // its behaviour stays under test; suppress the obsolete-usage warning for these deliberate calls. +#pragma warning disable CS0618 + [Theory, BitAutoData] + public async Task GetCipher_NoLeasedCipher_ThrowsNotFound( + Guid id, User user, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetUserByPrincipalAsync(Arg.Any()) + .Returns(user); + sutProvider.GetDependency() + .GetLeasedCipherAsync(user.Id, id) + .ReturnsNull(); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetCipher(id)); + } + + [Theory, BitAutoData] + public async Task GetCipher_LeasedCipher_ReturnsFullData( + Guid id, Guid organizationId, User user, SutProvider sutProvider) + { + var cipher = new CipherDetails + { + Id = id, + OrganizationId = organizationId, + Type = CipherType.Login, + Data = "2.iv|ct|mac", + }; + sutProvider.GetDependency() + .GetUserByPrincipalAsync(Arg.Any()) + .Returns(user); + sutProvider.GetDependency() + .GetLeasedCipherAsync(user.Id, id) + .Returns(cipher); + sutProvider.GetDependency() + .GetManyByUserIdCipherIdAsync(user.Id, id) + .Returns(new List()); + sutProvider.GetDependency() + .GetOrganizationAbilityAsync(organizationId) + .Returns(new OrganizationAbility { Id = organizationId }); + + var result = await sutProvider.Sut.GetCipher(id); + + Assert.IsAssignableFrom(result); + Assert.Equal(id, result.Id); + Assert.Equal("2.iv|ct|mac", result.Data); // full data present + Assert.Null(result.PartialData); // isPartial == false + } +#pragma warning restore CS0618 +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Api/Endpoints/AccessRequestEndpointsHandlerTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Api/Endpoints/AccessRequestEndpointsHandlerTests.cs new file mode 100644 index 000000000000..692c3ee7bc61 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Api/Endpoints/AccessRequestEndpointsHandlerTests.cs @@ -0,0 +1,128 @@ +using System.Security.Claims; +using Bit.Commercial.Pam.Api.Endpoints.Handlers; +using Bit.Commercial.Pam.Api.Models.Request; +using Bit.Commercial.Pam.Api.Models.Response; +using Bit.Commercial.Pam.Models; +using Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; +using Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; +using Bit.Core.Services; +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Pam.Models; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Api.Endpoints; + +[SutProviderCustomize] +public class AccessRequestEndpointsHandlerTests +{ + private static readonly ClaimsPrincipal _user = new(); + + [Theory, BitAutoData] + public async Task GetInbox_ReturnsMappedPendingRows( + Guid userId, AccessRequestDetails row, SutProvider sutProvider) + { + SetupUser(sutProvider, userId); + row.Status = AccessRequestStatus.Pending; + sutProvider.GetDependency().GetPendingAsync(userId).Returns([row]); + + var result = await sutProvider.Sut.GetInbox(_user); + + Assert.Single(result.Data); + Assert.Equal(row.Id, result.Data.First().Id); + } + + [Theory, BitAutoData] + public async Task GetHistory_ReturnsMappedHistoryRows( + Guid userId, AccessRequestDetails row, SutProvider sutProvider) + { + SetupUser(sutProvider, userId); + row.Status = AccessRequestStatus.Approved; + sutProvider.GetDependency().GetHistoryAsync(userId).Returns([row]); + + var result = await sutProvider.Sut.GetHistory(_user); + + Assert.Single(result.Data); + } + + [Theory, BitAutoData] + public async Task GetMine_ReturnsMappedRows( + Guid userId, AccessRequestDetails row, SutProvider sutProvider) + { + SetupUser(sutProvider, userId); + row.Status = AccessRequestStatus.Pending; + sutProvider.GetDependency().GetMineAsync(userId).Returns([row]); + + var result = (await sutProvider.Sut.GetMine(_user)).Data.ToList(); + + Assert.Single(result); + Assert.Equal(row.Id, result[0].Id); + Assert.Equal(AccessRequestStatusNames.Pending, result[0].Status); + } + + [Theory, BitAutoData] + public async Task GetMine_NoRows_ReturnsEmpty( + Guid userId, SutProvider sutProvider) + { + SetupUser(sutProvider, userId); + sutProvider.GetDependency().GetMineAsync(userId).Returns([]); + + var result = await sutProvider.Sut.GetMine(_user); + + Assert.Empty(result.Data); + } + + [Theory, BitAutoData] + public async Task Decide_ReturnsUpdatedRow( + Guid userId, Guid requestId, AccessRequestDetails updated, SutProvider sutProvider) + { + SetupUser(sutProvider, userId); + updated.Status = AccessRequestStatus.Approved; + updated.ProducedLeaseId = null; + sutProvider.GetDependency() + .DecideAsync(userId, requestId, Arg.Any()) + .Returns(updated); + + var result = await sutProvider.Sut.Decide(_user, requestId, new AccessDecisionRequestModel { Verdict = AccessDecisionVerdict.Approve }); + + Assert.Equal(updated.Id, result.Id); + Assert.Equal(AccessRequestStatusNames.Approved, result.Status); + } + + [Theory, BitAutoData] + public async Task Activate_ReturnsMintedLease( + Guid userId, Guid requestId, AccessLease lease, SutProvider sutProvider) + { + SetupUser(sutProvider, userId); + lease.Status = AccessLeaseStatus.Active; + sutProvider.GetDependency() + .ActivateAsync(userId, requestId) + .Returns(lease); + + var result = await sutProvider.Sut.Activate(_user, requestId); + + Assert.Equal(lease.Id, result.Id); + Assert.Equal(AccessLeaseStatusNames.Active, result.Status); + } + + [Theory, BitAutoData] + public async Task Revoke_InvokesCancelCommand( + Guid userId, Guid requestId, SutProvider sutProvider) + { + SetupUser(sutProvider, userId); + + await sutProvider.Sut.Revoke(_user, requestId); + + await sutProvider.GetDependency().Received(1).CancelAsync(userId, requestId); + } + + private static void SetupUser(SutProvider sutProvider, Guid userId) + { + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(userId); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Api/Endpoints/CipherLeaseEndpointsHandlerTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Api/Endpoints/CipherLeaseEndpointsHandlerTests.cs new file mode 100644 index 000000000000..4a31a96ebaf6 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Api/Endpoints/CipherLeaseEndpointsHandlerTests.cs @@ -0,0 +1,36 @@ +using System.Security.Claims; +using Bit.Commercial.Pam.Api.Endpoints.Handlers; +using Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Api.Endpoints; + +[SutProviderCustomize] +public class CipherLeaseEndpointsHandlerTests +{ + private static readonly ClaimsPrincipal _user = new(); + + [Theory, BitAutoData] + public async Task State_ReturnsSnapshotFromQuery( + Guid id, Guid userId, Bit.Pam.Entities.AccessLease activeLease, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(userId); + sutProvider.GetDependency() + .GetStateAsync(userId, id) + .Returns(new Bit.Commercial.Pam.Models.CipherAccessState(id, activeLease, null, null)); + + var result = await sutProvider.Sut.State(_user, id); + + Assert.Equal(id, result.CipherId); + Assert.NotNull(result.ActiveLease); + Assert.Equal(activeLease.Id, result.ActiveLease!.Id); + Assert.Null(result.PendingRequest); + Assert.Null(result.ApprovedRequest); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Api/Endpoints/Filters/PamExceptionHandlerEndpointFilterTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Api/Endpoints/Filters/PamExceptionHandlerEndpointFilterTests.cs new file mode 100644 index 000000000000..26e2c7a46180 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Api/Endpoints/Filters/PamExceptionHandlerEndpointFilterTests.cs @@ -0,0 +1,90 @@ +using Bit.Commercial.Pam.Api.Endpoints.Filters; +using Bit.Core.Exceptions; +using Bit.Core.Models.Api; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Api.Endpoints.Filters; + +public class PamExceptionHandlerEndpointFilterTests +{ + [Fact] + public async Task InvokeAsync_NoException_PassesResultThrough() + { + var context = CreateContext(); + EndpointFilterDelegate next = _ => ValueTask.FromResult("ok"); + + var result = await new PamExceptionHandlerEndpointFilter().InvokeAsync(context, next); + + Assert.Equal("ok", result); + } + + [Fact] + public async Task InvokeAsync_NotFoundException_Returns404ErrorResponseModel() + { + var context = CreateContext(); + EndpointFilterDelegate next = _ => throw new NotFoundException(); + + var result = await new PamExceptionHandlerEndpointFilter().InvokeAsync(context, next); + + var jsonResult = Assert.IsType>(result); + Assert.Equal(StatusCodes.Status404NotFound, jsonResult.StatusCode); + Assert.Equal("Resource not found.", jsonResult.Value!.Message); + } + + [Fact] + public async Task InvokeAsync_FeatureUnavailableException_Returns404() + { + // FeatureUnavailableException derives from NotFoundException, so the feature gate maps to 404 like the rest. + var context = CreateContext(); + EndpointFilterDelegate next = _ => throw new FeatureUnavailableException(); + + var result = await new PamExceptionHandlerEndpointFilter().InvokeAsync(context, next); + + var jsonResult = Assert.IsType>(result); + Assert.Equal(StatusCodes.Status404NotFound, jsonResult.StatusCode); + } + + [Fact] + public async Task InvokeAsync_BadRequestExceptionWithModelState_Returns400WithValidationErrors() + { + var modelState = new ModelStateDictionary(); + modelState.AddModelError("Name", "Required."); + var context = CreateContext(); + EndpointFilterDelegate next = _ => throw new BadRequestException(modelState); + + var result = await new PamExceptionHandlerEndpointFilter().InvokeAsync(context, next); + + var jsonResult = Assert.IsType>(result); + Assert.Equal(StatusCodes.Status400BadRequest, jsonResult.StatusCode); + Assert.True(jsonResult.Value!.ValidationErrors!.ContainsKey("Name")); + } + + [Fact] + public async Task InvokeAsync_UnhandledException_Returns500() + { + var context = CreateContext(); + EndpointFilterDelegate next = _ => throw new InvalidOperationException("boom"); + + var result = await new PamExceptionHandlerEndpointFilter().InvokeAsync(context, next); + + var jsonResult = Assert.IsType>(result); + Assert.Equal(StatusCodes.Status500InternalServerError, jsonResult.StatusCode); + } + + private static EndpointFilterInvocationContext CreateContext() + { + var environment = Substitute.For(); + environment.EnvironmentName.Returns("Production"); + var services = new ServiceCollection(); + services.AddSingleton(environment); + services.AddLogging(); + var httpContext = new DefaultHttpContext { RequestServices = services.BuildServiceProvider() }; + return EndpointFilterInvocationContext.Create(httpContext); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Api/Endpoints/Filters/PamValidationEndpointFilterTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Api/Endpoints/Filters/PamValidationEndpointFilterTests.cs new file mode 100644 index 000000000000..62c16934109a --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Api/Endpoints/Filters/PamValidationEndpointFilterTests.cs @@ -0,0 +1,73 @@ +using Bit.Commercial.Pam.Api.Endpoints.Filters; +using Bit.Commercial.Pam.Api.Models.Request; +using Bit.Core.Models.Api; +using Bit.Pam.Enums; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Api.Endpoints.Filters; + +public class PamValidationEndpointFilterTests +{ + [Fact] + public async Task InvokeAsync_InvalidRequestModel_ReturnsErrorResponseModel400AndSkipsNext() + { + // Verdict is [Required] and left null -> invalid. + var context = CreateContext(new AccessDecisionRequestModel()); + var nextCalled = false; + EndpointFilterDelegate next = _ => + { + nextCalled = true; + return ValueTask.FromResult("ok"); + }; + + var result = await new PamValidationEndpointFilter().InvokeAsync(context, next); + + Assert.False(nextCalled); + var jsonResult = Assert.IsType>(result); + Assert.Equal(StatusCodes.Status400BadRequest, jsonResult.StatusCode); + Assert.Equal("The model state is invalid.", jsonResult.Value!.Message); + Assert.True(jsonResult.Value.ValidationErrors!.ContainsKey(nameof(AccessDecisionRequestModel.Verdict))); + } + + [Fact] + public async Task InvokeAsync_ValidRequestModel_CallsNext() + { + var context = CreateContext(new AccessDecisionRequestModel { Verdict = AccessDecisionVerdict.Approve }); + var nextCalled = false; + EndpointFilterDelegate next = _ => + { + nextCalled = true; + return ValueTask.FromResult("ok"); + }; + + var result = await new PamValidationEndpointFilter().InvokeAsync(context, next); + + Assert.True(nextCalled); + Assert.Equal("ok", result); + } + + [Fact] + public async Task InvokeAsync_NonRequestModelArguments_AreIgnored() + { + // Route/service-style arguments (a Guid, a string) are not request models and must not be validated. + var context = CreateContext(Guid.NewGuid(), "not-a-model"); + var nextCalled = false; + EndpointFilterDelegate next = _ => + { + nextCalled = true; + return ValueTask.FromResult("ok"); + }; + + var result = await new PamValidationEndpointFilter().InvokeAsync(context, next); + + Assert.True(nextCalled); + Assert.Equal("ok", result); + } + + // Use DefaultEndpointFilterInvocationContext's params constructor rather than the static Create(...), whose + // generic overload would treat a passed object[] as one argument instead of spreading it. + private static EndpointFilterInvocationContext CreateContext(params object[] arguments) => + new DefaultEndpointFilterInvocationContext(new DefaultHttpContext(), arguments); +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Api/Endpoints/Filters/RequireFeatureEndpointFilterTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Api/Endpoints/Filters/RequireFeatureEndpointFilterTests.cs new file mode 100644 index 000000000000..0d6adf788bc3 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Api/Endpoints/Filters/RequireFeatureEndpointFilterTests.cs @@ -0,0 +1,59 @@ +using Bit.Commercial.Pam.Api.Endpoints.Filters; +using Bit.Core.Exceptions; +using Bit.Core.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Api.Endpoints.Filters; + +public class RequireFeatureEndpointFilterTests +{ + private const string Flag = "pm-test-flag"; + + [Fact] + public async Task InvokeAsync_FeatureEnabled_CallsNext() + { + var featureService = Substitute.For(); + featureService.IsEnabled(Flag).Returns(true); + var context = CreateContext(featureService); + var nextCalled = false; + EndpointFilterDelegate next = _ => + { + nextCalled = true; + return ValueTask.FromResult("ok"); + }; + + var result = await new RequireFeatureEndpointFilter(Flag).InvokeAsync(context, next); + + Assert.True(nextCalled); + Assert.Equal("ok", result); + } + + [Fact] + public async Task InvokeAsync_FeatureDisabled_ThrowsFeatureUnavailableAndSkipsNext() + { + var featureService = Substitute.For(); + featureService.IsEnabled(Flag).Returns(false); + var context = CreateContext(featureService); + var nextCalled = false; + EndpointFilterDelegate next = _ => + { + nextCalled = true; + return ValueTask.FromResult("ok"); + }; + + await Assert.ThrowsAsync( + async () => await new RequireFeatureEndpointFilter(Flag).InvokeAsync(context, next)); + Assert.False(nextCalled); + } + + private static EndpointFilterInvocationContext CreateContext(IFeatureService featureService) + { + var services = new ServiceCollection(); + services.AddSingleton(featureService); + var httpContext = new DefaultHttpContext { RequestServices = services.BuildServiceProvider() }; + return EndpointFilterInvocationContext.Create(httpContext); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Api/Endpoints/LeaseEndpointsHandlerTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Api/Endpoints/LeaseEndpointsHandlerTests.cs new file mode 100644 index 000000000000..476de4a9ce6f --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Api/Endpoints/LeaseEndpointsHandlerTests.cs @@ -0,0 +1,121 @@ +using System.Security.Claims; +using Bit.Commercial.Pam.Api.Endpoints.Handlers; +using Bit.Commercial.Pam.Api.Models.Request; +using Bit.Commercial.Pam.Api.Models.Response; +using Bit.Commercial.Pam.Models; +using Bit.Commercial.Pam.OrganizationFeatures.Commands.Interfaces; +using Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; +using Bit.Core.Services; +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Pam.Models; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Api.Endpoints; + +[SutProviderCustomize] +public class LeaseEndpointsHandlerTests +{ + private static readonly ClaimsPrincipal _user = new(); + + [Theory, BitAutoData] + public async Task GetActive_ReturnsMappedLeases( + Guid userId, AccessLease lease, SutProvider sutProvider) + { + SetupUser(sutProvider, userId); + lease.Status = AccessLeaseStatus.Active; + sutProvider.GetDependency().GetActiveAsync(userId).Returns([lease]); + + var result = (await sutProvider.Sut.GetActive(_user)).Data.ToList(); + + Assert.Single(result); + Assert.Equal(lease.Id, result[0].Id); + Assert.Equal(AccessLeaseStatusNames.Active, result[0].Status); + } + + [Theory, BitAutoData] + public async Task GetActive_NoLeases_ReturnsEmpty( + Guid userId, SutProvider sutProvider) + { + SetupUser(sutProvider, userId); + sutProvider.GetDependency().GetActiveAsync(userId).Returns([]); + + var result = await sutProvider.Sut.GetActive(_user); + + Assert.Empty(result.Data); + } + + [Theory, BitAutoData] + public async Task GetHistory_ReturnsMappedLeases( + Guid userId, AccessLease lease, SutProvider sutProvider) + { + SetupUser(sutProvider, userId); + lease.Status = AccessLeaseStatus.Revoked; + sutProvider.GetDependency().GetHistoryAsync(userId).Returns([lease]); + + var result = (await sutProvider.Sut.GetHistory(_user)).Data.ToList(); + + Assert.Single(result); + Assert.Equal(lease.Id, result[0].Id); + Assert.Equal(AccessLeaseStatusNames.Revoked, result[0].Status); + } + + [Theory, BitAutoData] + public async Task GetMine_ReturnsMappedLeases( + Guid userId, AccessLease lease, SutProvider sutProvider) + { + SetupUser(sutProvider, userId); + lease.Status = AccessLeaseStatus.Active; + sutProvider.GetDependency().GetMineActiveAsync(userId).Returns([lease]); + + var result = (await sutProvider.Sut.GetMine(_user)).Data.ToList(); + + Assert.Single(result); + Assert.Equal(lease.Id, result[0].Id); + Assert.Equal(AccessLeaseStatusNames.Active, result[0].Status); + } + + [Theory, BitAutoData] + public async Task Revoke_InvokesRevokeCommand( + Guid userId, Guid leaseId, SutProvider sutProvider) + { + SetupUser(sutProvider, userId); + + await sutProvider.Sut.Revoke(_user, leaseId, new AccessLeaseRevokeRequestModel { Reason = "policy" }); + + await sutProvider.GetDependency().Received(1).RevokeAsync(userId, leaseId, "policy"); + } + + [Theory, BitAutoData] + public async Task Extend_ForwardsRouteLeaseId_ReturnsApprovedExtensionDetails( + Guid userId, Guid leaseId, AccessLeaseExtensionRequestModel model, AccessRequestDetails details, + SutProvider sutProvider) + { + SetupUser(sutProvider, userId); + details.Status = AccessRequestStatus.Approved; + details.ProducedLeaseId = null; // an extension produces no lease of its own, so the status stays "approved" + sutProvider.GetDependency() + .ExtendAsync(userId, Arg.Any()) + .Returns(details); + + var result = await sutProvider.Sut.Extend(_user, leaseId, model); + + Assert.Equal(details.Id, result.Id); + Assert.Equal(AccessRequestStatusNames.Approved, result.Status); + Assert.Equal(details.ExtensionOfLeaseId, result.ExtensionOfLeaseId); + await sutProvider.GetDependency().Received(1).ExtendAsync( + userId, + Arg.Is(s => + s.LeaseId == leaseId && s.DurationSeconds == model.DurationSeconds && s.Reason == model.Reason)); + } + + private static void SetupUser(SutProvider sutProvider, Guid userId) + { + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(userId); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Api/Models/AccessDecisionRequestModelTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Api/Models/AccessDecisionRequestModelTests.cs new file mode 100644 index 000000000000..d21f371a1517 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Api/Models/AccessDecisionRequestModelTests.cs @@ -0,0 +1,66 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using Bit.Commercial.Pam.Api.Models.Request; +using Bit.Pam.Enums; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Api.Models; + +public class AccessDecisionRequestModelTests +{ + // Mirror the API's web JSON defaults (camelCase, case-insensitive) so the test exercises the real bind path. + private static readonly JsonSerializerOptions _web = new(JsonSerializerDefaults.Web); + + [Theory] + [InlineData(0, AccessDecisionVerdict.Deny)] + [InlineData(1, AccessDecisionVerdict.Approve)] + public void Deserialize_BindsIntegerVerdictToEnum(int wire, AccessDecisionVerdict expected) + { + var model = JsonSerializer.Deserialize($$"""{"verdict":{{wire}}}""", _web); + + Assert.NotNull(model); + Assert.Equal(expected, model!.Verdict); + Assert.Equal(expected, model.ToSubmission().Verdict); + } + + [Theory] + [InlineData("\"approve\"")] + [InlineData("\"1\"")] + public void Deserialize_StringVerdict_Throws(string wire) + { + // The wire format is the integer enum value; a string is no longer accepted. + Assert.Throws( + () => JsonSerializer.Deserialize($$"""{"verdict":{{wire}}}""", _web)); + } + + [Fact] + public void Validate_MissingVerdict_IsInvalid() + { + Assert.Contains( + Validate(new AccessDecisionRequestModel { Verdict = null }), + r => r.MemberNames.Contains(nameof(AccessDecisionRequestModel.Verdict))); + } + + [Fact] + public void Validate_UndefinedVerdict_IsInvalid() + { + Assert.Contains( + Validate(new AccessDecisionRequestModel { Verdict = (AccessDecisionVerdict)5 }), + r => r.MemberNames.Contains(nameof(AccessDecisionRequestModel.Verdict))); + } + + [Theory] + [InlineData(AccessDecisionVerdict.Approve)] + [InlineData(AccessDecisionVerdict.Deny)] + public void Validate_DefinedVerdict_IsValid(AccessDecisionVerdict verdict) + { + Assert.Empty(Validate(new AccessDecisionRequestModel { Verdict = verdict })); + } + + private static List Validate(AccessDecisionRequestModel model) + { + var results = new List(); + Validator.TryValidateObject(model, new ValidationContext(model), results, validateAllProperties: true); + return results; + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Api/Models/AccessLeaseResponseModelTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Api/Models/AccessLeaseResponseModelTests.cs new file mode 100644 index 000000000000..aeb27124278b --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Api/Models/AccessLeaseResponseModelTests.cs @@ -0,0 +1,71 @@ +using Bit.Commercial.Pam.Api.Models.Response; +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Api.Models; + +public class AccessLeaseResponseModelTests +{ + [Theory, BitAutoData] + public void Ctor_MapsLeaseToClientShape(AccessLease lease) + { + lease.Status = AccessLeaseStatus.Active; + + var model = new AccessLeaseResponseModel(lease); + + Assert.Equal(lease.Id, model.Id); + Assert.Equal(lease.AccessRequestId, model.RequestId); + Assert.Equal(lease.CipherId, model.CipherId); + Assert.Equal(lease.CollectionId, model.CollectionId); + Assert.Equal(lease.OrganizationId, model.OrganizationId); + Assert.Equal(lease.RequesterId, model.RequesterId); + Assert.Equal(AccessLeaseStatusNames.Active, model.Status); + Assert.Equal(lease.NotBefore, model.NotBefore); + Assert.Equal(lease.NotAfter, model.NotAfter); + Assert.Equal(lease.RevokedDate, model.RevokedAt); + Assert.Equal(lease.RevokedBy, model.RevokedByUserId); + Assert.Null(model.RuleId); + Assert.Null(model.RevocationReason); + } + + [Fact] + public void Ctor_MarksTimestampsAsUtc() + { + // Dapper materialises stored UTC instants with Kind=Unspecified; the model must relabel them UTC so the + // serialised JSON carries a 'Z' and clients don't reparse them as local time. + var unspecified = new DateTime(2026, 6, 15, 13, 0, 0, DateTimeKind.Unspecified); + var lease = new AccessLease + { + Status = AccessLeaseStatus.Active, + NotBefore = unspecified, + NotAfter = unspecified.AddHours(1), + RevokedDate = unspecified.AddMinutes(30), + }; + + var model = new AccessLeaseResponseModel(lease); + + Assert.Equal(DateTimeKind.Utc, model.NotBefore.Kind); + Assert.Equal(DateTimeKind.Utc, model.NotAfter.Kind); + Assert.Equal(DateTimeKind.Utc, model.RevokedAt!.Value.Kind); + // SpecifyKind relabels without shifting the wall clock. + Assert.Equal(unspecified.Ticks, model.NotBefore.Ticks); + } + + [Fact] + public void Ctor_LeavesNullRevokedDateNull() + { + var lease = new AccessLease + { + Status = AccessLeaseStatus.Active, + NotBefore = new DateTime(2026, 6, 15, 13, 0, 0, DateTimeKind.Unspecified), + NotAfter = new DateTime(2026, 6, 15, 14, 0, 0, DateTimeKind.Unspecified), + RevokedDate = null, + }; + + var model = new AccessLeaseResponseModel(lease); + + Assert.Null(model.RevokedAt); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Api/Models/AccessLeaseStatusNamesTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Api/Models/AccessLeaseStatusNamesTests.cs new file mode 100644 index 000000000000..4d7d62acb799 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Api/Models/AccessLeaseStatusNamesTests.cs @@ -0,0 +1,17 @@ +using Bit.Commercial.Pam.Api.Models.Response; +using Bit.Pam.Enums; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Api.Models; + +public class AccessLeaseStatusNamesTests +{ + [Theory] + [InlineData(AccessLeaseStatus.Active, AccessLeaseStatusNames.Active)] + [InlineData(AccessLeaseStatus.Expired, AccessLeaseStatusNames.Expired)] + [InlineData(AccessLeaseStatus.Revoked, AccessLeaseStatusNames.Revoked)] + public void From_MapsToFrontendVocabulary(AccessLeaseStatus status, string expected) + { + Assert.Equal(expected, AccessLeaseStatusNames.From(status)); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Api/Models/AccessRequestDetailsResponseModelTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Api/Models/AccessRequestDetailsResponseModelTests.cs new file mode 100644 index 000000000000..387c2eee7a17 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Api/Models/AccessRequestDetailsResponseModelTests.cs @@ -0,0 +1,149 @@ +using Bit.Commercial.Pam.Api.Models.Response; +using Bit.Pam.Enums; +using Bit.Pam.Models; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Api.Models; + +public class AccessRequestDetailsResponseModelTests +{ + [Fact] + public void Ctor_MarksTimestampsAsUtc() + { + // Regression guard: the approver inbox drops a request whose requested window has lapsed. When the stored + // UTC instants are serialised without a 'Z' (Kind=Unspecified), a client east of UTC reparses them as local + // time and the shift hides still-valid requests. The model must relabel the kind as UTC. + var unspecified = new DateTime(2026, 6, 15, 13, 0, 0, DateTimeKind.Unspecified); + var details = new AccessRequestDetails + { + Status = AccessRequestStatus.Pending, + NotBefore = unspecified, + NotAfter = unspecified.AddHours(1), + CreationDate = unspecified.AddMinutes(-5), + ResolvedDate = unspecified.AddMinutes(10), + }; + + var model = new AccessRequestDetailsResponseModel(details); + + Assert.Equal(DateTimeKind.Utc, model.RequestedNotBefore.Kind); + Assert.Equal(DateTimeKind.Utc, model.RequestedNotAfter.Kind); + Assert.Equal(DateTimeKind.Utc, model.SubmittedAt.Kind); + Assert.Equal(DateTimeKind.Utc, model.ResolvedAt!.Value.Kind); + // SpecifyKind relabels without shifting the wall clock. + Assert.Equal(unspecified.Ticks, model.RequestedNotBefore.Ticks); + } + + [Fact] + public void Ctor_LeavesNullResolvedDateNull() + { + var unspecified = new DateTime(2026, 6, 15, 13, 0, 0, DateTimeKind.Unspecified); + var details = new AccessRequestDetails + { + Status = AccessRequestStatus.Pending, + NotBefore = unspecified, + NotAfter = unspecified.AddHours(1), + CreationDate = unspecified, + ResolvedDate = null, + }; + + var model = new AccessRequestDetailsResponseModel(details); + + Assert.Null(model.ResolvedAt); + } + + [Fact] + public void Ctor_MapsHumanApproverAsSingleApproversElement() + { + // The requester's own request list names who decided the request; the resolved approver identity, verdict, and + // comment must flow through as a single Approvers element rather than being dropped (which would leave the + // client showing a raw id). The array shape future-proofs the contract for multi-party approval. + var approverId = Guid.NewGuid(); + var unspecified = new DateTime(2026, 6, 15, 13, 0, 0, DateTimeKind.Unspecified); + var decidedAt = unspecified.AddMinutes(20); + var details = new AccessRequestDetails + { + Status = AccessRequestStatus.Denied, + NotBefore = unspecified, + NotAfter = unspecified.AddHours(1), + CreationDate = unspecified, + ResolvedDate = decidedAt, + Decisions = + [ + new AccessRequestDecision + { + DeciderKind = AccessDeciderKind.Human, + Id = approverId, + Name = "Ada Approver", + Email = "ada@example.com", + Comment = "Outside approved hours", + Verdict = AccessDecisionVerdict.Deny, + DecidedAt = decidedAt, + }, + ], + }; + + var model = new AccessRequestDetailsResponseModel(details); + + var decision = Assert.Single(model.Decisions); + Assert.Equal(AccessDeciderKindNames.Human, decision.DeciderKind); + Assert.Equal(approverId, decision.Id!.Value); + Assert.Equal("Ada Approver", decision.Name); + Assert.Equal("ada@example.com", decision.Email); + Assert.Equal("Outside approved hours", decision.Comment); + Assert.Equal(AccessDecisionVerdict.Deny, decision.Verdict); + Assert.Equal(decidedAt.Ticks, decision.DecidedAt.Ticks); + Assert.Equal(DateTimeKind.Utc, decision.DecidedAt.Kind); + } + + [Fact] + public void Ctor_MapsAutomaticDecisionWithNoApproverIdentity() + { + // An automatic (access-rule) decision is surfaced like any other, but with deciderKind "automatic" and no + // approver identity — the client renders it as a rule-driven decision rather than a person. + var unspecified = new DateTime(2026, 6, 15, 13, 0, 0, DateTimeKind.Unspecified); + var details = new AccessRequestDetails + { + Status = AccessRequestStatus.Approved, + NotBefore = unspecified, + NotAfter = unspecified.AddHours(1), + CreationDate = unspecified, + ResolvedDate = unspecified.AddMinutes(1), + Decisions = + [ + new AccessRequestDecision + { + DeciderKind = AccessDeciderKind.Automatic, + Id = null, + Verdict = AccessDecisionVerdict.Approve, + DecidedAt = unspecified.AddMinutes(1), + }, + ], + }; + + var model = new AccessRequestDetailsResponseModel(details); + + var decision = Assert.Single(model.Decisions); + Assert.Equal(AccessDeciderKindNames.Automatic, decision.DeciderKind); + Assert.Null(decision.Id); + Assert.Null(decision.Name); + Assert.Equal(AccessDecisionVerdict.Approve, decision.Verdict); + } + + [Fact] + public void Ctor_LeavesDecisionsEmptyWhenPending() + { + // A pending request has no decision recorded yet, so the decision log is empty. + var unspecified = new DateTime(2026, 6, 15, 13, 0, 0, DateTimeKind.Unspecified); + var details = new AccessRequestDetails + { + Status = AccessRequestStatus.Pending, + NotBefore = unspecified, + NotAfter = unspecified.AddHours(1), + CreationDate = unspecified, + }; + + var model = new AccessRequestDetailsResponseModel(details); + + Assert.Empty(model.Decisions); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Api/Models/AccessRequestStatusNamesTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Api/Models/AccessRequestStatusNamesTests.cs new file mode 100644 index 000000000000..aa0472e07bdb --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Api/Models/AccessRequestStatusNamesTests.cs @@ -0,0 +1,20 @@ +using Bit.Commercial.Pam.Api.Models.Response; +using Bit.Pam.Enums; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Api.Models; + +public class AccessRequestStatusNamesTests +{ + [Theory] + [InlineData(AccessRequestStatus.Pending, false, AccessRequestStatusNames.Pending)] + [InlineData(AccessRequestStatus.Approved, false, AccessRequestStatusNames.Approved)] + [InlineData(AccessRequestStatus.Approved, true, AccessRequestStatusNames.Activated)] + [InlineData(AccessRequestStatus.Denied, false, AccessRequestStatusNames.Denied)] + [InlineData(AccessRequestStatus.Cancelled, false, AccessRequestStatusNames.Cancelled)] + [InlineData(AccessRequestStatus.ExpiredUnanswered, false, AccessRequestStatusNames.Expired)] + public void From_MapsToFrontendVocabulary(AccessRequestStatus status, bool hasLease, string expected) + { + Assert.Equal(expected, AccessRequestStatusNames.From(status, hasLease)); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Commands/ActivateAccessRequestCommandTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Commands/ActivateAccessRequestCommandTests.cs new file mode 100644 index 000000000000..8926c988d776 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Commands/ActivateAccessRequestCommandTests.cs @@ -0,0 +1,273 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Commands; +using Bit.Commercial.Pam.Services; +using Bit.Core.Exceptions; +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Pam.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Commands; + +[SutProviderCustomize] +public class ActivateAccessRequestCommandTests +{ + private static readonly DateTime _now = new(2026, 6, 10, 12, 0, 0, DateTimeKind.Utc); + + [Theory, BitAutoData] + public async Task ActivateAsync_RequestMissing_ThrowsNotFound(Guid userId, Guid requestId) + { + var sutProvider = Setup(); + sutProvider.GetDependency().GetByIdAsync(requestId).Returns((AccessRequest?)null); + + await Assert.ThrowsAsync(() => sutProvider.Sut.ActivateAsync(userId, requestId)); + } + + [Theory, BitAutoData] + public async Task ActivateAsync_NotOwner_ThrowsNotFound(Guid userId, AccessRequest request) + { + var sutProvider = Setup(); + SetupApprovedRequest(sutProvider, request); + + // Someone else's request is indistinguishable from a missing one, so ids can't be probed. + await Assert.ThrowsAsync(() => sutProvider.Sut.ActivateAsync(userId, request.Id)); + } + + [Theory] + [BitAutoData(AccessRequestStatus.Pending)] + [BitAutoData(AccessRequestStatus.Denied)] + [BitAutoData(AccessRequestStatus.Cancelled)] + [BitAutoData(AccessRequestStatus.ExpiredUnanswered)] + public async Task ActivateAsync_NotApproved_ThrowsConflict(AccessRequestStatus status, AccessRequest request) + { + var sutProvider = Setup(); + SetupApprovedRequest(sutProvider, request); + request.Status = status; + + await Assert.ThrowsAsync( + () => sutProvider.Sut.ActivateAsync(request.RequesterId, request.Id)); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateFromApprovedRequestAsync(default!, default, default); + } + + [Theory, BitAutoData] + public async Task ActivateAsync_AlreadyActivated_LiveLease_ReturnsExistingWithoutMinting( + AccessRequest request, AccessLease existing) + { + var sutProvider = Setup(); + SetupApprovedRequest(sutProvider, request); + existing.Status = AccessLeaseStatus.Active; + existing.NotAfter = _now.AddMinutes(30); + sutProvider.GetDependency().GetByAccessRequestIdAsync(request.Id).Returns(existing); + + var result = await sutProvider.Sut.ActivateAsync(request.RequesterId, request.Id); + + Assert.Same(existing, result); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateFromApprovedRequestAsync(default!, default, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .NotifyCollectionApproversAsync(default); + } + + [Theory] + [BitAutoData(AccessLeaseStatus.Revoked)] + [BitAutoData(AccessLeaseStatus.Expired)] + public async Task ActivateAsync_AlreadyActivated_DeadLease_ThrowsConflict( + AccessLeaseStatus leaseStatus, AccessRequest request, AccessLease existing) + { + var sutProvider = Setup(); + SetupApprovedRequest(sutProvider, request); + existing.Status = leaseStatus; + sutProvider.GetDependency().GetByAccessRequestIdAsync(request.Id).Returns(existing); + + // A request authorizes access at most once; a revoked or lapsed lease is final. + await Assert.ThrowsAsync( + () => sutProvider.Sut.ActivateAsync(request.RequesterId, request.Id)); + } + + [Theory, BitAutoData] + public async Task ActivateAsync_AlreadyActivated_ActiveButLapsedLease_ThrowsConflict( + AccessRequest request, AccessLease existing) + { + var sutProvider = Setup(); + SetupApprovedRequest(sutProvider, request); + existing.Status = AccessLeaseStatus.Active; + existing.NotAfter = _now.AddMinutes(-1); + sutProvider.GetDependency().GetByAccessRequestIdAsync(request.Id).Returns(existing); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.ActivateAsync(request.RequesterId, request.Id)); + } + + [Theory, BitAutoData] + public async Task ActivateAsync_WindowNotStarted_ThrowsBadRequest(AccessRequest request) + { + var sutProvider = Setup(); + SetupApprovedRequest(sutProvider, request); + request.NotBefore = _now.AddHours(1); + request.NotAfter = _now.AddHours(2); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.ActivateAsync(request.RequesterId, request.Id)); + Assert.Contains("not started", ex.Message); + } + + [Theory, BitAutoData] + public async Task ActivateAsync_WindowEnded_ThrowsBadRequest(AccessRequest request) + { + var sutProvider = Setup(); + SetupApprovedRequest(sutProvider, request); + request.NotBefore = _now.AddHours(-2); + request.NotAfter = _now.AddHours(-1); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.ActivateAsync(request.RequesterId, request.Id)); + Assert.Contains("already ended", ex.Message); + } + + [Theory, BitAutoData] + public async Task ActivateAsync_Approved_MintsLeaseSpanningRequestWindow(AccessRequest request) + { + var sutProvider = Setup(); + SetupApprovedRequest(sutProvider, request); + sutProvider.GetDependency() + .CreateFromApprovedRequestAsync(Arg.Any(), _now, Arg.Any()) + .Returns(AccessLeaseMintOutcome.Minted); + + var result = await sutProvider.Sut.ActivateAsync(request.RequesterId, request.Id); + + Assert.Equal(request.Id, result.AccessRequestId); + Assert.Equal(request.OrganizationId, result.OrganizationId); + Assert.Equal(request.CollectionId, result.CollectionId); + Assert.Equal(request.CipherId, result.CipherId); + Assert.Equal(request.RequesterId, result.RequesterId); + Assert.Equal(AccessLeaseStatus.Active, result.Status); + // Activation mints the window the approver approved, not a window anchored at activation time. + Assert.Equal(request.NotBefore, result.NotBefore); + Assert.Equal(request.NotAfter, result.NotAfter); + Assert.Equal(_now, result.CreationDate); + Assert.NotEqual(default, result.Id); + await sutProvider.GetDependency().Received(1) + .CreateFromApprovedRequestAsync(result, _now, Arg.Any()); + await sutProvider.GetDependency().Received(1) + .NotifyCollectionApproversAsync(request.CollectionId); + await sutProvider.GetDependency().Received(1) + .NotifyRequesterAsync(request.RequesterId); + } + + [Theory, BitAutoData] + public async Task ActivateAsync_LostRace_WinnerLive_ReturnsWinner(AccessRequest request, AccessLease winner) + { + var sutProvider = Setup(); + SetupApprovedRequest(sutProvider, request); + winner.Status = AccessLeaseStatus.Active; + winner.NotAfter = _now.AddMinutes(30); + sutProvider.GetDependency() + .CreateFromApprovedRequestAsync(Arg.Any(), _now, Arg.Any()) + .Returns(AccessLeaseMintOutcome.PreconditionFailed); + sutProvider.GetDependency().GetByAccessRequestIdAsync(request.Id) + .Returns((AccessLease?)null, winner); + + var result = await sutProvider.Sut.ActivateAsync(request.RequesterId, request.Id); + + Assert.Same(winner, result); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .NotifyCollectionApproversAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .NotifyRequesterAsync(default); + } + + [Theory, BitAutoData] + public async Task ActivateAsync_LostRace_NoLiveLease_ThrowsConflict(AccessRequest request) + { + var sutProvider = Setup(); + SetupApprovedRequest(sutProvider, request); + sutProvider.GetDependency() + .CreateFromApprovedRequestAsync(Arg.Any(), _now, Arg.Any()) + .Returns(AccessLeaseMintOutcome.PreconditionFailed); + sutProvider.GetDependency().GetByAccessRequestIdAsync(request.Id) + .Returns((AccessLease?)null); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.ActivateAsync(request.RequesterId, request.Id)); + } + + [Theory, BitAutoData] + public async Task ActivateAsync_SingleActiveLeaseApplies_PassesEnforceTrue_AndMints(AccessRequest request) + { + var sutProvider = Setup(); + SetupApprovedRequest(sutProvider, request); + // The constraint binds for this caller and cipher: enforcement must be passed through to the mint. + sutProvider.GetDependency().AppliesAsync(request.RequesterId, request.CipherId) + .Returns(true); + sutProvider.GetDependency() + .CreateFromApprovedRequestAsync(Arg.Any(), _now, true) + .Returns(AccessLeaseMintOutcome.Minted); + + var result = await sutProvider.Sut.ActivateAsync(request.RequesterId, request.Id); + + Assert.Equal(AccessLeaseStatus.Active, result.Status); + await sutProvider.GetDependency().Received(1) + .CreateFromApprovedRequestAsync(result, _now, true); + } + + [Theory, BitAutoData] + public async Task ActivateAsync_SingleActiveLeaseConflict_ThrowsConflict(AccessRequest request) + { + var sutProvider = Setup(); + SetupApprovedRequest(sutProvider, request); + sutProvider.GetDependency().AppliesAsync(request.RequesterId, request.CipherId) + .Returns(true); + sutProvider.GetDependency() + .CreateFromApprovedRequestAsync(Arg.Any(), _now, true) + .Returns(AccessLeaseMintOutcome.SingleActiveLeaseConflict); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.ActivateAsync(request.RequesterId, request.Id)); + Assert.Contains("Another active lease exists", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .NotifyCollectionApproversAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .NotifyRequesterAsync(default); + } + + [Theory, BitAutoData] + public async Task ActivateAsync_EscapePathExists_PassesEnforceFalse(AccessRequest request) + { + var sutProvider = Setup(); + SetupApprovedRequest(sutProvider, request); + // An escape path leaves the caller unconstrained, so enforcement must be passed as false. + sutProvider.GetDependency().AppliesAsync(request.RequesterId, request.CipherId) + .Returns(false); + sutProvider.GetDependency() + .CreateFromApprovedRequestAsync(Arg.Any(), _now, false) + .Returns(AccessLeaseMintOutcome.Minted); + + await sutProvider.Sut.ActivateAsync(request.RequesterId, request.Id); + + await sutProvider.GetDependency().Received(1) + .CreateFromApprovedRequestAsync(Arg.Any(), _now, false); + } + + private static SutProvider Setup() + { + var sutProvider = new SutProvider().WithFakeTimeProvider().Create(); + sutProvider.GetDependency().SetUtcNow(_now); + return sutProvider; + } + + // An approved request owned by its BitAutoData requester, with an open window containing _now and no produced + // lease. Tests override the specific precondition they exercise. + private static void SetupApprovedRequest(SutProvider sutProvider, AccessRequest request) + { + request.Status = AccessRequestStatus.Approved; + request.NotBefore = _now.AddMinutes(-5); + request.NotAfter = _now.AddHours(1); + sutProvider.GetDependency().GetByIdAsync(request.Id).Returns(request); + sutProvider.GetDependency().GetByAccessRequestIdAsync(request.Id) + .Returns((AccessLease?)null); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Commands/CancelAccessRequestCommandTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Commands/CancelAccessRequestCommandTests.cs new file mode 100644 index 000000000000..46aec34d9eb5 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Commands/CancelAccessRequestCommandTests.cs @@ -0,0 +1,157 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Commands; +using Bit.Commercial.Pam.Services; +using Bit.Core.Exceptions; +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Pam.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Commands; + +[SutProviderCustomize] +public class CancelAccessRequestCommandTests +{ + private static readonly DateTime _now = new(2026, 6, 11, 12, 0, 0, DateTimeKind.Utc); + + [Theory, BitAutoData] + public async Task CancelAsync_RequestMissing_ThrowsNotFound(Guid userId, Guid requestId) + { + var sutProvider = Setup(); + sutProvider.GetDependency().GetByIdAsync(requestId).Returns((AccessRequest?)null); + + await Assert.ThrowsAsync(() => sutProvider.Sut.CancelAsync(userId, requestId)); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CancelAsync(default, default); + } + + [Theory, BitAutoData] + public async Task CancelAsync_NeitherRequesterNorManager_ThrowsNotFound(Guid userId, AccessRequest request) + { + var sutProvider = Setup(); + request.Status = AccessRequestStatus.Pending; + sutProvider.GetDependency().GetByIdAsync(request.Id).Returns(request); + // userId is neither the requester nor a manager (CanManageCollectionAsync defaults to false). + + // A request the caller can't act on is indistinguishable from a missing one, so ids can't be probed. + await Assert.ThrowsAsync(() => sutProvider.Sut.CancelAsync(userId, request.Id)); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CancelAsync(default, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CancelWithDecisionAsync(default!, default!, default); + } + + [Theory] + [BitAutoData(AccessRequestStatus.Denied)] + [BitAutoData(AccessRequestStatus.Cancelled)] + [BitAutoData(AccessRequestStatus.ExpiredUnanswered)] + public async Task CancelAsync_TerminalStatus_ThrowsConflict(AccessRequestStatus status, AccessRequest request) + { + var sutProvider = Setup(); + request.Status = status; + sutProvider.GetDependency().GetByIdAsync(request.Id).Returns(request); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.CancelAsync(request.RequesterId, request.Id)); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CancelAsync(default, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .NotifyCollectionApproversAsync(default); + } + + [Theory] + [BitAutoData(AccessRequestStatus.Pending)] + [BitAutoData(AccessRequestStatus.Approved)] + public async Task CancelAsync_RequesterNoLease_CancelsAndNotifies(AccessRequestStatus status, AccessRequest request) + { + var sutProvider = Setup(); + request.Status = status; + sutProvider.GetDependency().GetByIdAsync(request.Id).Returns(request); + // No lease produced (GetByAccessRequestIdAsync defaults to null). + + await sutProvider.Sut.CancelAsync(request.RequesterId, request.Id); + + await sutProvider.GetDependency().Received(1).CancelAsync(request.Id, _now); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CancelWithDecisionAsync(default!, default!, default); + await sutProvider.GetDependency().Received(1) + .NotifyCollectionApproversAsync(request.CollectionId); + await sutProvider.GetDependency().Received(1) + .NotifyRequesterAsync(request.RequesterId); + } + + [Theory] + [BitAutoData(AccessRequestStatus.Pending)] + [BitAutoData(AccessRequestStatus.Approved)] + public async Task CancelAsync_ManagerNoLease_DeniesWithDecisionAndNotifies( + AccessRequestStatus status, Guid managerId, AccessRequest request) + { + var sutProvider = Setup(); + request.Status = status; + sutProvider.GetDependency().GetByIdAsync(request.Id).Returns(request); + sutProvider.GetDependency() + .CanManageCollectionAsync(managerId, request.CollectionId).Returns(true); + + await sutProvider.Sut.CancelAsync(managerId, request.Id); + + await sutProvider.GetDependency().Received(1).CancelWithDecisionAsync( + request, + Arg.Is(d => + d.AccessRequestId == request.Id + && d.ApproverId == managerId + && d.Verdict == AccessDecisionVerdict.Deny + && d.DeciderKind == AccessDeciderKind.Human), + _now); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CancelAsync(default, default); + await sutProvider.GetDependency().Received(1) + .NotifyCollectionApproversAsync(request.CollectionId); + await sutProvider.GetDependency().Received(1) + .NotifyRequesterAsync(request.RequesterId); + } + + [Theory, BitAutoData] + public async Task CancelAsync_ApprovedWithActiveLease_ThrowsConflict(AccessRequest request, AccessLease lease) + { + var sutProvider = Setup(); + request.Status = AccessRequestStatus.Approved; + lease.Status = AccessLeaseStatus.Active; + sutProvider.GetDependency().GetByIdAsync(request.Id).Returns(request); + sutProvider.GetDependency().GetByAccessRequestIdAsync(request.Id).Returns(lease); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.CancelAsync(request.RequesterId, request.Id)); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CancelAsync(default, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CancelWithDecisionAsync(default!, default!, default); + } + + [Theory] + [BitAutoData(AccessLeaseStatus.Revoked)] + [BitAutoData(AccessLeaseStatus.Expired)] + public async Task CancelAsync_ApprovedWithEndedLease_ThrowsConflict( + AccessLeaseStatus leaseStatus, AccessRequest request, AccessLease lease) + { + var sutProvider = Setup(); + request.Status = AccessRequestStatus.Approved; + lease.Status = leaseStatus; + sutProvider.GetDependency().GetByIdAsync(request.Id).Returns(request); + sutProvider.GetDependency().GetByAccessRequestIdAsync(request.Id).Returns(lease); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.CancelAsync(request.RequesterId, request.Id)); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CancelAsync(default, default); + } + + private static SutProvider Setup() + { + var sutProvider = new SutProvider().WithFakeTimeProvider().Create(); + sutProvider.GetDependency().SetUtcNow(_now); + return sutProvider; + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Commands/CreateAccessRuleCommandTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Commands/CreateAccessRuleCommandTests.cs new file mode 100644 index 000000000000..493007caef29 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Commands/CreateAccessRuleCommandTests.cs @@ -0,0 +1,260 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Commands; +using Bit.Commercial.Pam.Services; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Commands; + +[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; + sutProvider.GetDependency() + .Validate(rule.Conditions) + .Returns(AccessRuleValidationResult.Valid); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(rule.OrganizationId) + .Returns(new List()); + 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)); + } + + [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"}"""; + collectionA.OrganizationId = rule.OrganizationId; + collectionA.AccessRuleId = null; + collectionB.OrganizationId = rule.OrganizationId; + collectionB.AccessRuleId = null; + var collectionIds = new[] { collectionA.Id, collectionB.Id }; + sutProvider.GetDependency() + .Validate(rule.Conditions) + .Returns(AccessRuleValidationResult.Valid); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(rule.OrganizationId) + .Returns(new List()); + sutProvider.GetDependency() + .CreateAsync(rule) + .Returns(rule); + sutProvider.GetDependency() + .GetManyByManyIdsAsync(Arg.Is>(ids => ids.OrderBy(x => x).SequenceEqual(collectionIds.OrderBy(x => x)))) + .Returns(new List { collectionA, collectionB }); + + await sutProvider.Sut.CreateAsync(rule, collectionIds); + + await sutProvider.GetDependency().Received(1) + .SetCollectionAssociationsAsync(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_CollectionInDifferentOrg_ThrowsBadRequest(AccessRule rule, Collection collection) + { + var sutProvider = SetupSutProvider(); + rule.Name = "test"; + rule.Conditions = """{"kind":"human_approval"}"""; + collection.OrganizationId = Guid.NewGuid(); + sutProvider.GetDependency() + .Validate(rule.Conditions) + .Returns(AccessRuleValidationResult.Valid); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(rule.OrganizationId) + .Returns(new List()); + sutProvider.GetDependency() + .GetManyByManyIdsAsync(Arg.Any>()) + .Returns(new List { collection }); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.CreateAsync(rule, new[] { collection.Id })); + Assert.Contains("do not belong to this organization", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default!); + } + + [Theory, BitAutoData] + public async Task CreateAsync_CollectionGovernedByAnotherRule_ThrowsBadRequest(AccessRule rule, Collection collection) + { + var sutProvider = SetupSutProvider(); + rule.Name = "test"; + rule.Conditions = """{"kind":"human_approval"}"""; + collection.OrganizationId = rule.OrganizationId; + collection.AccessRuleId = Guid.NewGuid(); + sutProvider.GetDependency() + .Validate(rule.Conditions) + .Returns(AccessRuleValidationResult.Valid); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(rule.OrganizationId) + .Returns(new List()); + sutProvider.GetDependency() + .GetManyByManyIdsAsync(Arg.Any>()) + .Returns(new List { collection }); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.CreateAsync(rule, new[] { collection.Id })); + Assert.Contains("already governed by another access rule", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default!); + } + + [Theory, BitAutoData] + public async Task CreateAsync_CollectionNotFound_ThrowsBadRequest(AccessRule rule, Guid missingCollectionId) + { + var sutProvider = SetupSutProvider(); + rule.Name = "test"; + rule.Conditions = """{"kind":"human_approval"}"""; + sutProvider.GetDependency() + .Validate(rule.Conditions) + .Returns(AccessRuleValidationResult.Valid); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(rule.OrganizationId) + .Returns(new List()); + sutProvider.GetDependency() + .GetManyByManyIdsAsync(Arg.Any>()) + .Returns(new List()); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.CreateAsync(rule, new[] { missingCollectionId })); + Assert.Contains("could not be found", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default!); + } + + [Theory, BitAutoData] + public async Task CreateAsync_EmptyName_ThrowsBadRequest(AccessRule rule) + { + var sutProvider = SetupSutProvider(); + rule.Name = " "; + + var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(rule, [])); + Assert.Contains("Name is required", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default!); + } + + [Theory, BitAutoData] + public async Task CreateAsync_InvalidRule_ThrowsBadRequest(AccessRule rule) + { + var sutProvider = SetupSutProvider(); + rule.Name = "test"; + rule.Conditions = """{"kind":"bogus"}"""; + sutProvider.GetDependency() + .Validate(rule.Conditions) + .Returns(AccessRuleValidationResult.Invalid("Unsupported rule kind")); + + var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(rule, [])); + Assert.Equal("Unsupported rule kind", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default!); + } + + [Theory, BitAutoData] + public async Task CreateAsync_DuplicateName_ThrowsBadRequest(AccessRule rule, AccessRule existing) + { + var sutProvider = SetupSutProvider(); + rule.Name = "duplicate"; + rule.Conditions = """{"kind":"human_approval"}"""; + existing.OrganizationId = rule.OrganizationId; + existing.Name = "Duplicate"; // case-insensitive collision + sutProvider.GetDependency() + .Validate(rule.Conditions) + .Returns(AccessRuleValidationResult.Valid); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(rule.OrganizationId) + .Returns(new List { existing }); + + var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(rule, [])); + Assert.Contains("already exists", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default!); + } + + [Theory, BitAutoData] + public async Task CreateAsync_AllowsExtensionsWithoutMax_ThrowsBadRequest(AccessRule rule) + { + var sutProvider = SetupSutProvider(); + rule.Name = "extendable"; + rule.AllowsExtensions = true; + rule.MaxExtensionDurationSeconds = null; + + var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(rule, [])); + Assert.Contains("maximum extension length", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default!); + } + + [Theory] + [BitAutoData(0)] + [BitAutoData(-1)] + public async Task CreateAsync_AllowsExtensionsWithNonPositiveMax_ThrowsBadRequest(int maxExtensionDurationSeconds, AccessRule rule) + { + var sutProvider = SetupSutProvider(); + rule.Name = "extendable"; + rule.AllowsExtensions = true; + rule.MaxExtensionDurationSeconds = maxExtensionDurationSeconds; + + var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(rule, [])); + Assert.Contains("maximum extension length", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default!); + } + + [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; + sutProvider.GetDependency() + .Validate(rule.Conditions) + .Returns(AccessRuleValidationResult.Valid); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(rule.OrganizationId) + .Returns(new List()); + 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)); + } + + private static SutProvider SetupSutProvider() + { + var sutProvider = new SutProvider() + .WithFakeTimeProvider() + .Create(); + sutProvider.GetDependency().SetUtcNow(_now); + return sutProvider; + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Commands/DecideAccessRequestCommandTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Commands/DecideAccessRequestCommandTests.cs new file mode 100644 index 000000000000..4f849b49f094 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Commands/DecideAccessRequestCommandTests.cs @@ -0,0 +1,197 @@ +using Bit.Commercial.Pam.Models; +using Bit.Commercial.Pam.OrganizationFeatures.Commands; +using Bit.Commercial.Pam.Services; +using Bit.Core.Exceptions; +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Pam.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Commands; + +[SutProviderCustomize] +public class DecideAccessRequestCommandTests +{ + private static readonly DateTime _now = new(2026, 6, 5, 12, 0, 0, DateTimeKind.Utc); + + [Theory, BitAutoData] + public async Task DecideAsync_RequestMissing_ThrowsNotFound(Guid userId, Guid requestId) + { + var sutProvider = Setup(); + sutProvider.GetDependency().GetByIdAsync(requestId).Returns((AccessRequest?)null); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.DecideAsync(userId, requestId, Approve())); + } + + [Theory, BitAutoData] + public async Task DecideAsync_NotManageable_ThrowsNotFound(Guid userId, AccessRequest request) + { + var sutProvider = Setup(); + request.Status = AccessRequestStatus.Pending; + sutProvider.GetDependency().GetByIdAsync(request.Id).Returns(request); + sutProvider.GetDependency() + .CanManageCollectionAsync(userId, request.CollectionId).Returns(false); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.DecideAsync(userId, request.Id, Approve())); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .NotifyCollectionApproversAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .NotifyRequesterAsync(default); + } + + [Theory, BitAutoData] + public async Task DecideAsync_NotPending_ThrowsConflict(Guid userId, AccessRequest request) + { + var sutProvider = Setup(); + request.Status = AccessRequestStatus.Approved; + SetupManageableRequest(sutProvider, userId, request); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.DecideAsync(userId, request.Id, Approve())); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .NotifyCollectionApproversAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .NotifyRequesterAsync(default); + } + + [Theory, BitAutoData] + public async Task DecideAsync_SelfApproval_ThrowsBadRequest(Guid userId, AccessRequest request) + { + var sutProvider = Setup(); + request.Status = AccessRequestStatus.Pending; + request.RequesterId = userId; + SetupManageableRequest(sutProvider, userId, request); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.DecideAsync(userId, request.Id, Approve())); + Assert.Contains("your own request", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ResolveWithDecisionAsync(default!, default!, default, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .NotifyRequesterAsync(default); + } + + [Theory, BitAutoData] + public async Task DecideAsync_Approve_WindowAlreadyEnded_ThrowsBadRequest(Guid userId, AccessRequest request) + { + var sutProvider = Setup(); + request.Status = AccessRequestStatus.Pending; + request.NotBefore = _now.AddHours(-2); + request.NotAfter = _now.AddHours(-1); + SetupManageableRequest(sutProvider, userId, request); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.DecideAsync(userId, request.Id, Approve())); + Assert.Contains("already ended", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ResolveWithDecisionAsync(default!, default!, default, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .NotifyCollectionApproversAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .NotifyRequesterAsync(default); + } + + [Theory, BitAutoData] + public async Task DecideAsync_Deny_WindowAlreadyEnded_Succeeds(Guid userId, AccessRequest request) + { + var sutProvider = Setup(); + request.Status = AccessRequestStatus.Pending; + request.NotBefore = _now.AddHours(-2); + request.NotAfter = _now.AddHours(-1); + SetupManageableRequest(sutProvider, userId, request); + + // A lapsed window only blocks approval (it could never be activated); denial still closes the request out. + var result = await sutProvider.Sut.DecideAsync(userId, request.Id, Deny()); + + Assert.Equal(AccessRequestStatus.Denied, result.Status); + } + + [Theory, BitAutoData] + public async Task DecideAsync_Approve_ResolvesAndWritesHumanDecision(Guid userId, AccessRequest request) + { + var sutProvider = Setup(); + request.Status = AccessRequestStatus.Pending; + SetOpenWindow(request); + SetupManageableRequest(sutProvider, userId, request); + + var result = await sutProvider.Sut.DecideAsync(userId, request.Id, Approve("looks good")); + + Assert.Equal(AccessRequestStatus.Approved, result.Status); + Assert.Equal(_now, result.ResolvedDate); + var decision = Assert.Single(result.Decisions); + Assert.Equal(AccessDeciderKind.Human, decision.DeciderKind); + Assert.Equal(userId, decision.Id!.Value); + Assert.Equal(AccessDecisionVerdict.Approve, decision.Verdict); + Assert.Equal("looks good", decision.Comment); + Assert.Equal(_now, decision.DecidedAt); + // Approval records the verdict only; no lease is minted until the requester activates the approved request. + await sutProvider.GetDependency().Received(1).ResolveWithDecisionAsync( + request, + Arg.Is(d => + d.DeciderKind == AccessDeciderKind.Human && + d.ApproverId == userId && + d.Verdict == AccessDecisionVerdict.Approve && + d.Comment == "looks good"), + AccessRequestStatus.Approved, + _now); + await sutProvider.GetDependency().Received(1) + .NotifyCollectionApproversAsync(request.CollectionId); + await sutProvider.GetDependency().Received(1) + .NotifyRequesterAsync(request.RequesterId); + } + + [Theory, BitAutoData] + public async Task DecideAsync_Deny_ResolvesAsDenied(Guid userId, AccessRequest request) + { + var sutProvider = Setup(); + request.Status = AccessRequestStatus.Pending; + SetOpenWindow(request); + SetupManageableRequest(sutProvider, userId, request); + + var result = await sutProvider.Sut.DecideAsync(userId, request.Id, Deny()); + + Assert.Equal(AccessRequestStatus.Denied, result.Status); + await sutProvider.GetDependency().Received(1).ResolveWithDecisionAsync( + request, + Arg.Is(d => d.Verdict == AccessDecisionVerdict.Deny), + AccessRequestStatus.Denied, + _now); + // A denial reaches the requester too (their "My requests" view flips to denied). + await sutProvider.GetDependency().Received(1) + .NotifyRequesterAsync(request.RequesterId); + } + + private static AccessDecisionSubmission Approve(string? comment = null) => + new() { Verdict = AccessDecisionVerdict.Approve, Comment = comment }; + + private static AccessDecisionSubmission Deny(string? comment = null) => + new() { Verdict = AccessDecisionVerdict.Deny, Comment = comment }; + + private static SutProvider Setup() + { + var sutProvider = new SutProvider().WithFakeTimeProvider().Create(); + sutProvider.GetDependency().SetUtcNow(_now); + return sutProvider; + } + + private static void SetupManageableRequest(SutProvider sutProvider, Guid userId, AccessRequest request) + { + sutProvider.GetDependency().GetByIdAsync(request.Id).Returns(request); + sutProvider.GetDependency() + .CanManageCollectionAsync(userId, request.CollectionId).Returns(true); + } + + // BitAutoData generates arbitrary dates; pin a window containing _now so the lapsed-window approve guard + // doesn't trip in tests that aren't about it. + private static void SetOpenWindow(AccessRequest request) + { + request.NotBefore = _now.AddMinutes(-5); + request.NotAfter = _now.AddHours(1); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Commands/DeleteAccessRuleCommandTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Commands/DeleteAccessRuleCommandTests.cs new file mode 100644 index 000000000000..248c1279787c --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Commands/DeleteAccessRuleCommandTests.cs @@ -0,0 +1,53 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Commands; +using Bit.Core.Exceptions; +using Bit.Pam.Entities; +using Bit.Pam.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Commands; + +[SutProviderCustomize] +public class DeleteAccessRuleCommandTests +{ + [Theory, BitAutoData] + public async Task DeleteAsync_HappyPath_Deletes( + 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/Commercial.Pam.Test/Commands/RequestLeaseExtensionCommandTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Commands/RequestLeaseExtensionCommandTests.cs new file mode 100644 index 000000000000..bbd0d29ae113 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Commands/RequestLeaseExtensionCommandTests.cs @@ -0,0 +1,256 @@ +using Bit.Commercial.Pam.Engine; +using Bit.Commercial.Pam.Models; +using Bit.Commercial.Pam.Models.Conditions; +using Bit.Commercial.Pam.OrganizationFeatures.Commands; +using Bit.Commercial.Pam.Services; +using Bit.Core.Exceptions; +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Pam.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Commands; + +[SutProviderCustomize] +public class RequestLeaseExtensionCommandTests +{ + private static readonly DateTime _now = new(2026, 6, 12, 12, 0, 0, DateTimeKind.Utc); + private const int _maxExtensionDurationSeconds = 4 * 60 * 60; + + [Theory, BitAutoData] + public async Task ExtendAsync_LeaseMissing_ThrowsNotFound(Guid userId, Guid leaseId) + { + var sutProvider = Setup(); + sutProvider.GetDependency().GetByIdAsync(leaseId).Returns((AccessLease?)null); + + await Assert.ThrowsAsync(() => sutProvider.Sut.ExtendAsync(userId, Submission(leaseId))); + } + + [Theory, BitAutoData] + public async Task ExtendAsync_NotOwner_ThrowsNotFound(Guid userId, AccessLease lease) + { + var sutProvider = Setup(); + SetupExtendableLease(sutProvider, lease); + + // Someone else's lease is indistinguishable from a missing one, so ids can't be probed. + await Assert.ThrowsAsync(() => sutProvider.Sut.ExtendAsync(userId, Submission(lease.Id))); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateApprovedExtensionAsync(default!, default!, default); + } + + [Theory] + [BitAutoData(AccessLeaseStatus.Revoked)] + [BitAutoData(AccessLeaseStatus.Expired)] + public async Task ExtendAsync_LeaseNotActive_ThrowsConflict(AccessLeaseStatus status, AccessLease lease) + { + var sutProvider = Setup(); + SetupExtendableLease(sutProvider, lease); + lease.Status = status; + + await Assert.ThrowsAsync( + () => sutProvider.Sut.ExtendAsync(lease.RequesterId, Submission(lease.Id))); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateApprovedExtensionAsync(default!, default!, default); + } + + [Theory, BitAutoData] + public async Task ExtendAsync_LeaseWindowEnded_ThrowsConflict(AccessLease lease) + { + var sutProvider = Setup(); + SetupExtendableLease(sutProvider, lease); + lease.NotAfter = _now.AddMinutes(-1); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.ExtendAsync(lease.RequesterId, Submission(lease.Id))); + } + + [Theory, BitAutoData] + public async Task ExtendAsync_ItemNotGated_ThrowsBadRequest(AccessLease lease) + { + var sutProvider = Setup(); + SetupExtendableLease(sutProvider, lease); + sutProvider.GetDependency() + .ResolveAsync(lease.RequesterId, lease.CipherId, Arg.Any()) + .Returns((GoverningRule?)null); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.ExtendAsync(lease.RequesterId, Submission(lease.Id))); + } + + [Theory, BitAutoData] + public async Task ExtendAsync_ExtensionsNotAllowed_ThrowsBadRequest(AccessLease lease) + { + var sutProvider = Setup(); + SetupExtendableLease(sutProvider, lease, allowsExtensions: false); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.ExtendAsync(lease.RequesterId, Submission(lease.Id))); + Assert.Contains("does not allow extending", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateApprovedExtensionAsync(default!, default!, default); + } + + [Theory] + [BitAutoData(0)] + [BitAutoData(-60)] + public async Task ExtendAsync_NonPositiveDuration_ThrowsBadRequest(int durationSeconds, AccessLease lease) + { + var sutProvider = Setup(); + SetupExtendableLease(sutProvider, lease); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.ExtendAsync(lease.RequesterId, Submission(lease.Id, durationSeconds))); + } + + [Theory, BitAutoData] + public async Task ExtendAsync_DurationExceedsRuleMax_ThrowsBadRequest(AccessLease lease) + { + var sutProvider = Setup(); + SetupExtendableLease(sutProvider, lease); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.ExtendAsync(lease.RequesterId, + Submission(lease.Id, _maxExtensionDurationSeconds + 1))); + Assert.Contains("maximum extension length", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateApprovedExtensionAsync(default!, default!, default); + } + + [Theory] + [BitAutoData("")] + [BitAutoData(" ")] + public async Task ExtendAsync_BlankReason_ThrowsBadRequest(string reason, AccessLease lease) + { + var sutProvider = Setup(); + SetupExtendableLease(sutProvider, lease); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.ExtendAsync(lease.RequesterId, Submission(lease.Id, reason: reason))); + Assert.Contains("justification", ex.Message); + } + + [Theory, BitAutoData] + public async Task ExtendAsync_AlreadyExtended_ThrowsBadRequest(AccessLease lease) + { + var sutProvider = Setup(); + SetupExtendableLease(sutProvider, lease); + // A lease may be extended once; an existing extension request blocks another. + sutProvider.GetDependency() + .CountExtensionsByLeaseIdAsync(lease.Id).Returns(1); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.ExtendAsync(lease.RequesterId, Submission(lease.Id))); + Assert.Contains("already been extended", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateApprovedExtensionAsync(default!, default!, default); + } + + [Theory, BitAutoData] + public async Task ExtendAsync_Valid_RecordsApprovedExtensionAndExtendsLeaseInPlace(AccessLease lease) + { + var sutProvider = Setup(); + SetupExtendableLease(sutProvider, lease); + const int duration = 2 * 60 * 60; + var expectedNotAfter = lease.NotAfter.AddSeconds(duration); + + var result = await sutProvider.Sut.ExtendAsync(lease.RequesterId, Submission(lease.Id, duration, "incident")); + + // Auto-approved extension request, pointing at the parent lease, spanning [old end .. new end]. + Assert.Equal(AccessRequestStatus.Approved, result.Status); + Assert.Equal(lease.Id, result.ExtensionOfLeaseId); + Assert.Equal(lease.CipherId, result.CipherId); + Assert.Equal(lease.OrganizationId, result.OrganizationId); + Assert.Equal(lease.CollectionId, result.CollectionId); + Assert.Equal(lease.RequesterId, result.RequesterId); + Assert.Equal(lease.NotAfter, result.NotBefore); + Assert.Equal(expectedNotAfter, result.NotAfter); + Assert.Equal("incident", result.Reason); + Assert.Equal(_now, result.ResolvedDate); + + // The repo applies the request + decision + lease bump atomically. + await sutProvider.GetDependency().Received(1).CreateApprovedExtensionAsync( + Arg.Is(r => + r.ExtensionOfLeaseId == lease.Id + && r.Status == AccessRequestStatus.Approved + && r.NotBefore == lease.NotAfter + && r.NotAfter == expectedNotAfter), + Arg.Is(d => + d.DeciderKind == AccessDeciderKind.Automatic && d.Verdict == AccessDecisionVerdict.Approve), + _now); + + // The widened lease window must reach both the approvers (active-leases / history views) and the requester's + // other devices (banner / badge countdown). + await sutProvider.GetDependency().Received(1) + .NotifyCollectionApproversAsync(lease.CollectionId); + await sutProvider.GetDependency().Received(1) + .NotifyRequesterAsync(lease.RequesterId); + } + + [Theory, BitAutoData] + public async Task ExtendAsync_RepoReportsLeaseNotActive_ThrowsConflict(AccessLease lease) + { + var sutProvider = Setup(); + SetupExtendableLease(sutProvider, lease); + sutProvider.GetDependency() + .CreateApprovedExtensionAsync(Arg.Any(), Arg.Any(), _now) + .Returns(AccessLeaseExtendOutcome.LeaseNotActive); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.ExtendAsync(lease.RequesterId, Submission(lease.Id))); + } + + [Theory, BitAutoData] + public async Task ExtendAsync_RepoReportsAlreadyExtended_ThrowsBadRequest(AccessLease lease) + { + var sutProvider = Setup(); + SetupExtendableLease(sutProvider, lease); + // Lost a race: another extension landed between the pre-check and the guarded write. + sutProvider.GetDependency() + .CreateApprovedExtensionAsync(Arg.Any(), Arg.Any(), _now) + .Returns(AccessLeaseExtendOutcome.AlreadyExtended); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.ExtendAsync(lease.RequesterId, Submission(lease.Id))); + Assert.Contains("already been extended", ex.Message); + } + + private static AccessLeaseExtensionSubmission Submission( + Guid leaseId, int durationSeconds = 3600, string? reason = "Investigating an incident") => + new() { LeaseId = leaseId, DurationSeconds = durationSeconds, Reason = reason }; + + private static SutProvider Setup() + { + var sutProvider = new SutProvider().WithFakeTimeProvider().Create(); + sutProvider.GetDependency().SetUtcNow(_now); + return sutProvider; + } + + // An active, in-window lease owned by its BitAutoData requester, governed by an extension-enabled rule with no + // extension used yet, and a repo that extends successfully. Tests override the precondition they exercise. + private static void SetupExtendableLease( + SutProvider sutProvider, AccessLease lease, bool allowsExtensions = true) + { + lease.Status = AccessLeaseStatus.Active; + lease.NotAfter = _now.AddHours(1); + sutProvider.GetDependency().GetByIdAsync(lease.Id).Returns(lease); + + // A human-approval rule still yields automatic extensions — the approval gate never applies to extensions. + sutProvider.GetDependency() + .ResolveAsync(lease.RequesterId, lease.CipherId, Arg.Any()) + .Returns(new GoverningRule(lease.OrganizationId, lease.CollectionId, RequiresHumanApproval: true, + [new HumanApprovalCondition()]) + { + AllowsExtensions = allowsExtensions, + MaxExtensionDurationSeconds = _maxExtensionDurationSeconds, + }); + + sutProvider.GetDependency().CountExtensionsByLeaseIdAsync(lease.Id).Returns(0); + sutProvider.GetDependency() + .CreateApprovedExtensionAsync(Arg.Any(), Arg.Any(), _now) + .Returns(AccessLeaseExtendOutcome.Extended); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Commands/RevokeAccessLeaseCommandTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Commands/RevokeAccessLeaseCommandTests.cs new file mode 100644 index 000000000000..a667e6edb2e6 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Commands/RevokeAccessLeaseCommandTests.cs @@ -0,0 +1,117 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Commands; +using Bit.Commercial.Pam.Services; +using Bit.Core.Exceptions; +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Pam.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Commands; + +[SutProviderCustomize] +public class RevokeAccessLeaseCommandTests +{ + private static readonly DateTime _now = new(2026, 6, 5, 12, 0, 0, DateTimeKind.Utc); + + [Theory, BitAutoData] + public async Task RevokeAsync_LeaseMissing_ThrowsNotFound(Guid userId, Guid leaseId) + { + var sutProvider = Setup(); + sutProvider.GetDependency().GetByIdAsync(leaseId).Returns((AccessLease?)null); + + await Assert.ThrowsAsync(() => sutProvider.Sut.RevokeAsync(userId, leaseId, null)); + } + + [Theory, BitAutoData] + public async Task RevokeAsync_NeitherHolderNorManageable_ThrowsNotFound(Guid userId, AccessLease lease) + { + var sutProvider = Setup(); + lease.Status = AccessLeaseStatus.Active; + // userId is neither the lease holder (lease.RequesterId is a different AutoFixture Guid) nor a manager. + sutProvider.GetDependency().GetByIdAsync(lease.Id).Returns(lease); + sutProvider.GetDependency() + .CanManageCollectionAsync(userId, lease.CollectionId).Returns(false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.RevokeAsync(userId, lease.Id, null)); + } + + [Theory, BitAutoData] + public async Task RevokeAsync_HolderEndsOwnLease_RevokesWithoutManageRights(AccessLease lease) + { + var sutProvider = Setup(); + lease.Status = AccessLeaseStatus.Active; + // The caller IS the lease's own holder, but cannot Manage the collection — they may still end their own access. + sutProvider.GetDependency().GetByIdAsync(lease.Id).Returns(lease); + sutProvider.GetDependency() + .CanManageCollectionAsync(lease.RequesterId, lease.CollectionId).Returns(false); + + await sutProvider.Sut.RevokeAsync(lease.RequesterId, lease.Id, "done with it"); + + // Settles to revoked with the holder recorded as the revoker (RevokedBy via the audit decision's ApproverId). + await sutProvider.GetDependency().Received(1).RevokeAsync( + lease, + Arg.Is(d => + d.AccessRequestId == lease.AccessRequestId && + d.DeciderKind == AccessDeciderKind.Human && + d.ApproverId == lease.RequesterId && + d.Verdict == AccessDecisionVerdict.Deny && + d.Comment == "done with it"), + _now); + await sutProvider.GetDependency().Received(1) + .NotifyCollectionApproversAsync(lease.CollectionId); + await sutProvider.GetDependency().Received(1) + .NotifyRequesterAsync(lease.RequesterId); + } + + [Theory, BitAutoData] + public async Task RevokeAsync_NotActive_ThrowsConflict(Guid userId, AccessLease lease) + { + var sutProvider = Setup(); + lease.Status = AccessLeaseStatus.Revoked; + SetupManageableLease(sutProvider, userId, lease); + + await Assert.ThrowsAsync(() => sutProvider.Sut.RevokeAsync(userId, lease.Id, null)); + } + + [Theory, BitAutoData] + public async Task RevokeAsync_Active_RevokesAndWritesAuditDecision(Guid userId, AccessLease lease) + { + var sutProvider = Setup(); + lease.Status = AccessLeaseStatus.Active; + SetupManageableLease(sutProvider, userId, lease); + + await sutProvider.Sut.RevokeAsync(userId, lease.Id, "policy change"); + + await sutProvider.GetDependency().Received(1).RevokeAsync( + lease, + Arg.Is(d => + d.AccessRequestId == lease.AccessRequestId && + d.DeciderKind == AccessDeciderKind.Human && + d.ApproverId == userId && + d.Verdict == AccessDecisionVerdict.Deny && + d.Comment == "policy change"), + _now); + await sutProvider.GetDependency().Received(1) + .NotifyCollectionApproversAsync(lease.CollectionId); + await sutProvider.GetDependency().Received(1) + .NotifyRequesterAsync(lease.RequesterId); + } + + private static SutProvider Setup() + { + var sutProvider = new SutProvider().WithFakeTimeProvider().Create(); + sutProvider.GetDependency().SetUtcNow(_now); + return sutProvider; + } + + private static void SetupManageableLease(SutProvider sutProvider, Guid userId, AccessLease lease) + { + sutProvider.GetDependency().GetByIdAsync(lease.Id).Returns(lease); + sutProvider.GetDependency() + .CanManageCollectionAsync(userId, lease.CollectionId).Returns(true); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Commands/SubmitAccessRequestCommandTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Commands/SubmitAccessRequestCommandTests.cs new file mode 100644 index 000000000000..68123488abb3 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Commands/SubmitAccessRequestCommandTests.cs @@ -0,0 +1,417 @@ +using Bit.Commercial.Pam.Engine; +using Bit.Commercial.Pam.Enums; +using Bit.Commercial.Pam.Models; +using Bit.Commercial.Pam.Models.Conditions; +using Bit.Commercial.Pam.OrganizationFeatures.Commands; +using Bit.Commercial.Pam.Services; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Pam.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Commands; + +[SutProviderCustomize] +public class SubmitAccessRequestCommandTests +{ + private static readonly DateTime _now = new(2026, 6, 4, 12, 0, 0, DateTimeKind.Utc); + + [Theory, BitAutoData] + public async Task SubmitAsync_CipherNotAccessible_ThrowsNotFound(Guid userId, Guid cipherId) + { + var sutProvider = Setup(); + sutProvider.GetDependency().GetByIdAsync(cipherId, userId).Returns((CipherDetails?)null); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.SubmitAsync(userId, cipherId, new AccessRequestSubmission { DurationSeconds = 3600 })); + } + + [Theory, BitAutoData] + public async Task SubmitAsync_NotLeasingGated_ThrowsBadRequest(Guid userId, Guid cipherId) + { + var sutProvider = Setup(); + SetupCipher(sutProvider, userId, cipherId); + sutProvider.GetDependency().ResolveAsync(userId, cipherId, Arg.Any()) + .Returns((GoverningRule?)null); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.SubmitAsync(userId, cipherId, new AccessRequestSubmission { DurationSeconds = 3600 })); + Assert.Contains("does not require a lease", ex.Message); + } + + [Theory, BitAutoData] + public async Task SubmitAsync_Automatic_CreatesApprovedRequestWithoutMintingLease( + Guid userId, Guid cipherId, Guid orgId, Guid collectionId) + { + var sutProvider = Setup(); + SetupCipher(sutProvider, userId, cipherId); + SetupResolution(sutProvider, userId, cipherId, orgId, collectionId, requiresHuman: false); + SetupEvaluation(sutProvider, AccessEvaluation.Allow); + + var result = await sutProvider.Sut.SubmitAsync(userId, cipherId, + new AccessRequestSubmission { DurationSeconds = 3600, Reason = "deploy" }); + + // The automatic path no longer mints a lease at submit; it produces a startable, already-approved request the + // requester activates explicitly. The window spans the requested duration from now. + Assert.Equal(AccessApprovalMode.Automatic, result.ApprovalMode); + Assert.Equal(AccessRequestStatus.Approved, result.Request.Status); + Assert.Equal(_now, result.Request.NotBefore); + Assert.Equal(_now.AddSeconds(3600), result.Request.NotAfter); + Assert.Equal("deploy", result.Request.Reason); + + await sutProvider.GetDependency().Received(1) + .CreateAutoApprovedAsync( + Arg.Is(r => r.Status == AccessRequestStatus.Approved && r.NotBefore == _now + && r.NotAfter == _now.AddSeconds(3600)), + Arg.Is(d => d.DeciderKind == AccessDeciderKind.Automatic + && d.Verdict == AccessDecisionVerdict.Approve)); + } + + [Theory, BitAutoData] + public async Task SubmitAsync_AutomaticWithWindow_ThrowsBadRequest(Guid userId, Guid cipherId, Guid orgId, Guid collectionId) + { + var sutProvider = Setup(); + SetupCipher(sutProvider, userId, cipherId); + SetupResolution(sutProvider, userId, cipherId, orgId, collectionId, requiresHuman: false); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.SubmitAsync(userId, cipherId, + new AccessRequestSubmission { Start = _now, End = _now.AddHours(1) })); + Assert.Contains("provide a duration", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateAutoApprovedAsync(default!, default!); + } + + [Theory, BitAutoData] + public async Task SubmitAsync_AutomaticMissingDuration_ThrowsBadRequest(Guid userId, Guid cipherId, Guid orgId, Guid collectionId) + { + var sutProvider = Setup(); + SetupCipher(sutProvider, userId, cipherId); + SetupResolution(sutProvider, userId, cipherId, orgId, collectionId, requiresHuman: false); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.SubmitAsync(userId, cipherId, new AccessRequestSubmission())); + } + + [Theory, BitAutoData] + public async Task SubmitAsync_AutomaticDurationExceedsMax_ThrowsBadRequest(Guid userId, Guid cipherId, Guid orgId, Guid collectionId) + { + var sutProvider = Setup(); + SetupCipher(sutProvider, userId, cipherId); + SetupResolution(sutProvider, userId, cipherId, orgId, collectionId, requiresHuman: false); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.SubmitAsync(userId, cipherId, + new AccessRequestSubmission { DurationSeconds = SubmitAccessRequestCommand.MaxDurationSeconds + 1 })); + Assert.Contains("maximum", ex.Message); + } + + [Theory, BitAutoData] + public async Task SubmitAsync_AutomaticPolicyDenied_ThrowsBadRequestAndIssuesNoLease( + Guid userId, Guid cipherId, Guid orgId, Guid collectionId) + { + var sutProvider = Setup(); + SetupCipher(sutProvider, userId, cipherId); + SetupResolution(sutProvider, userId, cipherId, orgId, collectionId, requiresHuman: false); + SetupEvaluation(sutProvider, AccessEvaluation.Deny(DenyReason.NotWithinIpRange)); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.SubmitAsync(userId, cipherId, new AccessRequestSubmission { DurationSeconds = 3600 })); + Assert.Contains("network", ex.Message); + // A rule the caller fails to satisfy must not produce an approved request. + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateAutoApprovedAsync(default!, default!); + } + + [Theory, BitAutoData] + public async Task SubmitAsync_Human_CreatesPendingRequest(Guid userId, Guid cipherId, Guid orgId, Guid collectionId) + { + var sutProvider = Setup(); + SetupCipher(sutProvider, userId, cipherId); + SetupResolution(sutProvider, userId, cipherId, orgId, collectionId, requiresHuman: true); + sutProvider.GetDependency() + .CreateAsync(Arg.Any()) + .Returns(callInfo => callInfo.Arg()); + + var start = _now.AddHours(1); + var end = _now.AddHours(2); + var result = await sutProvider.Sut.SubmitAsync(userId, cipherId, + new AccessRequestSubmission { Start = start, End = end, Reason = "audit" }); + + Assert.Equal(AccessApprovalMode.Human, result.ApprovalMode); + Assert.NotNull(result.Request); + Assert.Equal(AccessRequestStatus.Pending, result.Request!.Status); + Assert.Equal(start, result.Request.NotBefore); + Assert.Equal(end, result.Request.NotAfter); + Assert.Equal("audit", result.Request.Reason); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateAutoApprovedAsync(default!, default!); + await sutProvider.GetDependency().Received(1) + .NotifyCollectionApproversAsync(collectionId); + await sutProvider.GetDependency().Received(1) + .NotifyRequesterAsync(userId); + } + + [Theory, BitAutoData] + public async Task SubmitAsync_Human_EmailsApproversExcludingRequester( + Guid userId, Guid cipherId, Guid orgId, Guid collectionId) + { + var sutProvider = Setup(); + SetupCipher(sutProvider, userId, cipherId); + SetupResolution(sutProvider, userId, cipherId, orgId, collectionId, requiresHuman: true); + SetupHumanCreate(sutProvider); + SetupApproverEmails(sutProvider, collectionId, userId, orgId); + + var start = _now.AddHours(1); + var end = _now.AddHours(2); + await sutProvider.Sut.SubmitAsync(userId, cipherId, + new AccessRequestSubmission { Start = start, End = end, Reason = "audit" }); + + await sutProvider.GetDependency().Received(1) + .SendPamPendingAccessRequestEmailsAsync( + Arg.Is>(e => + e.Contains("a@example.com") && e.Contains("b@example.com") && !e.Contains("requester@example.com")), + "Acme Corp", "Reqi", "requester@example.com", start, end, "audit"); + } + + [Theory, BitAutoData] + public async Task SubmitAsync_Human_NoApprovers_DoesNotEmail( + Guid userId, Guid cipherId, Guid orgId, Guid collectionId) + { + var sutProvider = Setup(); + SetupCipher(sutProvider, userId, cipherId); + SetupResolution(sutProvider, userId, cipherId, orgId, collectionId, requiresHuman: true); + SetupHumanCreate(sutProvider); + // Setup() defaults the collection to no managers, so there is nobody to email. + + var result = await sutProvider.Sut.SubmitAsync(userId, cipherId, + new AccessRequestSubmission { Start = _now.AddHours(1), End = _now.AddHours(2), Reason = "audit" }); + + Assert.Equal(AccessApprovalMode.Human, result.ApprovalMode); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .SendPamPendingAccessRequestEmailsAsync(default!, default!, default, default!, default, default, default); + } + + [Theory, BitAutoData] + public async Task SubmitAsync_Human_RequesterNotFound_DoesNotEmail( + Guid userId, Guid cipherId, Guid orgId, Guid collectionId) + { + var sutProvider = Setup(); + SetupCipher(sutProvider, userId, cipherId); + SetupResolution(sutProvider, userId, cipherId, orgId, collectionId, requiresHuman: true); + SetupHumanCreate(sutProvider); + var manager = new User { Id = Guid.NewGuid(), Email = "manager@example.com" }; + sutProvider.GetDependency().GetManagingUserIdsAsync(collectionId) + .Returns(new List { manager.Id }); + sutProvider.GetDependency().GetManyAsync(Arg.Any>()) + .Returns(new List { manager }); + sutProvider.GetDependency().GetByIdAsync(userId).Returns((User?)null); + + await sutProvider.Sut.SubmitAsync(userId, cipherId, + new AccessRequestSubmission { Start = _now.AddHours(1), End = _now.AddHours(2), Reason = "audit" }); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .SendPamPendingAccessRequestEmailsAsync(default!, default!, default, default!, default, default, default); + } + + [Theory, BitAutoData] + public async Task SubmitAsync_Human_MailServiceThrows_StillReturnsResult( + Guid userId, Guid cipherId, Guid orgId, Guid collectionId) + { + var sutProvider = Setup(); + SetupCipher(sutProvider, userId, cipherId); + SetupResolution(sutProvider, userId, cipherId, orgId, collectionId, requiresHuman: true); + SetupHumanCreate(sutProvider); + SetupApproverEmails(sutProvider, collectionId, userId, orgId); + sutProvider.GetDependency() + .SendPamPendingAccessRequestEmailsAsync(Arg.Any>(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromException(new Exception("smtp down"))); + + // An email failure must not fail the submission — the request is already persisted. + var result = await sutProvider.Sut.SubmitAsync(userId, cipherId, + new AccessRequestSubmission { Start = _now.AddHours(1), End = _now.AddHours(2), Reason = "audit" }); + + Assert.Equal(AccessApprovalMode.Human, result.ApprovalMode); + Assert.Equal(AccessRequestStatus.Pending, result.Request!.Status); + } + + [Theory, BitAutoData] + public async Task SubmitAsync_Automatic_DoesNotNotifyApprovers(Guid userId, Guid cipherId, Guid orgId, Guid collectionId) + { + var sutProvider = Setup(); + SetupCipher(sutProvider, userId, cipherId); + SetupResolution(sutProvider, userId, cipherId, orgId, collectionId, requiresHuman: false); + SetupEvaluation(sutProvider, AccessEvaluation.Allow); + + await sutProvider.Sut.SubmitAsync(userId, cipherId, + new AccessRequestSubmission { DurationSeconds = 3600, Reason = "deploy" }); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .NotifyCollectionApproversAsync(default); + // The auto path mints no approval gate, but the requester's other devices still learn of the new approved + // request. + await sutProvider.GetDependency().Received(1) + .NotifyRequesterAsync(userId); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .SendPamPendingAccessRequestEmailsAsync(default!, default!, default, default!, default, default, default); + } + + [Theory, BitAutoData] + public async Task SubmitAsync_HumanMissingReason_ThrowsBadRequest(Guid userId, Guid cipherId, Guid orgId, Guid collectionId) + { + var sutProvider = Setup(); + SetupCipher(sutProvider, userId, cipherId); + SetupResolution(sutProvider, userId, cipherId, orgId, collectionId, requiresHuman: true); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.SubmitAsync(userId, cipherId, + new AccessRequestSubmission { Start = _now.AddHours(1), End = _now.AddHours(2) })); + Assert.Contains("reason is required", ex.Message); + } + + [Theory, BitAutoData] + public async Task SubmitAsync_HumanWithDuration_ThrowsBadRequest(Guid userId, Guid cipherId, Guid orgId, Guid collectionId) + { + var sutProvider = Setup(); + SetupCipher(sutProvider, userId, cipherId); + SetupResolution(sutProvider, userId, cipherId, orgId, collectionId, requiresHuman: true); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.SubmitAsync(userId, cipherId, + new AccessRequestSubmission { DurationSeconds = 3600, Reason = "x" })); + Assert.Contains("requires human approval", ex.Message); + } + + [Theory, BitAutoData] + public async Task SubmitAsync_HumanStartNotBeforeEnd_ThrowsBadRequest(Guid userId, Guid cipherId, Guid orgId, Guid collectionId) + { + var sutProvider = Setup(); + SetupCipher(sutProvider, userId, cipherId); + SetupResolution(sutProvider, userId, cipherId, orgId, collectionId, requiresHuman: true); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.SubmitAsync(userId, cipherId, + new AccessRequestSubmission { Start = _now.AddHours(2), End = _now.AddHours(1), Reason = "x" })); + Assert.Contains("before the end date", ex.Message); + } + + [Theory, BitAutoData] + public async Task SubmitAsync_ExistingActiveLease_ThrowsBadRequest(Guid userId, Guid cipherId, Guid orgId, Guid collectionId, AccessLease lease) + { + var sutProvider = Setup(); + SetupCipher(sutProvider, userId, cipherId); + SetupResolution(sutProvider, userId, cipherId, orgId, collectionId, requiresHuman: false); + sutProvider.GetDependency() + .GetActiveByRequesterIdCipherIdAsync(userId, cipherId, _now) + .Returns(lease); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.SubmitAsync(userId, cipherId, new AccessRequestSubmission { DurationSeconds = 3600 })); + Assert.Contains("already have active access", ex.Message); + } + + [Theory, BitAutoData] + public async Task SubmitAsync_ExistingPendingRequest_ThrowsBadRequest(Guid userId, Guid cipherId, Guid orgId, Guid collectionId, AccessRequest pending) + { + var sutProvider = Setup(); + SetupCipher(sutProvider, userId, cipherId); + SetupResolution(sutProvider, userId, cipherId, orgId, collectionId, requiresHuman: true); + sutProvider.GetDependency() + .GetActivePendingByRequesterIdCipherIdAsync(userId, cipherId) + .Returns(pending); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.SubmitAsync(userId, cipherId, + new AccessRequestSubmission { Start = _now.AddHours(1), End = _now.AddHours(2), Reason = "x" })); + Assert.Contains("already have a pending request", ex.Message); + } + + [Theory, BitAutoData] + public async Task SubmitAsync_ExistingApprovedUnactivatedRequest_ThrowsBadRequest( + Guid userId, Guid cipherId, Guid orgId, Guid collectionId, AccessRequest approved) + { + var sutProvider = Setup(); + SetupCipher(sutProvider, userId, cipherId); + SetupResolution(sutProvider, userId, cipherId, orgId, collectionId, requiresHuman: true); + sutProvider.GetDependency() + .GetActiveApprovedByRequesterIdCipherIdAsync(userId, cipherId, _now) + .Returns(approved); + + // An approved-but-not-yet-activated request already grants startable access; a second request would stack. + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.SubmitAsync(userId, cipherId, + new AccessRequestSubmission { Start = _now.AddHours(1), End = _now.AddHours(2), Reason = "x" })); + Assert.Contains("already have an approved request", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateAsync(default!); + } + + private static SutProvider Setup() + { + var sutProvider = new SutProvider().WithFakeTimeProvider().Create(); + sutProvider.GetDependency().SetUtcNow(_now); + // Default to a collection with no managers so tests that don't exercise the approver email skip it cleanly. + sutProvider.GetDependency().GetManagingUserIdsAsync(Arg.Any()) + .Returns(new List()); + return sutProvider; + } + + private static void SetupHumanCreate(SutProvider sutProvider) => + sutProvider.GetDependency() + .CreateAsync(Arg.Any()) + .Returns(callInfo => callInfo.Arg()); + + /// + /// Wires the collection managers, requester, and organization so the approver email resolves and sends. + /// The requester () is intentionally also a manager, to assert they're excluded. + /// + private static void SetupApproverEmails(SutProvider sutProvider, + Guid collectionId, Guid userId, Guid orgId) + { + var managerA = new User { Id = Guid.NewGuid(), Email = "a@example.com", Name = "A" }; + var managerB = new User { Id = Guid.NewGuid(), Email = "b@example.com", Name = "B" }; + sutProvider.GetDependency().GetManagingUserIdsAsync(collectionId) + .Returns(new List { managerA.Id, managerB.Id, userId }); + sutProvider.GetDependency().GetManyAsync(Arg.Any>()) + .Returns(new List { managerA, managerB }); + sutProvider.GetDependency().GetByIdAsync(userId) + .Returns(new User { Id = userId, Email = "requester@example.com", Name = "Reqi" }); + sutProvider.GetDependency().GetByIdAsync(orgId) + .Returns(new Organization { Id = orgId, Name = "Acme Corp" }); + } + + private static void SetupCipher(SutProvider sutProvider, Guid userId, Guid cipherId) + { + sutProvider.GetDependency() + .GetByIdAsync(cipherId, userId) + .Returns(new CipherDetails { Id = cipherId }); + } + + private static void SetupResolution(SutProvider sutProvider, Guid userId, Guid cipherId, + Guid orgId, Guid collectionId, bool requiresHuman) + { + var condition = requiresHuman ? new HumanApprovalCondition() : (AccessCondition)new IpAllowlistCondition { Cidrs = ["10.0.0.0/8"] }; + sutProvider.GetDependency() + .ResolveAsync(userId, cipherId, Arg.Any()) + .Returns(new GoverningRule(orgId, collectionId, requiresHuman, [condition])); + } + + private static void SetupEvaluation(SutProvider sutProvider, AccessEvaluation evaluation) + { + sutProvider.GetDependency() + .Evaluate(Arg.Any>(), Arg.Any()) + .Returns(evaluation); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Commands/UpdateAccessRuleCommandTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Commands/UpdateAccessRuleCommandTests.cs new file mode 100644 index 000000000000..004936ea2c83 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Commands/UpdateAccessRuleCommandTests.cs @@ -0,0 +1,242 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Commands; +using Bit.Commercial.Pam.Services; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Models; +using Bit.Pam.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Commands; + +[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); + sutProvider.GetDependency() + .Validate(update.Conditions) + .Returns(AccessRuleValidationResult.Valid); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(orgId) + .Returns(new List { existing }); + + 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)); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_AllowsExtensionsWithoutMax_ThrowsBadRequest(AccessRule update) + { + var sutProvider = SetupSutProvider(); + update.Name = "renamed"; + update.AllowsExtensions = true; + update.MaxExtensionDurationSeconds = null; + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.UpdateAsync(update.OrganizationId, update.Id, update, [])); + Assert.Contains("maximum extension length", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default!); + } + + [Theory] + [BitAutoData(0)] + [BitAutoData(-1)] + public async Task UpdateAsync_AllowsExtensionsWithNonPositiveMax_ThrowsBadRequest(int maxExtensionDurationSeconds, AccessRule update) + { + var sutProvider = SetupSutProvider(); + update.Name = "renamed"; + update.AllowsExtensions = true; + update.MaxExtensionDurationSeconds = maxExtensionDurationSeconds; + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.UpdateAsync(update.OrganizationId, update.Id, update, [])); + Assert.Contains("maximum extension length", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default!); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_ReplacesCollections_AssignsNewAndClearsRemoved(AccessRuleDetails existing, + AccessRule update, Collection keep, Collection add) + { + var sutProvider = SetupSutProvider(); + var orgId = existing.OrganizationId; + update.Name = "renamed"; + update.Conditions = """{"kind":"human_approval"}"""; + keep.OrganizationId = orgId; + keep.AccessRuleId = existing.Id; // already governed by this rule + add.OrganizationId = orgId; + add.AccessRuleId = null; + var desired = new[] { keep.Id, add.Id }; + var removedId = Guid.NewGuid(); + existing.CollectionIds = [keep.Id, removedId]; + sutProvider.GetDependency() + .GetDetailsByIdAsync(existing.Id) + .Returns(existing); + sutProvider.GetDependency() + .Validate(update.Conditions) + .Returns(AccessRuleValidationResult.Valid); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(orgId) + .Returns(new List { existing }); + sutProvider.GetDependency() + .GetManyByManyIdsAsync(Arg.Any>()) + .Returns(new List { keep, add }); + + var result = await sutProvider.Sut.UpdateAsync(orgId, existing.Id, update, desired); + + Assert.Equal(desired, result.CollectionIds); + await sutProvider.GetDependency().Received(1) + .SetCollectionAssociationsAsync(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"; + update.Conditions = """{"kind":"human_approval"}"""; + var currentId = Guid.NewGuid(); + existing.CollectionIds = [currentId]; + sutProvider.GetDependency() + .GetDetailsByIdAsync(existing.Id) + .Returns(existing); + sutProvider.GetDependency() + .Validate(update.Conditions) + .Returns(AccessRuleValidationResult.Valid); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(orgId) + .Returns(new List { existing }); + + var result = await sutProvider.Sut.UpdateAsync(orgId, existing.Id, update, []); + + Assert.Empty(result.CollectionIds); + await sutProvider.GetDependency().Received(1) + .SetCollectionAssociationsAsync(orgId, existing.Id, + Arg.Is>(ids => !ids.Any()), + Arg.Is>(ids => ids.SequenceEqual(new[] { currentId }))); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_CollectionGovernedByAnotherRule_ThrowsBadRequest(AccessRuleDetails existing, + AccessRule update, Collection collection) + { + var sutProvider = SetupSutProvider(); + var orgId = existing.OrganizationId; + update.Name = "renamed"; + update.Conditions = """{"kind":"human_approval"}"""; + collection.OrganizationId = orgId; + collection.AccessRuleId = Guid.NewGuid(); // a different rule + sutProvider.GetDependency() + .GetDetailsByIdAsync(existing.Id) + .Returns(existing); + sutProvider.GetDependency() + .Validate(update.Conditions) + .Returns(AccessRuleValidationResult.Valid); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(orgId) + .Returns(new List { existing }); + sutProvider.GetDependency() + .GetManyByManyIdsAsync(Arg.Any>()) + .Returns(new List { collection }); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.UpdateAsync(orgId, existing.Id, update, new[] { collection.Id })); + Assert.Contains("already governed by another access rule", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default!); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_MissingExisting_ThrowsNotFound(AccessRule update) + { + var sutProvider = SetupSutProvider(); + sutProvider.GetDependency() + .GetDetailsByIdAsync(Arg.Any()) + .Returns((AccessRuleDetails?)null); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.UpdateAsync(Guid.NewGuid(), Guid.NewGuid(), update, [])); + } + + [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, [])); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_InvalidRule_ThrowsBadRequest(AccessRuleDetails existing, AccessRule update) + { + var sutProvider = SetupSutProvider(); + var orgId = existing.OrganizationId; + update.Name = "ok"; + update.Conditions = """{"kind":"bogus"}"""; + sutProvider.GetDependency() + .GetDetailsByIdAsync(existing.Id) + .Returns(existing); + sutProvider.GetDependency() + .Validate(update.Conditions) + .Returns(AccessRuleValidationResult.Invalid("nope")); + + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.UpdateAsync(orgId, existing.Id, update, [])); + Assert.Equal("nope", ex.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default!); + } + + private static SutProvider SetupSutProvider() + { + var sutProvider = new SutProvider() + .WithFakeTimeProvider() + .Create(); + sutProvider.GetDependency().SetUtcNow(_now); + return sutProvider; + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Commercial.Pam.Test.csproj b/bitwarden_license/test/Commercial.Pam.Test/Commercial.Pam.Test.csproj new file mode 100644 index 000000000000..c00aaffed34c --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Commercial.Pam.Test.csproj @@ -0,0 +1,26 @@ + + + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/bitwarden_license/test/Commercial.Pam.Test/Engine/AccessRuleEngineTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Engine/AccessRuleEngineTests.cs new file mode 100644 index 000000000000..19ae67dfb553 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Engine/AccessRuleEngineTests.cs @@ -0,0 +1,218 @@ +using System.Net; +using Bit.Commercial.Pam.Engine; +using Bit.Commercial.Pam.Enums; +using Bit.Commercial.Pam.Models.Conditions; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Engine; + +public class AccessRuleEngineTests +{ + // 2026-06-04T12:00:00Z is a Thursday, so "thu" windows match in UTC. + private static readonly DateTimeOffset _now = new(2026, 6, 4, 12, 0, 0, TimeSpan.Zero); + + private readonly AccessRuleEngine _sut = new(); + + private static AccessSignals Signals(IPAddress? ip = null, DateTimeOffset? at = null) => new() + { + IpAddress = ip, + Timestamp = at ?? _now, + }; + + private static AccessCondition[] Set(params AccessCondition[] conditions) => conditions; + + [Fact] + public void Evaluate_HumanApproval_RequiresApproval() + { + var evaluation = _sut.Evaluate(Set(new HumanApprovalCondition()), Signals()); + + Assert.Equal(AccessEvaluationOutcome.RequiresApproval, evaluation.Outcome); + } + + [Fact] + public void Evaluate_IpAllowlist_IpInRange_Allows() + { + var conditions = Set(new IpAllowlistCondition { Cidrs = ["10.0.0.0/8"] }); + + var evaluation = _sut.Evaluate(conditions, Signals(IPAddress.Parse("10.1.2.3"))); + + Assert.Equal(AccessEvaluationOutcome.Allow, evaluation.Outcome); + } + + [Fact] + public void Evaluate_IpAllowlist_IpOutOfRange_Denies() + { + var conditions = Set(new IpAllowlistCondition { Cidrs = ["10.0.0.0/8"] }); + + var evaluation = _sut.Evaluate(conditions, Signals(IPAddress.Parse("192.168.1.1"))); + + Assert.Equal(AccessEvaluationOutcome.Deny, evaluation.Outcome); + Assert.Equal(DenyReason.NotWithinIpRange, evaluation.Reason); + } + + [Fact] + public void Evaluate_IpAllowlist_UnknownIp_DeniesClosed() + { + var conditions = Set(new IpAllowlistCondition { Cidrs = ["10.0.0.0/8"] }); + + var evaluation = _sut.Evaluate(conditions, Signals(ip: null)); + + Assert.Equal(AccessEvaluationOutcome.Deny, evaluation.Outcome); + Assert.Equal(DenyReason.NotWithinIpRange, evaluation.Reason); + } + + [Fact] + public void Evaluate_IpAllowlist_NoEntries_DeniesClosed() + { + var evaluation = _sut.Evaluate(Set(new IpAllowlistCondition()), Signals(IPAddress.Parse("10.1.2.3"))); + + Assert.Equal(AccessEvaluationOutcome.Deny, evaluation.Outcome); + Assert.Equal(DenyReason.NotWithinIpRange, evaluation.Reason); + } + + [Fact] + public void Evaluate_TimeOfDay_WithinWindow_Allows() + { + var conditions = Set(new TimeOfDayCondition + { + Tz = "UTC", + Windows = [new TimeWindow { Days = [AccessWeekday.Thu], From = "09:00", To = "17:00" }], + }); + + var evaluation = _sut.Evaluate(conditions, Signals()); + + Assert.Equal(AccessEvaluationOutcome.Allow, evaluation.Outcome); + } + + [Fact] + public void Evaluate_TimeOfDay_OutsideTimeWindow_Denies() + { + var conditions = Set(new TimeOfDayCondition + { + Tz = "UTC", + Windows = [new TimeWindow { Days = [AccessWeekday.Thu], From = "00:00", To = "06:00" }], + }); + + var evaluation = _sut.Evaluate(conditions, Signals()); + + Assert.Equal(AccessEvaluationOutcome.Deny, evaluation.Outcome); + Assert.Equal(DenyReason.NotWithinTimeWindow, evaluation.Reason); + } + + [Fact] + public void Evaluate_TimeOfDay_DayNotListed_Denies() + { + var conditions = Set(new TimeOfDayCondition + { + Tz = "UTC", + Windows = [new TimeWindow { Days = [AccessWeekday.Fri], From = "00:00", To = "23:59" }], + }); + + var evaluation = _sut.Evaluate(conditions, Signals()); + + Assert.Equal(AccessEvaluationOutcome.Deny, evaluation.Outcome); + Assert.Equal(DenyReason.NotWithinTimeWindow, evaluation.Reason); + } + + [Fact] + public void Evaluate_TimeOfDay_EvaluatesInConfiguredTimezone() + { + // 23:00 UTC is 19:00 (Thursday) in America/New_York during June DST, inside the window. + var conditions = Set(new TimeOfDayCondition + { + Tz = "America/New_York", + Windows = [new TimeWindow { Days = [AccessWeekday.Thu], From = "18:00", To = "20:00" }], + }); + + var evaluation = _sut.Evaluate(conditions, Signals(at: new DateTimeOffset(2026, 6, 4, 23, 0, 0, TimeSpan.Zero))); + + Assert.Equal(AccessEvaluationOutcome.Allow, evaluation.Outcome); + } + + [Fact] + public void Evaluate_TimeOfDay_UnknownTimezone_DeniesClosed() + { + var conditions = Set(new TimeOfDayCondition + { + Tz = "Not/AZone", + Windows = [new TimeWindow { Days = [AccessWeekday.Thu], From = "00:00", To = "23:59" }], + }); + + var evaluation = _sut.Evaluate(conditions, Signals()); + + Assert.Equal(AccessEvaluationOutcome.Deny, evaluation.Outcome); + Assert.Equal(DenyReason.NotWithinTimeWindow, evaluation.Reason); + } + + [Fact] + public void Evaluate_AllConditionsAllow_Allows() + { + var conditions = Set( + new IpAllowlistCondition { Cidrs = ["10.0.0.0/8"] }, + new TimeOfDayCondition { Tz = "UTC", Windows = [new TimeWindow { Days = [AccessWeekday.Thu], From = "09:00", To = "17:00" }] }); + + var evaluation = _sut.Evaluate(conditions, Signals(IPAddress.Parse("10.1.2.3"))); + + Assert.Equal(AccessEvaluationOutcome.Allow, evaluation.Outcome); + } + + [Fact] + public void Evaluate_OneConditionDenies_DeniesWithThatReason() + { + var conditions = Set( + new IpAllowlistCondition { Cidrs = ["10.0.0.0/8"] }, + new TimeOfDayCondition { Tz = "UTC", Windows = [new TimeWindow { Days = [AccessWeekday.Thu], From = "00:00", To = "06:00" }] }); + + var evaluation = _sut.Evaluate(conditions, Signals(IPAddress.Parse("10.1.2.3"))); + + Assert.Equal(AccessEvaluationOutcome.Deny, evaluation.Outcome); + Assert.Equal(DenyReason.NotWithinTimeWindow, evaluation.Reason); + } + + [Fact] + public void Evaluate_AllowPlusHumanApproval_RequiresApproval() + { + var conditions = Set( + new IpAllowlistCondition { Cidrs = ["10.0.0.0/8"] }, + new HumanApprovalCondition()); + + var evaluation = _sut.Evaluate(conditions, Signals(IPAddress.Parse("10.1.2.3"))); + + Assert.Equal(AccessEvaluationOutcome.RequiresApproval, evaluation.Outcome); + } + + [Fact] + public void Evaluate_DenyOutranksApproval() + { + // A denying condition beats a pending approval: there is nothing to approve if access is barred outright. + var conditions = Set( + new HumanApprovalCondition(), + new IpAllowlistCondition { Cidrs = ["10.0.0.0/8"] }); + + var evaluation = _sut.Evaluate(conditions, Signals(IPAddress.Parse("192.168.1.1"))); + + Assert.Equal(AccessEvaluationOutcome.Deny, evaluation.Outcome); + Assert.Equal(DenyReason.NotWithinIpRange, evaluation.Reason); + } + + [Fact] + public void Evaluate_NoConditions_Allows() + { + // A rule with no conditions is vacuously satisfied: access is auto-granted while still flowing through + // PAM for audit logging. + var evaluation = _sut.Evaluate(Set(), Signals()); + + Assert.Equal(AccessEvaluationOutcome.Allow, evaluation.Outcome); + } + + [Fact] + public void Evaluate_UnsupportedConditionKind_DeniesClosed() + { + var evaluation = _sut.Evaluate(Set(new UnknownCondition()), Signals()); + + Assert.Equal(AccessEvaluationOutcome.Deny, evaluation.Outcome); + Assert.Equal(DenyReason.UnsupportedCondition, evaluation.Reason); + } + + private sealed class UnknownCondition : AccessCondition; +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Models/Conditions/AccessWeekdayJsonConverterTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Models/Conditions/AccessWeekdayJsonConverterTests.cs new file mode 100644 index 000000000000..c6db6a37f924 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Models/Conditions/AccessWeekdayJsonConverterTests.cs @@ -0,0 +1,52 @@ +using System.Text.Json; +using Bit.Commercial.Pam.Enums; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Models.Conditions; + +public class AccessWeekdayJsonConverterTests +{ + [Theory] + [InlineData(AccessWeekday.Sun, "\"sun\"")] + [InlineData(AccessWeekday.Mon, "\"mon\"")] + [InlineData(AccessWeekday.Tue, "\"tue\"")] + [InlineData(AccessWeekday.Wed, "\"wed\"")] + [InlineData(AccessWeekday.Thu, "\"thu\"")] + [InlineData(AccessWeekday.Fri, "\"fri\"")] + [InlineData(AccessWeekday.Sat, "\"sat\"")] + public void Serializes_AsLowercaseToken(AccessWeekday day, string expectedJson) + { + Assert.Equal(expectedJson, JsonSerializer.Serialize(day)); + } + + [Theory] + [InlineData("\"mon\"", AccessWeekday.Mon)] + [InlineData("\"MON\"", AccessWeekday.Mon)] + [InlineData("\"Sun\"", AccessWeekday.Sun)] + public void Deserializes_TokenCaseInsensitively(string json, AccessWeekday expected) + { + Assert.Equal(expected, JsonSerializer.Deserialize(json)); + } + + [Theory] + [InlineData("\"funday\"")] + [InlineData("\"\"")] + [InlineData("3")] + public void Deserialize_InvalidToken_Throws(string json) + { + Assert.Throws(() => JsonSerializer.Deserialize(json)); + } + + [Fact] + public void EnumValues_AlignWithSystemDayOfWeek() + { + // The engine casts AccessWeekday straight to System.DayOfWeek, so the numeric values must match. + Assert.Equal((int)DayOfWeek.Sunday, (int)AccessWeekday.Sun); + Assert.Equal((int)DayOfWeek.Monday, (int)AccessWeekday.Mon); + Assert.Equal((int)DayOfWeek.Tuesday, (int)AccessWeekday.Tue); + Assert.Equal((int)DayOfWeek.Wednesday, (int)AccessWeekday.Wed); + Assert.Equal((int)DayOfWeek.Thursday, (int)AccessWeekday.Thu); + Assert.Equal((int)DayOfWeek.Friday, (int)AccessWeekday.Fri); + Assert.Equal((int)DayOfWeek.Saturday, (int)AccessWeekday.Sat); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Queries/AccessPreCheckQueryTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Queries/AccessPreCheckQueryTests.cs new file mode 100644 index 000000000000..156466d28788 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Queries/AccessPreCheckQueryTests.cs @@ -0,0 +1,99 @@ +using Bit.Commercial.Pam.Engine; +using Bit.Commercial.Pam.Enums; +using Bit.Commercial.Pam.Models; +using Bit.Commercial.Pam.Models.Conditions; +using Bit.Commercial.Pam.OrganizationFeatures.Queries; +using Bit.Commercial.Pam.Services; +using Bit.Core.Exceptions; +using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Queries; + +[SutProviderCustomize] +public class AccessPreCheckQueryTests +{ + [Theory, BitAutoData] + public async Task PreCheckAsync_CipherNotAccessible_ThrowsNotFound( + SutProvider sutProvider, Guid userId, Guid cipherId) + { + sutProvider.GetDependency() + .GetByIdAsync(cipherId, userId) + .Returns((CipherDetails?)null); + + await Assert.ThrowsAsync(() => sutProvider.Sut.PreCheckAsync(userId, cipherId)); + } + + [Theory, BitAutoData] + public async Task PreCheckAsync_HumanApprovalCondition_ReturnsHuman( + SutProvider sutProvider, Guid userId, Guid cipherId, Guid orgId, Guid collectionId) + { + SetupCipher(sutProvider, userId, cipherId); + sutProvider.GetDependency() + .ResolveAsync(userId, cipherId, Arg.Any()) + .Returns(new GoverningRule(orgId, collectionId, RequiresHumanApproval: true, + [new HumanApprovalCondition()])); + + var result = await sutProvider.Sut.PreCheckAsync(userId, cipherId); + + Assert.Equal(AccessApprovalMode.Human, result.ApprovalMode); + } + + [Theory, BitAutoData] + public async Task PreCheckAsync_AutoApproveRule_ReturnsAutomatic( + SutProvider sutProvider, Guid userId, Guid cipherId, Guid orgId, Guid collectionId) + { + SetupCipher(sutProvider, userId, cipherId); + sutProvider.GetDependency() + .ResolveAsync(userId, cipherId, Arg.Any()) + .Returns(new GoverningRule(orgId, collectionId, RequiresHumanApproval: false, + [new IpAllowlistCondition { Cidrs = ["10.0.0.0/8"] }])); + + var result = await sutProvider.Sut.PreCheckAsync(userId, cipherId); + + Assert.Equal(AccessApprovalMode.Automatic, result.ApprovalMode); + } + + [Theory, BitAutoData] + public async Task PreCheckAsync_ExistingActiveLease_ReturnsHasActiveLease( + SutProvider sutProvider, Guid userId, Guid cipherId, AccessLease activeLease) + { + SetupCipher(sutProvider, userId, cipherId); + sutProvider.GetDependency() + .GetActiveByRequesterIdCipherIdAsync(userId, cipherId, Arg.Any()) + .Returns(activeLease); + + var result = await sutProvider.Sut.PreCheckAsync(userId, cipherId); + + Assert.True(result.HasActiveLease); + // The approval path is irrelevant once a lease is held, so the rule resolver is never consulted. + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ResolveAsync(default, default, default); + } + + [Theory, BitAutoData] + public async Task PreCheckAsync_NotLeasingGated_ReturnsAutomatic( + SutProvider sutProvider, Guid userId, Guid cipherId) + { + SetupCipher(sutProvider, userId, cipherId); + sutProvider.GetDependency() + .ResolveAsync(userId, cipherId, Arg.Any()) + .Returns((GoverningRule?)null); + + var result = await sutProvider.Sut.PreCheckAsync(userId, cipherId); + + Assert.Equal(AccessApprovalMode.Automatic, result.ApprovalMode); + } + + private static void SetupCipher(SutProvider sutProvider, Guid userId, Guid cipherId) + { + sutProvider.GetDependency() + .GetByIdAsync(cipherId, userId) + .Returns(new CipherDetails { Id = cipherId }); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Queries/GetCipherAccessStateQueryTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Queries/GetCipherAccessStateQueryTests.cs new file mode 100644 index 000000000000..d16ef8a8e34a --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Queries/GetCipherAccessStateQueryTests.cs @@ -0,0 +1,255 @@ +using Bit.Commercial.Pam.Engine; +using Bit.Commercial.Pam.Models; +using Bit.Commercial.Pam.Models.Conditions; +using Bit.Commercial.Pam.OrganizationFeatures.Queries; +using Bit.Commercial.Pam.Services; +using Bit.Core.Exceptions; +using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Pam.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Queries; + +[SutProviderCustomize] +public class GetCipherAccessStateQueryTests +{ + [Theory, BitAutoData] + public async Task GetStateAsync_CipherNotAccessible_ThrowsNotFound( + SutProvider sutProvider, Guid userId, Guid cipherId) + { + sutProvider.GetDependency() + .GetByIdAsync(cipherId, userId) + .Returns((CipherDetails?)null); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetStateAsync(userId, cipherId)); + } + + [Theory, BitAutoData] + public async Task GetStateAsync_NotGatedAndNothingHeld_ThrowsNotFound( + SutProvider sutProvider, Guid userId, Guid cipherId) + { + SetupCipher(sutProvider, userId, cipherId); + // No active lease, no pending request, and the resolver finds no governing rule. + sutProvider.GetDependency() + .ResolveAsync(userId, cipherId, Arg.Any()) + .Returns((GoverningRule?)null); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetStateAsync(userId, cipherId)); + } + + [Theory, BitAutoData] + public async Task GetStateAsync_ActiveLease_ReturnsSnapshotWithLease( + SutProvider sutProvider, Guid userId, Guid cipherId, AccessLease activeLease) + { + SetupCipher(sutProvider, userId, cipherId); + sutProvider.GetDependency() + .GetActiveByRequesterIdCipherIdAsync(userId, cipherId, Arg.Any()) + .Returns(activeLease); + + var result = await sutProvider.Sut.GetStateAsync(userId, cipherId); + + Assert.Equal(cipherId, result.CipherId); + Assert.Same(activeLease, result.ActiveLease); + Assert.Null(result.PendingRequest); + } + + [Theory, BitAutoData] + public async Task GetStateAsync_LeaseHeldButRuleRemoved_StillReturnsSnapshot( + SutProvider sutProvider, Guid userId, Guid cipherId, AccessLease activeLease) + { + SetupCipher(sutProvider, userId, cipherId); + sutProvider.GetDependency() + .GetActiveByRequesterIdCipherIdAsync(userId, cipherId, Arg.Any()) + .Returns(activeLease); + // Access rule since removed: resolver returns null, but the held lease must not be hidden. + sutProvider.GetDependency() + .ResolveAsync(userId, cipherId, Arg.Any()) + .Returns((GoverningRule?)null); + + var result = await sutProvider.Sut.GetStateAsync(userId, cipherId); + + Assert.Same(activeLease, result.ActiveLease); + } + + [Theory, BitAutoData] + public async Task GetStateAsync_PendingRequest_MapsToDetails( + SutProvider sutProvider, Guid userId, Guid cipherId, AccessRequest pending) + { + SetupCipher(sutProvider, userId, cipherId); + pending.CipherId = cipherId; + pending.RequesterId = userId; + pending.Status = AccessRequestStatus.Pending; + sutProvider.GetDependency() + .GetActivePendingByRequesterIdCipherIdAsync(userId, cipherId) + .Returns(pending); + + var result = await sutProvider.Sut.GetStateAsync(userId, cipherId); + + Assert.Null(result.ActiveLease); + Assert.NotNull(result.PendingRequest); + Assert.Equal(pending.Id, result.PendingRequest!.Id); + Assert.Equal(pending.ExtensionOfLeaseId, result.PendingRequest.ExtensionOfLeaseId); + Assert.Equal(AccessRequestStatus.Pending, result.PendingRequest.Status); + // Pending has produced no lease and has no resolver yet; display-name fields are not populated. + Assert.Null(result.PendingRequest.ProducedLeaseId); + Assert.Empty(result.PendingRequest.Decisions); + Assert.Null(result.PendingRequest.CipherName); + } + + [Theory, BitAutoData] + public async Task GetStateAsync_ApprovedRequest_MapsToDetails( + SutProvider sutProvider, Guid userId, Guid cipherId, AccessRequest approved) + { + SetupCipher(sutProvider, userId, cipherId); + approved.CipherId = cipherId; + approved.RequesterId = userId; + approved.Status = AccessRequestStatus.Approved; + sutProvider.GetDependency() + .GetActiveApprovedByRequesterIdCipherIdAsync(userId, cipherId, Arg.Any()) + .Returns(approved); + + var result = await sutProvider.Sut.GetStateAsync(userId, cipherId); + + Assert.Null(result.ActiveLease); + Assert.Null(result.PendingRequest); + Assert.NotNull(result.ApprovedRequest); + Assert.Equal(approved.Id, result.ApprovedRequest!.Id); + Assert.Equal(AccessRequestStatus.Approved, result.ApprovedRequest.Status); + Assert.Equal(approved.NotBefore, result.ApprovedRequest.NotBefore); + Assert.Equal(approved.NotAfter, result.ApprovedRequest.NotAfter); + // The approved read excludes activated rows, so no lease id; the caller-scoped snapshot carries no approver + // identity or display-name fields. + Assert.Null(result.ApprovedRequest.ProducedLeaseId); + Assert.Empty(result.ApprovedRequest.Decisions); + Assert.Null(result.ApprovedRequest.CipherName); + } + + [Theory, BitAutoData] + public async Task GetStateAsync_ApprovedHeldButRuleRemoved_StillReturnsSnapshot( + SutProvider sutProvider, Guid userId, Guid cipherId, AccessRequest approved) + { + SetupCipher(sutProvider, userId, cipherId); + approved.Status = AccessRequestStatus.Approved; + sutProvider.GetDependency() + .GetActiveApprovedByRequesterIdCipherIdAsync(userId, cipherId, Arg.Any()) + .Returns(approved); + // Access rule since removed: resolver returns null, but the startable approval must not be hidden. + sutProvider.GetDependency() + .ResolveAsync(userId, cipherId, Arg.Any()) + .Returns((GoverningRule?)null); + + var result = await sutProvider.Sut.GetStateAsync(userId, cipherId); + + Assert.NotNull(result.ApprovedRequest); + Assert.Equal(approved.Id, result.ApprovedRequest!.Id); + } + + [Theory, BitAutoData] + public async Task GetStateAsync_GatedButEmpty_ReturnsEmptySnapshot( + SutProvider sutProvider, Guid userId, Guid cipherId, Guid orgId, Guid collectionId) + { + SetupCipher(sutProvider, userId, cipherId); + sutProvider.GetDependency() + .ResolveAsync(userId, cipherId, Arg.Any()) + .Returns(new GoverningRule(orgId, collectionId, RequiresHumanApproval: true, + [new HumanApprovalCondition()])); + + var result = await sutProvider.Sut.GetStateAsync(userId, cipherId); + + Assert.Equal(cipherId, result.CipherId); + Assert.Null(result.ActiveLease); + Assert.Null(result.PendingRequest); + Assert.Null(result.ApprovedRequest); + } + + [Theory, BitAutoData] + public async Task GetStateAsync_ActiveLease_NotYetExtended_AllowedWithMaxLength( + SutProvider sutProvider, Guid userId, Guid cipherId, Guid orgId, Guid collectionId, + AccessLease activeLease) + { + SetupCipher(sutProvider, userId, cipherId); + sutProvider.GetDependency() + .GetActiveByRequesterIdCipherIdAsync(userId, cipherId, Arg.Any()) + .Returns(activeLease); + sutProvider.GetDependency() + .ResolveAsync(userId, cipherId, Arg.Any()) + .Returns(new GoverningRule(orgId, collectionId, RequiresHumanApproval: false, + [new HumanApprovalCondition()]) + { + AllowsExtensions = true, + MaxExtensionDurationSeconds = 4 * 60 * 60, + }); + sutProvider.GetDependency() + .CountExtensionsByLeaseIdAsync(activeLease.Id).Returns(0); + + var result = await sutProvider.Sut.GetStateAsync(userId, cipherId); + + Assert.True(result.ExtensionsAllowed); + Assert.Equal(4 * 60 * 60, result.MaxExtensionDurationSeconds); + } + + [Theory, BitAutoData] + public async Task GetStateAsync_ActiveLease_AlreadyExtended_NotAllowed( + SutProvider sutProvider, Guid userId, Guid cipherId, Guid orgId, Guid collectionId, + AccessLease activeLease) + { + SetupCipher(sutProvider, userId, cipherId); + sutProvider.GetDependency() + .GetActiveByRequesterIdCipherIdAsync(userId, cipherId, Arg.Any()) + .Returns(activeLease); + sutProvider.GetDependency() + .ResolveAsync(userId, cipherId, Arg.Any()) + .Returns(new GoverningRule(orgId, collectionId, RequiresHumanApproval: false, + [new HumanApprovalCondition()]) + { + AllowsExtensions = true, + MaxExtensionDurationSeconds = 2 * 60 * 60, + }); + // A lease may be extended once; an existing extension means no more are allowed. + sutProvider.GetDependency() + .CountExtensionsByLeaseIdAsync(activeLease.Id).Returns(1); + + var result = await sutProvider.Sut.GetStateAsync(userId, cipherId); + + Assert.False(result.ExtensionsAllowed); + Assert.Equal(2 * 60 * 60, result.MaxExtensionDurationSeconds); + } + + [Theory, BitAutoData] + public async Task GetStateAsync_ActiveLease_ExtensionsDisallowed_ReportsNotAllowed( + SutProvider sutProvider, Guid userId, Guid cipherId, Guid orgId, Guid collectionId, + AccessLease activeLease) + { + SetupCipher(sutProvider, userId, cipherId); + sutProvider.GetDependency() + .GetActiveByRequesterIdCipherIdAsync(userId, cipherId, Arg.Any()) + .Returns(activeLease); + sutProvider.GetDependency() + .ResolveAsync(userId, cipherId, Arg.Any()) + .Returns(new GoverningRule(orgId, collectionId, RequiresHumanApproval: false, + [new HumanApprovalCondition()]) + { + AllowsExtensions = false, + }); + + var result = await sutProvider.Sut.GetStateAsync(userId, cipherId); + + Assert.False(result.ExtensionsAllowed); + Assert.Null(result.MaxExtensionDurationSeconds); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CountExtensionsByLeaseIdAsync(default); + } + + private static void SetupCipher(SutProvider sutProvider, Guid userId, Guid cipherId) + { + sutProvider.GetDependency() + .GetByIdAsync(cipherId, userId) + .Returns(new CipherDetails { Id = cipherId }); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Queries/GetLeasedCipherQueryTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Queries/GetLeasedCipherQueryTests.cs new file mode 100644 index 000000000000..34a759a98899 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Queries/GetLeasedCipherQueryTests.cs @@ -0,0 +1,159 @@ +using Bit.Commercial.Pam.Engine; +using Bit.Commercial.Pam.Models; +using Bit.Commercial.Pam.Models.Conditions; +using Bit.Commercial.Pam.OrganizationFeatures.Queries; +using Bit.Commercial.Pam.Services; +using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Queries; + +[SutProviderCustomize] +public class GetLeasedCipherQueryTests +{ + private static readonly DateTime _now = new(2026, 6, 4, 12, 0, 0, DateTimeKind.Utc); + + [Theory, BitAutoData] + public async Task GetLeasedCipherAsync_NoActiveLease_ReturnsNull(Guid userId, Guid cipherId) + { + var sutProvider = Setup(); + sutProvider.GetDependency() + .GetActiveByRequesterIdCipherIdAsync(userId, cipherId, _now) + .Returns((AccessLease?)null); + + var result = await sutProvider.Sut.GetLeasedCipherAsync(userId, cipherId); + + Assert.Null(result); + // Accessibility is never consulted when there is no lease. + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .GetByIdAsync(default, default); + } + + [Theory, BitAutoData] + public async Task GetLeasedCipherAsync_ActiveLeaseButCipherNotAccessible_ReturnsNull( + Guid userId, Guid cipherId, AccessLease lease) + { + var sutProvider = Setup(); + sutProvider.GetDependency() + .GetActiveByRequesterIdCipherIdAsync(userId, cipherId, _now) + .Returns(lease); + sutProvider.GetDependency() + .GetByIdAsync(cipherId, userId) + .Returns((CipherDetails?)null); + + var result = await sutProvider.Sut.GetLeasedCipherAsync(userId, cipherId); + + Assert.Null(result); + } + + [Theory, BitAutoData] + public async Task GetLeasedCipherAsync_ActiveLeaseAndAccessible_ReturnsCipher( + Guid userId, Guid cipherId, AccessLease lease) + { + var sutProvider = Setup(); + var cipher = new CipherDetails { Id = cipherId, Data = "2.iv|ct|mac" }; + sutProvider.GetDependency() + .GetActiveByRequesterIdCipherIdAsync(userId, cipherId, _now) + .Returns(lease); + sutProvider.GetDependency() + .GetByIdAsync(cipherId, userId) + .Returns(cipher); + + var result = await sutProvider.Sut.GetLeasedCipherAsync(userId, cipherId); + + Assert.NotNull(result); + Assert.Equal(cipherId, result!.Id); + Assert.Equal("2.iv|ct|mac", result.Data); + // The active-lease lookup uses the TimeProvider's now. + await sutProvider.GetDependency().Received(1) + .GetActiveByRequesterIdCipherIdAsync(userId, cipherId, _now); + } + + [Theory, BitAutoData] + public async Task GetLeasedCipherAsync_ConditionsDeny_WithholdsDataAndReturnsNull( + Guid userId, Guid cipherId, AccessLease lease, Guid orgId, Guid collectionId) + { + var sutProvider = Setup(); + sutProvider.GetDependency() + .GetActiveByRequesterIdCipherIdAsync(userId, cipherId, _now) + .Returns(lease); + SetupResolution(sutProvider, userId, cipherId, orgId, collectionId); + SetupEvaluation(sutProvider, AccessEvaluation.Deny(DenyReason.NotWithinIpRange)); + + var result = await sutProvider.Sut.GetLeasedCipherAsync(userId, cipherId); + + Assert.Null(result); + // A denied evaluation must withhold the data: the cipher is never read. + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .GetByIdAsync(default, default); + } + + [Theory, BitAutoData] + public async Task GetLeasedCipherAsync_ConditionsAllow_ReturnsCipher( + Guid userId, Guid cipherId, AccessLease lease, Guid orgId, Guid collectionId) + { + var sutProvider = Setup(); + var cipher = new CipherDetails { Id = cipherId }; + sutProvider.GetDependency() + .GetActiveByRequesterIdCipherIdAsync(userId, cipherId, _now) + .Returns(lease); + sutProvider.GetDependency().GetByIdAsync(cipherId, userId).Returns(cipher); + SetupResolution(sutProvider, userId, cipherId, orgId, collectionId); + SetupEvaluation(sutProvider, AccessEvaluation.Allow); + + var result = await sutProvider.Sut.GetLeasedCipherAsync(userId, cipherId); + + Assert.Same(cipher, result); + } + + [Theory, BitAutoData] + public async Task GetLeasedCipherAsync_ConditionsRequireApproval_StillReturnsCipher( + Guid userId, Guid cipherId, AccessLease lease, Guid orgId, Guid collectionId) + { + var sutProvider = Setup(); + var cipher = new CipherDetails { Id = cipherId }; + sutProvider.GetDependency() + .GetActiveByRequesterIdCipherIdAsync(userId, cipherId, _now) + .Returns(lease); + sutProvider.GetDependency().GetByIdAsync(cipherId, userId).Returns(cipher); + SetupResolution(sutProvider, userId, cipherId, orgId, collectionId); + // Holding the lease is proof approval was already granted, so a deferred-approval outcome must not re-gate. + SetupEvaluation(sutProvider, AccessEvaluation.RequiresApproval); + + var result = await sutProvider.Sut.GetLeasedCipherAsync(userId, cipherId); + + Assert.Same(cipher, result); + } + + private static SutProvider Setup() + { + var sutProvider = new SutProvider().WithFakeTimeProvider().Create(); + sutProvider.GetDependency().SetUtcNow(_now); + return sutProvider; + } + + private static void SetupResolution(SutProvider sutProvider, Guid userId, Guid cipherId, + Guid orgId, Guid collectionId) + { + sutProvider.GetDependency() + .ResolveAsync(userId, cipherId, Arg.Any()) + .Returns(new GoverningRule(orgId, collectionId, false, + [new IpAllowlistCondition { Cidrs = ["10.0.0.0/8"] }])); + } + + private static void SetupEvaluation(SutProvider sutProvider, AccessEvaluation decision) + { + sutProvider.GetDependency() + .Evaluate(Arg.Any>(), Arg.Any()) + .Returns(decision); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Queries/ListActiveLeasesQueryTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Queries/ListActiveLeasesQueryTests.cs new file mode 100644 index 000000000000..a64701cff4f0 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Queries/ListActiveLeasesQueryTests.cs @@ -0,0 +1,56 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Queries; +using Bit.Commercial.Pam.Services; +using Bit.Pam.Entities; +using Bit.Pam.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Queries; + +[SutProviderCustomize] +public class ListActiveLeasesQueryTests +{ + private static readonly DateTime _now = new(2026, 6, 5, 12, 0, 0, DateTimeKind.Utc); + + [Theory, BitAutoData] + public async Task GetActiveAsync_NoManageableCollections_ReturnsEmptyWithoutQuerying(Guid userId) + { + var sutProvider = Setup(); + sutProvider.GetDependency() + .GetManageableCollectionIdsAsync(userId).Returns([]); + + var result = await sutProvider.Sut.GetActiveAsync(userId); + + Assert.Empty(result); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .GetManyActiveByCollectionIdsAsync(default!, default); + } + + [Theory, BitAutoData] + public async Task GetActiveAsync_ManageableCollections_FiltersByThatSetAtNow( + Guid userId, Guid collectionId, AccessLease lease) + { + var sutProvider = Setup(); + var manageable = new HashSet { collectionId }; + sutProvider.GetDependency() + .GetManageableCollectionIdsAsync(userId).Returns(manageable); + sutProvider.GetDependency() + .GetManyActiveByCollectionIdsAsync(manageable, _now).Returns([lease]); + + var result = await sutProvider.Sut.GetActiveAsync(userId); + + Assert.Single(result); + await sutProvider.GetDependency().Received(1) + .GetManyActiveByCollectionIdsAsync(manageable, _now); + } + + private static SutProvider Setup() + { + var sutProvider = new SutProvider().WithFakeTimeProvider().Create(); + sutProvider.GetDependency().SetUtcNow(_now); + return sutProvider; + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Queries/ListInboxHistoryQueryTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Queries/ListInboxHistoryQueryTests.cs new file mode 100644 index 000000000000..0b29ab4d9a09 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Queries/ListInboxHistoryQueryTests.cs @@ -0,0 +1,56 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Queries; +using Bit.Commercial.Pam.Services; +using Bit.Pam.Models; +using Bit.Pam.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Queries; + +[SutProviderCustomize] +public class ListInboxHistoryQueryTests +{ + private static readonly DateTime _now = new(2026, 6, 5, 12, 0, 0, DateTimeKind.Utc); + + [Theory, BitAutoData] + public async Task GetHistoryAsync_NoManageableCollections_ReturnsEmptyWithoutQuerying(Guid userId) + { + var sutProvider = Setup(); + sutProvider.GetDependency() + .GetManageableCollectionIdsAsync(userId).Returns([]); + + var result = await sutProvider.Sut.GetHistoryAsync(userId); + + Assert.Empty(result); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .GetManyInboxHistoryByCollectionIdsAsync(default!, default); + } + + [Theory, BitAutoData] + public async Task GetHistoryAsync_QueriesWithRetentionWindow(Guid userId, Guid collectionId, AccessRequestDetails row) + { + var sutProvider = Setup(); + var manageable = new HashSet { collectionId }; + sutProvider.GetDependency() + .GetManageableCollectionIdsAsync(userId).Returns(manageable); + var expectedSince = _now.AddDays(-ListInboxHistoryQuery.HistoryRetentionDays); + sutProvider.GetDependency() + .GetManyInboxHistoryByCollectionIdsAsync(manageable, expectedSince).Returns([row]); + + var result = await sutProvider.Sut.GetHistoryAsync(userId); + + Assert.Single(result); + await sutProvider.GetDependency().Received(1) + .GetManyInboxHistoryByCollectionIdsAsync(manageable, expectedSince); + } + + private static SutProvider Setup() + { + var sutProvider = new SutProvider().WithFakeTimeProvider().Create(); + sutProvider.GetDependency().SetUtcNow(_now); + return sutProvider; + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Queries/ListInboxRequestsQueryTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Queries/ListInboxRequestsQueryTests.cs new file mode 100644 index 000000000000..d8b183cdc073 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Queries/ListInboxRequestsQueryTests.cs @@ -0,0 +1,45 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Queries; +using Bit.Commercial.Pam.Services; +using Bit.Pam.Models; +using Bit.Pam.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Queries; + +[SutProviderCustomize] +public class ListInboxRequestsQueryTests +{ + [Theory, BitAutoData] + public async Task GetPendingAsync_NoManageableCollections_ReturnsEmptyWithoutQuerying( + SutProvider sutProvider, Guid userId) + { + sutProvider.GetDependency() + .GetManageableCollectionIdsAsync(userId).Returns([]); + + var result = await sutProvider.Sut.GetPendingAsync(userId); + + Assert.Empty(result); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .GetManyInboxPendingByCollectionIdsAsync(default!); + } + + [Theory, BitAutoData] + public async Task GetPendingAsync_ManageableCollections_FiltersByThatSet( + SutProvider sutProvider, Guid userId, Guid collectionId, AccessRequestDetails row) + { + var manageable = new HashSet { collectionId }; + sutProvider.GetDependency() + .GetManageableCollectionIdsAsync(userId).Returns(manageable); + sutProvider.GetDependency() + .GetManyInboxPendingByCollectionIdsAsync(manageable).Returns([row]); + + var result = await sutProvider.Sut.GetPendingAsync(userId); + + Assert.Single(result); + await sutProvider.GetDependency().Received(1) + .GetManyInboxPendingByCollectionIdsAsync(manageable); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Queries/ListLeaseHistoryQueryTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Queries/ListLeaseHistoryQueryTests.cs new file mode 100644 index 000000000000..1923374c8b39 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Queries/ListLeaseHistoryQueryTests.cs @@ -0,0 +1,58 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Queries; +using Bit.Commercial.Pam.Services; +using Bit.Pam.Entities; +using Bit.Pam.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Queries; + +[SutProviderCustomize] +public class ListLeaseHistoryQueryTests +{ + private static readonly DateTime _now = new(2026, 6, 5, 12, 0, 0, DateTimeKind.Utc); + + [Theory, BitAutoData] + public async Task GetHistoryAsync_NoManageableCollections_ReturnsEmptyWithoutQuerying(Guid userId) + { + var sutProvider = Setup(); + sutProvider.GetDependency() + .GetManageableCollectionIdsAsync(userId).Returns([]); + + var result = await sutProvider.Sut.GetHistoryAsync(userId); + + Assert.Empty(result); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .GetManyEndedByCollectionIdsAsync(default!, default); + } + + [Theory, BitAutoData] + public async Task GetHistoryAsync_QueriesWithSharedRetentionWindow( + Guid userId, Guid collectionId, AccessLease lease) + { + var sutProvider = Setup(); + var manageable = new HashSet { collectionId }; + sutProvider.GetDependency() + .GetManageableCollectionIdsAsync(userId).Returns(manageable); + // Shares the approver inbox's history window. + var expectedSince = _now.AddDays(-ListInboxHistoryQuery.HistoryRetentionDays); + sutProvider.GetDependency() + .GetManyEndedByCollectionIdsAsync(manageable, expectedSince).Returns([lease]); + + var result = await sutProvider.Sut.GetHistoryAsync(userId); + + Assert.Single(result); + await sutProvider.GetDependency().Received(1) + .GetManyEndedByCollectionIdsAsync(manageable, expectedSince); + } + + private static SutProvider Setup() + { + var sutProvider = new SutProvider().WithFakeTimeProvider().Create(); + sutProvider.GetDependency().SetUtcNow(_now); + return sutProvider; + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Queries/ListMyAccessRequestsQueryTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Queries/ListMyAccessRequestsQueryTests.cs new file mode 100644 index 000000000000..c5492af628c1 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Queries/ListMyAccessRequestsQueryTests.cs @@ -0,0 +1,40 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Queries; +using Bit.Pam.Models; +using Bit.Pam.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Queries; + +[SutProviderCustomize] +public class ListMyAccessRequestsQueryTests +{ + [Theory, BitAutoData] + public async Task GetMineAsync_ReturnsRequesterRows( + SutProvider sutProvider, Guid userId, AccessRequestDetails row) + { + sutProvider.GetDependency() + .GetManyByRequesterIdAsync(userId) + .Returns([row]); + + var result = await sutProvider.Sut.GetMineAsync(userId); + + Assert.Single(result); + Assert.Equal(row.Id, result.First().Id); + } + + [Theory, BitAutoData] + public async Task GetMineAsync_NoRequests_ReturnsEmpty( + SutProvider sutProvider, Guid userId) + { + sutProvider.GetDependency() + .GetManyByRequesterIdAsync(userId) + .Returns([]); + + var result = await sutProvider.Sut.GetMineAsync(userId); + + Assert.Empty(result); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Queries/ListMyActiveAccessLeasesQueryTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Queries/ListMyActiveAccessLeasesQueryTests.cs new file mode 100644 index 000000000000..60763aecf016 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Queries/ListMyActiveAccessLeasesQueryTests.cs @@ -0,0 +1,40 @@ +using Bit.Commercial.Pam.OrganizationFeatures.Queries; +using Bit.Pam.Entities; +using Bit.Pam.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Queries; + +[SutProviderCustomize] +public class ListMyActiveAccessLeasesQueryTests +{ + [Theory, BitAutoData] + public async Task GetMineActiveAsync_ReturnsActiveLeases( + SutProvider sutProvider, Guid userId, AccessLease lease) + { + sutProvider.GetDependency() + .GetManyActiveByRequesterIdAsync(userId, Arg.Any()) + .Returns([lease]); + + var result = await sutProvider.Sut.GetMineActiveAsync(userId); + + Assert.Single(result); + Assert.Equal(lease.Id, result.First().Id); + } + + [Theory, BitAutoData] + public async Task GetMineActiveAsync_NoLeases_ReturnsEmpty( + SutProvider sutProvider, Guid userId) + { + sutProvider.GetDependency() + .GetManyActiveByRequesterIdAsync(userId, Arg.Any()) + .Returns([]); + + var result = await sutProvider.Sut.GetMineActiveAsync(userId); + + Assert.Empty(result); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Services/AccessRuleValidatorTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Services/AccessRuleValidatorTests.cs new file mode 100644 index 000000000000..ba0384343f6a --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Services/AccessRuleValidatorTests.cs @@ -0,0 +1,173 @@ +using Bit.Commercial.Pam.Services; +using Xunit; + +namespace Bit.Commercial.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_TimeOfDay_Valid_IsValid() + { + var result = _sut.Validate(""" + [ + { + "kind": "time_of_day", + "tz": "UTC", + "windows": [ + { "days": ["mon","tue","wed","thu","fri"], "from": "09:00", "to": "18:00" } + ] + } + ] + """); + + Assert.True(result.IsValid); + } + + [Theory] + [InlineData("""[{"kind":"time_of_day","tz":"Invalid/Zone","windows":[{"days":["mon"],"from":"09:00","to":"17:00"}]}]""", "timezone")] + [InlineData("""[{"kind":"time_of_day","tz":"UTC","windows":[]}]""", "at least one window")] + [InlineData("""[{"kind":"time_of_day","tz":"UTC","windows":[{"days":[],"from":"09:00","to":"17:00"}]}]""", "at least one day")] + [InlineData("""[{"kind":"time_of_day","tz":"UTC","windows":[{"days":["funday"],"from":"09:00","to":"17:00"}]}]""", "day")] + [InlineData("""[{"kind":"time_of_day","tz":"UTC","windows":[{"days":["mon"],"from":"9am","to":"5pm"}]}]""", "Expected HH:mm")] + [InlineData("""[{"kind":"time_of_day","tz":"UTC","windows":[{"days":["mon"],"from":"25:00","to":"26:00"}]}]""", "Expected HH:mm")] + public void Validate_TimeOfDay_Invalid_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/Commercial.Pam.Test/Services/ApproverCollectionAccessQueryTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Services/ApproverCollectionAccessQueryTests.cs new file mode 100644 index 000000000000..fc79d8186bcf --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Services/ApproverCollectionAccessQueryTests.cs @@ -0,0 +1,107 @@ +using Bit.Commercial.Pam.Services; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Services; + +[SutProviderCustomize] +public class ApproverCollectionAccessQueryTests +{ + [Theory, BitAutoData] + public async Task GetManageableCollectionIdsAsync_ReturnsOnlyAssignedManageCollections( + SutProvider sutProvider, Guid userId, Guid manageId, Guid readOnlyId) + { + sutProvider.GetDependency().GetManyByUserIdAsync(userId).Returns(new List + { + new() { Id = manageId, Manage = true }, + new() { Id = readOnlyId, Manage = false }, + }); + sutProvider.GetDependency().Organizations.Returns(new List()); + + var result = await sutProvider.Sut.GetManageableCollectionIdsAsync(userId); + + Assert.Contains(manageId, result); + Assert.DoesNotContain(readOnlyId, result); + } + + [Theory, BitAutoData] + public async Task GetManageableCollectionIdsAsync_OwnerWithAdminAccess_IncludesAllOrgCollections( + SutProvider sutProvider, Guid userId, Guid orgId, Guid orgCollectionId) + { + sutProvider.GetDependency().GetManyByUserIdAsync(userId) + .Returns(new List()); + sutProvider.GetDependency().Organizations.Returns(new List + { + new() { Id = orgId, Type = OrganizationUserType.Owner }, + }); + sutProvider.GetDependency().GetOrganizationAbilityAsync(orgId) + .Returns(new OrganizationAbility { Id = orgId, AllowAdminAccessToAllCollectionItems = true }); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(orgId) + .Returns(new List { new() { Id = orgCollectionId, OrganizationId = orgId } }); + + var result = await sutProvider.Sut.GetManageableCollectionIdsAsync(userId); + + Assert.Contains(orgCollectionId, result); + } + + [Theory, BitAutoData] + public async Task GetManageableCollectionIdsAsync_OwnerWithoutAdminAccess_DoesNotIncludeAllOrgCollections( + SutProvider sutProvider, Guid userId, Guid orgId) + { + sutProvider.GetDependency().GetManyByUserIdAsync(userId) + .Returns(new List()); + sutProvider.GetDependency().Organizations.Returns(new List + { + new() { Id = orgId, Type = OrganizationUserType.Owner }, + }); + sutProvider.GetDependency().GetOrganizationAbilityAsync(orgId) + .Returns(new OrganizationAbility { Id = orgId, AllowAdminAccessToAllCollectionItems = false }); + + var result = await sutProvider.Sut.GetManageableCollectionIdsAsync(userId); + + Assert.Empty(result); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .GetManyByOrganizationIdAsync(default); + } + + [Theory, BitAutoData] + public async Task GetManageableCollectionIdsAsync_EditAnyCollection_IncludesAllOrgCollections( + SutProvider sutProvider, Guid userId, Guid orgId, Guid orgCollectionId) + { + sutProvider.GetDependency().GetManyByUserIdAsync(userId) + .Returns(new List()); + sutProvider.GetDependency().Organizations.Returns(new List + { + new() { Id = orgId, Type = OrganizationUserType.Custom, Permissions = new Permissions { EditAnyCollection = true } }, + }); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(orgId) + .Returns(new List { new() { Id = orgCollectionId, OrganizationId = orgId } }); + + var result = await sutProvider.Sut.GetManageableCollectionIdsAsync(userId); + + Assert.Contains(orgCollectionId, result); + } + + [Theory, BitAutoData] + public async Task CanManageCollectionAsync_DelegatesToManageableSet( + SutProvider sutProvider, Guid userId, Guid manageId, Guid otherId) + { + sutProvider.GetDependency().GetManyByUserIdAsync(userId).Returns(new List + { + new() { Id = manageId, Manage = true }, + }); + sutProvider.GetDependency().Organizations.Returns(new List()); + + Assert.True(await sutProvider.Sut.CanManageCollectionAsync(userId, manageId)); + Assert.False(await sutProvider.Sut.CanManageCollectionAsync(userId, otherId)); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Services/ApproverInboxNotifierTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Services/ApproverInboxNotifierTests.cs new file mode 100644 index 000000000000..1011e3796386 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Services/ApproverInboxNotifierTests.cs @@ -0,0 +1,41 @@ +using Bit.Commercial.Pam.Services; +using Bit.Core.Platform.Push; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Services; + +[SutProviderCustomize] +public class ApproverInboxNotifierTests +{ + [Theory, BitAutoData] + public async Task NotifyCollectionApproversAsync_PushesToEachManager( + SutProvider sutProvider, Guid collectionId, Guid userA, Guid userB) + { + sutProvider.GetDependency() + .GetManagingUserIdsAsync(collectionId) + .Returns(new List { userA, userB }); + + await sutProvider.Sut.NotifyCollectionApproversAsync(collectionId); + + await sutProvider.GetDependency().Received(1).PushRefreshApproverInboxAsync(userA); + await sutProvider.GetDependency().Received(1).PushRefreshApproverInboxAsync(userB); + } + + [Theory, BitAutoData] + public async Task NotifyCollectionApproversAsync_NoManagers_PushesNothing( + SutProvider sutProvider, Guid collectionId) + { + sutProvider.GetDependency() + .GetManagingUserIdsAsync(collectionId) + .Returns(new List()); + + await sutProvider.Sut.NotifyCollectionApproversAsync(collectionId); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .PushRefreshApproverInboxAsync(default); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Services/CipherLeaseGateTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Services/CipherLeaseGateTests.cs new file mode 100644 index 000000000000..e32bf7295abf --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Services/CipherLeaseGateTests.cs @@ -0,0 +1,223 @@ +using Bit.Commercial.Pam.Engine; +using Bit.Commercial.Pam.Models; +using Bit.Commercial.Pam.Models.Conditions; +using Bit.Commercial.Pam.Services; +using Bit.Core; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.Services; +using Bit.Core.Vault.Entities; +using Bit.Pam.Entities; +using Bit.Pam.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Services; + +[SutProviderCustomize] +public class CipherLeaseGateTests +{ + private static void EnableFlag(SutProvider sutProvider) => + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.Pam).Returns(true); + + private static void Gated(SutProvider sutProvider, Guid userId, Guid cipherId) => + sutProvider.GetDependency() + .ResolveAsync(userId, cipherId, Arg.Any()) + .Returns(new GoverningRule(Guid.NewGuid(), Guid.NewGuid(), RequiresHumanApproval: false, + Array.Empty())); + + private static void HasActiveLease(SutProvider sutProvider, Guid userId, Guid cipherId) => + sutProvider.GetDependency() + .GetActiveByRequesterIdCipherIdAsync(userId, cipherId, Arg.Any()) + .Returns(new AccessLease { CipherId = cipherId }); + + // --- AuthorizeReadAsync ------------------------------------------------------------------------ + + [Theory, BitAutoData] + public async Task AuthorizeReadAsync_FlagOff_AuthorizesWithoutQuerying( + SutProvider sutProvider, Guid userId, Guid cipherId) + { + var access = await sutProvider.Sut.AuthorizeReadAsync(userId, new Cipher { Id = cipherId }); + + Assert.NotNull(access); + Assert.True(access!.Authorizes(cipherId)); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs().ResolveAsync(default, default, default!); + } + + [Theory, BitAutoData] + public async Task AuthorizeReadAsync_NotGated_Authorizes( + SutProvider sutProvider, Guid userId, Guid cipherId) + { + EnableFlag(sutProvider); + sutProvider.GetDependency() + .ResolveAsync(userId, cipherId, Arg.Any()) + .Returns((GoverningRule?)null); + + var access = await sutProvider.Sut.AuthorizeReadAsync(userId, new Cipher { Id = cipherId }); + + Assert.NotNull(access); + Assert.True(access!.Authorizes(cipherId)); + } + + [Theory, BitAutoData] + public async Task AuthorizeReadAsync_GatedNoLease_ReturnsNull( + SutProvider sutProvider, Guid userId, Guid cipherId) + { + EnableFlag(sutProvider); + Gated(sutProvider, userId, cipherId); + + var access = await sutProvider.Sut.AuthorizeReadAsync(userId, new Cipher { Id = cipherId }); + + Assert.Null(access); + } + + [Theory, BitAutoData] + public async Task AuthorizeReadAsync_GatedWithLease_Authorizes( + SutProvider sutProvider, Guid userId, Guid cipherId) + { + EnableFlag(sutProvider); + Gated(sutProvider, userId, cipherId); + HasActiveLease(sutProvider, userId, cipherId); + + var access = await sutProvider.Sut.AuthorizeReadAsync(userId, new Cipher { Id = cipherId }); + + Assert.NotNull(access); + Assert.True(access!.Authorizes(cipherId)); + } + + // --- GetGatedCipherIds ------------------------------------------------------------------------- + + [Theory, BitAutoData] + public void GetGatedCipherIds_FlagOff_Empty( + SutProvider sutProvider, Guid leasingCollectionId, Guid cipherId) + { + var collections = new[] { new CollectionDetails { Id = leasingCollectionId, AccessRuleId = Guid.NewGuid() } }; + var mappings = Group(new CollectionCipher { CipherId = cipherId, CollectionId = leasingCollectionId }); + + var gated = sutProvider.Sut.GetGatedCipherIds(collections, mappings); + + Assert.Empty(gated); + } + + [Theory, BitAutoData] + public void GetGatedCipherIds_ReachableOnlyThroughLeasingCollection_IsGated( + SutProvider sutProvider, Guid leasingCollectionId, Guid cipherId) + { + EnableFlag(sutProvider); + var collections = new[] { new CollectionDetails { Id = leasingCollectionId, AccessRuleId = Guid.NewGuid() } }; + var mappings = Group(new CollectionCipher { CipherId = cipherId, CollectionId = leasingCollectionId }); + + var gated = sutProvider.Sut.GetGatedCipherIds(collections, mappings); + + Assert.Contains(cipherId, gated); + } + + [Theory, BitAutoData] + public void GetGatedCipherIds_AlsoReachableThroughNonLeasingCollection_NotGated( + SutProvider sutProvider, Guid leasingCollectionId, Guid plainCollectionId, Guid cipherId) + { + EnableFlag(sutProvider); + var collections = new[] + { + new CollectionDetails { Id = leasingCollectionId, AccessRuleId = Guid.NewGuid() }, + new CollectionDetails { Id = plainCollectionId, AccessRuleId = null }, + }; + var mappings = Group( + new CollectionCipher { CipherId = cipherId, CollectionId = leasingCollectionId }, + new CollectionCipher { CipherId = cipherId, CollectionId = plainCollectionId }); + + var gated = sutProvider.Sut.GetGatedCipherIds(collections, mappings); + + Assert.DoesNotContain(cipherId, gated); + } + + // --- AuthorizeReadManyAsync -------------------------------------------------------------------- + + [Theory, BitAutoData] + public async Task AuthorizeReadManyAsync_AuthorizesNonGatedOnly( + SutProvider sutProvider, Guid userId, Guid leasingCollectionId, Guid gatedCipherId, Guid plainCipherId) + { + EnableFlag(sutProvider); + var collections = new[] { new CollectionDetails { Id = leasingCollectionId, AccessRuleId = Guid.NewGuid() } }; + var mappings = Group(new CollectionCipher { CipherId = gatedCipherId, CollectionId = leasingCollectionId }); + var ciphers = new[] { new Cipher { Id = gatedCipherId }, new Cipher { Id = plainCipherId } }; + + var access = await sutProvider.Sut.AuthorizeReadManyAsync(userId, ciphers, collections, mappings); + + Assert.False(access.Authorizes(gatedCipherId)); + Assert.True(access.Authorizes(plainCipherId)); + } + + // --- EnsureCanMutateAsync ---------------------------------------------------------------------- + + [Theory, BitAutoData] + public async Task EnsureCanMutateAsync_FlagOff_DoesNotThrow( + SutProvider sutProvider, Guid userId, Guid cipherId) + { + var access = await sutProvider.Sut.EnsureCanMutateAsync(userId, new Cipher { Id = cipherId }); + Assert.True(access.Authorizes(cipherId)); + } + + [Theory, BitAutoData] + public async Task EnsureCanMutateAsync_GatedNoLease_ThrowsNotFound( + SutProvider sutProvider, Guid userId, Guid cipherId) + { + EnableFlag(sutProvider); + Gated(sutProvider, userId, cipherId); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.EnsureCanMutateAsync(userId, new Cipher { Id = cipherId })); + } + + [Theory, BitAutoData] + public async Task EnsureCanMutateAsync_GatedWithLease_DoesNotThrow( + SutProvider sutProvider, Guid userId, Guid cipherId) + { + EnableFlag(sutProvider); + Gated(sutProvider, userId, cipherId); + HasActiveLease(sutProvider, userId, cipherId); + + var access = await sutProvider.Sut.EnsureCanMutateAsync(userId, new Cipher { Id = cipherId }); + + Assert.True(access.Authorizes(cipherId)); + } + + // --- EnsureCanMutateManyAsync ------------------------------------------------------------------ + + [Theory, BitAutoData] + public async Task EnsureCanMutateManyAsync_LeasedCipher_SkipsResolve( + SutProvider sutProvider, Guid userId, Guid cipherId) + { + EnableFlag(sutProvider); + sutProvider.GetDependency() + .GetManyActiveByRequesterIdAsync(userId, Arg.Any()) + .Returns(new List { new() { CipherId = cipherId } }); + + await sutProvider.Sut.EnsureCanMutateManyAsync(userId, new[] { new Cipher { Id = cipherId } }); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs().ResolveAsync(default, default, default!); + } + + [Theory, BitAutoData] + public async Task EnsureCanMutateManyAsync_OneGatedNoLease_ThrowsNotFound( + SutProvider sutProvider, Guid userId, Guid gatedCipherId, Guid plainCipherId) + { + EnableFlag(sutProvider); + sutProvider.GetDependency() + .GetManyActiveByRequesterIdAsync(userId, Arg.Any()) + .Returns(new List()); + Gated(sutProvider, userId, gatedCipherId); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.EnsureCanMutateManyAsync(userId, + new[] { new Cipher { Id = plainCipherId }, new Cipher { Id = gatedCipherId } })); + } + + private static IDictionary> Group(params CollectionCipher[] collectionCiphers) => + collectionCiphers.GroupBy(cc => cc.CipherId).ToDictionary(g => g.Key); +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Services/GoverningRuleResolverTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Services/GoverningRuleResolverTests.cs new file mode 100644 index 000000000000..7875024f3d52 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Services/GoverningRuleResolverTests.cs @@ -0,0 +1,255 @@ +using System.Net; +using Bit.Commercial.Pam.Engine; +using Bit.Commercial.Pam.Models.Conditions; +using Bit.Commercial.Pam.Services; +using Bit.Core.Entities; +using Bit.Core.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Services; + +[SutProviderCustomize] +public class GoverningRuleResolverTests +{ + // The resolver now evaluates each candidate rule, so the tests drive the real engine through the substitute. + private static readonly IAccessRuleEngine _engine = new AccessRuleEngine(); + + // An in-range IP for the 10.0.0.0/8 allowlists below; out-of-range for the 192.168/172.16 allowlists, which + // therefore deny. No time-of-day conditions are used, so the timestamp is arbitrary. + private static readonly AccessSignals _signals = new() + { + IpAddress = IPAddress.Parse("10.0.0.5"), + Timestamp = new DateTimeOffset(2026, 1, 1, 12, 0, 0, TimeSpan.Zero), + }; + + [Theory, BitAutoData] + public async Task ResolveAsync_NoReachableCollections_ReturnsNull( + SutProvider sutProvider, Guid userId, Guid cipherId) + { + sutProvider.GetDependency() + .GetManyByUserIdCipherIdAsync(userId, cipherId) + .Returns(new List()); + + Assert.Null(await sutProvider.Sut.ResolveAsync(userId, cipherId, _signals)); + } + + [Theory, BitAutoData] + public async Task ResolveAsync_CollectionWithoutAccessRule_ReturnsNull( + SutProvider sutProvider, Guid userId, Guid cipherId, Collection collection) + { + collection.AccessRuleId = null; + SetupReachableCollections(sutProvider, userId, cipherId, collection); + + Assert.Null(await sutProvider.Sut.ResolveAsync(userId, cipherId, _signals)); + } + + [Theory, BitAutoData] + public async Task ResolveAsync_HumanApprovalCondition_RequiresHumanApproval( + SutProvider sutProvider, Guid userId, Guid cipherId, Collection collection, AccessRule rule) + { + rule.Conditions = """[{"kind":"human_approval"}]"""; + SetupGovernedCollection(sutProvider, userId, cipherId, collection, rule); + + var result = await sutProvider.Sut.ResolveAsync(userId, cipherId, _signals); + + Assert.NotNull(result); + Assert.True(result!.RequiresHumanApproval); + Assert.Equal(collection.Id, result.CollectionId); + Assert.Equal(collection.OrganizationId, result.OrganizationId); + Assert.IsType(Assert.Single(result.Conditions)); + } + + [Theory, BitAutoData] + public async Task ResolveAsync_PassingIpAllowlistCondition_DoesNotRequireHumanApproval( + SutProvider sutProvider, Guid userId, Guid cipherId, Collection collection, AccessRule rule) + { + rule.Conditions = """[{"kind":"ip_allowlist","cidrs":["10.0.0.0/8"]}]"""; + SetupGovernedCollection(sutProvider, userId, cipherId, collection, rule); + + var result = await sutProvider.Sut.ResolveAsync(userId, cipherId, _signals); + + Assert.NotNull(result); + Assert.False(result!.RequiresHumanApproval); + var ip = Assert.IsType(Assert.Single(result.Conditions)); + Assert.Equal("10.0.0.0/8", Assert.Single(ip.Cidrs)); + } + + [Theory, BitAutoData] + public async Task ResolveAsync_ConditionsContainingHumanApproval_RequiresHumanApproval( + SutProvider sutProvider, Guid userId, Guid cipherId, Collection collection, AccessRule rule) + { + rule.Conditions = """[{"kind":"ip_allowlist","cidrs":["10.0.0.0/8"]},{"kind":"human_approval"}]"""; + SetupGovernedCollection(sutProvider, userId, cipherId, collection, rule); + + var result = await sutProvider.Sut.ResolveAsync(userId, cipherId, _signals); + + Assert.NotNull(result); + Assert.True(result!.RequiresHumanApproval); + Assert.Equal(2, result.Conditions.Count); + Assert.Contains(result.Conditions, condition => condition is HumanApprovalCondition); + } + + [Theory, BitAutoData] + public async Task ResolveAsync_EmptyConditions_DoesNotRequireHumanApproval( + SutProvider sutProvider, Guid userId, Guid cipherId, Collection collection, AccessRule rule) + { + // A conditionless rule governs the collection for audit logging but auto-approves access. + rule.Conditions = "[]"; + SetupGovernedCollection(sutProvider, userId, cipherId, collection, rule); + + var result = await sutProvider.Sut.ResolveAsync(userId, cipherId, _signals); + + Assert.NotNull(result); + Assert.False(result!.RequiresHumanApproval); + Assert.Empty(result.Conditions); + } + + [Theory, BitAutoData] + public async Task ResolveAsync_MalformedRule_FailsSafeToHumanApproval( + SutProvider sutProvider, Guid userId, Guid cipherId, Collection collection, AccessRule rule) + { + rule.Conditions = "not json"; + SetupGovernedCollection(sutProvider, userId, cipherId, collection, rule); + + var result = await sutProvider.Sut.ResolveAsync(userId, cipherId, _signals); + + Assert.NotNull(result); + Assert.True(result!.RequiresHumanApproval); + // An unparseable rule fails safe to human approval rather than surfacing a rule the engine cannot evaluate. + Assert.IsType(Assert.Single(result.Conditions)); + } + + [Theory, BitAutoData] + public async Task ResolveAsync_AutomaticGrantPath_BeatsHumanApprovalPath( + SutProvider sutProvider, Guid userId, Guid cipherId, + Collection automaticCollection, AccessRule automaticRule, Collection humanCollection, AccessRule humanRule) + { + // One reachable collection auto-grants (passing IP allowlist); the other needs human approval. + automaticRule.Conditions = """[{"kind":"ip_allowlist","cidrs":["10.0.0.0/8"]}]"""; + humanRule.Conditions = """[{"kind":"human_approval"}]"""; + SetupGovernedCollections(sutProvider, userId, cipherId, + (automaticCollection, automaticRule), (humanCollection, humanRule)); + + var result = await sutProvider.Sut.ResolveAsync(userId, cipherId, _signals); + + // Least-restrictive wins: the caller is never routed to an approver when some path would auto-grant. + Assert.NotNull(result); + Assert.False(result!.RequiresHumanApproval); + Assert.Equal(automaticCollection.Id, result.CollectionId); + } + + [Theory, BitAutoData] + public async Task ResolveAsync_FailingAutomaticRule_DoesNotPreemptGrantingHumanApprovalPath( + SutProvider sutProvider, Guid userId, Guid cipherId, + Collection automaticCollection, AccessRule automaticRule, Collection humanCollection, AccessRule humanRule) + { + // The automatic rule's IP allowlist fails for this caller; the human-approval rule would still grant. + automaticRule.Conditions = """[{"kind":"ip_allowlist","cidrs":["192.168.0.0/16"]}]"""; + humanRule.Conditions = """[{"kind":"human_approval"}]"""; + SetupGovernedCollections(sutProvider, userId, cipherId, + (automaticCollection, automaticRule), (humanCollection, humanRule)); + + var result = await sutProvider.Sut.ResolveAsync(userId, cipherId, _signals); + + // A failing automatic rule must not pre-empt a path that needs approval but would grant. + Assert.NotNull(result); + Assert.True(result!.RequiresHumanApproval); + Assert.Equal(humanCollection.Id, result.CollectionId); + } + + [Theory, BitAutoData] + public async Task ResolveAsync_NoRulePasses_ResolvesToAutoDenyPath( + SutProvider sutProvider, Guid userId, Guid cipherId, + Collection firstCollection, AccessRule firstRule, Collection secondCollection, AccessRule secondRule) + { + // Neither automatic rule's allowlist matches the caller, and no approval path exists, so every path denies. + firstRule.Conditions = """[{"kind":"ip_allowlist","cidrs":["192.168.0.0/16"]}]"""; + secondRule.Conditions = """[{"kind":"ip_allowlist","cidrs":["172.16.0.0/12"]}]"""; + SetupGovernedCollections(sutProvider, userId, cipherId, + (firstCollection, firstRule), (secondCollection, secondRule)); + + var result = await sutProvider.Sut.ResolveAsync(userId, cipherId, _signals); + + // Falls through to a deny path (not routed to a human); the auto path then surfaces the denial downstream. + Assert.NotNull(result); + Assert.False(result!.RequiresHumanApproval); + } + + [Theory, BitAutoData] + public async Task ResolveAsync_TwoAutomaticRules_ResolvesToThePassingOne( + SutProvider sutProvider, Guid userId, Guid cipherId, + Collection failingCollection, AccessRule failingRule, Collection passingCollection, AccessRule passingRule) + { + failingRule.Conditions = """[{"kind":"ip_allowlist","cidrs":["192.168.0.0/16"]}]"""; + passingRule.Conditions = """[{"kind":"ip_allowlist","cidrs":["10.0.0.0/8"]}]"""; + SetupGovernedCollections(sutProvider, userId, cipherId, + (failingCollection, failingRule), (passingCollection, passingRule)); + + var result = await sutProvider.Sut.ResolveAsync(userId, cipherId, _signals); + + Assert.NotNull(result); + Assert.False(result!.RequiresHumanApproval); + Assert.Equal(passingCollection.Id, result.CollectionId); + } + + [Theory, BitAutoData] + public async Task ResolveAsync_MalformedRuleAlongsidePassingAutomatic_AutoGrants( + SutProvider sutProvider, Guid userId, Guid cipherId, + Collection malformedCollection, AccessRule malformedRule, Collection automaticCollection, AccessRule automaticRule) + { + // The malformed rule fails safe to human approval for its own path, but a different parseable rule auto-grants, + // so the union/OR auto-grant path wins. + malformedRule.Conditions = "not json"; + automaticRule.Conditions = """[{"kind":"ip_allowlist","cidrs":["10.0.0.0/8"]}]"""; + SetupGovernedCollections(sutProvider, userId, cipherId, + (malformedCollection, malformedRule), (automaticCollection, automaticRule)); + + var result = await sutProvider.Sut.ResolveAsync(userId, cipherId, _signals); + + Assert.NotNull(result); + Assert.False(result!.RequiresHumanApproval); + Assert.Equal(automaticCollection.Id, result.CollectionId); + } + + private static void SetupReachableCollections( + SutProvider sutProvider, Guid userId, Guid cipherId, params Collection[] collections) + { + sutProvider.GetDependency() + .GetManyByUserIdCipherIdAsync(userId, cipherId) + .Returns(collections.Select(c => new CollectionCipher { CollectionId = c.Id, CipherId = cipherId }).ToList()); + sutProvider.GetDependency() + .GetManyByManyIdsAsync(Arg.Any>()) + .Returns(collections.ToList()); + } + + private static void SetupGovernedCollection( + SutProvider sutProvider, Guid userId, Guid cipherId, Collection collection, AccessRule rule) + => SetupGovernedCollections(sutProvider, userId, cipherId, (collection, rule)); + + private static void SetupGovernedCollections( + SutProvider sutProvider, Guid userId, Guid cipherId, + params (Collection collection, AccessRule rule)[] pairs) + { + foreach (var (collection, rule) in pairs) + { + collection.AccessRuleId = rule.Id; + } + + SetupReachableCollections(sutProvider, userId, cipherId, pairs.Select(p => p.collection).ToArray()); + + foreach (var (_, rule) in pairs) + { + sutProvider.GetDependency().GetByIdAsync(rule.Id).Returns(rule); + } + + // Drive the real engine through the substitute so resolution exercises true IP/time evaluation. + sutProvider.GetDependency() + .Evaluate(Arg.Any>(), Arg.Any()) + .Returns(ci => _engine.Evaluate(ci.ArgAt>(0), ci.ArgAt(1))); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Services/RequesterNotifierTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Services/RequesterNotifierTests.cs new file mode 100644 index 000000000000..b59925cb072e --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Services/RequesterNotifierTests.cs @@ -0,0 +1,22 @@ +using Bit.Commercial.Pam.Services; +using Bit.Core.Platform.Push; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Services; + +[SutProviderCustomize] +public class RequesterNotifierTests +{ + [Theory, BitAutoData] + public async Task NotifyRequesterAsync_PushesToRequester( + SutProvider sutProvider, Guid requesterId) + { + await sutProvider.Sut.NotifyRequesterAsync(requesterId); + + await sutProvider.GetDependency().Received(1) + .PushRefreshAccessRequestAsync(requesterId); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/Services/SingleActiveLeaseEvaluatorTests.cs b/bitwarden_license/test/Commercial.Pam.Test/Services/SingleActiveLeaseEvaluatorTests.cs new file mode 100644 index 000000000000..c92655a7f91a --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/Services/SingleActiveLeaseEvaluatorTests.cs @@ -0,0 +1,99 @@ +using Bit.Commercial.Pam.Services; +using Bit.Core.Entities; +using Bit.Core.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Pam.Test.Services; + +[SutProviderCustomize] +public class SingleActiveLeaseEvaluatorTests +{ + [Theory, BitAutoData] + public async Task AppliesAsync_NoReachableCollections_ReturnsFalse( + SutProvider sutProvider, Guid userId, Guid cipherId) + { + sutProvider.GetDependency() + .GetManyByUserIdCipherIdAsync(userId, cipherId) + .Returns(new List()); + + // No path at all: the constraint does not bind. + Assert.False(await sutProvider.Sut.AppliesAsync(userId, cipherId)); + } + + [Theory, BitAutoData] + public async Task AppliesAsync_EveryPathGovernedBySingletonRule_ReturnsTrue( + SutProvider sutProvider, Guid userId, Guid cipherId, + Collection collectionA, Collection collectionB, AccessRule ruleA, AccessRule ruleB) + { + ruleA.SingleActiveLease = true; + ruleB.SingleActiveLease = true; + SetupGovernedCollection(sutProvider, collectionA, ruleA); + SetupGovernedCollection(sutProvider, collectionB, ruleB); + SetupReachableCollections(sutProvider, userId, cipherId, collectionA, collectionB); + + Assert.True(await sutProvider.Sut.AppliesAsync(userId, cipherId)); + } + + [Theory, BitAutoData] + public async Task AppliesAsync_OneUngatedPath_ReturnsFalse( + SutProvider sutProvider, Guid userId, Guid cipherId, + Collection singletonCollection, Collection ungatedCollection, AccessRule singletonRule) + { + singletonRule.SingleActiveLease = true; + SetupGovernedCollection(sutProvider, singletonCollection, singletonRule); + // An ungated path is an escape: the caller can reach the cipher without any singleton rule. + ungatedCollection.AccessRuleId = null; + SetupReachableCollections(sutProvider, userId, cipherId, singletonCollection, ungatedCollection); + + Assert.False(await sutProvider.Sut.AppliesAsync(userId, cipherId)); + } + + [Theory, BitAutoData] + public async Task AppliesAsync_OneNonSingletonRulePath_ReturnsFalse( + SutProvider sutProvider, Guid userId, Guid cipherId, + Collection singletonCollection, Collection plainCollection, AccessRule singletonRule, AccessRule plainRule) + { + singletonRule.SingleActiveLease = true; + plainRule.SingleActiveLease = false; + SetupGovernedCollection(sutProvider, singletonCollection, singletonRule); + SetupGovernedCollection(sutProvider, plainCollection, plainRule); + SetupReachableCollections(sutProvider, userId, cipherId, singletonCollection, plainCollection); + + // A non-singleton rule on any path is an escape. + Assert.False(await sutProvider.Sut.AppliesAsync(userId, cipherId)); + } + + [Theory, BitAutoData] + public async Task AppliesAsync_MissingRuleOnPath_ReturnsFalse( + SutProvider sutProvider, Guid userId, Guid cipherId, Collection collection, Guid ruleId) + { + collection.AccessRuleId = ruleId; + sutProvider.GetDependency().GetByIdAsync(ruleId).Returns((AccessRule?)null); + SetupReachableCollections(sutProvider, userId, cipherId, collection); + + Assert.False(await sutProvider.Sut.AppliesAsync(userId, cipherId)); + } + + private static void SetupReachableCollections( + SutProvider sutProvider, Guid userId, Guid cipherId, params Collection[] collections) + { + sutProvider.GetDependency() + .GetManyByUserIdCipherIdAsync(userId, cipherId) + .Returns(collections.Select(c => new CollectionCipher { CollectionId = c.Id, CipherId = cipherId }).ToList()); + sutProvider.GetDependency() + .GetManyByManyIdsAsync(Arg.Any>()) + .Returns(collections.ToList()); + } + + private static void SetupGovernedCollection( + SutProvider sutProvider, Collection collection, AccessRule rule) + { + collection.AccessRuleId = rule.Id; + sutProvider.GetDependency().GetByIdAsync(rule.Id).Returns(rule); + } +} diff --git a/bitwarden_license/test/Commercial.Pam.Test/packages.lock.json b/bitwarden_license/test/Commercial.Pam.Test/packages.lock.json new file mode 100644 index 000000000000..51a81cccdcf9 --- /dev/null +++ b/bitwarden_license/test/Commercial.Pam.Test/packages.lock.json @@ -0,0 +1,1883 @@ +{ + "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" + } + }, + "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" + } + }, + "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==" + }, + "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.1.1", + "contentHash": "J557Na/XeYV8JjgWbGRMzi5eOgXrEMqcuPBtkZP/y7EgFLiWgvaPBGDm+hq766Kp0Kxs2MaZaNTQn4Fog7Hebw==", + "dependencies": { + "LaunchDarkly.Logging": "2.0.0" + } + }, + "LaunchDarkly.EventSource": { + "type": "Transitive", + "resolved": "5.3.0", + "contentHash": "i++YvdrzvTc1tOxfVreU2yjo/E+iTFcVtwiB4PZ/3uTouNdhABLbJfz1jmGN3jmnvJKWhsMeEZLZM0dzkI+PXQ==", + "dependencies": { + "LaunchDarkly.Logging": "[2.0.0, 3.0.0)" + } + }, + "LaunchDarkly.InternalSdk": { + "type": "Transitive", + "resolved": "3.6.0", + "contentHash": "Drf1rL+sZ/4UZDUovDLgRN9qQPf8J4QTQjJR2A8nCuxkFU7QjVFXqqW9+KL8RfX3DmJkhpgC3QVA1B9B6xEYJQ==", + "dependencies": { + "LaunchDarkly.CommonSdk": "[7.1.1, 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.11.0", + "contentHash": "MMXfyGMDtul9UhEleHXdGNe2amLY1gjTZ9K2R18XoZp5OOlSGu4CmBqE4MXMsXhLYoEtYg2GIibmoTSPaU2Y+A==", + "dependencies": { + "LaunchDarkly.Cache": "1.0.2", + "LaunchDarkly.CommonSdk": "7.1.1", + "LaunchDarkly.EventSource": "5.3.0", + "LaunchDarkly.InternalSdk": "3.6.0", + "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.16.0", + "contentHash": "trJ82DOpAmo8i1jO1vNE+dGn4mPRyeYfy4swRcAGgMJhPoI1Kohf4OFJJf0+YIj4iUxgxPn8W+ht7e7KiYzSjg==", + "dependencies": { + "MimeKit": "4.16.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.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": "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", + "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.8", + "contentHash": "I63esIFbL3h5pSt7gXpXOlmcwDmYBUoYNEglKfDPFUqtYvSV84f2l28hO2lfVXsV0wdlplgAM7IVz16matapSg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.8" + } + }, + "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.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.8", + "contentHash": "21nbDV60SRPWGIivsyl6lqBeEJNG1sginhhfWgRrr3Ais7aQ12To25OAHQxgoiJkjqy1aQ6RxpZBGYuTi7Ge6A==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "5Ou6varcxLBzQ+Agfm0k0pnH7vrEITYlXMDuE6s7ZHlZHz6/G8XJ3iISZDr5rfwfge6RnXJ1+Wc479mMn52vjA==" + }, + "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.8", + "contentHash": "+f4C5g78QCGNyxzUfrTYsB7qYx06Zca0e88s3qFlea9/lQhgPImYdNprlgzl1uHhRU3fVHLfmbijayU2sJEZ6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8", + "Microsoft.Extensions.Options": "10.0.8" + } + }, + "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": "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.8", + "contentHash": "U+oquaPxFdY8lYeEIWO/AD7jDIl9sPW6aVWMQRHU/pZ/SWpLcOrAj2fcLe1HwXl4sYw1ONI56K/eELT3xr4RRQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.8" + } + }, + "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.Abstractions": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "MoOWFPT88/pDfmWpbU9PydKRX/rJFQkliowE/L9wbQcl94IicUphb5BFgepkWiDkYYxPnuEqjN4buzOGW4vJpQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.8", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.8", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.8", + "Microsoft.Extensions.Logging.Abstractions": "10.0.8" + } + }, + "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.8", + "contentHash": "fdVadZmsC8jRP0KvKy8mO8f6GV/HyBvElfcSxEhd+5FM5boAw/01iSaCto5G3G37ApJira4A3pNaVvBv8cUiLQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "j8zcwhS6bYB6FEfaY3nYSgHdpiL2T+/V3xjpHtslVAegyI1JUbB9yAt/BFdvZdsNbY0Udm4xFtvfT/hUwcOOOg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.0", + "Microsoft.Extensions.Configuration.Binder": "10.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging": "10.0.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.0" + } + }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "aQBFbY8i/dacE0fP+ZJ8Lhx/unYRnGHhtM+tHb46GLkeNjBdOzgFk88sX6BVZBhoa6JrYIOBGYTc5K4WItBsag==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "VBD+131DpTNCNDfA4kIyKTiCySvJGNhwibdWBSdFRu7GMfXLXcXODkgA+KStKbbhzraLglZWUN4nXyHgW4JIRA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8", + "Microsoft.Extensions.Primitives": "10.0.8" + } + }, + "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.8", + "contentHash": "OBPo4nYhMyIbtueoC10CBm6AGAbo/A9IV8QQ/6ryZS7VvmqpGT7hunazeHLxFawRzn3oLOq4jhqhpBX4tfswWQ==" + }, + "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", + "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.16.0", + "contentHash": "X0LFxeM4gPRIhODyY/HYS9b+zRZ7y//v59rFzgS6wLxcPuZThnMtNZHtrr0fjLyRRkg3gqJBtvW36XfUzZ7Djw==", + "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)" + } + }, + "NSubstitute": { + "type": "Transitive", + "resolved": "5.1.0", + "contentHash": "ZCqOP3Kpp2ea7QcLyjMU4wzE+0wmrMN35PQMsdPOHYc2IrvjmusG9hICOiqiOTPKN0gJon6wyCn6ZuGHdNs9hQ==", + "dependencies": { + "Castle.Core": "5.1.1" + } + }, + "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.0", + "contentHash": "mte1nRYefxjed2syXgVWq3UCfMKO7MkebvTZmf0O1aLgVgCktLsVjQ6mftyjIbWGBBCHN0wg+Glxj8BSFS70pQ==", + "dependencies": { + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.0, 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.0", + "contentHash": "uToc7bUp8IEdb0ny9mKsL6FrrYelINPzxxiSShJgOf4XmQc4Azww6S5RjRj24YhsOn2a1MABOrxfVTZXtDk4Eg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.0, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.Runtime": { + "type": "Transitive", + "resolved": "1.15.0", + "contentHash": "OOvpqR/j2Pb6+tWhHNODIbSJ53Or/MDtTiXEyrsWI02K2lLAgvBFcxUOrHggS/8015cYR3AdSaXv6NZrkz5yQA==", + "dependencies": { + "OpenTelemetry.Api": "[1.15.0, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.SqlClient": { + "type": "Transitive", + "resolved": "1.12.0-beta.3", + "contentHash": "w1cOTM5U6c9MYbBALgqynwGNuGGn6uxbh0hV1LW7zmsQyq6e4kJrW0jIMcGgYIEYnIomOvUwoTEwEFb1ZClAeg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.12.0, 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": "9.0.13", + "contentHash": "675Rk4RwaVrWo09wrR2rpDVKixtgtnhd5NhPrn6O21uj92JvE61KTGupn76M2N6Ff/xJjY3SHSfSg0MIanEzGw==" + }, + "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]", + "Commercial.Core": "[2026.6.1, )", + "Commercial.Infrastructure.EntityFramework": "[2026.6.1, )", + "Commercial.Pam": "[2026.6.1, )", + "Core": "[2026.6.1, )", + "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", + "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", + "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", + "OpenTelemetry.Instrumentation.EntityFrameworkCore": "[1.12.0-beta.2, )", + "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", + "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", + "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", + "Pam.Domain": "[2026.6.1, )", + "SharedWeb": "[2026.6.1, )", + "Swashbuckle.AspNetCore": "[10.1.7, 10.1.7]" + } + }, + "api.test": { + "type": "Project", + "dependencies": { + "Api": "[2026.6.1, )", + "AutoFixture.Xunit2": "[4.18.1, )", + "Common": "[2026.6.1, )", + "Core": "[2026.6.1, )", + "Core.Test": "[2026.6.1, )", + "Microsoft.NET.Test.Sdk": "[18.0.1, )", + "NSubstitute": "[5.1.0, )", + "Pam.Domain": "[2026.6.1, )", + "xunit": "[2.6.6, )" + } + }, + "commercial.core": { + "type": "Project", + "dependencies": { + "Core": "[2026.6.1, )", + "CsvHelper": "[33.1.0, 33.1.0]" + } + }, + "commercial.infrastructure.entityframework": { + "type": "Project", + "dependencies": { + "AutoMapper": "[14.0.0, 14.0.0]", + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" + } + }, + "commercial.pam": { + "type": "Project", + "dependencies": { + "Core": "[2026.6.1, )", + "Pam.Domain": "[2026.6.1, )", + "SharedWeb": "[2026.6.1, )" + } + }, + "common": { + "type": "Project", + "dependencies": { + "AutoFixture.AutoNSubstitute": "[4.18.1, )", + "AutoFixture.Xunit2": "[4.18.1, )", + "Core": "[2026.6.1, )", + "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]", + "Braintree": "[5.36.0, 5.36.0]", + "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", + "DnsClient": "[1.8.0, 1.8.0]", + "Duende.IdentityServer": "[7.4.6, 7.4.6]", + "DuoUniversal": "[1.3.1, 1.3.1]", + "Fido2.AspNet": "[3.0.1, 3.0.1]", + "Handlebars.Net": "[2.1.6, 2.1.6]", + "LaunchDarkly.ServerSdk": "[8.11.0, 8.11.0]", + "MailKit": "[4.16.0, 4.16.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]", + "Pam.Domain": "[2026.6.1, )", + "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]", + "Serilog.Extensions.Logging.File": "[3.0.0, 3.0.0]", + "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]" + } + }, + "core.test": { + "type": "Project", + "dependencies": { + "AutoFixture.AutoNSubstitute": "[4.18.1, )", + "AutoFixture.Xunit2": "[4.18.1, )", + "Common": "[2026.6.1, )", + "Core": "[2026.6.1, )", + "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, )", + "Pam.Domain": "[2026.6.1, )", + "xunit": "[2.6.6, )" + } + }, + "data": { + "type": "Project" + }, + "infrastructure.dapper": { + "type": "Project", + "dependencies": { + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" + } + }, + "infrastructure.entityframework": { + "type": "Project", + "dependencies": { + "AutoMapper": "[14.0.0, 14.0.0]", + "Core": "[2026.6.1, )", + "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.6.1, )", + "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", + "linq2db": "[5.4.1, 5.4.1]", + "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" + } + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, + "sharedweb": { + "type": "Project", + "dependencies": { + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", + "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", + "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" + } + } + } + } +} \ No newline at end of file diff --git a/bitwarden_license/test/SSO.Test/packages.lock.json b/bitwarden_license/test/SSO.Test/packages.lock.json index fda68f5fde6b..fe0d57d810bd 100644 --- a/bitwarden_license/test/SSO.Test/packages.lock.json +++ b/bitwarden_license/test/SSO.Test/packages.lock.json @@ -1633,7 +1633,7 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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, )", @@ -1657,6 +1657,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1681,6 +1682,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1694,33 +1696,44 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } @@ -1728,7 +1741,7 @@ "sso": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", @@ -1736,7 +1749,7 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )", + "SharedWeb": "[2026.6.1, )", "Sustainsys.Saml2.AspNetCore2": "[2.11.0, 2.11.0]" } } diff --git a/bitwarden_license/test/Scim.IntegrationTest/packages.lock.json b/bitwarden_license/test/Scim.IntegrationTest/packages.lock.json index 8e88b4035e93..aca999974981 100644 --- a/bitwarden_license/test/Scim.IntegrationTest/packages.lock.json +++ b/bitwarden_license/test/Scim.IntegrationTest/packages.lock.json @@ -1278,7 +1278,7 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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, )", @@ -1302,6 +1302,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1326,6 +1327,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1339,10 +1341,13 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "identity": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", @@ -1350,25 +1355,27 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )" + "SharedWeb": "[2026.6.1, )" } }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" @@ -1377,28 +1384,34 @@ "integrationtestcommon": { "type": "Project", "dependencies": { - "Common": "[2026.6.0, )", - "Identity": "[2026.6.0, )", + "Common": "[2026.6.1, )", + "Identity": "[2026.6.1, )", "Microsoft.AspNetCore.Mvc.Testing": "[10.0.8, 10.0.8]", - "Migrator": "[2026.6.0, )", - "Seeder": "[2026.6.0, )" + "Migrator": "[2026.6.1, )", + "Seeder": "[2026.6.1, )" } }, "migrator": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "Microsoft.Extensions.Logging": "[10.0.8, 10.0.8]", "dbup-sqlserver": "[7.2.0, 7.2.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "rustsdk": { "type": "Project" }, "scim": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", @@ -1406,25 +1419,25 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )" + "SharedWeb": "[2026.6.1, )" } }, "seeder": { "type": "Project", "dependencies": { "Bogus": "[35.6.5, 35.6.5]", - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", - "RustSdk": "[2026.6.0, )", - "SharedWeb": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", + "RustSdk": "[2026.6.1, )", + "SharedWeb": "[2026.6.1, )" } }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/bitwarden_license/test/Scim.Test/packages.lock.json b/bitwarden_license/test/Scim.Test/packages.lock.json index 44bdcb9b4a6d..9a775b5d9785 100644 --- a/bitwarden_license/test/Scim.Test/packages.lock.json +++ b/bitwarden_license/test/Scim.Test/packages.lock.json @@ -1593,7 +1593,7 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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, )", @@ -1617,6 +1617,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1641,6 +1642,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1654,31 +1656,42 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "scim": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", @@ -1686,15 +1699,15 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )" + "SharedWeb": "[2026.6.1, )" } }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/bitwarden_license/test/Sso.IntegrationTest/packages.lock.json b/bitwarden_license/test/Sso.IntegrationTest/packages.lock.json index d956296c4a2e..47f721714bc8 100644 --- a/bitwarden_license/test/Sso.IntegrationTest/packages.lock.json +++ b/bitwarden_license/test/Sso.IntegrationTest/packages.lock.json @@ -1316,7 +1316,7 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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, )", @@ -1340,6 +1340,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1364,6 +1365,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1377,10 +1379,13 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "identity": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", @@ -1388,25 +1393,27 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )" + "SharedWeb": "[2026.6.1, )" } }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" @@ -1415,21 +1422,27 @@ "integrationtestcommon": { "type": "Project", "dependencies": { - "Common": "[2026.6.0, )", - "Identity": "[2026.6.0, )", + "Common": "[2026.6.1, )", + "Identity": "[2026.6.1, )", "Microsoft.AspNetCore.Mvc.Testing": "[10.0.8, 10.0.8]", - "Migrator": "[2026.6.0, )", - "Seeder": "[2026.6.0, )" + "Migrator": "[2026.6.1, )", + "Seeder": "[2026.6.1, )" } }, "migrator": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "Microsoft.Extensions.Logging": "[10.0.8, 10.0.8]", "dbup-sqlserver": "[7.2.0, 7.2.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "rustsdk": { "type": "Project" }, @@ -1437,18 +1450,18 @@ "type": "Project", "dependencies": { "Bogus": "[35.6.5, 35.6.5]", - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", - "RustSdk": "[2026.6.0, )", - "SharedWeb": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", + "RustSdk": "[2026.6.1, )", + "SharedWeb": "[2026.6.1, )" } }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } @@ -1456,7 +1469,7 @@ "sso": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", @@ -1464,7 +1477,7 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )", + "SharedWeb": "[2026.6.1, )", "Sustainsys.Saml2.AspNetCore2": "[2.11.0, 2.11.0]" } } diff --git a/perf/MicroBenchmarks/packages.lock.json b/perf/MicroBenchmarks/packages.lock.json index 9ef61383d7c7..ef28435b5174 100644 --- a/perf/MicroBenchmarks/packages.lock.json +++ b/perf/MicroBenchmarks/packages.lock.json @@ -1555,6 +1555,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1579,6 +1580,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1592,10 +1594,13 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "identity": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", @@ -1603,36 +1608,44 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )" + "SharedWeb": "[2026.6.1, )" } }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/src/Admin/packages.lock.json b/src/Admin/packages.lock.json index ee16eaa92ca4..b40f13db4ee4 100644 --- a/src/Admin/packages.lock.json +++ b/src/Admin/packages.lock.json @@ -1103,7 +1103,7 @@ "commercial.core": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "CsvHelper": "[33.1.0, 33.1.0]" } }, @@ -1111,8 +1111,8 @@ "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" } }, "core": { @@ -1131,6 +1131,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1155,6 +1156,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1168,22 +1170,27 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" @@ -1192,7 +1199,7 @@ "migrator": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "Microsoft.Extensions.Logging": "[10.0.8, 10.0.8]", "dbup-sqlserver": "[7.2.0, 7.2.0]" } @@ -1200,23 +1207,29 @@ "mysqlmigrations": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" + } + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" } }, "postgresmigrations": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" } }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } @@ -1224,8 +1237,8 @@ "sqlitemigrations": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" } } } diff --git a/src/Api/AdminConsole/Controllers/CollectionsController.cs b/src/Api/AdminConsole/Controllers/CollectionsController.cs index 873845dd4378..59da568e6273 100644 --- a/src/Api/AdminConsole/Controllers/CollectionsController.cs +++ b/src/Api/AdminConsole/Controllers/CollectionsController.cs @@ -14,6 +14,7 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/AdminConsole/Controllers/GroupsController.cs b/src/Api/AdminConsole/Controllers/GroupsController.cs index bf7551817fb7..14936d9353d6 100644 --- a/src/Api/AdminConsole/Controllers/GroupsController.cs +++ b/src/Api/AdminConsole/Controllers/GroupsController.cs @@ -6,7 +6,6 @@ using Bit.Api.AdminConsole.Authorization.Requirements; using Bit.Api.AdminConsole.Models.Request; using Bit.Api.AdminConsole.Models.Response; -using Bit.Api.Models.Response; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; @@ -14,6 +13,7 @@ using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/AdminConsole/Controllers/OrganizationAuthRequestsController.cs b/src/Api/AdminConsole/Controllers/OrganizationAuthRequestsController.cs index 915a17ff54f1..bd17609e5b3f 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationAuthRequestsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationAuthRequestsController.cs @@ -1,12 +1,12 @@ using Bit.Api.AdminConsole.Models.Request; using Bit.Api.AdminConsole.Models.Response; -using Bit.Api.Models.Response; using Bit.Core.AdminConsole.OrganizationAuth.Interfaces; using Bit.Core.Auth.Models.Api.Request.AuthRequest; using Bit.Core.Auth.Services; using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Repositories; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs b/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs index 534e27121cdc..26fc75a616ab 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs @@ -1,12 +1,12 @@ using Bit.Api.AdminConsole.Models.Request; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; -using Bit.Api.Models.Response; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Repositories; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/AdminConsole/Controllers/OrganizationInviteLinksController.cs b/src/Api/AdminConsole/Controllers/OrganizationInviteLinksController.cs index a8f8387cbd6b..c378cdbd5494 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationInviteLinksController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationInviteLinksController.cs @@ -2,10 +2,10 @@ using Bit.Api.AdminConsole.Authorization.Requirements; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; -using Bit.Api.Models.Response; using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.InviteLinks.Interfaces; using Bit.Core.Utilities; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 837f4d202232..f4da228e07d1 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -8,7 +8,6 @@ using Bit.Api.AdminConsole.Authorization.Requirements; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; -using Bit.Api.Models.Response; using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Data; @@ -42,6 +41,7 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; +using Bit.SharedWeb.Models.Response; using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; using Microsoft.AspNetCore.Authorization; diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index ee4a31654c56..3b499b1723f8 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -34,6 +34,7 @@ using Bit.Core.Settings; using Bit.Core.Tokens; using Bit.Core.Utilities; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/AdminConsole/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Controllers/PoliciesController.cs index 8f0e4e8952f7..d5049af697ac 100644 --- a/src/Api/AdminConsole/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Controllers/PoliciesController.cs @@ -6,7 +6,6 @@ using Bit.Api.AdminConsole.Models.Request; using Bit.Api.AdminConsole.Models.Response.Helpers; using Bit.Api.AdminConsole.Models.Response.Organizations; -using Bit.Api.Models.Response; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; @@ -16,6 +15,7 @@ using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Tokens; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/AdminConsole/Controllers/ProviderOrganizationsController.cs b/src/Api/AdminConsole/Controllers/ProviderOrganizationsController.cs index f18b2916e718..81204684fed6 100644 --- a/src/Api/AdminConsole/Controllers/ProviderOrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/ProviderOrganizationsController.cs @@ -5,7 +5,6 @@ using Bit.Api.AdminConsole.Authorization.Providers.Requirements; using Bit.Api.AdminConsole.Models.Request.Providers; using Bit.Api.AdminConsole.Models.Response.Providers; -using Bit.Api.Models.Response; using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; @@ -14,6 +13,7 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/AdminConsole/Controllers/ProviderUsersController.cs b/src/Api/AdminConsole/Controllers/ProviderUsersController.cs index 7746b5cf246f..cbc1c72af9c0 100644 --- a/src/Api/AdminConsole/Controllers/ProviderUsersController.cs +++ b/src/Api/AdminConsole/Controllers/ProviderUsersController.cs @@ -5,12 +5,12 @@ using Bit.Api.AdminConsole.Authorization.Providers.Requirements; using Bit.Api.AdminConsole.Models.Request.Providers; using Bit.Api.AdminConsole.Models.Response.Providers; -using Bit.Api.Models.Response; using Bit.Core.AdminConsole.Models.Business.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Exceptions; using Bit.Core.Services; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/AdminConsole/Models/Request/CollectionRequestModel.cs b/src/Api/AdminConsole/Models/Request/CollectionRequestModel.cs index b981cff2b80b..fcd3a1494d54 100644 --- a/src/Api/AdminConsole/Models/Request/CollectionRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/CollectionRequestModel.cs @@ -18,6 +18,7 @@ public class CreateCollectionRequestModel public string ExternalId { get; set; } public IEnumerable Groups { get; set; } public IEnumerable Users { get; set; } + public Guid? AccessRuleId { get; set; } public Collection ToCollection(Guid orgId) { @@ -31,6 +32,7 @@ public virtual Collection ToCollection(Collection existingCollection) { existingCollection.Name = Name; existingCollection.ExternalId = ExternalId; + existingCollection.AccessRuleId = AccessRuleId; return existingCollection; } } @@ -65,7 +67,7 @@ public override Collection ToCollection(Collection existingCollection) existingCollection.Name = Name; } existingCollection.ExternalId = ExternalId; + existingCollection.AccessRuleId = AccessRuleId; return existingCollection; } - } diff --git a/src/Api/AdminConsole/Models/Response/CollectionResponseModel.cs b/src/Api/AdminConsole/Models/Response/CollectionResponseModel.cs index 44ead9c5a233..569aa9906881 100644 --- a/src/Api/AdminConsole/Models/Response/CollectionResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/CollectionResponseModel.cs @@ -25,6 +25,7 @@ public CollectionResponseModel(Collection collection, string obj = "collection") ExternalId = collection.ExternalId; Type = collection.Type; DefaultUserCollectionEmail = collection.DefaultUserCollectionEmail; + AccessRuleId = collection.AccessRuleId; } public Guid Id { get; set; } @@ -33,6 +34,7 @@ public CollectionResponseModel(Collection collection, string obj = "collection") public string ExternalId { get; set; } public CollectionType Type { get; set; } public string DefaultUserCollectionEmail { get; set; } + public Guid? AccessRuleId { get; set; } } /// diff --git a/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailsResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailsResponseModel.cs index 178060d9b147..3fd5d343b7d5 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailsResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailsResponseModel.cs @@ -1,7 +1,7 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable -using Bit.Api.Models.Response; +using Bit.SharedWeb.Models.Response; namespace Bit.Api.AdminConsole.Models.Response.Organizations; diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index ed91f16ac0ad..793374eccdd8 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -23,6 +23,7 @@ + @@ -30,8 +31,15 @@ + + + + + + + diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 0d23b2cb2504..e4dc022664cf 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -28,6 +28,7 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/Auth/Controllers/AuthRequestsController.cs b/src/Api/Auth/Controllers/AuthRequestsController.cs index 7e7169463bf7..635076730002 100644 --- a/src/Api/Auth/Controllers/AuthRequestsController.cs +++ b/src/Api/Auth/Controllers/AuthRequestsController.cs @@ -2,7 +2,6 @@ #nullable disable using Bit.Api.Auth.Models.Response; -using Bit.Api.Models.Response; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Identity; using Bit.Core.Auth.Models.Api.Request.AuthRequest; @@ -11,6 +10,7 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/Auth/Controllers/EmergencyAccessController.cs b/src/Api/Auth/Controllers/EmergencyAccessController.cs index 4e9f9bf96851..9cc638cfd575 100644 --- a/src/Api/Auth/Controllers/EmergencyAccessController.cs +++ b/src/Api/Auth/Controllers/EmergencyAccessController.cs @@ -5,13 +5,14 @@ using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Response; -using Bit.Api.Models.Response; using Bit.Api.Vault.Models.Response; using Bit.Core.Auth.UserFeatures.EmergencyAccess; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; +using Bit.Pam.Services; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -25,17 +26,20 @@ public class EmergencyAccessController : Controller private readonly IEmergencyAccessRepository _emergencyAccessRepository; private readonly IEmergencyAccessService _emergencyAccessService; private readonly IGlobalSettings _globalSettings; + private readonly ICipherLeaseGate _cipherLeaseGate; public EmergencyAccessController( IUserService userService, IEmergencyAccessRepository emergencyAccessRepository, IEmergencyAccessService emergencyAccessService, - IGlobalSettings globalSettings) + IGlobalSettings globalSettings, + ICipherLeaseGate cipherLeaseGate) { _userService = userService; _emergencyAccessRepository = emergencyAccessRepository; _emergencyAccessService = emergencyAccessService; _globalSettings = globalSettings; + _cipherLeaseGate = cipherLeaseGate; } [HttpGet("trusted")] @@ -203,7 +207,8 @@ public async Task ViewCiphers(Guid id) { var user = await _userService.GetUserByPrincipalAsync(User); var viewResult = await _emergencyAccessService.ViewAsync(id, user); - return new EmergencyAccessViewResponseModel(_globalSettings, viewResult.EmergencyAccess, viewResult.Ciphers, user); + return new EmergencyAccessViewResponseModel(_globalSettings, viewResult.EmergencyAccess, viewResult.Ciphers, user, + _cipherLeaseGate.Unrestricted()); } [HttpGet("{id}/{cipherId}/attachment/{attachmentId}")] diff --git a/src/Api/Auth/Controllers/TwoFactorController.cs b/src/Api/Auth/Controllers/TwoFactorController.cs index 91ff038d98bd..f0acec0b0f07 100644 --- a/src/Api/Auth/Controllers/TwoFactorController.cs +++ b/src/Api/Auth/Controllers/TwoFactorController.cs @@ -19,6 +19,7 @@ using Bit.Core.Services; using Bit.Core.Tokens; using Bit.Core.Utilities; +using Bit.SharedWeb.Models.Response; using Fido2NetLib; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; diff --git a/src/Api/Auth/Controllers/WebAuthnController.cs b/src/Api/Auth/Controllers/WebAuthnController.cs index de6f7e9ddce4..34797baf8425 100644 --- a/src/Api/Auth/Controllers/WebAuthnController.cs +++ b/src/Api/Auth/Controllers/WebAuthnController.cs @@ -1,7 +1,6 @@ using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.Auth.Models.Response.WebAuthn; -using Bit.Api.Models.Response; using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.Auth.Enums; @@ -13,6 +12,7 @@ using Bit.Core.Exceptions; using Bit.Core.Services; using Bit.Core.Tokens; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs b/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs index aa21b18bef91..eee91cfddfeb 100644 --- a/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs +++ b/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs @@ -9,6 +9,7 @@ using Bit.Core.Enums; using Bit.Core.Models.Api; using Bit.Core.Settings; +using Bit.Core.Vault.Authorization; using Bit.Core.Vault.Models.Data; namespace Bit.Api.Auth.Models.Response; @@ -129,15 +130,19 @@ public EmergencyAccessViewResponseModel( IGlobalSettings globalSettings, EmergencyAccess emergencyAccess, IEnumerable ciphers, - User user) + User user, + FullCipherAccess fullCipherAccess) : base("emergencyAccessView") { KeyEncrypted = emergencyAccess.KeyEncrypted; + // Emergency access only retrieves personal ciphers, which are never leasing-gated, so full data + // is released (organizationAbility is not needed for personal ciphers). Ciphers = ciphers.Select(cipher => - new CipherResponseModel( + new FullCipherResponseModel( + fullCipherAccess, cipher, user, - null, // Emergency access only retrieves personal ciphers so organizationAbility is not needed + null, globalSettings)); } diff --git a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs index 1ab06377bd79..999092f873cc 100644 --- a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs @@ -4,7 +4,6 @@ using Bit.Api.AdminConsole.Authorization; using Bit.Api.AdminConsole.Authorization.Requirements; using Bit.Api.Models.Request.Organizations; -using Bit.Api.Models.Response; using Bit.Api.Models.Response.Organizations; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces; @@ -19,6 +18,7 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/Billing/Controllers/PlansController.cs b/src/Api/Billing/Controllers/PlansController.cs index f9b52747808b..0d029ec63f3a 100644 --- a/src/Api/Billing/Controllers/PlansController.cs +++ b/src/Api/Billing/Controllers/PlansController.cs @@ -1,5 +1,6 @@ using Bit.Api.Models.Response; using Bit.Core.Billing.Pricing; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/Controllers/DevicesController.cs b/src/Api/Controllers/DevicesController.cs index d54b3c7b8c59..7d03da0adae3 100644 --- a/src/Api/Controllers/DevicesController.cs +++ b/src/Api/Controllers/DevicesController.cs @@ -13,6 +13,7 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs index 67f21bdf37e0..35f63c8056c9 100644 --- a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs +++ b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs @@ -4,7 +4,6 @@ using Bit.Api.AdminConsole.Authorization; using Bit.Api.AdminConsole.Authorization.Requirements; using Bit.Api.Models.Request.Organizations; -using Bit.Api.Models.Response; using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Models.Api.Response.OrganizationSponsorships; @@ -13,6 +12,7 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/Dirt/Controllers/EventsController.cs b/src/Api/Dirt/Controllers/EventsController.cs index f8de6334810a..a073bb68524c 100644 --- a/src/Api/Dirt/Controllers/EventsController.cs +++ b/src/Api/Dirt/Controllers/EventsController.cs @@ -2,7 +2,6 @@ #nullable disable using Bit.Api.Dirt.Models.Response; -using Bit.Api.Models.Response; using Bit.Api.Utilities; using Bit.Api.Utilities.DiagnosticTools; using Bit.Core.AdminConsole.Repositories; @@ -15,6 +14,7 @@ using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; using Bit.Core.Vault.Repositories; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/NotificationCenter/Controllers/NotificationsController.cs b/src/Api/NotificationCenter/Controllers/NotificationsController.cs index 9dc1505cb8d6..137355a7ad25 100644 --- a/src/Api/NotificationCenter/Controllers/NotificationsController.cs +++ b/src/Api/NotificationCenter/Controllers/NotificationsController.cs @@ -1,11 +1,11 @@ #nullable enable -using Bit.Api.Models.Response; using Bit.Api.NotificationCenter.Models.Request; using Bit.Api.NotificationCenter.Models.Response; using Bit.Core.Models.Data; using Bit.Core.NotificationCenter.Commands.Interfaces; using Bit.Core.NotificationCenter.Models.Filter; using Bit.Core.NotificationCenter.Queries.Interfaces; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/Pam/Controllers/CipherLeaseController.cs b/src/Api/Pam/Controllers/CipherLeaseController.cs new file mode 100644 index 000000000000..6472e772b861 --- /dev/null +++ b/src/Api/Pam/Controllers/CipherLeaseController.cs @@ -0,0 +1,68 @@ +using Bit.Api.Vault.Models.Response; +using Bit.Commercial.Pam.OrganizationFeatures.Queries.Interfaces; +using Bit.Core; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Core.Utilities; +using Bit.Pam.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.Pam.Controllers; + +/// +/// Hosts the single deprecated full-cipher read-back endpoint. The rest of the ciphers/{id}/lease resource +/// (pre-check, state, submit) is served as Minimal API endpoints from the Commercial.Pam library. This action stays +/// in the Api project because it depends on the Vault response models (), +/// which live here; it is scheduled for removal, after which the Api project carries no PAM code. +/// +[Route("ciphers/{id:guid}/lease")] +[Authorize("Application")] +[RequireFeature(FeatureFlagKeys.Pam)] +public class CipherLeaseController( + IUserService userService, + IGetLeasedCipherQuery getLeasedCipherQuery, + IApplicationCacheService applicationCacheService, + ICollectionCipherRepository collectionCipherRepository, + ICipherLeaseGate cipherLeaseGate, + GlobalSettings globalSettings) + : Controller +{ + /// + /// Returns the cipher with its complete data, but only if the caller currently holds an active lease for it. + /// This is the read-back counterpart to the partial data sync returns for leasing-gated ciphers. The data is + /// still client-encrypted; the lease only gates whether the server hands it over. + /// + // DEPRECATED: scheduled for removal; the full leased cipher will be served through the standard cipher read path + // rather than this dedicated endpoint. Kept fully functional during the PAM pre-release. Removal is a later task. + [Obsolete("Deprecated and scheduled for removal; the full leased cipher will be served through the standard " + + "cipher read path instead of this dedicated endpoint. Kept functional for the PAM pre-release.")] + [HttpGet("cipher")] + public async Task GetCipher(Guid id) + { + var user = await userService.GetUserByPrincipalAsync(User); + if (user == null) + { + throw new NotFoundException(); + } + + var cipher = await getLeasedCipherQuery.GetLeasedCipherAsync(user.Id, id); + if (cipher == null) + { + throw new NotFoundException(); + } + + var organizationAbility = cipher.OrganizationId.HasValue + ? await applicationCacheService.GetOrganizationAbilityAsync(cipher.OrganizationId.Value) + : null; + var collectionCiphers = await collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id); + + // The query above already confirmed an active lease, so the gate authorizes full data here. + var access = await cipherLeaseGate.AuthorizeReadAsync(user.Id, cipher); + return access is null + ? new CipherDetailsResponseModel(cipher, user, organizationAbility, globalSettings, collectionCiphers) + : new FullCipherDetailsResponseModel(access, cipher, user, organizationAbility, globalSettings, collectionCiphers); + } +} diff --git a/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs b/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs index ad5d5e092b9a..e859d0ae578c 100644 --- a/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs +++ b/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs @@ -1,5 +1,4 @@ -using Bit.Api.Models.Response; -using Bit.Api.SecretsManager.Models.Request; +using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; using Bit.Core.Context; using Bit.Core.Enums; @@ -11,6 +10,7 @@ using Bit.Core.SecretsManager.Queries.Interfaces; using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/SecretsManager/Controllers/ProjectsController.cs b/src/Api/SecretsManager/Controllers/ProjectsController.cs index 5dce032eceb4..a78358c36f90 100644 --- a/src/Api/SecretsManager/Controllers/ProjectsController.cs +++ b/src/Api/SecretsManager/Controllers/ProjectsController.cs @@ -1,7 +1,6 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable -using Bit.Api.Models.Response; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; using Bit.Core.Auth.Identity; @@ -14,6 +13,7 @@ using Bit.Core.SecretsManager.Queries.Projects.Interfaces; using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/SecretsManager/Controllers/SecretVersionsController.cs b/src/Api/SecretsManager/Controllers/SecretVersionsController.cs index 86e2d1f7e9c1..a9cf47e92255 100644 --- a/src/Api/SecretsManager/Controllers/SecretVersionsController.cs +++ b/src/Api/SecretsManager/Controllers/SecretVersionsController.cs @@ -1,5 +1,4 @@ -using Bit.Api.Models.Response; -using Bit.Api.SecretsManager.Models.Request; +using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; using Bit.Core.Auth.Identity; using Bit.Core.Context; @@ -8,6 +7,7 @@ using Bit.Core.Repositories; using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/SecretsManager/Controllers/SecretsController.cs b/src/Api/SecretsManager/Controllers/SecretsController.cs index dcfe1be11188..6e29c75bf2d9 100644 --- a/src/Api/SecretsManager/Controllers/SecretsController.cs +++ b/src/Api/SecretsManager/Controllers/SecretsController.cs @@ -1,7 +1,6 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable -using Bit.Api.Models.Response; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; using Bit.Core.Auth.Identity; @@ -19,6 +18,7 @@ using Bit.Core.SecretsManager.Queries.Secrets.Interfaces; using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/SecretsManager/Controllers/SecretsManagerEventsController.cs b/src/Api/SecretsManager/Controllers/SecretsManagerEventsController.cs index 0f467a4c7881..ed8c7b9715ee 100644 --- a/src/Api/SecretsManager/Controllers/SecretsManagerEventsController.cs +++ b/src/Api/SecretsManager/Controllers/SecretsManagerEventsController.cs @@ -2,13 +2,13 @@ #nullable disable using Bit.Api.Dirt.Models.Response; -using Bit.Api.Models.Response; using Bit.Api.Utilities; using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Core.SecretsManager.AuthorizationRequirements; using Bit.Core.SecretsManager.Repositories; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs b/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs index 0afdc3a1bf0f..b8affceca8bf 100644 --- a/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs +++ b/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs @@ -1,7 +1,6 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable -using Bit.Api.Models.Response; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; using Bit.Core.Billing.Pricing; @@ -18,6 +17,7 @@ using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces; using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/SecretsManager/Models/Response/SecretsSyncResponseModel.cs b/src/Api/SecretsManager/Models/Response/SecretsSyncResponseModel.cs index 56b88113616d..926b93a8a678 100644 --- a/src/Api/SecretsManager/Models/Response/SecretsSyncResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/SecretsSyncResponseModel.cs @@ -1,7 +1,7 @@ #nullable enable -using Bit.Api.Models.Response; using Bit.Core.Models.Api; using Bit.Core.SecretsManager.Entities; +using Bit.SharedWeb.Models.Response; namespace Bit.Api.SecretsManager.Models.Response; diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 7ead64850e2a..58a7008eed37 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -37,9 +37,12 @@ #if !OSS +using Bit.Commercial.Pam.Api; +using Bit.Commercial.Pam.Api.Endpoints; using Bit.Commercial.Core.SecretsManager; using Bit.Commercial.Core.Utilities; using Bit.Commercial.Infrastructure.EntityFramework.SecretsManager; +using Bit.Commercial.Pam.Utilities; #endif namespace Bit.Api; @@ -204,6 +207,8 @@ public void ConfigureServices(IServiceCollection services) #else services.AddCommercialCoreServices(); services.AddCommercialSecretsManagerServices(); + services.AddCommercialPamServices(); + services.AddPamApiServices(); services.AddSecretsManagerEfRepositories(); Jobs.JobsHostedService.AddCommercialSecretsManagerJobServices(services); #endif @@ -215,6 +220,8 @@ public void ConfigureServices(IServiceCollection services) config.Conventions.Add(new PublicApiControllersModelConvention()); }); + // Required for the PAM Minimal API endpoints to be discovered by ApiExplorer/Swagger. + services.AddEndpointsApiExplorer(); services.AddSwaggerGen(globalSettings, Environment); Jobs.JobsHostedService.AddJobsServices(services, globalSettings.SelfHosted); services.AddHostedService(); @@ -278,6 +285,11 @@ public void Configure( { endpoints.MapDefaultControllerRoute(); +#if !OSS + // PAM is a commercial feature; its Minimal API endpoints are only mapped in non-OSS builds. + endpoints.MapPamEndpoints(); +#endif + if (!globalSettings.SelfHosted) { endpoints.MapHealthChecks("/healthz"); diff --git a/src/Api/Tools/Controllers/OrganizationExportController.cs b/src/Api/Tools/Controllers/OrganizationExportController.cs index ff0bff1150d7..944a2e9f63b2 100644 --- a/src/Api/Tools/Controllers/OrganizationExportController.cs +++ b/src/Api/Tools/Controllers/OrganizationExportController.cs @@ -6,6 +6,7 @@ using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Vault.Queries; +using Bit.Pam.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -20,6 +21,7 @@ public class OrganizationExportController : Controller private readonly IAuthorizationService _authorizationService; private readonly IOrganizationCiphersQuery _organizationCiphersQuery; private readonly ICollectionRepository _collectionRepository; + private readonly ICipherLeaseGate _cipherLeaseGate; public OrganizationExportController( IUserService userService, @@ -27,13 +29,15 @@ public OrganizationExportController( IAuthorizationService authorizationService, IOrganizationCiphersQuery organizationCiphersQuery, ICollectionRepository collectionRepository, - IFeatureService featureService) + IFeatureService featureService, + ICipherLeaseGate cipherLeaseGate) { _userService = userService; _globalSettings = globalSettings; _authorizationService = authorizationService; _organizationCiphersQuery = organizationCiphersQuery; _collectionRepository = collectionRepository; + _cipherLeaseGate = cipherLeaseGate; } [HttpGet("export")] @@ -50,8 +54,9 @@ public async Task Export(Guid organizationId) .GetManySharedCollectionsByOrganizationIdAsync(organizationId); await Task.WhenAll(ciphersTask, collectionsTask); + // Whole-vault export is authorized through org-wide permissions, so nothing is leasing-gated. return Ok(new OrganizationExportResponseModel(ciphersTask.Result, collectionsTask.Result, - _globalSettings)); + _globalSettings, _cipherLeaseGate.Unrestricted())); } var canExportManaged = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(organizationId), @@ -68,7 +73,9 @@ public async Task Export(Guid organizationId) var managedCiphers = await _organizationCiphersQuery.GetOrganizationCiphersByCollectionIds(organizationId, managedOrgCollections.Select(c => c.Id)); - return Ok(new OrganizationExportResponseModel(managedCiphers, managedOrgCollections, _globalSettings)); + // Leasing-gated ciphers the exporter holds no valid lease for are excluded from the export. + var fullAccess = await _cipherLeaseGate.AuthorizeReadManyAsync(userId, managedCiphers); + return Ok(new OrganizationExportResponseModel(managedCiphers, managedOrgCollections, _globalSettings, fullAccess)); } // Unauthorized diff --git a/src/Api/Tools/Controllers/SendsController.cs b/src/Api/Tools/Controllers/SendsController.cs index 99267e2b7ed0..9d2b0f9facea 100644 --- a/src/Api/Tools/Controllers/SendsController.cs +++ b/src/Api/Tools/Controllers/SendsController.cs @@ -1,6 +1,5 @@ using System.Text.Json; using Azure.Messaging.EventGrid; -using Bit.Api.Models.Response; using Bit.Api.Tools.Models.Request; using Bit.Api.Tools.Models.Response; using Bit.Api.Utilities; @@ -20,6 +19,7 @@ using Bit.Core.Tools.SendFeatures.Queries.Interfaces; using Bit.Core.Tools.Services; using Bit.Core.Utilities; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/Tools/Models/Response/OrganizationExportResponseModel.cs b/src/Api/Tools/Models/Response/OrganizationExportResponseModel.cs index 208c7f7aefde..ff0d1c3a9fa8 100644 --- a/src/Api/Tools/Models/Response/OrganizationExportResponseModel.cs +++ b/src/Api/Tools/Models/Response/OrganizationExportResponseModel.cs @@ -2,12 +2,13 @@ #nullable disable using Bit.Api.AdminConsole.Models.Response; -using Bit.Api.Models.Response; using Bit.Api.Vault.Models.Response; using Bit.Core.Entities; using Bit.Core.Models.Api; using Bit.Core.Settings; +using Bit.Core.Vault.Authorization; using Bit.Core.Vault.Models.Data; +using Bit.SharedWeb.Models.Response; namespace Bit.Api.Tools.Models.Response; @@ -18,9 +19,13 @@ public OrganizationExportResponseModel() : base("organizationExport") } public OrganizationExportResponseModel(IEnumerable ciphers, - IEnumerable collections, GlobalSettings globalSettings) : this() + IEnumerable collections, GlobalSettings globalSettings, FullCipherAccess fullCipherAccess) : this() { - Ciphers = ciphers.Select(c => new CipherMiniDetailsResponseModel(c, globalSettings)); + // Under PAM credential leasing, a leasing-gated cipher the exporter cannot fully access is + // excluded from the export entirely — a partially-stripped export is not a usable backup. + Ciphers = ciphers + .Where(c => fullCipherAccess.Authorizes(c.Id)) + .Select(c => new FullCipherMiniDetailsResponseModel(fullCipherAccess, c, globalSettings)); Collections = collections.Select(c => new CollectionResponseModel(c)); } diff --git a/src/Api/Vault/Authorization/LeaseFilterExemptAttribute.cs b/src/Api/Vault/Authorization/LeaseFilterExemptAttribute.cs new file mode 100644 index 000000000000..64c52693f996 --- /dev/null +++ b/src/Api/Vault/Authorization/LeaseFilterExemptAttribute.cs @@ -0,0 +1,20 @@ +namespace Bit.Api.Vault.Authorization; + +/// +/// Documents an intentional choice that a controller action returning cipher data does not need PAM +/// credential-leasing filtering — for example, because it only ever handles personal ciphers, or +/// because authorization is enforced out-of-band. The lease-filter fitness test allows actions that +/// carry this marker (and otherwise requires a sanctioned response type), mirroring how +/// NoopAuthorizeAttribute documents intentional non-authorization. +/// +[AttributeUsage(AttributeTargets.Method)] +public class LeaseFilterExemptAttribute : Attribute +{ + public LeaseFilterExemptAttribute(string reason) + { + Reason = reason; + } + + /// Why this action is exempt from lease filtering. Required, for auditability. + public string Reason { get; } +} diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 6b91ed1b0f15..c1b4c408ffba 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -5,7 +5,6 @@ using System.Text.Json; using Azure.Messaging.EventGrid; using Bit.Api.Auth.Models.Request.Accounts; -using Bit.Api.Models.Response; using Bit.Api.Utilities; using Bit.Api.Vault.Models.Request; using Bit.Api.Vault.Models.Response; @@ -14,6 +13,7 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations; using Bit.Core.Repositories; using Bit.Core.Services; @@ -27,6 +27,8 @@ using Bit.Core.Vault.Queries; using Bit.Core.Vault.Repositories; using Bit.Core.Vault.Services; +using Bit.Pam.Services; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -51,6 +53,7 @@ public class CiphersController : Controller private readonly ICollectionRepository _collectionRepository; private readonly IArchiveCiphersCommand _archiveCiphersCommand; private readonly IUnarchiveCiphersCommand _unarchiveCiphersCommand; + private readonly ICipherLeaseGate _cipherLeaseGate; public CiphersController( ICipherRepository cipherRepository, @@ -65,7 +68,8 @@ public CiphersController( IApplicationCacheService applicationCacheService, ICollectionRepository collectionRepository, IArchiveCiphersCommand archiveCiphersCommand, - IUnarchiveCiphersCommand unarchiveCiphersCommand) + IUnarchiveCiphersCommand unarchiveCiphersCommand, + ICipherLeaseGate cipherLeaseGate) { _cipherRepository = cipherRepository; _collectionCipherRepository = collectionCipherRepository; @@ -80,6 +84,49 @@ public CiphersController( _collectionRepository = collectionRepository; _archiveCiphersCommand = archiveCiphersCommand; _unarchiveCiphersCommand = unarchiveCiphersCommand; + _cipherLeaseGate = cipherLeaseGate; + } + + /// + /// Builds a cipher response for a personal/member read or write-return, honouring credential leasing: + /// a leasing-gated cipher with no valid active lease yields the partial shape, otherwise the full one. + /// + private async Task BuildCipherResponseAsync(CipherDetails cipher, User user) + { + var organizationAbility = await GetOrganizationAbilityAsync(cipher); + var access = await _cipherLeaseGate.AuthorizeReadAsync(user.Id, cipher); + return access is null + ? new CipherResponseModel(cipher, user, organizationAbility, _globalSettings) + : new FullCipherResponseModel(access, cipher, user, organizationAbility, _globalSettings); + } + + /// Details variant of . + private async Task BuildCipherDetailsResponseAsync( + CipherDetails cipher, User user, IEnumerable collectionCiphers) + { + var organizationAbility = await GetOrganizationAbilityAsync(cipher); + var access = await _cipherLeaseGate.AuthorizeReadAsync(user.Id, cipher); + return access is null + ? new CipherDetailsResponseModel(cipher, user, organizationAbility, _globalSettings, collectionCiphers) + : new FullCipherDetailsResponseModel(access, cipher, user, organizationAbility, _globalSettings, collectionCiphers); + } + + /// + /// Bulk variant for write-returns over a set of ciphers (archive/unarchive). Leasing-gated ciphers + /// the caller cannot fully access fall back to the partial shape; the gated set is resolved once. + /// + private async Task> BuildCipherResponsesAsync( + ICollection ciphers, User user, + IDictionary organizationAbilities) + { + var fullAccess = await _cipherLeaseGate.AuthorizeReadManyAsync(user.Id, ciphers); + return ciphers.Select(cipher => + { + var organizationAbility = GetOrganizationAbility(cipher, organizationAbilities); + return fullAccess.Authorizes(cipher.Id) + ? new FullCipherResponseModel(fullAccess, cipher, user, organizationAbility, _globalSettings) + : new CipherResponseModel(cipher, user, organizationAbility, _globalSettings); + }); } [HttpGet("{id}")] @@ -92,9 +139,7 @@ public async Task Get(Guid id) throw new NotFoundException(); } - var organizationAbility = await GetOrganizationAbilityAsync(cipher); - - return new CipherResponseModel(cipher, user, organizationAbility, _globalSettings); + return await BuildCipherResponseAsync(cipher, user); } [HttpGet("{id}/admin")] @@ -110,7 +155,10 @@ public async Task GetAdmin(string id) var collectionCiphers = await _collectionCipherRepository.GetManyByOrganizationIdAsync(cipher.OrganizationId.Value); var collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key); - return new CipherMiniDetailsResponseModel(cipher, _globalSettings, collectionCiphersGroupDict, cipher.OrganizationUseTotp); + // Admin/org-wide path: authorized through org permissions with no collection-membership path, so + // the cipher is never leasing-gated for this caller and full data is released. + return new FullCipherMiniDetailsResponseModel(_cipherLeaseGate.Unrestricted(), cipher, _globalSettings, + collectionCiphersGroupDict, cipher.OrganizationUseTotp); } [HttpGet("{id}/details")] @@ -123,9 +171,8 @@ public async Task GetDetails(Guid id) throw new NotFoundException(); } - var organizationAbility = await GetOrganizationAbilityAsync(cipher); var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id); - return new CipherDetailsResponseModel(cipher, user, organizationAbility, _globalSettings, collectionCiphers); + return await BuildCipherDetailsResponseAsync(cipher, user, collectionCiphers); } [HttpGet("{id}/full-details")] @@ -143,18 +190,25 @@ public async Task> GetAll() // TODO: Use hasOrgs proper for cipher listing here? var ciphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, withOrganizations: true); Dictionary> collectionCiphersGroupDict = null; + IEnumerable collections = null; if (hasOrgs) { var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdAsync(user.Id); collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key); + collections = await _collectionRepository.GetManyByUserIdAsync(user.Id); } var organizationAbilities = await GetOrganizationAbilitiesAsync(ciphers); - var responses = ciphers.Select(cipher => new CipherDetailsResponseModel( - cipher, - user, - GetOrganizationAbility(cipher, organizationAbilities), - _globalSettings, - collectionCiphersGroupDict)).ToArray(); + + // Bulk reads strip every leasing-gated cipher regardless of lease state; secrets are only ever + // released through the single-cipher GET above. The gated set is computed in-memory. + var fullAccess = await _cipherLeaseGate.AuthorizeReadManyAsync(user.Id, ciphers, collections, collectionCiphersGroupDict); + var responses = ciphers.Select(cipher => + { + var organizationAbility = GetOrganizationAbility(cipher, organizationAbilities); + return fullAccess.Authorizes(cipher.Id) + ? new FullCipherDetailsResponseModel(fullAccess, cipher, user, organizationAbility, _globalSettings, collectionCiphersGroupDict) + : new CipherDetailsResponseModel(cipher, user, organizationAbility, _globalSettings, collectionCiphersGroupDict); + }).ToArray(); return new ListResponseModel(responses); } @@ -181,8 +235,7 @@ public async Task Post([FromBody] CipherRequestModel model) } await _cipherService.SaveDetailsAsync(cipher, user.Id, model.LastKnownRevisionDate, null, cipher.OrganizationId.HasValue); - var response = new CipherResponseModel(cipher, user, await GetOrganizationAbilityAsync(cipher), _globalSettings); - return response; + return await BuildCipherResponseAsync(cipher, user); } [HttpPost("create")] @@ -235,8 +288,8 @@ public async Task PostAdmin([FromBody] CipherCreateRequ await _cipherService.SaveAsync(cipher, userId, model.Cipher.LastKnownRevisionDate, model.CollectionIds, true, false); - var response = new CipherMiniResponseModel(cipher, _globalSettings, false); - return response; + // Admin create through org-wide permissions; the cipher is not leasing-gated for this caller. + return new FullCipherMiniResponseModel(_cipherLeaseGate.Unrestricted(), cipher, _globalSettings, false); } [HttpPut("{id}")] @@ -272,8 +325,7 @@ public async Task Put(Guid id, [FromBody] CipherRequestMode await _cipherService.SaveDetailsAsync(model.ToCipherDetails(cipher), user.Id, model.LastKnownRevisionDate, collectionIds); - var response = new CipherResponseModel(cipher, user, await GetOrganizationAbilityAsync(cipher), _globalSettings); - return response; + return await BuildCipherResponseAsync(cipher, user); } [HttpPost("{id}")] @@ -312,8 +364,8 @@ public async Task PutAdmin(Guid id, [FromBody] CipherRe var cipherClone = model.ToCipher(cipher).Clone(); await _cipherService.SaveAsync(cipherClone, userId, model.LastKnownRevisionDate, collectionIds, true, false); - var response = new CipherMiniResponseModel(cipherClone, _globalSettings, cipher.OrganizationUseTotp); - return response; + // Admin edit through org-wide permissions; the cipher is not leasing-gated for this caller. + return new FullCipherMiniResponseModel(_cipherLeaseGate.Unrestricted(), cipherClone, _globalSettings, cipher.OrganizationUseTotp); } [HttpPost("{id}/admin")] @@ -337,9 +389,12 @@ await _organizationCiphersQuery.GetAllOrganizationCiphersExcludingDefaultUserCol : await _organizationCiphersQuery.GetAllOrganizationCiphers(organizationId); + // Reaching this endpoint requires can-access-all-ciphers (org-wide) permission, so these ciphers + // are not leasing-gated for this caller and full data is released. + var fullAccess = _cipherLeaseGate.Unrestricted(); var allOrganizationCipherResponses = allOrganizationCiphers.Select(c => - new CipherMiniDetailsResponseModel(c, _globalSettings, c.OrganizationUseTotp) + new FullCipherMiniDetailsResponseModel(fullAccess, c, _globalSettings, c.OrganizationUseTotp) ); return new ListResponseModel(allOrganizationCipherResponses); @@ -366,10 +421,24 @@ public async Task> GetAssignedOrga })); } + var cipherList = ciphers.ToList(); var user = await _userService.GetUserByPrincipalAsync(User); var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId); - var responses = ciphers.Select(cipher => - new CipherDetailsResponseModel(cipher, user, organizationAbility, _globalSettings)); + + // Member read: leasing-gated ciphers (reachable only through leasing-enabled collections) are + // delivered partial here too, computed in-memory from the user's collections and each cipher's + // own collection memberships. Secrets are only released through the single-cipher GET. + var collections = await _collectionRepository.GetManyByUserIdAsync(user.Id); + var collectionCiphersGroupDict = cipherList + .SelectMany(c => (c.CollectionIds ?? []).Select(cid => new CollectionCipher { CipherId = c.Id, CollectionId = cid })) + .GroupBy(cc => cc.CipherId) + .ToDictionary(g => g.Key); + var fullAccess = await _cipherLeaseGate.AuthorizeReadManyAsync(user.Id, cipherList, collections, collectionCiphersGroupDict); + + var responses = cipherList.Select(cipher => + fullAccess.Authorizes(cipher.Id) + ? new FullCipherDetailsResponseModel(fullAccess, cipher, user, organizationAbility, _globalSettings) + : new CipherDetailsResponseModel(cipher, user, organizationAbility, _globalSettings)); return new ListResponseModel(responses); } @@ -709,8 +778,7 @@ public async Task PutPartial(Guid id, [FromBody] CipherPart await _cipherRepository.UpdatePartialAsync(id, user.Id, folderId, model.Favorite); var updatedCipher = await GetByIdAsync(id, user.Id); - var response = new CipherResponseModel(updatedCipher, user, await GetOrganizationAbilityAsync(updatedCipher), _globalSettings); - return response; + return await BuildCipherResponseAsync(updatedCipher, user); } [HttpPost("{id}/partial")] @@ -748,8 +816,7 @@ public async Task PutShare(Guid id, [FromBody] CipherShareR model.CollectionIds.Select(c => new Guid(c)), user.Id, model.Cipher.LastKnownRevisionDate); var sharedCipher = await GetByIdAsync(id, user.Id); - var response = new CipherResponseModel(sharedCipher, user, await GetOrganizationAbilityAsync(sharedCipher), _globalSettings); - return response; + return await BuildCipherResponseAsync(sharedCipher, user); } [HttpPost("{id}/share")] @@ -776,7 +843,7 @@ await _cipherService.SaveCollectionsAsync(cipher, var updatedCipher = await GetByIdAsync(id, user.Id); var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id); - return new CipherDetailsResponseModel(updatedCipher, user, await GetOrganizationAbilityAsync(updatedCipher), _globalSettings, collectionCiphers); + return await BuildCipherDetailsResponseAsync(updatedCipher, user, collectionCiphers); } [HttpPost("{id}/collections")] @@ -809,7 +876,7 @@ await _cipherService.SaveCollectionsAsync(cipher, Unavailable = updatedCipher is null, Cipher = updatedCipher is null ? null - : new CipherDetailsResponseModel(updatedCipher, user, await GetOrganizationAbilityAsync(updatedCipher), _globalSettings, collectionCiphers) + : await BuildCipherDetailsResponseAsync(updatedCipher, user, collectionCiphers) }; return response; } @@ -847,7 +914,10 @@ public async Task PutCollectionsAdmin(string id, var collectionCiphers = await _collectionCipherRepository.GetManyByOrganizationIdAsync(cipher.OrganizationId.Value); var collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key); - return new CipherMiniDetailsResponseModel(cipher, _globalSettings, collectionCiphersGroupDict, cipher.OrganizationUseTotp); + // Admin/org-wide path: authorized through org permissions with no collection-membership path, so + // the cipher is never leasing-gated for this caller and full data is released. + return new FullCipherMiniDetailsResponseModel(_cipherLeaseGate.Unrestricted(), cipher, _globalSettings, + collectionCiphersGroupDict, cipher.OrganizationUseTotp); } [HttpPost("{id}/collections-admin")] @@ -892,7 +962,8 @@ public async Task PutArchive(Guid id) } var archivedCipher = archivedCipherOrganizationDetails.First(); - return new CipherResponseModel(archivedCipher, await _userService.GetUserByPrincipalAsync(User), await GetOrganizationAbilityAsync(archivedCipher), _globalSettings); + var user = await _userService.GetUserByPrincipalAsync(User); + return await BuildCipherResponseAsync(archivedCipher, user); } [HttpPut("archive")] @@ -916,8 +987,7 @@ public async Task> PutArchiveMany([FromBo } var organizationAbilities = await GetOrganizationAbilitiesAsync(archivedCiphers); - var responses = archivedCiphers.Select(cipher => - new CipherResponseModel(cipher, user, GetOrganizationAbility(cipher, organizationAbilities), _globalSettings)).ToArray(); + var responses = (await BuildCipherResponsesAsync(archivedCiphers, user, organizationAbilities)).ToArray(); return new ListResponseModel(responses); } @@ -1092,11 +1162,8 @@ public async Task PutUnarchive(Guid id) } var unarchivedCipher = unarchivedCipherDetails.First(); - return new CipherResponseModel(unarchivedCipher, - await _userService.GetUserByPrincipalAsync(User), - await GetOrganizationAbilityAsync(unarchivedCipher), - _globalSettings - ); + var user = await _userService.GetUserByPrincipalAsync(User); + return await BuildCipherResponseAsync(unarchivedCipher, user); } [HttpPut("unarchive")] @@ -1120,8 +1187,7 @@ public async Task> PutUnarchiveMany([From } var organizationAbilities = await GetOrganizationAbilitiesAsync(unarchivedCipherOrganizationDetails); - var responses = unarchivedCipherOrganizationDetails.Select(cipher => - new CipherResponseModel(cipher, user, GetOrganizationAbility(cipher, organizationAbilities), _globalSettings)).ToArray(); + var responses = (await BuildCipherResponsesAsync(unarchivedCipherOrganizationDetails, user, organizationAbilities)).ToArray(); return new ListResponseModel(responses); } @@ -1137,11 +1203,7 @@ public async Task PutRestore(Guid id) } await _cipherService.RestoreAsync(cipher, user.Id); - return new CipherResponseModel( - cipher, - user, - await GetOrganizationAbilityAsync(cipher), - _globalSettings); + return await BuildCipherResponseAsync(cipher, user); } [HttpPut("{id}/restore-admin")] @@ -1156,7 +1218,8 @@ public async Task PutRestoreAdmin(Guid id) } await _cipherService.RestoreAsync(new CipherDetails(cipher), userId, true); - return new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp); + // Admin restore through org-wide permissions; the cipher is not leasing-gated for this caller. + return new FullCipherMiniResponseModel(_cipherLeaseGate.Unrestricted(), cipher, _globalSettings, cipher.OrganizationUseTotp); } [HttpPut("restore")] @@ -1171,7 +1234,10 @@ public async Task> PutRestoreMany([Fr var cipherIdsToRestore = new HashSet(model.Ids.Select(i => new Guid(i))); var restoredCiphers = await _cipherService.RestoreManyAsync(cipherIdsToRestore, userId); - var responses = restoredCiphers.Select(c => new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp)); + var fullAccess = await _cipherLeaseGate.AuthorizeReadManyAsync(userId, restoredCiphers); + var responses = restoredCiphers.Select(c => fullAccess.Authorizes(c.Id) + ? new FullCipherMiniResponseModel(fullAccess, c, _globalSettings, c.OrganizationUseTotp) + : new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp)); return new ListResponseModel(responses); } @@ -1198,7 +1264,9 @@ public async Task> PutRestoreManyAdmi var userId = _userService.GetProperUserId(User).Value; var restoredCiphers = await _cipherService.RestoreManyAsync(cipherIdsToRestore, userId, model.OrganizationId, true); - var responses = restoredCiphers.Select(c => new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp)); + // Admin restore through org-wide permissions; these ciphers are not leasing-gated for this caller. + var fullAccess = _cipherLeaseGate.Unrestricted(); + var responses = restoredCiphers.Select(c => new FullCipherMiniResponseModel(fullAccess, c, _globalSettings, c.OrganizationUseTotp)); return new ListResponseModel(responses); } @@ -1266,7 +1334,10 @@ public async Task> PutShareMany([From userId ); - var response = updated.Select(c => new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp)); + var fullAccess = await _cipherLeaseGate.AuthorizeReadManyAsync(userId, updated); + var response = updated.Select(c => fullAccess.Authorizes(c.Id) + ? new FullCipherMiniResponseModel(fullAccess, c, _globalSettings, c.OrganizationUseTotp) + : new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp)); return new ListResponseModel(response); } @@ -1340,12 +1411,10 @@ await _cipherRepository.GetOrganizationDetailsByIdAsync(id) : AttachmentId = attachmentId, Url = uploadUrl, FileUploadType = _attachmentStorageService.FileUploadType, - CipherResponse = request.AdminRequest ? null : new CipherResponseModel( - cipherDetails, - user, - await GetOrganizationAbilityAsync(cipherDetails), - _globalSettings), - CipherMiniResponse = request.AdminRequest ? new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp) : null, + CipherResponse = request.AdminRequest ? null : await BuildCipherResponseAsync(cipherDetails, user), + CipherMiniResponse = request.AdminRequest + ? new FullCipherMiniResponseModel(_cipherLeaseGate.Unrestricted(), cipher, _globalSettings, cipher.OrganizationUseTotp) + : null, }; } @@ -1433,11 +1502,7 @@ await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, key, Request.ContentLength.GetValueOrDefault(0), user.Id, false, lastKnownRevisionDate); }); - return new CipherResponseModel( - cipher, - user, - await GetOrganizationAbilityAsync(cipher), - _globalSettings); + return await BuildCipherResponseAsync(cipher, user); } [HttpPost("{id}/attachment-admin")] @@ -1465,7 +1530,8 @@ await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, key, Request.ContentLength.GetValueOrDefault(0), userId, true, lastKnownRevisionDate); }); - return new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp); + // Admin attachment upload through org-wide permissions; the cipher is not leasing-gated here. + return new FullCipherMiniResponseModel(_cipherLeaseGate.Unrestricted(), cipher, _globalSettings, cipher.OrganizationUseTotp); } [HttpGet("{id}/attachment/{attachmentId}/admin")] diff --git a/src/Api/Vault/Controllers/FoldersController.cs b/src/Api/Vault/Controllers/FoldersController.cs index 195931f60cbc..3da222437fc3 100644 --- a/src/Api/Vault/Controllers/FoldersController.cs +++ b/src/Api/Vault/Controllers/FoldersController.cs @@ -1,13 +1,13 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable -using Bit.Api.Models.Response; using Bit.Api.Vault.Models.Request; using Bit.Api.Vault.Models.Response; using Bit.Core.Exceptions; using Bit.Core.Services; using Bit.Core.Vault.Repositories; using Bit.Core.Vault.Services; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/Vault/Controllers/SecurityTaskController.cs b/src/Api/Vault/Controllers/SecurityTaskController.cs index efff200e864a..24021747abf8 100644 --- a/src/Api/Vault/Controllers/SecurityTaskController.cs +++ b/src/Api/Vault/Controllers/SecurityTaskController.cs @@ -1,7 +1,6 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable -using Bit.Api.Models.Response; using Bit.Api.Vault.Models.Request; using Bit.Api.Vault.Models.Response; using Bit.Core.Services; @@ -9,6 +8,7 @@ using Bit.Core.Vault.Entities; using Bit.Core.Vault.Enums; using Bit.Core.Vault.Queries; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/Vault/Controllers/SyncController.cs b/src/Api/Vault/Controllers/SyncController.cs index 13eddbc98eb1..b113265cfb7b 100644 --- a/src/Api/Vault/Controllers/SyncController.cs +++ b/src/Api/Vault/Controllers/SyncController.cs @@ -23,6 +23,7 @@ using Bit.Core.Tools.Repositories; using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Repositories; +using Bit.Pam.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -50,6 +51,7 @@ public class SyncController : Controller private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository; private readonly IUserAccountKeysQuery _userAccountKeysQuery; + private readonly ICipherLeaseGate _cipherLeaseGate; public SyncController( IUserService userService, @@ -67,7 +69,8 @@ public SyncController( IApplicationCacheService applicationCacheService, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IWebAuthnCredentialRepository webAuthnCredentialRepository, - IUserAccountKeysQuery userAccountKeysQuery) + IUserAccountKeysQuery userAccountKeysQuery, + ICipherLeaseGate cipherLeaseGate) { _userService = userService; _folderRepository = folderRepository; @@ -85,6 +88,7 @@ public SyncController( _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _webAuthnCredentialRepository = webAuthnCredentialRepository; _userAccountKeysQuery = userAccountKeysQuery; + _cipherLeaseGate = cipherLeaseGate; } [HttpGet("")] @@ -121,6 +125,11 @@ await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id, collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key); } + // PAM credential leasing: ciphers reachable only through leasing-enabled collections are delivered + // with reduced data during the passive sync. The active GET /ciphers/{id} path is unchanged. The + // witness authorizes the non-gated subset; gated ciphers fall through to the partial shape. + var fullCipherAccess = await _cipherLeaseGate.AuthorizeReadManyAsync(user.Id, ciphers, collections, collectionCiphersGroupDict); + var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); var organizationClaimingActiveUser = await _userService.GetOrganizationsClaimingUserAsync(user.Id); @@ -147,7 +156,7 @@ await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id, var response = new SyncResponseModel(_globalSettings, user, userAccountKeys, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities, organizationIdsClaimingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails, folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends, webAuthnCredentials, - policiesNew, organizationUserDetailsNew); + policiesNew, organizationUserDetailsNew, fullCipherAccess); return response; } diff --git a/src/Api/Vault/Models/Response/CipherResponseModel.cs b/src/Api/Vault/Models/Response/CipherResponseModel.cs index 808e9e94b921..75953d198cc6 100644 --- a/src/Api/Vault/Models/Response/CipherResponseModel.cs +++ b/src/Api/Vault/Models/Response/CipherResponseModel.cs @@ -1,10 +1,12 @@  using System.Text.Json; +using System.Text.Json.Serialization; using Bit.Core.Entities; using Bit.Core.Models.Api; using Bit.Core.Models.Data.Organizations; using Bit.Core.Settings; +using Bit.Core.Vault.Authorization; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Enums; using Bit.Core.Vault.Models.Data; @@ -15,7 +17,19 @@ namespace Bit.Api.Vault.Models.Response; public class CipherMiniResponseModel : ResponseModel { - public CipherMiniResponseModel(Cipher cipher, IGlobalSettings globalSettings, bool orgUseTotp, string obj = "cipherMini") + // PARTIAL/safe constructor. Under PAM credential leasing the secret Data blob is never emitted from + // here — only the reduced PartialData. Any path that uses this type without a FullCipherAccess witness + // therefore fails closed: a missed migration returns partial data (a visible bug), never a leak. + public CipherMiniResponseModel(Cipher cipher, IGlobalSettings globalSettings, bool orgUseTotp, + string obj = "cipherMini") + : this(cipher, globalSettings, orgUseTotp, obj, partial: true) + { + } + + // Shared construction. When partial is false the secret Data is left null for a derived Full* type to + // populate via PopulateFullData; this constructor never emits secret data on its own. + protected CipherMiniResponseModel(Cipher cipher, IGlobalSettings globalSettings, bool orgUseTotp, + string obj, bool partial) : base(obj) { if (cipher == null) @@ -25,7 +39,6 @@ public CipherMiniResponseModel(Cipher cipher, IGlobalSettings globalSettings, bo Id = cipher.Id; Type = cipher.Type; - Data = cipher.Data; RevisionDate = cipher.RevisionDate; OrganizationId = cipher.OrganizationId; Attachments = AttachmentResponseModel.FromCipher(cipher, globalSettings); @@ -35,6 +48,25 @@ public CipherMiniResponseModel(Cipher cipher, IGlobalSettings globalSettings, bo Reprompt = cipher.Reprompt.GetValueOrDefault(CipherRepromptType.None); Key = cipher.Key; + if (partial && !cipher.IsDataBlobEncrypted()) + { + // The reduced blob signals the cipher is leasing-gated; the client decrypts PartialData itself. + // An opaque (SDK-encrypted) blob can't be reshaped without decrypting, so nothing is returned. + PartialData = PartialCipherData.Strip(cipher.Type, cipher.Data); + } + } + + /// + /// Populates the full secret data blob (and the obsolete typed fields) for a Full* response. Requires a + /// witness authorizing this cipher, so full secret data cannot be emitted + /// without first passing through the leasing gate that mints the witness. + /// + protected void PopulateFullData(FullCipherAccess access, Cipher cipher) + { + access.Require(cipher.Id); + + Data = cipher.Data; + if (cipher.IsDataBlobEncrypted()) { return; @@ -96,43 +128,54 @@ public CipherMiniResponseModel(Cipher cipher, IGlobalSettings globalSettings, bo public Guid Id { get; set; } public Guid? OrganizationId { get; set; } public CipherType Type { get; set; } - public string Data { get; set; } + + // Setter is locked so the secret blob can only ever be populated through the witness-gated + // PopulateFullData path, never via a public constructor or object initializer. + public string Data { get; protected set; } + + /// + /// The reduced data blob returned in place of when the caller can only reach this + /// cipher through leasing-enabled collections (PAM credential leasing). Contains the encrypted title + /// and, for logins, the encrypted URIs — never the dropped secrets. Null for full responses. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string PartialData { get; set; } [Obsolete("Use Data instead.")] - public string Name { get; set; } + public string Name { get; protected set; } [Obsolete("Use Data instead.")] - public string Notes { get; set; } + public string Notes { get; protected set; } [Obsolete("Use Data instead.")] - public CipherLoginModel Login { get; set; } + public CipherLoginModel Login { get; protected set; } [Obsolete("Use Data instead.")] - public CipherCardModel Card { get; set; } + public CipherCardModel Card { get; protected set; } [Obsolete("Use Data instead.")] - public CipherIdentityModel Identity { get; set; } + public CipherIdentityModel Identity { get; protected set; } [Obsolete("Use Data instead.")] - public CipherSecureNoteModel SecureNote { get; set; } + public CipherSecureNoteModel SecureNote { get; protected set; } [Obsolete("Use Data instead.")] - public CipherSSHKeyModel SSHKey { get; set; } + public CipherSSHKeyModel SSHKey { get; protected set; } [Obsolete("Use Data instead.")] - public CipherBankAccountModel BankAccount { get; set; } + public CipherBankAccountModel BankAccount { get; protected set; } [Obsolete("Use Data instead.")] - public CipherDriversLicenseModel DriversLicense { get; set; } + public CipherDriversLicenseModel DriversLicense { get; protected set; } [Obsolete("Use Data instead.")] - public CipherPassportModel Passport { get; set; } + public CipherPassportModel Passport { get; protected set; } [Obsolete("Use Data instead.")] - public IEnumerable Fields { get; set; } + public IEnumerable Fields { get; protected set; } [Obsolete("Use Data instead.")] - public IEnumerable PasswordHistory { get; set; } + public IEnumerable PasswordHistory { get; protected set; } public IEnumerable Attachments { get; set; } public bool OrganizationUseTotp { get; set; } public DateTime RevisionDate { get; set; } @@ -141,6 +184,22 @@ public CipherMiniResponseModel(Cipher cipher, IGlobalSettings globalSettings, bo public CipherRepromptType Reprompt { get; set; } public string Key { get; set; } } + +/// +/// The full-data counterpart of . Its constructor requires a +/// witness (minted only by the leasing gate), making emission of full +/// secret data a deliberate, type-checked act. +/// +public class FullCipherMiniResponseModel : CipherMiniResponseModel +{ + public FullCipherMiniResponseModel(FullCipherAccess access, Cipher cipher, IGlobalSettings globalSettings, + bool orgUseTotp, string obj = "cipherMini") + : base(cipher, globalSettings, orgUseTotp, obj, partial: false) + { + PopulateFullData(access, cipher); + } +} + #nullable enable public class CipherResponseModel : CipherMiniResponseModel { @@ -150,7 +209,18 @@ public CipherResponseModel( OrganizationAbility? organizationAbility, IGlobalSettings globalSettings, string obj = "cipher") - : base(cipher, globalSettings, cipher.OrganizationUseTotp, obj) + : this(cipher, user, organizationAbility, globalSettings, obj, partial: true) + { + } + + protected CipherResponseModel( + CipherDetails cipher, + User user, + OrganizationAbility? organizationAbility, + IGlobalSettings globalSettings, + string obj, + bool partial) + : base(cipher, globalSettings, cipher.OrganizationUseTotp, obj, partial) { FolderId = cipher.FolderId; Favorite = cipher.Favorite; @@ -168,6 +238,22 @@ public CipherResponseModel( public CipherPermissionsResponseModel Permissions { get; set; } } +/// Full-data counterpart of ; requires a gate-minted witness. +public class FullCipherResponseModel : CipherResponseModel +{ + public FullCipherResponseModel( + FullCipherAccess access, + CipherDetails cipher, + User user, + OrganizationAbility? organizationAbility, + IGlobalSettings globalSettings, + string obj = "cipher") + : base(cipher, user, organizationAbility, globalSettings, obj, partial: false) + { + PopulateFullData(access, cipher); + } +} + public class CipherDetailsResponseModel : CipherResponseModel { public CipherDetailsResponseModel( @@ -176,7 +262,17 @@ public CipherDetailsResponseModel( OrganizationAbility? organizationAbility, GlobalSettings globalSettings, IDictionary> collectionCiphers, string obj = "cipherDetails") - : base(cipher, user, organizationAbility, globalSettings, obj) + : this(cipher, user, organizationAbility, globalSettings, collectionCiphers, obj, partial: true) + { + } + + protected CipherDetailsResponseModel( + CipherDetails cipher, + User user, + OrganizationAbility? organizationAbility, + GlobalSettings globalSettings, + IDictionary> collectionCiphers, string obj, bool partial) + : base(cipher, user, organizationAbility, globalSettings, obj, partial) { if (collectionCiphers?.TryGetValue(cipher.Id, out var collectionCipher) ?? false) { @@ -194,7 +290,17 @@ public CipherDetailsResponseModel( OrganizationAbility? organizationAbility, GlobalSettings globalSettings, IEnumerable collectionCiphers, string obj = "cipherDetails") - : base(cipher, user, organizationAbility, globalSettings, obj) + : this(cipher, user, organizationAbility, globalSettings, collectionCiphers, obj, partial: true) + { + } + + protected CipherDetailsResponseModel( + CipherDetails cipher, + User user, + OrganizationAbility? organizationAbility, + GlobalSettings globalSettings, + IEnumerable collectionCiphers, string obj, bool partial) + : base(cipher, user, organizationAbility, globalSettings, obj, partial) { CollectionIds = collectionCiphers?.Select(c => c.CollectionId) ?? []; } @@ -205,7 +311,17 @@ public CipherDetailsResponseModel( OrganizationAbility? organizationAbility, GlobalSettings globalSettings, string obj = "cipherDetails") - : base(cipher, user, organizationAbility, globalSettings, obj) + : this(cipher, user, organizationAbility, globalSettings, obj, partial: true) + { + } + + protected CipherDetailsResponseModel( + CipherDetailsWithCollections cipher, + User user, + OrganizationAbility? organizationAbility, + GlobalSettings globalSettings, + string obj, bool partial) + : base(cipher, user, organizationAbility, globalSettings, obj, partial) { CollectionIds = cipher.CollectionIds ?? []; } @@ -213,11 +329,59 @@ public CipherDetailsResponseModel( public IEnumerable CollectionIds { get; set; } } +/// Full-data counterpart of ; requires a gate-minted witness. +public class FullCipherDetailsResponseModel : CipherDetailsResponseModel +{ + public FullCipherDetailsResponseModel( + FullCipherAccess access, + CipherDetails cipher, + User user, + OrganizationAbility? organizationAbility, + GlobalSettings globalSettings, + IDictionary> collectionCiphers, string obj = "cipherDetails") + : base(cipher, user, organizationAbility, globalSettings, collectionCiphers, obj, partial: false) + { + PopulateFullData(access, cipher); + } + + public FullCipherDetailsResponseModel( + FullCipherAccess access, + CipherDetails cipher, + User user, + OrganizationAbility? organizationAbility, + GlobalSettings globalSettings, + IEnumerable collectionCiphers, string obj = "cipherDetails") + : base(cipher, user, organizationAbility, globalSettings, collectionCiphers, obj, partial: false) + { + PopulateFullData(access, cipher); + } + + public FullCipherDetailsResponseModel( + FullCipherAccess access, + CipherDetailsWithCollections cipher, + User user, + OrganizationAbility? organizationAbility, + GlobalSettings globalSettings, + string obj = "cipherDetails") + : base(cipher, user, organizationAbility, globalSettings, obj, partial: false) + { + PopulateFullData(access, cipher); + } +} + public class CipherMiniDetailsResponseModel : CipherMiniResponseModel { public CipherMiniDetailsResponseModel(Cipher cipher, GlobalSettings globalSettings, - IDictionary> collectionCiphers, bool orgUseTotp, string obj = "cipherMiniDetails") - : base(cipher, globalSettings, orgUseTotp, obj) + IDictionary> collectionCiphers, bool orgUseTotp, + string obj = "cipherMiniDetails") + : this(cipher, globalSettings, collectionCiphers, orgUseTotp, obj, partial: true) + { + } + + protected CipherMiniDetailsResponseModel(Cipher cipher, GlobalSettings globalSettings, + IDictionary> collectionCiphers, bool orgUseTotp, + string obj, bool partial) + : base(cipher, globalSettings, orgUseTotp, obj, partial) { if (collectionCiphers?.TryGetValue(cipher.Id, out var collectionCipher) ?? false) { @@ -231,17 +395,50 @@ public CipherMiniDetailsResponseModel(Cipher cipher, GlobalSettings globalSettin public CipherMiniDetailsResponseModel(CipherOrganizationDetailsWithCollections cipher, GlobalSettings globalSettings, bool orgUseTotp, string obj = "cipherMiniDetails") - : base(cipher, globalSettings, orgUseTotp, obj) + : this(cipher, globalSettings, orgUseTotp, obj, partial: true) { - CollectionIds = cipher.CollectionIds ?? []; + } + + protected CipherMiniDetailsResponseModel(CipherOrganizationDetailsWithCollections cipher, + GlobalSettings globalSettings, bool orgUseTotp, string obj, bool partial) + : base(cipher, globalSettings, orgUseTotp, obj, partial) + { + CollectionIds = cipher.CollectionIds ?? new List(); } public CipherMiniDetailsResponseModel(CipherOrganizationDetailsWithCollections cipher, GlobalSettings globalSettings, string obj = "cipherMiniDetails") - : base(cipher, globalSettings, cipher.OrganizationUseTotp, obj) + : this(cipher, globalSettings, cipher.OrganizationUseTotp, obj, partial: true) { - CollectionIds = cipher.CollectionIds ?? new List(); } public IEnumerable CollectionIds { get; set; } } + +/// Full-data counterpart of ; requires a gate-minted witness. +public class FullCipherMiniDetailsResponseModel : CipherMiniDetailsResponseModel +{ + public FullCipherMiniDetailsResponseModel(FullCipherAccess access, Cipher cipher, GlobalSettings globalSettings, + IDictionary> collectionCiphers, bool orgUseTotp, + string obj = "cipherMiniDetails") + : base(cipher, globalSettings, collectionCiphers, orgUseTotp, obj, partial: false) + { + PopulateFullData(access, cipher); + } + + public FullCipherMiniDetailsResponseModel(FullCipherAccess access, + CipherOrganizationDetailsWithCollections cipher, GlobalSettings globalSettings, bool orgUseTotp, + string obj = "cipherMiniDetails") + : base(cipher, globalSettings, orgUseTotp, obj, partial: false) + { + PopulateFullData(access, cipher); + } + + public FullCipherMiniDetailsResponseModel(FullCipherAccess access, + CipherOrganizationDetailsWithCollections cipher, GlobalSettings globalSettings, + string obj = "cipherMiniDetails") + : base(cipher, globalSettings, cipher.OrganizationUseTotp, obj, partial: false) + { + PopulateFullData(access, cipher); + } +} diff --git a/src/Api/Vault/Models/Response/SyncResponseModel.cs b/src/Api/Vault/Models/Response/SyncResponseModel.cs index 47bbc16c4976..fb33a72b3b39 100644 --- a/src/Api/Vault/Models/Response/SyncResponseModel.cs +++ b/src/Api/Vault/Models/Response/SyncResponseModel.cs @@ -19,6 +19,7 @@ using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Settings; using Bit.Core.Tools.Entities; +using Bit.Core.Vault.Authorization; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Models.Data; @@ -46,20 +47,23 @@ public SyncResponseModel( IEnumerable sends, IEnumerable webAuthnCredentials, IEnumerable policiesNew = null, - IEnumerable organizationUserDetailsNew = null) + IEnumerable organizationUserDetailsNew = null, + FullCipherAccess fullCipherAccess = null) : this() { Profile = new ProfileResponseModel(user, userAccountKeysData, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingingUser, organizationUserDetailsNew); Folders = folders.Select(f => new FolderResponseModel(f)); + // A leasing-gated cipher (one the witness does not authorize) is delivered partial; when no + // witness is supplied every cipher falls back to the partial shape, keeping sync fail-closed. Ciphers = ciphers.Select(cipher => - new CipherDetailsResponseModel( - cipher, - user, - GetOrganizationAbility(cipher, organizationAbilities), - globalSettings, - collectionCiphersDict)); + { + var organizationAbility = GetOrganizationAbility(cipher, organizationAbilities); + return fullCipherAccess is not null && fullCipherAccess.Authorizes(cipher.Id) + ? new FullCipherDetailsResponseModel(fullCipherAccess, cipher, user, organizationAbility, globalSettings, collectionCiphersDict) + : new CipherDetailsResponseModel(cipher, user, organizationAbility, globalSettings, collectionCiphersDict); + }); Collections = collections?.Select( c => new CollectionDetailsResponseModel(c)) ?? new List(); Domains = excludeDomains ? null : new DomainsResponseModel(user, false); diff --git a/src/Api/packages.lock.json b/src/Api/packages.lock.json index 3a8cd6c2ca35..f8154fb34d95 100644 --- a/src/Api/packages.lock.json +++ b/src/Api/packages.lock.json @@ -1139,7 +1139,7 @@ "commercial.core": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "CsvHelper": "[33.1.0, 33.1.0]" } }, @@ -1147,8 +1147,16 @@ "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" + } + }, + "commercial.pam": { + "type": "Project", + "dependencies": { + "Core": "[2026.6.1, )", + "Pam.Domain": "[2026.6.1, )", + "SharedWeb": "[2026.6.1, )" } }, "core": { @@ -1167,6 +1175,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1191,6 +1200,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1204,33 +1214,44 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/src/Billing/packages.lock.json b/src/Billing/packages.lock.json index d5f5ba53040e..fef7a5432876 100644 --- a/src/Billing/packages.lock.json +++ b/src/Billing/packages.lock.json @@ -1112,7 +1112,7 @@ "commercial.core": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "CsvHelper": "[33.1.0, 33.1.0]" } }, @@ -1132,6 +1132,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1156,6 +1157,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1169,33 +1171,44 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/src/Core/AdminConsole/Entities/Collection.cs b/src/Core/AdminConsole/Entities/Collection.cs index 15e8f9cfa0fe..688e3fac9f37 100644 --- a/src/Core/AdminConsole/Entities/Collection.cs +++ b/src/Core/AdminConsole/Entities/Collection.cs @@ -46,6 +46,11 @@ public class Collection : ITableObject /// unknown user). Unencrypted. /// public string? DefaultUserCollectionEmail { get; set; } + /// + /// Reference to a that gates + /// PAM credential leasing for this collection. Null means leasing is disabled for the collection. + /// + public Guid? AccessRuleId { get; set; } /// /// Initializes to a new COMB GUID. diff --git a/src/Core/AdminConsole/Repositories/ICollectionRepository.cs b/src/Core/AdminConsole/Repositories/ICollectionRepository.cs index 4f34c7a3187c..4848de9893a3 100644 --- a/src/Core/AdminConsole/Repositories/ICollectionRepository.cs +++ b/src/Core/AdminConsole/Repositories/ICollectionRepository.cs @@ -59,6 +59,14 @@ public interface ICollectionRepository : IRepository Task DeleteUserAsync(Guid collectionId, Guid organizationUserId); Task UpdateUsersAsync(Guid id, IEnumerable users); Task> GetManyUsersByIdAsync(Guid id); + + /// + /// Returns the distinct user ids of every confirmed member who can Manage the collection: direct Manage + /// assignments, Manage via group, org Owners/Admins (when the organization allows admin access to all collection + /// items), and Custom users with the EditAnyCollection permission. + /// + Task> GetManagingUserIdsAsync(Guid collectionId); + Task DeleteManyAsync(IEnumerable collectionIds); /// diff --git a/src/Core/AssemblyInfo.cs b/src/Core/AssemblyInfo.cs index 66f5b58ef845..3efafdab91b0 100644 --- a/src/Core/AssemblyInfo.cs +++ b/src/Core/AssemblyInfo.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Core.Test")] +[assembly: InternalsVisibleTo("Api.Test")] [assembly: InternalsVisibleTo("Identity.IntegrationTest")] diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index eac2a51fb0f2..69762b3de489 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -314,6 +314,11 @@ public static List GetAllKeys() public static Dictionary GetLocalOverrideFlagValues() { // place overriding values when needed locally (offline), or return null - return null; + return new Dictionary + { + // PAM is enabled by default for dev/demo purposes only. + // This MUST be set to false (or removed) before going to production. + { Pam, "true" }, + }; } } diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 5d6466dfef6e..339665fd410d 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -77,6 +77,11 @@ + + + + + @@ -84,5 +89,6 @@ + diff --git a/src/Core/MailTemplates/Handlebars/Pam/PendingAccessRequest.html.hbs b/src/Core/MailTemplates/Handlebars/Pam/PendingAccessRequest.html.hbs new file mode 100644 index 000000000000..a9e3024510b3 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/Pam/PendingAccessRequest.html.hbs @@ -0,0 +1,26 @@ +{{#>FullHtmlLayout}} + + + + + + + + + + +
+ A member of {{OrganizationName}} has requested time-limited access to a protected item and is waiting for an approver. Log in to your approver inbox to approve or deny the request. +
+ Requested by: {{RequesterName}} ({{RequesterEmail}}) +
+ Access window: {{NotBefore}} – {{NotAfter}} +
+ Reason: {{Reason}} +
+ + Review the request + +
+
+{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/Pam/PendingAccessRequest.text.hbs b/src/Core/MailTemplates/Handlebars/Pam/PendingAccessRequest.text.hbs new file mode 100644 index 000000000000..1b7fbd4bcbd5 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/Pam/PendingAccessRequest.text.hbs @@ -0,0 +1,9 @@ +{{#>BasicTextLayout}} +A member of {{OrganizationName}} has requested time-limited access to a protected item and is waiting for an approver. + +Requested by: {{RequesterName}} ({{RequesterEmail}}) +Access window: {{NotBefore}} - {{NotAfter}} +Reason: {{Reason}} + +Log in to your approver inbox to approve or deny the request: {{ApproverInboxUrl}} +{{/BasicTextLayout}} diff --git a/src/Core/Models/Mail/PamPendingAccessRequestViewModel.cs b/src/Core/Models/Mail/PamPendingAccessRequestViewModel.cs new file mode 100644 index 000000000000..f80ffc11ea13 --- /dev/null +++ b/src/Core/Models/Mail/PamPendingAccessRequestViewModel.cs @@ -0,0 +1,15 @@ +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; + +public class PamPendingAccessRequestViewModel : BaseMailModel +{ + public string OrganizationName { get; set; } + public string RequesterName { get; set; } + public string RequesterEmail { get; set; } + public string NotBefore { get; set; } + public string NotAfter { get; set; } + public string Reason { get; set; } + public string ApproverInboxUrl => $"{WebVaultUrl}/pam/approver-inbox/approvals"; +} diff --git a/src/Core/Pam/Services/ICipherLeaseGate.cs b/src/Core/Pam/Services/ICipherLeaseGate.cs new file mode 100644 index 000000000000..68a3e6c15a52 --- /dev/null +++ b/src/Core/Pam/Services/ICipherLeaseGate.cs @@ -0,0 +1,70 @@ +using Bit.Core.Entities; +using Bit.Core.Models.Data; +using Bit.Core.Vault.Authorization; +using Bit.Core.Vault.Entities; + +namespace Bit.Pam.Services; + +/// +/// The single decision point for PAM credential leasing in Vault code. A cipher reachable only through +/// leasing-enabled collections is "leasing-gated": its secrets are withheld (partial data) unless the +/// caller holds a valid active lease, and mutating it is refused without one. Every method is a no-op / +/// "unrestricted" when the Pam feature flag is off, so flag-off behaviour matches main. +/// +public interface ICipherLeaseGate +{ + /// + /// Per-cipher read decision. Returns a witness authorizing full data + /// when the caller may see it (not gated, or gated with a valid active lease), or null when + /// the caller is blocked and must receive the partial shape. + /// + Task AuthorizeReadAsync(Guid userId, Cipher cipher); + + /// + /// Bulk read decision. Returns a single witness authorizing full data for the non-gated subset of + /// , computed in-memory from the supplied collections and mappings (no + /// per-cipher queries). Bulk reads strip every gated cipher regardless of lease state — + /// secrets are only ever released through . + /// + Task AuthorizeReadManyAsync( + Guid userId, + IEnumerable ciphers, + IEnumerable? collections, + IDictionary>? collectionCiphersByCipher); + + /// + /// Self-loading variant of + /// for callers (e.g. bulk write-returns) that have not already loaded the caller's collections and + /// mappings. Loads them once — but only when the flag is on, so the flag-off path stays query-free. + /// + Task AuthorizeReadManyAsync(Guid userId, IEnumerable ciphers); + + /// + /// The set of cipher ids reachable only through leasing-enabled collections (those carrying + /// a ). Computed in-memory, no queries. A cipher reachable + /// through any non-leasing collection — or personally owned with no mapping — is excluded. + /// + ISet GetGatedCipherIds( + IEnumerable? collections, + IDictionary>? collectionCiphersByCipher); + + /// + /// Throws when mutating + /// is refused (gated, no valid active lease); otherwise returns a witness authorizing the cipher so + /// the caller can build a full response for the mutated cipher without re-querying. + /// + Task EnsureCanMutateAsync(Guid userId, Cipher cipher); + + /// + /// Bulk variant of . Throws if any cipher is gated with no valid + /// active lease; otherwise returns a witness authorizing all of them. + /// + Task EnsureCanMutateManyAsync(Guid userId, IEnumerable ciphers); + + /// + /// Mints an unrestricted witness for a context that has already been authorized out-of-band — org + /// admins acting through org-wide permissions, personal vaults, and export/manage flows the + /// controller has already gated. Use deliberately; it authorizes full data for any cipher. + /// + FullCipherAccess Unrestricted(); +} diff --git a/src/Core/Pam/Services/NoopCipherLeaseGate.cs b/src/Core/Pam/Services/NoopCipherLeaseGate.cs new file mode 100644 index 000000000000..ae78a87e485d --- /dev/null +++ b/src/Core/Pam/Services/NoopCipherLeaseGate.cs @@ -0,0 +1,40 @@ +using Bit.Core.Entities; +using Bit.Core.Models.Data; +using Bit.Core.Vault.Authorization; +using Bit.Core.Vault.Entities; + +namespace Bit.Pam.Services; + +/// +/// Open-source fallback for . PAM credential leasing is a commercial feature, so in +/// builds without the commercial implementation the gate never gates: every cipher is fully accessible, matching +/// the behaviour when the PAM feature flag is off. The real gating logic lives in the commercial Pam library. +/// +public class NoopCipherLeaseGate : ICipherLeaseGate +{ + public Task AuthorizeReadAsync(Guid userId, Cipher cipher) + => Task.FromResult(FullCipherAccess.Unrestricted()); + + public Task AuthorizeReadManyAsync( + Guid userId, + IEnumerable ciphers, + IEnumerable? collections, + IDictionary>? collectionCiphersByCipher) + => Task.FromResult(FullCipherAccess.Unrestricted()); + + public Task AuthorizeReadManyAsync(Guid userId, IEnumerable ciphers) + => Task.FromResult(FullCipherAccess.Unrestricted()); + + public ISet GetGatedCipherIds( + IEnumerable? collections, + IDictionary>? collectionCiphersByCipher) + => new HashSet(); + + public Task EnsureCanMutateAsync(Guid userId, Cipher cipher) + => Task.FromResult(FullCipherAccess.Unrestricted()); + + public Task EnsureCanMutateManyAsync(Guid userId, IEnumerable ciphers) + => Task.FromResult(FullCipherAccess.Unrestricted()); + + public FullCipherAccess Unrestricted() => FullCipherAccess.Unrestricted(); +} diff --git a/src/Core/Platform/Mail/HandlebarsMailService.cs b/src/Core/Platform/Mail/HandlebarsMailService.cs index 7e97f3648212..0ffd460af323 100644 --- a/src/Core/Platform/Mail/HandlebarsMailService.cs +++ b/src/Core/Platform/Mail/HandlebarsMailService.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Globalization; using System.Net; using System.Reflection; using System.Text.Json; @@ -342,6 +343,40 @@ public async Task SendOrganizationAcceptedEmailAsync(Organization organization, await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendPamPendingAccessRequestEmailsAsync(IEnumerable managerEmails, string organizationName, + string? requesterName, string requesterEmail, DateTime notBefore, DateTime notAfter, string? reason) + { + // The body is identical for every approver, so build the view model once and reuse it. + var model = new PamPendingAccessRequestViewModel + { + // Only server-readable request metadata is rendered. The requested item and collection names are + // encrypted vault data and are intentionally never passed to, or shown by, this email. The requester + // controls their name, email, and reason, so each is anti-phishing sanitized to neutralize links and + // spoofed addresses smuggled into the notice. + OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false), + RequesterName = CoreHelpers.SanitizeForEmail( + string.IsNullOrWhiteSpace(requesterName) ? requesterEmail : requesterName, false), + RequesterEmail = CoreHelpers.SanitizeForEmail(requesterEmail, false), + NotBefore = notBefore.ToString("MMMM d, yyyy h:mm tt", CultureInfo.InvariantCulture) + " " + _utcTimeZoneDisplay, + NotAfter = notAfter.ToString("MMMM d, yyyy h:mm tt", CultureInfo.InvariantCulture) + " " + _utcTimeZoneDisplay, + Reason = CoreHelpers.SanitizeForEmail(string.IsNullOrWhiteSpace(reason) ? "(no reason provided)" : reason, false), + WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, + SiteName = _globalSettings.SiteName, + }; + + // One message per recipient so approvers are not disclosed to one another. + var queueMessages = managerEmails.Select(email => + { + var message = CreateDefaultMessage("New access request awaiting your approval", email); + return new MailQueueMessage(message, "Pam.PendingAccessRequest", model) + { + Category = "PamPendingAccessRequest", + }; + }); + + await EnqueueMailAsync(queueMessages); + } + public async Task SendOrganizationConfirmedEmailAsync(string organizationName, string email, bool hasAccessSecretsManager = false) { var message = CreateDefaultMessage($"You Have Been Confirmed To {organizationName}", email); diff --git a/src/Core/Platform/Mail/IMailService.cs b/src/Core/Platform/Mail/IMailService.cs index 6c91943e59fe..9787539939f6 100644 --- a/src/Core/Platform/Mail/IMailService.cs +++ b/src/Core/Platform/Mail/IMailService.cs @@ -77,6 +77,20 @@ Task SendTrialInitiationSignupEmailAsync( Task SendOrganizationConfirmedEmailAsync(string organizationName, string email, bool hasAccessSecretsManager = false); Task SendUpdatedOrganizationConfirmedEmailAsync(Organization organization, string userEmail, bool accessSecretsManager = false); Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email); + /// + /// Notifies a collection's managers (the approvers) that a new PAM access request is pending their review. + /// Sent as a single bulk message to all manager emails. Carries only server-readable request metadata; it + /// must never include the requested item or collection name, which are encrypted vault data. + /// + /// Email addresses of the collection's managers/approvers. + /// The organization the request belongs to. + /// Display name of the requester (falls back to email when blank). + /// Email address of the requester. + /// Start of the requested access window (UTC). + /// End of the requested access window (UTC). + /// The requester's stated reason for access. + Task SendPamPendingAccessRequestEmailsAsync(IEnumerable managerEmails, string organizationName, + string? requesterName, string requesterEmail, DateTime notBefore, DateTime notAfter, string? reason); Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email); Task SendPasswordlessSignInAsync(string returnUrl, string token, string email); Task SendInvoiceUpcoming( diff --git a/src/Core/Platform/Mail/NoopMailService.cs b/src/Core/Platform/Mail/NoopMailService.cs index ef8b5f9c78f8..26f43dba3c91 100644 --- a/src/Core/Platform/Mail/NoopMailService.cs +++ b/src/Core/Platform/Mail/NoopMailService.cs @@ -68,6 +68,12 @@ public Task SendOrganizationAutoscaledEmailAsync(Organization organization, int return Task.FromResult(0); } + public Task SendPamPendingAccessRequestEmailsAsync(IEnumerable managerEmails, string organizationName, + string? requesterName, string requesterEmail, DateTime notBefore, DateTime notAfter, string? reason) + { + return Task.FromResult(0); + } + public Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable adminEmails, bool hasAccessSecretsManager = false) { diff --git a/src/Core/Platform/Push/IPushNotificationService.cs b/src/Core/Platform/Push/IPushNotificationService.cs index 056a200664ce..090906be6d68 100644 --- a/src/Core/Platform/Push/IPushNotificationService.cs +++ b/src/Core/Platform/Push/IPushNotificationService.cs @@ -416,6 +416,38 @@ Task PushRefreshSecurityTasksAsync(Guid userId) UserId = userId, #pragma warning disable BWP0001 // Type or member is obsolete Date = TimeProvider.GetUtcNow().UtcDateTime, +#pragma warning restore BWP0001 // Type or member is obsolete + }, + ExcludeCurrentContext = false, + }); + + Task PushRefreshApproverInboxAsync(Guid userId) + => PushAsync(new PushNotification + { + Type = PushType.RefreshApproverInbox, + Target = NotificationTarget.User, + TargetId = userId, + Payload = new UserPushNotification + { + UserId = userId, +#pragma warning disable BWP0001 // Type or member is obsolete + Date = TimeProvider.GetUtcNow().UtcDateTime, +#pragma warning restore BWP0001 // Type or member is obsolete + }, + ExcludeCurrentContext = false, + }); + + Task PushRefreshAccessRequestAsync(Guid userId) + => PushAsync(new PushNotification + { + Type = PushType.RefreshAccessRequest, + Target = NotificationTarget.User, + TargetId = userId, + Payload = new UserPushNotification + { + UserId = userId, +#pragma warning disable BWP0001 // Type or member is obsolete + Date = TimeProvider.GetUtcNow().UtcDateTime, #pragma warning restore BWP0001 // Type or member is obsolete }, ExcludeCurrentContext = false, diff --git a/src/Core/Platform/Push/PushType.cs b/src/Core/Platform/Push/PushType.cs index c1569d5108fd..59dec637fe22 100644 --- a/src/Core/Platform/Push/PushType.cs +++ b/src/Core/Platform/Push/PushType.cs @@ -105,4 +105,10 @@ public enum PushType : byte [NotificationInfo("@bitwarden/team-billing-dev", typeof(Billing.Models.PremiumStatusPushNotification))] PremiumStatusChanged = 27, + + [NotificationInfo("@bitwarden/team-vault-dev", typeof(Models.UserPushNotification))] + RefreshApproverInbox = 28, + + [NotificationInfo("@bitwarden/team-vault-dev", typeof(Models.UserPushNotification))] + RefreshAccessRequest = 29, } diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index 4c090b850e3f..1b931a44ee9b 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -45,11 +45,11 @@ public static class CoreHelpers /// information for sequential ordering. This should be preferred to for any database IDs. ///
/// - /// ref: https://github.com/nhibernate/nhibernate-core/blob/master/src/NHibernate/Id/GuidCombGenerator.cs + /// Delegates to , which is the shared implementation in the Data project. /// /// A comb Guid. public static Guid GenerateComb() - => GenerateComb(Guid.NewGuid(), DateTime.UtcNow); + => CombGuid.Generate(); /// /// Implementation of with input parameters to remove randomness. @@ -59,28 +59,7 @@ public static Guid GenerateComb() /// You probably don't want to use this method and instead want to use with no parameters /// internal static Guid GenerateComb(Guid startingGuid, DateTime time) - { - var guidArray = startingGuid.ToByteArray(); - - // Get the days and milliseconds which will be used to build the byte string - var days = new TimeSpan(time.Ticks - _baseDateTicks); - var msecs = time.TimeOfDay; - - // Convert to a byte array - // Note that SQL Server is accurate to 1/300th of a millisecond so we divide by 3.333333 - var daysArray = BitConverter.GetBytes(days.Days); - var msecsArray = BitConverter.GetBytes((long)(msecs.TotalMilliseconds / 3.333333)); - - // Reverse the bytes to match SQL Servers ordering - Array.Reverse(daysArray); - Array.Reverse(msecsArray); - - // Copy the bytes into the guid - Array.Copy(daysArray, daysArray.Length - 2, guidArray, guidArray.Length - 6, 2); - Array.Copy(msecsArray, msecsArray.Length - 4, guidArray, guidArray.Length - 4, 4); - - return new Guid(guidArray); - } + => CombGuid.Generate(startingGuid, time); internal static DateTime DateFromComb(Guid combGuid) { diff --git a/src/Core/Vault/Authorization/FullCipherAccess.cs b/src/Core/Vault/Authorization/FullCipherAccess.cs new file mode 100644 index 000000000000..52a188847c04 --- /dev/null +++ b/src/Core/Vault/Authorization/FullCipherAccess.cs @@ -0,0 +1,51 @@ +namespace Bit.Core.Vault.Authorization; + +/// +/// A capability that authorizes returning a cipher's full secret data under PAM credential +/// leasing. It is minted only by the leasing gate (ICipherLeaseGate) — application code cannot +/// fabricate one — and is required by the constructors of the Full* cipher response models. This +/// makes emitting full secret data a deliberate, type-checked act: the default (partial) response +/// shapes need no witness, so a path that forgets to obtain one fails closed. +/// +public sealed class FullCipherAccess +{ + private readonly bool _unrestricted; + private readonly HashSet _authorizedCipherIds; + + private FullCipherAccess(bool unrestricted, HashSet? authorizedCipherIds) + { + _unrestricted = unrestricted; + _authorizedCipherIds = authorizedCipherIds ?? new HashSet(); + } + + /// + /// Authorizes full data for any cipher. Minted by the gate for contexts that have already been + /// authorized out-of-band (org admins, personal vaults, the flag-off no-op path). + /// + internal static FullCipherAccess Unrestricted() => new(unrestricted: true, authorizedCipherIds: null); + + /// Authorizes full data for exactly the given cipher. + internal static FullCipherAccess ForCipher(Guid cipherId) => new(unrestricted: false, [cipherId]); + + /// Authorizes full data for exactly the given set of ciphers. + internal static FullCipherAccess ForCiphers(IEnumerable cipherIds) => + new(unrestricted: false, cipherIds.ToHashSet()); + + /// Whether this witness authorizes full data for . + public bool Authorizes(Guid cipherId) => _unrestricted || _authorizedCipherIds.Contains(cipherId); + + /// + /// Throws when this witness does not authorize . Called by the + /// Full* response model constructors so a full response cannot be built for a cipher the + /// witness does not cover — keeping bulk lists fail-closed per element, not just at single reads. + /// + public void Require(Guid cipherId) + { + if (!Authorizes(cipherId)) + { + throw new InvalidOperationException( + "A full cipher response was constructed for a cipher the caller is not authorized to " + + "read in full. This indicates a credential-leasing filtering bug."); + } + } +} diff --git a/src/Core/Vault/Models/Data/PartialCipherData.cs b/src/Core/Vault/Models/Data/PartialCipherData.cs new file mode 100644 index 000000000000..5372cff268cc --- /dev/null +++ b/src/Core/Vault/Models/Data/PartialCipherData.cs @@ -0,0 +1,57 @@ +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; +using Bit.Core.Utilities; +using Bit.Core.Vault.Enums; + +namespace Bit.Core.Vault.Models.Data; + +/// +/// Produces a "partial" version of a cipher's encrypted Data blob for PAM credential leasing. +/// When a user can only reach a cipher through leasing-enabled collections, sync delivers this reduced +/// blob instead of the full one. +/// +/// +/// Zero-knowledge is preserved: nothing is ever decrypted. This only reshapes the plaintext JSON +/// envelope, keeping the encrypted title (and, for logins, the encrypted URIs) and dropping every other +/// encrypted field (username, password, TOTP, notes, custom fields, etc.). The retained values remain +/// individually-encrypted EncStrings. +/// +public static class PartialCipherData +{ + /// + /// Reduces a cipher's JSON Data blob to the fields allowed under credential leasing. + /// Logins keep Name and Uris; all other types keep only Name. + /// + /// The cipher's type. + /// The full, encrypted JSON data blob. Must be JSON (not an SDK-encrypted blob). + /// A reduced JSON data blob, or the input unchanged when it is null/empty. + public static string Strip(CipherType type, string data) + { + if (string.IsNullOrWhiteSpace(data)) + { + return data; + } + + if (type == CipherType.Login) + { + var login = JsonSerializer.Deserialize(data); + var partial = new CipherLoginData + { + Name = login.Name, + Uris = login.Uris, + }; + return JsonSerializer.Serialize(partial, JsonHelpers.IgnoreWritingNull); + } + + var nameOnly = JsonSerializer.Deserialize(data); + return JsonSerializer.Serialize( + new NameOnlyData { Name = nameOnly.Name }, JsonHelpers.IgnoreWritingNull); + } + + private class NameOnlyData + { + public string Name { get; set; } + } +} diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index 58f64a2d2fc6..e9f23ef358d4 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -17,6 +17,7 @@ using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Queries; using Bit.Core.Vault.Repositories; +using Bit.Pam.Services; namespace Bit.Core.Vault.Services; public class CipherService : ICipherService @@ -41,6 +42,7 @@ public class CipherService : ICipherService private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly IApplicationCacheService _applicationCacheService; private readonly IPricingClient _pricingClient; + private readonly ICipherLeaseGate _cipherLeaseGate; public CipherService( ICipherRepository cipherRepository, @@ -59,7 +61,8 @@ public CipherService( IGetCipherPermissionsForUserQuery getCipherPermissionsForUserQuery, IPolicyRequirementQuery policyRequirementQuery, IApplicationCacheService applicationCacheService, - IPricingClient pricingClient) + IPricingClient pricingClient, + ICipherLeaseGate cipherLeaseGate) { _cipherRepository = cipherRepository; _folderRepository = folderRepository; @@ -78,6 +81,7 @@ public CipherService( _policyRequirementQuery = policyRequirementQuery; _applicationCacheService = applicationCacheService; _pricingClient = pricingClient; + _cipherLeaseGate = cipherLeaseGate; } public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate, @@ -88,6 +92,13 @@ public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnow throw new BadRequestException("You do not have permissions to edit this."); } + // Editing an existing leasing-gated cipher requires a valid active lease. New ciphers have no + // collection paths yet, so they are never gated; admin/internal flows skip the lease gate too. + if (!skipPermissionCheck && cipher.Id != default(Guid)) + { + await _cipherLeaseGate.EnsureCanMutateAsync(savingUserId, cipher); + } + if (cipher.Id == default(Guid)) { if (cipher.OrganizationId.HasValue && collectionIds != null) @@ -128,6 +139,12 @@ public async Task SaveDetailsAsync(CipherDetails cipher, Guid savingUserId, Date throw new BadRequestException("You do not have permissions to edit this."); } + // Editing an existing leasing-gated cipher requires a valid active lease; new ciphers are never gated. + if (!skipPermissionCheck && cipher.Id != default(Guid)) + { + await _cipherLeaseGate.EnsureCanMutateAsync(savingUserId, cipher); + } + cipher.UserId = savingUserId; if (cipher.Id == default(Guid)) { @@ -432,6 +449,11 @@ public async Task DeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, throw new BadRequestException("You do not have permissions to delete this."); } + if (!orgAdmin) + { + await _cipherLeaseGate.EnsureCanMutateAsync(deletingUserId, cipherDetails); + } + var collectionIds = await GetCollectionIdsForPushAsync(cipherDetails); await _cipherRepository.DeleteAsync(cipherDetails); @@ -458,6 +480,7 @@ public async Task DeleteManyAsync(IEnumerable cipherIds, Guid deletingUser var ciphers = await _cipherRepository.GetManyByUserIdAsync(deletingUserId); var filteredCiphers = await FilterCiphersByDeletePermission(ciphers, cipherIdsSet, deletingUserId); deletingCiphers = filteredCiphers.Select(c => (Cipher)c).ToList(); + await _cipherLeaseGate.EnsureCanMutateManyAsync(deletingUserId, deletingCiphers); await _cipherRepository.DeleteAsync(deletingCiphers.Select(c => c.Id), deletingUserId); } @@ -524,6 +547,10 @@ public async Task DeleteAttachmentsForOrganizationAsync(Guid organizationId) public async Task MoveManyAsync(IEnumerable cipherIds, Guid? destinationFolderId, Guid movingUserId) { + // Moving a leasing-gated cipher (changing its folder) requires a valid active lease. The gate only + // reads each cipher's id, so lightweight placeholders avoid an extra repository read. + await _cipherLeaseGate.EnsureCanMutateManyAsync(movingUserId, cipherIds.Select(id => new Cipher { Id = id })); + if (destinationFolderId.HasValue) { var folder = await _folderRepository.GetByIdAsync(destinationFolderId.Value); @@ -726,6 +753,7 @@ await _collectionCipherRepository.UpdateCollectionsForAdminAsync(cipher.Id, { throw new BadRequestException("You do not have permissions to edit this."); } + await _cipherLeaseGate.EnsureCanMutateAsync(savingUserId, cipher); await _collectionCipherRepository.UpdateCollectionsAsync(cipher.Id, savingUserId, collectionIds); } @@ -742,6 +770,11 @@ public async Task SoftDeleteAsync(CipherDetails cipherDetails, Guid deletingUser throw new BadRequestException("You do not have permissions to soft delete this."); } + if (!orgAdmin) + { + await _cipherLeaseGate.EnsureCanMutateAsync(deletingUserId, cipherDetails); + } + if (cipherDetails.DeletedDate.HasValue) { // Already soft-deleted, we can safely ignore this @@ -774,6 +807,7 @@ public async Task SoftDeleteManyAsync(IEnumerable cipherIds, Guid deleting var ciphers = await _cipherRepository.GetManyByUserIdAsync(deletingUserId); var filteredCiphers = await FilterCiphersByDeletePermission(ciphers, cipherIdsSet, deletingUserId); deletingCiphers = filteredCiphers.Select(c => (Cipher)c).ToList(); + await _cipherLeaseGate.EnsureCanMutateManyAsync(deletingUserId, deletingCiphers); await _cipherRepository.SoftDeleteAsync(deletingCiphers.Select(c => c.Id), deletingUserId); } @@ -797,6 +831,11 @@ public async Task RestoreAsync(CipherDetails cipherDetails, Guid restoringUserId throw new BadRequestException("You do not have permissions to delete this."); } + if (!orgAdmin) + { + await _cipherLeaseGate.EnsureCanMutateAsync(restoringUserId, cipherDetails); + } + if (!cipherDetails.DeletedDate.HasValue) { // Already restored, we can safely ignore this @@ -835,6 +874,7 @@ public async Task> RestoreManyAsync(IEnum var ciphers = await _cipherRepository.GetManyByUserIdAsync(restoringUserId); var filteredCiphers = await FilterCiphersByDeletePermission(ciphers, cipherIdsSet, restoringUserId); restoringCiphers = filteredCiphers.Select(c => (CipherOrganizationDetails)c).ToList(); + await _cipherLeaseGate.EnsureCanMutateManyAsync(restoringUserId, restoringCiphers); revisionDate = await _cipherRepository.RestoreAsync(restoringCiphers.Select(c => c.Id), restoringUserId); } @@ -948,6 +988,11 @@ public async Task ValidateCipherEditForAttachmentAsync(Cipher cipher, Guid savin throw new BadRequestException("You do not have permissions to edit this."); } + if (!orgAdmin) + { + await _cipherLeaseGate.EnsureCanMutateAsync(savingUserId, cipher); + } + if (requestLength < 1) { throw new BadRequestException("No data to attach."); diff --git a/src/Core/packages.lock.json b/src/Core/packages.lock.json index 9be0c037339c..b2ff44bfec31 100644 --- a/src/Core/packages.lock.json +++ b/src/Core/packages.lock.json @@ -1189,6 +1189,15 @@ "type": "Transitive", "resolved": "4.3.0", "contentHash": "A/uxsWi/Ifzkmd4ArTLISMbfFs6XpRPsXZonrIqyTY70xi8t+mDtvSM5Os0RqyRDobjMBwIDHDL4NOIbkDwf7A==" + }, + "data": { + "type": "Project" + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } } } } diff --git a/src/Data/Data.csproj b/src/Data/Data.csproj new file mode 100644 index 000000000000..61718a1f8749 --- /dev/null +++ b/src/Data/Data.csproj @@ -0,0 +1,3 @@ + + + diff --git a/src/Core/Entities/ITableObject.cs b/src/Data/Entities/ITableObject.cs similarity index 100% rename from src/Core/Entities/ITableObject.cs rename to src/Data/Entities/ITableObject.cs diff --git a/src/Core/Repositories/IRepository.cs b/src/Data/Repositories/IRepository.cs similarity index 100% rename from src/Core/Repositories/IRepository.cs rename to src/Data/Repositories/IRepository.cs diff --git a/src/Data/Utilities/CombGuid.cs b/src/Data/Utilities/CombGuid.cs new file mode 100644 index 000000000000..2393c66f19ae --- /dev/null +++ b/src/Data/Utilities/CombGuid.cs @@ -0,0 +1,54 @@ +#nullable enable + +namespace Bit.Core.Utilities; + +/// +/// Generates sequential ("comb") values for SQL Server. Embedding timestamp information makes +/// the IDs sort roughly in creation order, which prevents SQL Server index fragmentation. Prefer +/// over for any database IDs. +/// +/// +/// ref: https://github.com/nhibernate/nhibernate-core/blob/master/src/NHibernate/Id/GuidCombGenerator.cs +/// +public static class CombGuid +{ + private static readonly long _baseDateTicks = new DateTime(1900, 1, 1).Ticks; + + /// + /// Generate a sequential comb for the current instant. + /// + /// A comb Guid. + public static Guid Generate() + => Generate(Guid.NewGuid(), DateTime.UtcNow); + + /// + /// Implementation of with input parameters to remove randomness. + /// This should NOT be used outside of testing. + /// + /// + /// You probably don't want to use this method and instead want to use with no parameters + /// + public static Guid Generate(Guid startingGuid, DateTime time) + { + var guidArray = startingGuid.ToByteArray(); + + // Get the days and milliseconds which will be used to build the byte string + var days = new TimeSpan(time.Ticks - _baseDateTicks); + var msecs = time.TimeOfDay; + + // Convert to a byte array + // Note that SQL Server is accurate to 1/300th of a millisecond so we divide by 3.333333 + var daysArray = BitConverter.GetBytes(days.Days); + var msecsArray = BitConverter.GetBytes((long)(msecs.TotalMilliseconds / 3.333333)); + + // Reverse the bytes to match SQL Servers ordering + Array.Reverse(daysArray); + Array.Reverse(msecsArray); + + // Copy the bytes into the guid + Array.Copy(daysArray, daysArray.Length - 2, guidArray, guidArray.Length - 6, 2); + Array.Copy(msecsArray, msecsArray.Length - 4, guidArray, guidArray.Length - 4, 4); + + return new Guid(guidArray); + } +} diff --git a/src/Data/packages.lock.json b/src/Data/packages.lock.json new file mode 100644 index 000000000000..4a91a8cd78fb --- /dev/null +++ b/src/Data/packages.lock.json @@ -0,0 +1,6 @@ +{ + "version": 1, + "dependencies": { + "net10.0": {} + } +} \ No newline at end of file diff --git a/src/Events/packages.lock.json b/src/Events/packages.lock.json index 4337d161981f..903bc87d916f 100644 --- a/src/Events/packages.lock.json +++ b/src/Events/packages.lock.json @@ -1097,6 +1097,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1121,6 +1122,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1134,33 +1136,44 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/src/EventsProcessor/packages.lock.json b/src/EventsProcessor/packages.lock.json index 4337d161981f..903bc87d916f 100644 --- a/src/EventsProcessor/packages.lock.json +++ b/src/EventsProcessor/packages.lock.json @@ -1097,6 +1097,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1121,6 +1122,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1134,33 +1136,44 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/src/Icons/packages.lock.json b/src/Icons/packages.lock.json index ef1bb389f4a4..9bde5c6dbeec 100644 --- a/src/Icons/packages.lock.json +++ b/src/Icons/packages.lock.json @@ -1103,6 +1103,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1127,6 +1128,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1140,33 +1142,44 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/src/Identity/packages.lock.json b/src/Identity/packages.lock.json index 4337d161981f..903bc87d916f 100644 --- a/src/Identity/packages.lock.json +++ b/src/Identity/packages.lock.json @@ -1097,6 +1097,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1121,6 +1122,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1134,33 +1136,44 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/CollectionRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/CollectionRepository.cs index 4eb8fc12e525..a74d523671ce 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/CollectionRepository.cs @@ -370,6 +370,19 @@ public async Task> GetManyUsersByIdAsync( } } + public async Task> GetManagingUserIdsAsync(Guid collectionId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[Collection_ReadManagingUserIds]", + new { CollectionId = collectionId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + public async Task CreateDefaultCollectionsAsync(Guid organizationId, IEnumerable organizationUserIds, string defaultCollectionName) { organizationUserIds = organizationUserIds.ToList(); @@ -486,6 +499,7 @@ public CollectionWithGroupsAndUsers(Collection collection, Type = collection.Type; ExternalId = collection.ExternalId; DefaultUserCollectionEmail = collection.DefaultUserCollectionEmail; + AccessRuleId = collection.AccessRuleId; Groups = groups.ToArrayTVP(); Users = users.ToArrayTVP(); } @@ -510,6 +524,7 @@ public CollectionWithGroups(Collection collection, IEnumerable(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj b/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj index 49164781a6c6..a46bf1c2fe90 100644 --- a/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj +++ b/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Infrastructure.Dapper/Pam/Repositories/AccessLeaseRepository.cs b/src/Infrastructure.Dapper/Pam/Repositories/AccessLeaseRepository.cs new file mode 100644 index 000000000000..8ce083cbe1a9 --- /dev/null +++ b/src/Infrastructure.Dapper/Pam/Repositories/AccessLeaseRepository.cs @@ -0,0 +1,135 @@ +using System.Data; +using Bit.Core.Settings; +using Bit.Infrastructure.Dapper.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Pam.Repositories; +using Dapper; +using Microsoft.Data.SqlClient; + +#nullable enable + +namespace Bit.Infrastructure.Dapper.Pam.Repositories; + +public class AccessLeaseRepository : Repository, IAccessLeaseRepository +{ + public AccessLeaseRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { } + + public AccessLeaseRepository(string connectionString, string readOnlyConnectionString) + : base(connectionString, readOnlyConnectionString) + { } + + public async Task GetByAccessRequestIdAsync(Guid accessRequestId) + { + await using var connection = new SqlConnection(ConnectionString); + var results = await connection.QueryAsync( + $"[{Schema}].[AccessLease_ReadByAccessRequestId]", + new { AccessRequestId = accessRequestId }, + commandType: CommandType.StoredProcedure); + + return results.FirstOrDefault(); + } + + public async Task GetActiveByRequesterIdCipherIdAsync(Guid requesterId, Guid cipherId, DateTime now) + { + await using var connection = new SqlConnection(ConnectionString); + var results = await connection.QueryAsync( + $"[{Schema}].[AccessLease_ReadActiveByRequesterIdCipherId]", + new { RequesterId = requesterId, CipherId = cipherId, Now = now }, + commandType: CommandType.StoredProcedure); + + return results.FirstOrDefault(); + } + + public async Task> GetManyActiveByRequesterIdAsync(Guid requesterId, DateTime now) + { + await using var connection = new SqlConnection(ConnectionString); + var results = await connection.QueryAsync( + $"[{Schema}].[AccessLease_ReadManyActiveByRequesterId]", + new { RequesterId = requesterId, Now = now }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + + public async Task> GetManyActiveByCollectionIdsAsync(IEnumerable collectionIds, DateTime now) + { + var ids = collectionIds.ToList(); + if (ids.Count == 0) + { + return new List(); + } + + await using var connection = new SqlConnection(ConnectionString); + var results = await connection.QueryAsync( + $"[{Schema}].[AccessLease_ReadManyActiveByCollectionIds]", + new { CollectionIds = ids.ToGuidIdArrayTVP(), Now = now }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + + public async Task> GetManyEndedByCollectionIdsAsync(IEnumerable collectionIds, DateTime since) + { + var ids = collectionIds.ToList(); + if (ids.Count == 0) + { + return new List(); + } + + await using var connection = new SqlConnection(ConnectionString); + var results = await connection.QueryAsync( + $"[{Schema}].[AccessLease_ReadManyEndedByCollectionIds]", + new { CollectionIds = ids.ToGuidIdArrayTVP(), Since = since }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + + public async Task CreateFromApprovedRequestAsync(AccessLease lease, DateTime now, + bool enforceSingleActiveLease) + { + await using var connection = new SqlConnection(ConnectionString); + try + { + var result = await connection.ExecuteScalarAsync( + $"[{Schema}].[AccessLease_CreateFromApprovedRequest]", + new + { + AccessLeaseId = lease.Id, + lease.AccessRequestId, + lease.RequesterId, + Now = now, + EnforceSingleActiveLease = enforceSingleActiveLease, + }, + commandType: CommandType.StoredProcedure); + + return (AccessLeaseMintOutcome)result; + } + catch (SqlException e) when (e.Number is 2601 or 2627) + { + // Unique-index backstop ([IX_AccessLease_AccessRequestId]): a concurrent activation won the race after + // our NOT EXISTS guard passed. Same outcome as the guard catching it — the caller re-reads the winner. + return AccessLeaseMintOutcome.PreconditionFailed; + } + } + + public async Task RevokeAsync(AccessLease lease, AccessDecision auditDecision, DateTime now) + { + await using var connection = new SqlConnection(ConnectionString); + await connection.ExecuteAsync( + $"[{Schema}].[AccessLease_Revoke]", + new + { + AccessLeaseId = lease.Id, + AccessRequestId = lease.AccessRequestId, + RevokedBy = auditDecision.ApproverId, + AccessDecisionId = auditDecision.Id, + Reason = auditDecision.Comment, + Now = now, + }, + commandType: CommandType.StoredProcedure); + } +} diff --git a/src/Infrastructure.Dapper/Pam/Repositories/AccessRequestRepository.cs b/src/Infrastructure.Dapper/Pam/Repositories/AccessRequestRepository.cs new file mode 100644 index 000000000000..28795fb1b502 --- /dev/null +++ b/src/Infrastructure.Dapper/Pam/Repositories/AccessRequestRepository.cs @@ -0,0 +1,238 @@ +using System.Data; +using Bit.Core.Settings; +using Bit.Infrastructure.Dapper.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Pam.Models; +using Bit.Pam.Repositories; +using Dapper; +using Microsoft.Data.SqlClient; + +#nullable enable + +namespace Bit.Infrastructure.Dapper.Pam.Repositories; + +public class AccessRequestRepository : Repository, IAccessRequestRepository +{ + public AccessRequestRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { } + + public AccessRequestRepository(string connectionString, string readOnlyConnectionString) + : base(connectionString, readOnlyConnectionString) + { } + + public async Task CreateAutoApprovedAsync(AccessRequest request, AccessDecision decision) + { + await using var connection = new SqlConnection(ConnectionString); + await connection.ExecuteAsync( + $"[{Schema}].[AccessRequest_CreateAutoApproved]", + new + { + AccessRequestId = request.Id, + AccessDecisionId = decision.Id, + request.OrganizationId, + request.CollectionId, + request.CipherId, + request.RequesterId, + request.NotBefore, + request.NotAfter, + request.Reason, + decision.ConditionKind, + CreationDate = request.CreationDate, + }, + commandType: CommandType.StoredProcedure); + } + + public async Task GetActivePendingByRequesterIdCipherIdAsync(Guid requesterId, Guid cipherId) + { + await using var connection = new SqlConnection(ConnectionString); + var results = await connection.QueryAsync( + $"[{Schema}].[AccessRequest_ReadActivePendingByRequesterIdCipherId]", + new { RequesterId = requesterId, CipherId = cipherId }, + commandType: CommandType.StoredProcedure); + + return results.FirstOrDefault(); + } + + public async Task GetActiveApprovedByRequesterIdCipherIdAsync(Guid requesterId, Guid cipherId, DateTime now) + { + await using var connection = new SqlConnection(ConnectionString); + var results = await connection.QueryAsync( + $"[{Schema}].[AccessRequest_ReadActiveApprovedByRequesterIdCipherId]", + new { RequesterId = requesterId, CipherId = cipherId, Now = now }, + commandType: CommandType.StoredProcedure); + + return results.FirstOrDefault(); + } + + public async Task> GetManyByRequesterIdAsync(Guid requesterId) + { + await using var connection = new SqlConnection(ConnectionString); + using var results = await connection.QueryMultipleAsync( + $"[{Schema}].[AccessRequest_ReadManyByRequesterId]", + new { RequesterId = requesterId }, + commandType: CommandType.StoredProcedure); + + return await ReadDetailsWithDecisionsAsync(results); + } + + public async Task> GetManyInboxPendingByCollectionIdsAsync(IEnumerable collectionIds) + { + var ids = collectionIds.ToList(); + if (ids.Count == 0) + { + return new List(); + } + + await using var connection = new SqlConnection(ConnectionString); + var results = await connection.QueryAsync( + $"[{Schema}].[AccessRequest_ReadInboxPendingByCollectionIds]", + new { CollectionIds = ids.ToGuidIdArrayTVP() }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + + public async Task> GetManyInboxHistoryByCollectionIdsAsync(IEnumerable collectionIds, DateTime since) + { + var ids = collectionIds.ToList(); + if (ids.Count == 0) + { + return new List(); + } + + await using var connection = new SqlConnection(ConnectionString); + using var results = await connection.QueryMultipleAsync( + $"[{Schema}].[AccessRequest_ReadInboxHistoryByCollectionIds]", + new { CollectionIds = ids.ToGuidIdArrayTVP(), Since = since }, + commandType: CommandType.StoredProcedure); + + return await ReadDetailsWithDecisionsAsync(results); + } + + public async Task ResolveWithDecisionAsync(AccessRequest request, AccessDecision decision, AccessRequestStatus status, DateTime now) + { + await using var connection = new SqlConnection(ConnectionString); + await connection.ExecuteAsync( + $"[{Schema}].[AccessRequest_ResolveWithDecision]", + new + { + AccessRequestId = request.Id, + Status = status, + AccessDecisionId = decision.Id, + ApproverId = decision.ApproverId, + Verdict = decision.Verdict, + decision.Comment, + Now = now, + }, + commandType: CommandType.StoredProcedure); + } + + public async Task CancelAsync(Guid id, DateTime now) + { + await using var connection = new SqlConnection(ConnectionString); + await connection.ExecuteAsync( + $"[{Schema}].[AccessRequest_Cancel]", + new { AccessRequestId = id, Now = now }, + commandType: CommandType.StoredProcedure); + } + + public async Task CancelWithDecisionAsync(AccessRequest request, AccessDecision decision, DateTime now) + { + await using var connection = new SqlConnection(ConnectionString); + await connection.ExecuteAsync( + $"[{Schema}].[AccessRequest_CancelWithDecision]", + new + { + AccessRequestId = request.Id, + AccessDecisionId = decision.Id, + ApproverId = decision.ApproverId, + Verdict = decision.Verdict, + decision.Comment, + Now = now, + }, + commandType: CommandType.StoredProcedure); + } + + public async Task CountExtensionsByLeaseIdAsync(Guid leaseId) + { + await using var connection = new SqlConnection(ConnectionString); + return await connection.ExecuteScalarAsync( + $"[{Schema}].[AccessRequest_CountExtensionsByLeaseId]", + new { LeaseId = leaseId }, + commandType: CommandType.StoredProcedure); + } + + public async Task CreateApprovedExtensionAsync(AccessRequest request, + AccessDecision decision, DateTime now) + { + await using var connection = new SqlConnection(ConnectionString); + var result = await connection.ExecuteScalarAsync( + $"[{Schema}].[AccessRequest_CreateApprovedExtension]", + new + { + AccessRequestId = request.Id, + AccessDecisionId = decision.Id, + request.ExtensionOfLeaseId, + request.OrganizationId, + request.CollectionId, + request.CipherId, + request.RequesterId, + request.NotBefore, + request.NotAfter, + request.Reason, + Now = now, + }, + commandType: CommandType.StoredProcedure); + + return (AccessLeaseExtendOutcome)result; + } + + /// + /// Reads a two-result-set access-request projection: result 1 is the request rows, result 2 is every decision row + /// (human or automatic) keyed by AccessRequestId (ordered oldest-first by the procedure). Groups the decisions onto + /// each request's ; a pending request keeps its empty list. + /// + private static async Task> ReadDetailsWithDecisionsAsync(SqlMapper.GridReader reader) + { + var details = (await reader.ReadAsync()).ToList(); + var decisionsByRequest = (await reader.ReadAsync()) + .GroupBy(row => row.AccessRequestId) + .ToDictionary(group => group.Key, group => group.Select(row => row.ToDecision()).ToList()); + + foreach (var detail in details) + { + if (decisionsByRequest.TryGetValue(detail.Id, out var decisions)) + { + detail.Decisions = decisions; + } + } + + return details; + } + + /// A decision row from the decision result set, carrying its AccessRequestId for grouping. + private sealed class DecisionRow + { + public Guid AccessRequestId { get; set; } + public AccessDeciderKind DeciderKind { get; set; } + public Guid? Id { get; set; } + public string? Name { get; set; } + public string? Email { get; set; } + public string? Comment { get; set; } + public AccessDecisionVerdict Verdict { get; set; } + public DateTime DecidedAt { get; set; } + + public AccessRequestDecision ToDecision() => new() + { + DeciderKind = DeciderKind, + Id = Id, + Name = Name, + Email = Email, + Comment = Comment, + Verdict = Verdict, + DecidedAt = DecidedAt, + }; + } +} diff --git a/src/Infrastructure.Dapper/Pam/Repositories/AccessRuleRepository.cs b/src/Infrastructure.Dapper/Pam/Repositories/AccessRuleRepository.cs new file mode 100644 index 000000000000..50d64955838c --- /dev/null +++ b/src/Infrastructure.Dapper/Pam/Repositories/AccessRuleRepository.cs @@ -0,0 +1,98 @@ +using System.Data; +using Bit.Core.Settings; +using Bit.Infrastructure.Dapper.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Models; +using Bit.Pam.Repositories; +using Dapper; +using Microsoft.Data.SqlClient; + +#nullable enable + +namespace Bit.Infrastructure.Dapper.Pam.Repositories; + +public class AccessRuleRepository : Repository, IAccessRuleRepository +{ + public AccessRuleRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { } + + public AccessRuleRepository(string connectionString, string readOnlyConnectionString) + : base(connectionString, readOnlyConnectionString) + { } + + public async Task> GetManyByOrganizationIdAsync(Guid organizationId) + { + using var connection = new SqlConnection(ConnectionString); + var results = await connection.QueryAsync( + $"[{Schema}].[AccessRule_ReadByOrganizationId]", + new { OrganizationId = organizationId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + + public async Task GetDetailsByIdAsync(Guid id) + { + using var connection = new SqlConnection(ConnectionString); + using var results = await connection.QueryMultipleAsync( + $"[{Schema}].[AccessRule_ReadDetailsById]", + new { Id = id }, + commandType: CommandType.StoredProcedure); + + var rule = (await results.ReadAsync()).SingleOrDefault(); + if (rule is null) + { + return null; + } + + rule.CollectionIds = (await results.ReadAsync()).ToList(); + return rule; + } + + public async Task> GetManyDetailsByOrganizationIdAsync(Guid organizationId) + { + using var connection = new SqlConnection(ConnectionString); + using var results = await connection.QueryMultipleAsync( + $"[{Schema}].[AccessRule_ReadDetailsByOrganizationId]", + new { OrganizationId = organizationId }, + commandType: CommandType.StoredProcedure); + + var rules = (await results.ReadAsync()).ToList(); + var collectionIdsByRule = (await results.ReadAsync()) + .GroupBy(m => m.AccessRuleId) + .ToDictionary(g => g.Key, g => g.Select(m => m.CollectionId).ToList()); + + foreach (var rule in rules) + { + if (collectionIdsByRule.TryGetValue(rule.Id, out var collectionIds)) + { + rule.CollectionIds = collectionIds; + } + } + + return rules; + } + + public async Task SetCollectionAssociationsAsync(Guid organizationId, Guid accessRuleId, + IEnumerable collectionIdsToAssign, IEnumerable collectionIdsToClear) + { + using var connection = new SqlConnection(ConnectionString); + await connection.ExecuteAsync( + $"[{Schema}].[Collection_SetAccessRuleAssociations]", + new + { + AccessRuleId = accessRuleId, + OrganizationId = organizationId, + ToAssign = collectionIdsToAssign.ToGuidIdArrayTVP(), + ToClear = collectionIdsToClear.ToGuidIdArrayTVP(), + }, + commandType: CommandType.StoredProcedure); + } + + private sealed class CollectionAccessRuleMapping + { + public Guid AccessRuleId { get; init; } + public Guid CollectionId { get; init; } + } +} diff --git a/src/Infrastructure.Dapper/packages.lock.json b/src/Infrastructure.Dapper/packages.lock.json index 40467ba44e1e..8aa40bb7da88 100644 --- a/src/Infrastructure.Dapper/packages.lock.json +++ b/src/Infrastructure.Dapper/packages.lock.json @@ -1164,6 +1164,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1188,6 +1189,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1200,6 +1202,15 @@ "ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis": "[2.0.2, 2.0.2]", "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } + }, + "data": { + "type": "Project" + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } } } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/CollectionRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/CollectionRepository.cs index 0df120fedb83..098cd1bd88a9 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/CollectionRepository.cs @@ -3,6 +3,7 @@ using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Repositories; +using Bit.Core.Utilities; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries; using Bit.Infrastructure.EntityFramework.Repositories; @@ -538,6 +539,82 @@ public async Task> GetManyUsersByIdAsync( } } + public async Task> GetManagingUserIdsAsync(Guid collectionId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + + var collection = await dbContext.Collections.FindAsync(collectionId); + if (collection == null) + { + return Array.Empty(); + } + + var organizationId = collection.OrganizationId; + + // Confirmed members of the organization with a linked account. + var confirmedOrgUsers = await dbContext.OrganizationUsers + .Where(ou => ou.OrganizationId == organizationId + && ou.Status == OrganizationUserStatusType.Confirmed + && ou.UserId != null) + .Select(ou => new { ou.Id, ou.UserId, ou.Type, ou.Permissions }) + .ToListAsync(); + var orgUsersById = confirmedOrgUsers.ToDictionary(ou => ou.Id); + + var managerUserIds = new HashSet(); + + void AddIfConfirmed(Guid organizationUserId) + { + if (orgUsersById.TryGetValue(organizationUserId, out var ou) && ou.UserId.HasValue) + { + managerUserIds.Add(ou.UserId.Value); + } + } + + // Direct Manage assignments. + var directManageOrgUserIds = await dbContext.CollectionUsers + .Where(cu => cu.CollectionId == collectionId && cu.Manage) + .Select(cu => cu.OrganizationUserId) + .ToListAsync(); + foreach (var organizationUserId in directManageOrgUserIds) + { + AddIfConfirmed(organizationUserId); + } + + // Manage via group membership. + var groupManageOrgUserIds = await (from cg in dbContext.CollectionGroups + join gu in dbContext.GroupUsers on cg.GroupId equals gu.GroupId + where cg.CollectionId == collectionId && cg.Manage + select gu.OrganizationUserId) + .ToListAsync(); + foreach (var organizationUserId in groupManageOrgUserIds) + { + AddIfConfirmed(organizationUserId); + } + + // Org Owners/Admins (when the org permits) and Custom users with EditAnyCollection (permission parsed in + // memory because the JSON column isn't portably queryable across providers). + var allowAdminAccess = await dbContext.Organizations + .Where(o => o.Id == organizationId) + .Select(o => o.AllowAdminAccessToAllCollectionItems) + .FirstOrDefaultAsync(); + foreach (var ou in confirmedOrgUsers) + { + var isAdminManager = allowAdminAccess + && ou.Type is OrganizationUserType.Owner or OrganizationUserType.Admin; + var hasEditAnyCollection = ou.Type == OrganizationUserType.Custom + && CoreHelpers.LoadClassFromJsonData(ou.Permissions).EditAnyCollection; + if ((isAdminManager || hasEditAnyCollection) && ou.UserId.HasValue) + { + managerUserIds.Add(ou.UserId.Value); + } + } + + return managerUserIds.ToList(); + } + } + public async Task ReplaceAsync(Core.Entities.Collection collection, IEnumerable? groups, IEnumerable? users) { diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index cd0d1feb4dfd..1d4c7474ca56 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -26,6 +26,7 @@ using Bit.Infrastructure.EntityFramework.SecretsManager.Repositories; using Bit.Infrastructure.EntityFramework.Tools.Repositories; using Bit.Infrastructure.EntityFramework.Vault.Repositories; +using Bit.Pam.Repositories; using LinqToDB.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -102,6 +103,7 @@ public static void AddPasswordManagerEFRepositories(this IServiceCollection serv services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj b/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj index b9852ba11cdf..8285e6778441 100644 --- a/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj +++ b/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj @@ -19,5 +19,6 @@ + diff --git a/src/Infrastructure.EntityFramework/Pam/Models/AccessRule.cs b/src/Infrastructure.EntityFramework/Pam/Models/AccessRule.cs new file mode 100644 index 000000000000..8fb27d2990bd --- /dev/null +++ b/src/Infrastructure.EntityFramework/Pam/Models/AccessRule.cs @@ -0,0 +1,21 @@ +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; +using Bit.Infrastructure.EntityFramework.AdminConsole.Models; + +namespace Bit.Infrastructure.EntityFramework.Pam.Models; + +public class AccessRule : Bit.Pam.Entities.AccessRule +{ + public virtual Organization Organization { get; set; } +} + +public class AccessRuleMapperProfile : Profile +{ + public AccessRuleMapperProfile() + { + CreateMap().ReverseMap(); + CreateMap(); + } +} diff --git a/src/Infrastructure.EntityFramework/Pam/Repositories/AccessRuleRepository.cs b/src/Infrastructure.EntityFramework/Pam/Repositories/AccessRuleRepository.cs new file mode 100644 index 000000000000..87862f65713b --- /dev/null +++ b/src/Infrastructure.EntityFramework/Pam/Repositories/AccessRuleRepository.cs @@ -0,0 +1,141 @@ +using AutoMapper; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Pam.Models; +using Bit.Pam.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using CoreEntity = Bit.Pam.Entities.AccessRule; +using EfModel = Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule; + +#nullable enable + +namespace Bit.Infrastructure.EntityFramework.Pam.Repositories; + +public class AccessRuleRepository : Repository, IAccessRuleRepository +{ + public AccessRuleRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) + : base(serviceScopeFactory, mapper, context => context.AccessRules) + { } + + public async Task> GetManyByOrganizationIdAsync(Guid organizationId) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var rules = await dbContext.AccessRules + .Where(p => p.OrganizationId == organizationId) + .AsNoTracking() + .ToListAsync(); + return Mapper.Map>(rules); + } + + public async Task GetDetailsByIdAsync(Guid id) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var rule = await dbContext.AccessRules + .Where(p => p.Id == id) + .AsNoTracking() + .FirstOrDefaultAsync(); + if (rule is null) + { + return null; + } + + var details = Mapper.Map(rule); + details.CollectionIds = await dbContext.Collections + .Where(c => c.AccessRuleId == id) + .Select(c => c.Id) + .ToListAsync(); + return details; + } + + public async Task> GetManyDetailsByOrganizationIdAsync(Guid organizationId) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var rules = await dbContext.AccessRules + .Where(p => p.OrganizationId == organizationId) + .AsNoTracking() + .ToListAsync(); + + var collectionIdsByRule = (await dbContext.Collections + .Where(c => c.OrganizationId == organizationId && c.AccessRuleId != null) + .Select(c => new { AccessRuleId = c.AccessRuleId!.Value, CollectionId = c.Id }) + .ToListAsync()) + .GroupBy(r => r.AccessRuleId) + .ToDictionary(g => g.Key, g => g.Select(r => r.CollectionId).ToList()); + + return rules + .Select(rule => + { + var details = Mapper.Map(rule); + if (collectionIdsByRule.TryGetValue(rule.Id, out var collectionIds)) + { + details.CollectionIds = collectionIds; + } + return details; + }) + .ToList(); + } + + public override async Task DeleteAsync(CoreEntity accessRule) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var now = DateTime.UtcNow; + + await using var transaction = await dbContext.Database.BeginTransactionAsync(); + + // The Collection -> AccessRule FK is Restrict, so clear it from governed collections before deleting. + await dbContext.Collections + .Where(c => c.AccessRuleId == accessRule.Id) + .ExecuteUpdateAsync(s => s + .SetProperty(c => c.AccessRuleId, (Guid?)null) + .SetProperty(c => c.RevisionDate, now)); + + await dbContext.AccessRules + .Where(r => r.Id == accessRule.Id) + .ExecuteDeleteAsync(); + + await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(accessRule.OrganizationId); + await dbContext.SaveChangesAsync(); + await transaction.CommitAsync(); + } + + public async Task SetCollectionAssociationsAsync(Guid organizationId, Guid accessRuleId, + IEnumerable collectionIdsToAssign, IEnumerable collectionIdsToClear) + { + var assignIds = collectionIdsToAssign.ToList(); + var clearIds = collectionIdsToClear.ToList(); + + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var now = DateTime.UtcNow; + + await using var transaction = await dbContext.Database.BeginTransactionAsync(); + + if (clearIds.Count > 0) + { + await dbContext.Collections + .Where(c => c.OrganizationId == organizationId + && c.AccessRuleId == accessRuleId + && clearIds.Contains(c.Id)) + .ExecuteUpdateAsync(s => s + .SetProperty(c => c.AccessRuleId, (Guid?)null) + .SetProperty(c => c.RevisionDate, now)); + } + + if (assignIds.Count > 0) + { + await dbContext.Collections + .Where(c => c.OrganizationId == organizationId && assignIds.Contains(c.Id)) + .ExecuteUpdateAsync(s => s + .SetProperty(c => c.AccessRuleId, accessRuleId) + .SetProperty(c => c.RevisionDate, now)); + } + + await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(organizationId); + await dbContext.SaveChangesAsync(); + await transaction.CommitAsync(); + } +} diff --git a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs index 67b459ea377f..c29fab3cc926 100644 --- a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs +++ b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs @@ -8,6 +8,7 @@ using Bit.Infrastructure.EntityFramework.Dirt.Models; using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.NotificationCenter.Models; +using Bit.Infrastructure.EntityFramework.Pam.Models; using Bit.Infrastructure.EntityFramework.Platform; using Bit.Infrastructure.EntityFramework.SecretsManager.Models; using Bit.Infrastructure.EntityFramework.Vault.Models; @@ -43,6 +44,7 @@ public DatabaseContext(DbContextOptions options) public DbSet CollectionCiphers { get; set; } public DbSet CollectionGroups { get; set; } public DbSet CollectionUsers { get; set; } + public DbSet AccessRules { get; set; } public DbSet Devices { get; set; } public DbSet EmergencyAccesses { get; set; } public DbSet Events { get; set; } @@ -107,6 +109,7 @@ protected override void OnModelCreating(ModelBuilder builder) var eCollectionCipher = builder.Entity(); var eCollectionUser = builder.Entity(); var eCollectionGroup = builder.Entity(); + var eAccessRule = builder.Entity(); var eEmergencyAccess = builder.Entity(); var eFolder = builder.Entity(); var eGroup = builder.Entity(); @@ -145,6 +148,14 @@ protected override void OnModelCreating(ModelBuilder builder) eCollectionGroup.HasKey(cg => new { cg.CollectionId, cg.GroupId }); eGroupUser.HasKey(gu => new { gu.GroupId, gu.OrganizationUserId }); + eAccessRule.Property(p => p.Id).ValueGeneratedNever(); + eAccessRule.HasIndex(p => new { p.OrganizationId, p.Name }).IsUnique(); + eCollection + .HasOne() + .WithMany() + .HasForeignKey(c => c.AccessRuleId) + .OnDelete(DeleteBehavior.Restrict); + eOrganizationMemberBaseDetail.HasNoKey(); var dataProtector = this.GetService().CreateProtector( @@ -167,6 +178,7 @@ protected override void OnModelCreating(ModelBuilder builder) eCipher.ToTable(nameof(Cipher)); eCollection.ToTable(nameof(Collection)); eCollectionCipher.ToTable(nameof(CollectionCipher)); + eAccessRule.ToTable(nameof(AccessRule)); eEmergencyAccess.ToTable(nameof(EmergencyAccess)); eFolder.ToTable(nameof(Folder)); eGroup.ToTable(nameof(Group)); diff --git a/src/Infrastructure.EntityFramework/packages.lock.json b/src/Infrastructure.EntityFramework/packages.lock.json index 012f6402bfa8..1e189c4d2eea 100644 --- a/src/Infrastructure.EntityFramework/packages.lock.json +++ b/src/Infrastructure.EntityFramework/packages.lock.json @@ -1322,6 +1322,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1346,6 +1347,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1358,6 +1360,15 @@ "ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis": "[2.0.2, 2.0.2]", "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } + }, + "data": { + "type": "Project" + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } } } } diff --git a/src/Notifications/HubHelpers.cs b/src/Notifications/HubHelpers.cs index aaa64cb80c67..9cad096aba35 100644 --- a/src/Notifications/HubHelpers.cs +++ b/src/Notifications/HubHelpers.cs @@ -221,16 +221,18 @@ await _hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup( break; case PushType.RefreshSecurityTasks: - var pendingTasksData = + case PushType.RefreshApproverInbox: + case PushType.RefreshAccessRequest: + var userRefreshData = JsonSerializer.Deserialize>(notificationJson, _deserializerOptions); - if (pendingTasksData is null) + if (userRefreshData is null) { break; } - await _hubContext.Clients.User(pendingTasksData.Payload.UserId.ToString()) - .SendAsync(_receiveMessageMethod, pendingTasksData, cancellationToken); + await _hubContext.Clients.User(userRefreshData.Payload.UserId.ToString()) + .SendAsync(_receiveMessageMethod, userRefreshData, cancellationToken); break; case PushType.PolicyChanged: await policyChangedNotificationHandler(notificationJson, cancellationToken); diff --git a/src/Notifications/packages.lock.json b/src/Notifications/packages.lock.json index 52f050d9b3c3..e455c7be9234 100644 --- a/src/Notifications/packages.lock.json +++ b/src/Notifications/packages.lock.json @@ -1142,6 +1142,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1166,6 +1167,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1179,33 +1181,44 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/src/Pam.Domain/Entities/AccessDecision.cs b/src/Pam.Domain/Entities/AccessDecision.cs new file mode 100644 index 000000000000..a46b45e5954b --- /dev/null +++ b/src/Pam.Domain/Entities/AccessDecision.cs @@ -0,0 +1,49 @@ +using Bit.Core.Entities; +using Bit.Core.Utilities; +using Bit.Pam.Enums; + +namespace Bit.Pam.Entities; + +/// +/// A single decision on a . In v0 there is exactly one decision per request: an automated +/// verdict for auto-approval, or a +/// verdict once approver endpoints land. +/// +public class AccessDecision : ITableObject +{ + public Guid Id { get; set; } + + public Guid AccessRequestId { get; set; } + + public AccessDeciderKind DeciderKind { get; set; } + + /// + /// The human approver. NULL when is . + /// + public Guid? ApproverId { get; set; } + + /// + /// The condition kind that decided (e.g. ). NULL when + /// is . + /// + public AccessConditionKind? ConditionKind { get; set; } + + public AccessDecisionVerdict Verdict { get; set; } + + /// + /// Human comment, or a future automatic-evaluation reason string. + /// + public string? Comment { get; set; } + + /// + /// Forward-compatible snapshot of the inputs the evaluation saw. Null in this slice (no signals are evaluated). + /// + public string? EvaluationContext { get; set; } + + public DateTime CreationDate { get; set; } = DateTime.UtcNow; + + public void SetNewId() + { + Id = CombGuid.Generate(); + } +} diff --git a/src/Pam.Domain/Entities/AccessLease.cs b/src/Pam.Domain/Entities/AccessLease.cs new file mode 100644 index 000000000000..9901bcc3ff7f --- /dev/null +++ b/src/Pam.Domain/Entities/AccessLease.cs @@ -0,0 +1,39 @@ +using Bit.Core.Entities; +using Bit.Core.Utilities; +using Bit.Pam.Enums; + +namespace Bit.Pam.Entities; + +/// +/// An active grant of access to a cipher, born from an approved . Only +/// leases inside their / window +/// authorize access. +/// +public class AccessLease : ITableObject +{ + public Guid Id { get; set; } + + /// + /// The request that birthed this lease. + /// + public Guid AccessRequestId { get; set; } + + public Guid OrganizationId { get; set; } + public Guid CollectionId { get; set; } + public Guid CipherId { get; set; } + public Guid RequesterId { get; set; } + + public AccessLeaseStatus Status { get; set; } + public DateTime NotBefore { get; set; } + public DateTime NotAfter { get; set; } + + public DateTime? RevokedDate { get; set; } + public Guid? RevokedBy { get; set; } + + public DateTime CreationDate { get; set; } = DateTime.UtcNow; + + public void SetNewId() + { + Id = CombGuid.Generate(); + } +} diff --git a/src/Pam.Domain/Entities/AccessRequest.cs b/src/Pam.Domain/Entities/AccessRequest.cs new file mode 100644 index 000000000000..85c7f12e6141 --- /dev/null +++ b/src/Pam.Domain/Entities/AccessRequest.cs @@ -0,0 +1,57 @@ +using Bit.Core.Entities; +using Bit.Core.Utilities; +using Bit.Pam.Enums; + +namespace Bit.Pam.Entities; + +/// +/// A request to lease access to a cipher in a leasing-governed collection. Auto-approved requests are created +/// already ; requests that require human approval are created +/// and resolved later by an approver. Neither approval mints the lease — +/// the requester activates the approved request within its window, and that activation produces the +/// . +/// +public class AccessRequest : ITableObject +{ + public Guid Id { get; set; } + + /// + /// NULL for original requests. Set only for extension requests, which point at the lease being extended. + /// + public Guid? ExtensionOfLeaseId { get; set; } + + public Guid OrganizationId { get; set; } + public Guid CollectionId { get; set; } + public Guid CipherId { get; set; } + public Guid RequesterId { get; set; } + + /// + /// The requested access window. For automatic approval this is now; for human approval it is the + /// requester-supplied start. + /// + public DateTime NotBefore { get; set; } + + /// + /// The end of the requested access window. For automatic approval this is now + duration; for human + /// approval it is the requester-supplied end. + /// + public DateTime NotAfter { get; set; } + + /// + /// Optional for automatic approval, required for human approval (enforced in the command). + /// + public string? Reason { get; set; } + + public AccessRequestStatus Status { get; set; } + public DateTime CreationDate { get; set; } = DateTime.UtcNow; + + /// + /// Set when the request leaves . + /// + public DateTime? ResolvedDate { get; set; } + + public void SetNewId() + { + Id = CombGuid.Generate(); + } +} diff --git a/src/Pam.Domain/Entities/AccessRule.cs b/src/Pam.Domain/Entities/AccessRule.cs new file mode 100644 index 000000000000..b2a108f668b4 --- /dev/null +++ b/src/Pam.Domain/Entities/AccessRule.cs @@ -0,0 +1,72 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Entities; +using Bit.Core.Utilities; + +namespace Bit.Pam.Entities; + +/// +/// A reusable, org-scoped PAM access rule. Referenced by collections (and eventually Secrets Manager +/// entities) via FK to govern credential lease decisions. +/// +public class AccessRule : ITableObject +{ + public Guid Id { get; set; } + public Guid OrganizationId { get; set; } + + [MaxLength(256)] + public string Name { get; set; } = null!; + + public string? Description { get; set; } + + /// + /// JSON conditions document (an AccessCondition tree). Validated by AccessRuleValidator before + /// being persisted. + /// + public string Conditions { get; set; } = null!; + + /// + /// When true, the rule asks for a per-cipher singleton: at most one active lease may exist for a given cipher + /// across all users. The constraint binds for a member only when every collection through which they reach the + /// cipher is governed by a rule with this flag set; any ungated or non-singleton path is an escape that leaves + /// the member unconstrained. + /// + public bool SingleActiveLease { get; set; } + + /// + /// Default lease duration in seconds, used to pre-fill a request opened under this rule. Null means no + /// rule-specific default is stored and the backend default applies. + /// + public int? DefaultLeaseDurationSeconds { get; set; } + + /// + /// Hard ceiling on the duration of any single lease granted under this rule, in seconds. Null means no + /// per-rule cap (the global maximum still applies). + /// + public int? MaxLeaseDurationSeconds { get; set; } + + /// + /// When false, the rule is inactive: it does not gate access for the collections it governs. New rules + /// default to enabled. + /// + public bool Enabled { get; set; } = true; + + /// + /// When true, a member holding an active lease under this rule may extend it once. Extensions are always + /// auto-approved (regardless of the rule's approval conditions). + /// + public bool AllowsExtensions { get; set; } + + /// + /// The longest a single extension granted under this rule may run, in seconds. Required to be a positive value + /// when is true; ignored otherwise. + /// + public int? MaxExtensionDurationSeconds { get; set; } + + public DateTime CreationDate { get; set; } = DateTime.UtcNow; + public DateTime RevisionDate { get; set; } = DateTime.UtcNow; + + public void SetNewId() + { + Id = CombGuid.Generate(); + } +} diff --git a/src/Pam.Domain/Enums/AccessConditionKind.cs b/src/Pam.Domain/Enums/AccessConditionKind.cs new file mode 100644 index 000000000000..5546e62b3f89 --- /dev/null +++ b/src/Pam.Domain/Enums/AccessConditionKind.cs @@ -0,0 +1,13 @@ +namespace Bit.Pam.Enums; + +/// +/// The kind of access condition that produced an automatic . Mirrors the +/// kind discriminator on , which remains the JSON wire format +/// for a rule's conditions; this enum is the persisted, type-safe form recorded against the decision. +/// +public enum AccessConditionKind : byte +{ + HumanApproval = 0, + IpAllowlist = 1, + TimeOfDay = 2, +} diff --git a/src/Pam.Domain/Enums/AccessDeciderKind.cs b/src/Pam.Domain/Enums/AccessDeciderKind.cs new file mode 100644 index 000000000000..5aa0f2bbc2e0 --- /dev/null +++ b/src/Pam.Domain/Enums/AccessDeciderKind.cs @@ -0,0 +1,19 @@ +namespace Bit.Pam.Enums; + +/// +/// Who made a : an automatic condition evaluation or a human approver. +/// +public enum AccessDeciderKind : byte +{ + Automatic = 0, + Human = 1, +} + +/// +/// The verdict recorded on a . +/// +public enum AccessDecisionVerdict : byte +{ + Deny = 0, + Approve = 1, +} diff --git a/src/Pam.Domain/Enums/AccessLeaseExtendOutcome.cs b/src/Pam.Domain/Enums/AccessLeaseExtendOutcome.cs new file mode 100644 index 000000000000..a1baadaeb510 --- /dev/null +++ b/src/Pam.Domain/Enums/AccessLeaseExtendOutcome.cs @@ -0,0 +1,23 @@ +namespace Bit.Pam.Enums; + +/// +/// The result of a race-safe lease extension. The extension stored procedure returns a distinct integer code so the +/// caller can tell a lease that is no longer extendable apart from one that has already been extended. +/// +public enum AccessLeaseExtendOutcome +{ + /// The lease's window was extended (stored proc returned 1). + Extended = 1, + + /// + /// The lease was no longer active, or its window had already ended, when the guarded update ran (stored proc + /// returned 0). A concurrent revoke or expiry likely won. + /// + LeaseNotActive = 0, + + /// + /// The lease has already been extended (a lease may be extended once; stored proc returned -1). Nothing was + /// persisted. + /// + AlreadyExtended = -1, +} diff --git a/src/Pam.Domain/Enums/AccessLeaseMintOutcome.cs b/src/Pam.Domain/Enums/AccessLeaseMintOutcome.cs new file mode 100644 index 000000000000..822b8f14baf7 --- /dev/null +++ b/src/Pam.Domain/Enums/AccessLeaseMintOutcome.cs @@ -0,0 +1,23 @@ +namespace Bit.Pam.Enums; + +/// +/// The result of a race-safe lease mint. The mint stored procedures return a distinct integer code so the caller can +/// tell a lost-the-race precondition failure apart from a per-cipher single-active-lease conflict. +/// +public enum AccessLeaseMintOutcome +{ + /// The active lease was minted (stored proc returned 1). + Minted = 1, + + /// + /// A precondition no longer held when the guarded insert ran (stored proc returned 0, or the unique-index + /// backstop fired). A concurrent activation likely won; the caller re-reads the winner. + /// + PreconditionFailed = 0, + + /// + /// Another active in-window lease already exists for this cipher and the governing rule enforces a per-cipher + /// singleton (stored proc returned -1). Nothing was persisted. + /// + SingleActiveLeaseConflict = -1, +} diff --git a/src/Pam.Domain/Enums/AccessLeaseStatus.cs b/src/Pam.Domain/Enums/AccessLeaseStatus.cs new file mode 100644 index 000000000000..04fae936d560 --- /dev/null +++ b/src/Pam.Domain/Enums/AccessLeaseStatus.cs @@ -0,0 +1,11 @@ +namespace Bit.Pam.Enums; + +/// +/// Lifecycle of a . Only leases authorize access. +/// +public enum AccessLeaseStatus : byte +{ + Active = 0, + Expired = 1, + Revoked = 2, +} diff --git a/src/Pam.Domain/Enums/AccessRequestStatus.cs b/src/Pam.Domain/Enums/AccessRequestStatus.cs new file mode 100644 index 000000000000..d6dcd6ad36b1 --- /dev/null +++ b/src/Pam.Domain/Enums/AccessRequestStatus.cs @@ -0,0 +1,14 @@ +namespace Bit.Pam.Enums; + +/// +/// Lifecycle of a . A request starts and moves to exactly +/// one terminal state. Auto-approved requests are created already . +/// +public enum AccessRequestStatus : byte +{ + Pending = 0, + Approved = 1, + Denied = 2, + Cancelled = 3, + ExpiredUnanswered = 4, +} diff --git a/src/Pam.Domain/Models/AccessRequestDecision.cs b/src/Pam.Domain/Models/AccessRequestDecision.cs new file mode 100644 index 000000000000..088699d3b834 --- /dev/null +++ b/src/Pam.Domain/Models/AccessRequestDecision.cs @@ -0,0 +1,33 @@ +using Bit.Pam.Enums; + +namespace Bit.Pam.Models; + +/// +/// One decision on an , projected from an +/// row. The element of — there is one per recorded decision, human or +/// automatic. A human decision carries the approver's identity ( plus the denormalized name/email); an +/// automatic decision has none ( null — it was decided by an access-rule condition). +/// +public class AccessRequestDecision +{ + /// Who decided: a human approver or an automatic condition evaluation. + public AccessDeciderKind DeciderKind { get; set; } + + /// The human approver, or null for an automatic decision. + public Guid? Id { get; set; } + + /// The human approver's display name, or null (automatic, or the server could not resolve the user). + public string? Name { get; set; } + + /// The human approver's email, the fallback display when is unset. + public string? Email { get; set; } + + /// The decision's comment (a human approver's note, or a future automatic-evaluation reason), if any. + public string? Comment { get; set; } + + /// The verdict reached. + public AccessDecisionVerdict Verdict { get; set; } + + /// When the decision was made (the decision's CreationDate). + public DateTime DecidedAt { get; set; } +} diff --git a/src/Pam.Domain/Models/AccessRequestDetails.cs b/src/Pam.Domain/Models/AccessRequestDetails.cs new file mode 100644 index 000000000000..27ea42cb2468 --- /dev/null +++ b/src/Pam.Domain/Models/AccessRequestDetails.cs @@ -0,0 +1,54 @@ +using Bit.Pam.Enums; + +namespace Bit.Pam.Models; + +/// +/// A lease request projected for the approver inbox: every field plus the +/// denormalized display data the client needs (cipher/collection names, requester identity), the lease the request +/// produced (if any), and the human resolver's identity/comment. Populated by a single join in the read procedures so +/// the client avoids an N+1. +/// +public class AccessRequestDetails +{ + public Guid Id { get; set; } + + /// The parent lease for an extension request; null for original requests. + public Guid? ExtensionOfLeaseId { get; set; } + + public Guid OrganizationId { get; set; } + public Guid CollectionId { get; set; } + public Guid CipherId { get; set; } + public Guid RequesterId { get; set; } + public DateTime NotBefore { get; set; } + public DateTime NotAfter { get; set; } + public string? Reason { get; set; } + public AccessRequestStatus Status { get; set; } + public DateTime CreationDate { get; set; } + public DateTime? ResolvedDate { get; set; } + + /// The lease this request produced once activated, or null if it has not produced a lease. + public Guid? ProducedLeaseId { get; set; } + + /// + /// The produced lease's current status (Active/Expired/Revoked), or null when the request has not produced a + /// lease. Lets the inbox distinguish a still-live lease from one that has ended, so an ended lease is not offered + /// for revocation. + /// + public AccessLeaseStatus? ProducedLeaseStatus { get; set; } + + /// + /// Every decision recorded against this request, oldest first — one element per + /// row (human or automatic; identity denormalized from the User join for + /// human decisions). Empty only while pending (no decision recorded yet). The resolved reads return the decisions + /// as a second result set that the repository groups onto this list; the constructed reads (decision result, + /// cipher access-state snapshot) set it directly. + /// + public List Decisions { get; set; } = new(); + + /// The cipher's client-encrypted name. The only cipher attribute the inbox exposes. + public string? CipherName { get; set; } + + public string? CollectionName { get; set; } + public string? RequesterName { get; set; } + public string? RequesterEmail { get; set; } +} diff --git a/src/Pam.Domain/Models/AccessRuleDetails.cs b/src/Pam.Domain/Models/AccessRuleDetails.cs new file mode 100644 index 000000000000..1bb0c7d6c99b --- /dev/null +++ b/src/Pam.Domain/Models/AccessRuleDetails.cs @@ -0,0 +1,29 @@ +using Bit.Pam.Entities; + +namespace Bit.Pam.Models; + +/// +/// An together with the IDs of the collections it governs. +/// +public class AccessRuleDetails : AccessRule +{ + public IEnumerable CollectionIds { get; set; } = []; + + public static AccessRuleDetails From(AccessRule rule, IEnumerable collectionIds) => new() + { + Id = rule.Id, + OrganizationId = rule.OrganizationId, + Name = rule.Name, + Description = rule.Description, + Conditions = rule.Conditions, + SingleActiveLease = rule.SingleActiveLease, + DefaultLeaseDurationSeconds = rule.DefaultLeaseDurationSeconds, + MaxLeaseDurationSeconds = rule.MaxLeaseDurationSeconds, + Enabled = rule.Enabled, + AllowsExtensions = rule.AllowsExtensions, + MaxExtensionDurationSeconds = rule.MaxExtensionDurationSeconds, + CreationDate = rule.CreationDate, + RevisionDate = rule.RevisionDate, + CollectionIds = collectionIds, + }; +} diff --git a/src/Pam.Domain/Pam.Domain.csproj b/src/Pam.Domain/Pam.Domain.csproj new file mode 100644 index 000000000000..6cd371bf4747 --- /dev/null +++ b/src/Pam.Domain/Pam.Domain.csproj @@ -0,0 +1,11 @@ + + + + Bit.Pam + + + + + + + diff --git a/src/Pam.Domain/Repositories/IAccessLeaseRepository.cs b/src/Pam.Domain/Repositories/IAccessLeaseRepository.cs new file mode 100644 index 000000000000..d75965b77ea6 --- /dev/null +++ b/src/Pam.Domain/Repositories/IAccessLeaseRepository.cs @@ -0,0 +1,58 @@ +using Bit.Pam.Entities; +using Bit.Pam.Enums; + +namespace Bit.Pam.Repositories; + +public interface IAccessLeaseRepository +{ + Task GetByIdAsync(Guid id); + + /// + /// Returns the lease the request produced (whatever its status), or null if the request has not been activated. + /// + Task GetByAccessRequestIdAsync(Guid accessRequestId); + + /// + /// Returns the caller's active lease for the cipher whose window contains , or null. + /// + Task GetActiveByRequesterIdCipherIdAsync(Guid requesterId, Guid cipherId, DateTime now); + + /// + /// Returns the caller's currently-active leases (status Active, window containing , not + /// revoked) across every organization they belong to. Returns an empty collection when none are active. + /// + Task> GetManyActiveByRequesterIdAsync(Guid requesterId, DateTime now); + + /// + /// Returns every currently-active lease (status Active, window containing ) on the given + /// collections, across all members — the governance view over a set of caller-manageable collections. Returns an + /// empty collection when none are active. + /// + Task> GetManyActiveByCollectionIdsAsync(IEnumerable collectionIds, DateTime now); + + /// + /// Returns the ended leases (status Expired or Revoked) on the given collections that ended on or after + /// — the governance history view over a set of caller-manageable collections. A revoked + /// lease's end is its revoked date; an expired lease's end is its not-after. Returns an empty collection when none + /// qualify. + /// + Task> GetManyEndedByCollectionIdsAsync(IEnumerable collectionIds, DateTime since); + + /// + /// Race-safely mints the active lease for an approved request, copying the request's window. The insert + /// re-checks ownership, Approved status, an open window, and that the request has not already produced a lease; + /// returns when any precondition no longer holds (e.g. a + /// concurrent activation won). When is true and another active + /// in-window lease already exists for the cipher, returns + /// without minting. The lease must already have its id assigned. + /// + Task CreateFromApprovedRequestAsync(AccessLease lease, DateTime now, + bool enforceSingleActiveLease); + + /// + /// Atomically revokes an active lease (setting its revoked date and revoker) and records the revocation reason as + /// a human against the lease's originating request. The decision must already + /// have its id assigned. + /// + Task RevokeAsync(AccessLease lease, AccessDecision auditDecision, DateTime now); +} diff --git a/src/Pam.Domain/Repositories/IAccessRequestRepository.cs b/src/Pam.Domain/Repositories/IAccessRequestRepository.cs new file mode 100644 index 000000000000..875616976e4e --- /dev/null +++ b/src/Pam.Domain/Repositories/IAccessRequestRepository.cs @@ -0,0 +1,97 @@ +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Pam.Models; + +namespace Bit.Pam.Repositories; + +public interface IAccessRequestRepository +{ + Task CreateAsync(AccessRequest request); + + /// + /// Atomically creates an auto-approved (status , + /// resolved now) and its automatic in a single transaction. No lease is minted: the + /// requester activates the approved request later via , + /// just like the human path after approval. Both supplied entities must already have their ids assigned. + /// + Task CreateAutoApprovedAsync(AccessRequest request, AccessDecision decision); + + Task GetByIdAsync(Guid id); + + /// + /// Returns the caller's pending (unresolved) lease request for the cipher, or null if there is none. + /// + Task GetActivePendingByRequesterIdCipherIdAsync(Guid requesterId, Guid cipherId); + + /// + /// Returns the caller's approved-but-not-yet-activated request for the cipher whose window has not lapsed + /// (NotAfter after ), or null. Future windows are included so the client can show the + /// upcoming window; a request that has produced a lease is activated, not approved, and is excluded. + /// + Task GetActiveApprovedByRequesterIdCipherIdAsync(Guid requesterId, Guid cipherId, DateTime now); + + /// + /// Returns the caller's own lease requests across every organization they belong to, regardless of status, most + /// recent first and capped server-side. Display-name fields are not populated for this caller-scoped surface. + /// + Task> GetManyByRequesterIdAsync(Guid requesterId); + + /// + /// Returns the pending approver-inbox rows for the given collections, joined with their denormalized display + /// fields. An empty yields an empty result. + /// + Task> GetManyInboxPendingByCollectionIdsAsync(IEnumerable collectionIds); + + /// + /// Returns the resolved approver-inbox rows (anything no longer pending) created on or after + /// for the given collections. An empty yields an empty + /// result. + /// + Task> GetManyInboxHistoryByCollectionIdsAsync(IEnumerable collectionIds, DateTime since); + + /// + /// Atomically transitions a pending request to (setting its resolved date) and records + /// the approver's human . No lease is created here: the requester activates an + /// approved request later via . Both supplied + /// entities must already have their ids assigned. + /// + Task ResolveWithDecisionAsync(AccessRequest request, AccessDecision decision, AccessRequestStatus status, DateTime now); + + /// + /// Withdraws a not-yet-activated request on the requester's behalf: transitions it to + /// (from or an + /// request the requester has not activated) and stamps + /// as its resolved date. No is written — a cancellation is the + /// requester acting on their own request, not an approver verdict. The write is guarded so a request that has + /// already left the cancellable set or produced a lease is left untouched (race-safe / idempotent). + /// + Task CancelAsync(Guid id, DateTime now); + + /// + /// Retracts a not-yet-activated request on a managing approver's behalf: transitions it to + /// (from or an unactivated + /// request), stamps the resolved date, and records the approver's human + /// Deny so the audit trail names them. The write is guarded so a request that has + /// already left the cancellable set or produced a lease is left untouched (race-safe); the decision is recorded + /// only when the transition happens. Both supplied entities must already have their ids assigned. + /// + Task CancelWithDecisionAsync(AccessRequest request, AccessDecision decision, DateTime now); + + /// + /// Returns the number of extension requests recorded against the lease (a lease may be extended once, so this is + /// 0 or 1). Used to gate whether another extension is allowed. + /// + Task CountExtensionsByLeaseIdAsync(Guid leaseId); + + /// + /// Atomically records an auto-approved extension request (with its automatic decision) and pushes the parent + /// lease's end out to the request's NotAfter, all under a per-lease lock. Returns + /// when the lease is no longer active or its window has + /// ended, or when the lease has already been extended (a + /// lease may be extended once); otherwise . Both supplied entities + /// must already have their ids assigned, and the request's ExtensionOfLeaseId identifies the lease being + /// extended. + /// + Task CreateApprovedExtensionAsync(AccessRequest request, AccessDecision decision, + DateTime now); +} diff --git a/src/Pam.Domain/Repositories/IAccessRuleRepository.cs b/src/Pam.Domain/Repositories/IAccessRuleRepository.cs new file mode 100644 index 000000000000..f7aa426ca486 --- /dev/null +++ b/src/Pam.Domain/Repositories/IAccessRuleRepository.cs @@ -0,0 +1,31 @@ +using Bit.Core.Repositories; +using Bit.Pam.Entities; +using Bit.Pam.Models; + +namespace Bit.Pam.Repositories; + +public interface IAccessRuleRepository : IRepository +{ + Task> GetManyByOrganizationIdAsync(Guid organizationId); + + /// + /// Returns the access rule along with the IDs of the collections it governs, or null if it does not exist. + /// + Task GetDetailsByIdAsync(Guid id); + + /// + /// Returns all access rules in the organization, each along with the IDs of the collections it governs. + /// + Task> GetManyDetailsByOrganizationIdAsync(Guid organizationId); + + /// + /// Points the given collections at the access rule and clears the rule from any collections that should no + /// longer reference it. Both sets are scoped to the organization. + /// + /// The organization that owns the access rule and collections. + /// The access rule to associate. + /// Collections that should reference the access rule. + /// Collections whose reference to the access rule should be removed. + Task SetCollectionAssociationsAsync(Guid organizationId, Guid accessRuleId, + IEnumerable collectionIdsToAssign, IEnumerable collectionIdsToClear); +} diff --git a/src/Pam.Domain/packages.lock.json b/src/Pam.Domain/packages.lock.json new file mode 100644 index 000000000000..322ce177390b --- /dev/null +++ b/src/Pam.Domain/packages.lock.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "dependencies": { + "net10.0": { + "data": { + "type": "Project" + } + } + } +} \ No newline at end of file diff --git a/src/Api/Models/Response/ListResponseModel.cs b/src/SharedWeb/Models/Response/ListResponseModel.cs similarity index 92% rename from src/Api/Models/Response/ListResponseModel.cs rename to src/SharedWeb/Models/Response/ListResponseModel.cs index 746e6c197b04..cf00271eb534 100644 --- a/src/Api/Models/Response/ListResponseModel.cs +++ b/src/SharedWeb/Models/Response/ListResponseModel.cs @@ -3,7 +3,7 @@ using Bit.Core.Models.Api; -namespace Bit.Api.Models.Response; +namespace Bit.SharedWeb.Models.Response; public class ListResponseModel : ResponseModel where T : ResponseModel { diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 7b4199931c79..81ce7671bb1f 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -60,6 +60,7 @@ using Bit.Core.Vault.Services; using Bit.Infrastructure.Dapper; using Bit.Infrastructure.EntityFramework; +using Bit.Pam.Services; using Bit.SharedWeb.Play; using DnsClient; using Duende.IdentityModel; @@ -164,6 +165,7 @@ public static void AddBaseServices(this IServiceCollection services, IGlobalSett services.AddUserServices(globalSettings); services.AddTrialInitiationServices(); services.AddOrganizationServices(globalSettings); + services.TryAddScoped(); services.AddPolicyServices(); services.AddScoped(); services.AddScoped(); diff --git a/src/SharedWeb/packages.lock.json b/src/SharedWeb/packages.lock.json index 8162537388fb..4eb20a3a74a3 100644 --- a/src/SharedWeb/packages.lock.json +++ b/src/SharedWeb/packages.lock.json @@ -1342,6 +1342,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1366,6 +1367,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1379,26 +1381,37 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } } } } diff --git a/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_Create.sql b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_Create.sql index 2b3b14fd6bd9..a5cdefd64201 100644 --- a/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_Create.sql +++ b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_Create.sql @@ -6,7 +6,8 @@ @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7), @DefaultUserCollectionEmail NVARCHAR(256) = NULL, - @Type TINYINT = 0 + @Type TINYINT = 0, + @AccessRuleId UNIQUEIDENTIFIER = NULL AS BEGIN SET NOCOUNT ON @@ -20,7 +21,8 @@ BEGIN [CreationDate], [RevisionDate], [DefaultUserCollectionEmail], - [Type] + [Type], + [AccessRuleId] ) VALUES ( @@ -31,7 +33,8 @@ BEGIN @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, - @Type + @Type, + @AccessRuleId ) EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId diff --git a/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_CreateWithGroupsAndUsers.sql b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_CreateWithGroupsAndUsers.sql index 92ffd366e69e..cfd80ae9136e 100644 --- a/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_CreateWithGroupsAndUsers.sql +++ b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_CreateWithGroupsAndUsers.sql @@ -8,12 +8,13 @@ CREATE PROCEDURE [dbo].[Collection_CreateWithGroupsAndUsers] @Groups AS [dbo].[CollectionAccessSelectionType] READONLY, @Users AS [dbo].[CollectionAccessSelectionType] READONLY, @DefaultUserCollectionEmail NVARCHAR(256) = NULL, - @Type TINYINT = 0 + @Type TINYINT = 0, + @AccessRuleId UNIQUEIDENTIFIER = NULL AS BEGIN SET NOCOUNT ON - EXEC [dbo].[Collection_Create] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type + EXEC [dbo].[Collection_Create] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type, @AccessRuleId -- Groups ;WITH [AvailableGroupsCTE] AS( diff --git a/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_ReadByIdWithPermissions.sql b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_ReadByIdWithPermissions.sql index 9f2caeb87f81..cb23b2cb4d9e 100644 --- a/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_ReadByIdWithPermissions.sql +++ b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_ReadByIdWithPermissions.sql @@ -75,7 +75,8 @@ BEGIN C.[RevisionDate], C.[ExternalId], C.[DefaultUserCollectionEmail], - C.[Type] + C.[Type], + C.[AccessRuleId] IF (@IncludeAccessRelationships = 1) BEGIN diff --git a/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_ReadByUserId.sql b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_ReadByUserId.sql index 4180dc6909ed..c9bd7e96bee1 100644 --- a/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_ReadByUserId.sql +++ b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_ReadByUserId.sql @@ -15,7 +15,8 @@ BEGIN MIN([HidePasswords]) AS [HidePasswords], MAX([Manage]) AS [Manage], [DefaultUserCollectionEmail], - [Type] + [Type], + [AccessRuleId] FROM [dbo].[UserCollectionDetails](@UserId) GROUP BY @@ -26,5 +27,6 @@ BEGIN RevisionDate, ExternalId, [DefaultUserCollectionEmail], - [Type] + [Type], + [AccessRuleId] END diff --git a/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_ReadManagingUserIds.sql b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_ReadManagingUserIds.sql new file mode 100644 index 000000000000..b0faaa9e1a03 --- /dev/null +++ b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_ReadManagingUserIds.sql @@ -0,0 +1,50 @@ +CREATE PROCEDURE [dbo].[Collection_ReadManagingUserIds] + @CollectionId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + -- Every confirmed member who can Manage the collection: direct Manage assignments, Manage via group membership, + -- plus org Owners/Admins (when the org allows admin access to all collection items) and Custom users with the + -- EditAnyCollection permission. Returns distinct user ids. + DECLARE @OrganizationId UNIQUEIDENTIFIER + SELECT @OrganizationId = [OrganizationId] FROM [dbo].[Collection] WHERE [Id] = @CollectionId + + SELECT DISTINCT [UserId] + FROM + ( + SELECT OU.[UserId] + FROM [dbo].[CollectionUser] CU + INNER JOIN [dbo].[OrganizationUser] OU ON OU.[Id] = CU.[OrganizationUserId] + WHERE CU.[CollectionId] = @CollectionId + AND CU.[Manage] = 1 + AND OU.[Status] = 2 -- Confirmed + AND OU.[UserId] IS NOT NULL + + UNION + + SELECT OU.[UserId] + FROM [dbo].[CollectionGroup] CG + INNER JOIN [dbo].[GroupUser] GU ON GU.[GroupId] = CG.[GroupId] + INNER JOIN [dbo].[OrganizationUser] OU ON OU.[Id] = GU.[OrganizationUserId] + WHERE CG.[CollectionId] = @CollectionId + AND CG.[Manage] = 1 + AND OU.[Status] = 2 -- Confirmed + AND OU.[UserId] IS NOT NULL + + UNION + + SELECT OU.[UserId] + FROM [dbo].[OrganizationUser] OU + INNER JOIN [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] + WHERE OU.[OrganizationId] = @OrganizationId + AND OU.[Status] = 2 -- Confirmed + AND OU.[UserId] IS NOT NULL + AND ( + (O.[AllowAdminAccessToAllCollectionItems] = 1 AND OU.[Type] IN (0, 1)) -- Owner, Admin + OR (OU.[Type] = 4 -- Custom + AND ISJSON(OU.[Permissions]) = 1 + AND JSON_VALUE(OU.[Permissions], '$.editAnyCollection') = 'true') + ) + ) AS ManagingUsers +END diff --git a/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_ReadSharedCollectionsByOrganizationIdWithPermissions.sql b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_ReadSharedCollectionsByOrganizationIdWithPermissions.sql index 52120fe28af2..9310c102e023 100644 --- a/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_ReadSharedCollectionsByOrganizationIdWithPermissions.sql +++ b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_ReadSharedCollectionsByOrganizationIdWithPermissions.sql @@ -76,7 +76,8 @@ BEGIN C.[RevisionDate], C.[ExternalId], C.[DefaultUserCollectionEmail], - C.[Type] + C.[Type], + C.[AccessRuleId] IF (@IncludeAccessRelationships = 1) BEGIN diff --git a/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_Update.sql b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_Update.sql index 69a009e27a4e..5bf3566a51af 100644 --- a/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_Update.sql +++ b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_Update.sql @@ -6,7 +6,8 @@ @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7), @DefaultUserCollectionEmail NVARCHAR(256) = NULL, - @Type TINYINT = 0 + @Type TINYINT = 0, + @AccessRuleId UNIQUEIDENTIFIER = NULL AS BEGIN SET NOCOUNT ON @@ -20,7 +21,8 @@ BEGIN [CreationDate] = @CreationDate, [RevisionDate] = @RevisionDate, [DefaultUserCollectionEmail] = @DefaultUserCollectionEmail, - [Type] = @Type + [Type] = @Type, + [AccessRuleId] = @AccessRuleId WHERE [Id] = @Id diff --git a/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_UpdateWithGroups.sql b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_UpdateWithGroups.sql index 13b03e8d98a4..d0380f899fc6 100644 --- a/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_UpdateWithGroups.sql +++ b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_UpdateWithGroups.sql @@ -7,12 +7,13 @@ CREATE PROCEDURE [dbo].[Collection_UpdateWithGroups] @RevisionDate DATETIME2(7), @Groups AS [dbo].[CollectionAccessSelectionType] READONLY, @DefaultUserCollectionEmail NVARCHAR(256) = NULL, - @Type TINYINT = 0 + @Type TINYINT = 0, + @AccessRuleId UNIQUEIDENTIFIER = NULL AS BEGIN SET NOCOUNT ON - EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type + EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type, @AccessRuleId -- Bump RevisionDate on all affected groups (old + new) before modifying CollectionGroup ;WITH [AffectedGroupsCTE] AS ( diff --git a/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_UpdateWithGroupsAndUsers.sql b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_UpdateWithGroupsAndUsers.sql index 80e980019da4..3be2d74dc61b 100644 --- a/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_UpdateWithGroupsAndUsers.sql +++ b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_UpdateWithGroupsAndUsers.sql @@ -8,12 +8,13 @@ @Groups AS [dbo].[CollectionAccessSelectionType] READONLY, @Users AS [dbo].[CollectionAccessSelectionType] READONLY, @DefaultUserCollectionEmail NVARCHAR(256) = NULL, - @Type TINYINT = 0 + @Type TINYINT = 0, + @AccessRuleId UNIQUEIDENTIFIER = NULL AS BEGIN SET NOCOUNT ON - EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type + EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type, @AccessRuleId -- Bump RevisionDate on all affected groups (old + new) before modifying CollectionGroup ;WITH [AffectedGroupsCTE] AS ( diff --git a/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_UpdateWithUsers.sql b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_UpdateWithUsers.sql index 60fccc51d558..ae51a43fabb0 100644 --- a/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_UpdateWithUsers.sql +++ b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_UpdateWithUsers.sql @@ -7,12 +7,13 @@ CREATE PROCEDURE [dbo].[Collection_UpdateWithUsers] @RevisionDate DATETIME2(7), @Users AS [dbo].[CollectionAccessSelectionType] READONLY, @DefaultUserCollectionEmail NVARCHAR(256) = NULL, - @Type TINYINT = 0 + @Type TINYINT = 0, + @AccessRuleId UNIQUEIDENTIFIER = NULL AS BEGIN SET NOCOUNT ON - EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type + EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type, @AccessRuleId -- Users -- Delete users that are no longer in source diff --git a/src/Sql/dbo/AdminConsole/Tables/Collection.sql b/src/Sql/dbo/AdminConsole/Tables/Collection.sql index 2f0d3b943bdb..b5bef98062a0 100644 --- a/src/Sql/dbo/AdminConsole/Tables/Collection.sql +++ b/src/Sql/dbo/AdminConsole/Tables/Collection.sql @@ -7,8 +7,10 @@ [RevisionDate] DATETIME2 (7) NOT NULL, [DefaultUserCollectionEmail] NVARCHAR(256) NULL, [Type] TINYINT NOT NULL DEFAULT(0), + [AccessRuleId] UNIQUEIDENTIFIER NULL, CONSTRAINT [PK_Collection] PRIMARY KEY CLUSTERED ([Id] ASC), - CONSTRAINT [FK_Collection_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE + CONSTRAINT [FK_Collection_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE, + CONSTRAINT [FK_Collection_AccessRule] FOREIGN KEY ([AccessRuleId]) REFERENCES [dbo].[AccessRule] ([Id]) ON DELETE NO ACTION ); GO @@ -17,3 +19,7 @@ CREATE NONCLUSTERED INDEX [IX_Collection_OrganizationId_IncludeAll] INCLUDE([CreationDate], [Name], [RevisionDate], [Type]); GO +CREATE NONCLUSTERED INDEX [IX_Collection_AccessRuleId] + ON [dbo].[Collection]([AccessRuleId] ASC); +GO + diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessLease_CreateFromApprovedRequest.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessLease_CreateFromApprovedRequest.sql new file mode 100644 index 000000000000..760da15a47ee --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessLease_CreateFromApprovedRequest.sql @@ -0,0 +1,65 @@ +CREATE PROCEDURE [dbo].[AccessLease_CreateFromApprovedRequest] + @AccessLeaseId UNIQUEIDENTIFIER, + @AccessRequestId UNIQUEIDENTIFIER, + @RequesterId UNIQUEIDENTIFIER, + @Now DATETIME2(7), + @EnforceSingleActiveLease BIT = 0 +AS +BEGIN + SET NOCOUNT ON + -- An explicit transaction is required so the singleton guard's range lock is held until the INSERT commits; + -- XACT_ABORT guarantees the transaction is rolled back (and the pooled connection left clean) if the + -- unique-index backstop [IX_AccessLease_AccessRequestId] trips on a concurrent activation of the same request. + SET XACT_ABORT ON + + BEGIN TRANSACTION + + -- Per-cipher singleton guard. When the governing rule(s) ask for a single active lease, activation is allowed + -- only if no other active in-window lease exists for the same cipher across all users. The UPDLOCK, HOLDLOCK + -- range lock is held for the life of this transaction, so it serializes against the INSERT below: a concurrent + -- same-cipher activation blocks here until this transaction commits, then sees the new lease and is rejected. + -- Outcome -1 is distinct from the precondition-fail outcome (0) so the caller can surface a 409 conflict. + IF @EnforceSingleActiveLease = 1 + AND EXISTS ( + SELECT 1 + FROM [dbo].[AccessLease] WITH (UPDLOCK, HOLDLOCK) + WHERE [CipherId] = (SELECT [CipherId] FROM [dbo].[AccessRequest] WHERE [Id] = @AccessRequestId) + AND [Status] = 0 /* Active */ + AND [NotBefore] <= @Now + AND [NotAfter] > @Now + ) + BEGIN + ROLLBACK TRANSACTION + SELECT -1 + RETURN + END + + -- Activation of an approved request: mints the active lease that authorizes access, spanning the request's + -- approved window. Every application-level precondition is re-checked inside the INSERT so a concurrent + -- activation cannot double-mint; zero rows inserted means a precondition no longer held and the caller decides + -- how to surface that. [IX_AccessLease_AccessRequestId] (unique) is the backstop when two calls pass the + -- NOT EXISTS check simultaneously. + INSERT INTO [dbo].[AccessLease] + ( + [Id], [AccessRequestId], [OrganizationId], [CollectionId], [CipherId], [RequesterId], + [Status], [NotBefore], [NotAfter], [RevokedDate], [RevokedBy], [CreationDate] + ) + SELECT + @AccessLeaseId, AR.[Id], AR.[OrganizationId], AR.[CollectionId], AR.[CipherId], AR.[RequesterId], + 0 /* Active */, AR.[NotBefore], AR.[NotAfter], NULL, NULL, @Now + FROM [dbo].[AccessRequest] AR + WHERE + AR.[Id] = @AccessRequestId + AND AR.[RequesterId] = @RequesterId + AND AR.[Status] = 1 -- Approved + AND AR.[NotBefore] <= @Now + AND AR.[NotAfter] > @Now + AND NOT EXISTS (SELECT 1 FROM [dbo].[AccessLease] AL WHERE AL.[AccessRequestId] = AR.[Id]) + + DECLARE @Rows INT = @@ROWCOUNT + + COMMIT TRANSACTION + + -- 1 = minted, 0 = precondition no longer held (caller re-reads the winner). + SELECT CASE WHEN @Rows = 1 THEN 1 ELSE 0 END +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessLease_ReadActiveByRequesterIdCipherId.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessLease_ReadActiveByRequesterIdCipherId.sql new file mode 100644 index 000000000000..0739a53896de --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessLease_ReadActiveByRequesterIdCipherId.sql @@ -0,0 +1,21 @@ +CREATE PROCEDURE [dbo].[AccessLease_ReadActiveByRequesterIdCipherId] + @RequesterId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + SELECT TOP 1 + * + FROM + [dbo].[AccessLease] + WHERE + [RequesterId] = @RequesterId + AND [CipherId] = @CipherId + AND [Status] = 0 -- Active + AND [NotBefore] <= @Now + AND [NotAfter] > @Now + ORDER BY + [NotAfter] DESC +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessLease_ReadByAccessRequestId.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessLease_ReadByAccessRequestId.sql new file mode 100644 index 000000000000..131b99d22022 --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessLease_ReadByAccessRequestId.sql @@ -0,0 +1,16 @@ +CREATE PROCEDURE [dbo].[AccessLease_ReadByAccessRequestId] + @AccessRequestId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + -- A request produces at most one lease ([IX_AccessLease_AccessRequestId] is unique); TOP 1 is belt and braces. + SELECT TOP 1 + * + FROM + [dbo].[AccessLease] + WHERE + [AccessRequestId] = @AccessRequestId + ORDER BY + [CreationDate] DESC +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessLease_ReadById.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessLease_ReadById.sql new file mode 100644 index 000000000000..1f33b649baf5 --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessLease_ReadById.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[AccessLease_ReadById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[AccessLease] + WHERE + [Id] = @Id +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessLease_ReadManyActiveByCollectionIds.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessLease_ReadManyActiveByCollectionIds.sql new file mode 100644 index 000000000000..dc8bd84c25f9 --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessLease_ReadManyActiveByCollectionIds.sql @@ -0,0 +1,21 @@ +CREATE PROCEDURE [dbo].[AccessLease_ReadManyActiveByCollectionIds] + @CollectionIds [dbo].[GuidIdArray] READONLY, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- Governance view: every currently-active lease (Active, window containing @Now) on the supplied + -- (caller-manageable) collections, across all members -- not just the caller's own. + SELECT + L.* + FROM + [dbo].[AccessLease] L + INNER JOIN @CollectionIds CI ON CI.[Id] = L.[CollectionId] + WHERE + L.[Status] = 0 -- Active + AND L.[NotBefore] <= @Now + AND L.[NotAfter] > @Now + ORDER BY + L.[NotAfter] ASC +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessLease_ReadManyActiveByRequesterId.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessLease_ReadManyActiveByRequesterId.sql new file mode 100644 index 000000000000..e1d08bb94c4f --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessLease_ReadManyActiveByRequesterId.sql @@ -0,0 +1,19 @@ +CREATE PROCEDURE [dbo].[AccessLease_ReadManyActiveByRequesterId] + @RequesterId UNIQUEIDENTIFIER, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[AccessLease] + WHERE + [RequesterId] = @RequesterId + AND [Status] = 0 -- Active + AND [NotBefore] <= @Now + AND [NotAfter] > @Now + ORDER BY + [NotAfter] ASC +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessLease_ReadManyEndedByCollectionIds.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessLease_ReadManyEndedByCollectionIds.sql new file mode 100644 index 000000000000..52c0b1e2245b --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessLease_ReadManyEndedByCollectionIds.sql @@ -0,0 +1,24 @@ +CREATE PROCEDURE [dbo].[AccessLease_ReadManyEndedByCollectionIds] + @CollectionIds [dbo].[GuidIdArray] READONLY, + @Since DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- Governance history: leases that have ended (Expired or Revoked) on the supplied (caller-manageable) + -- collections, that ended on or after @Since. A revoked lease's end is its RevokedDate; an expired lease's end + -- is its NotAfter. Most recently ended first. + SELECT + L.* + FROM + [dbo].[AccessLease] L + INNER JOIN @CollectionIds CI ON CI.[Id] = L.[CollectionId] + WHERE + L.[Status] IN (1, 2) -- Expired, Revoked + AND ( + (L.[Status] = 2 AND L.[RevokedDate] >= @Since) -- Revoked + OR (L.[Status] = 1 AND L.[NotAfter] >= @Since) -- Expired + ) + ORDER BY + CASE WHEN L.[Status] = 2 THEN L.[RevokedDate] ELSE L.[NotAfter] END DESC +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessLease_Revoke.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessLease_Revoke.sql new file mode 100644 index 000000000000..5295360d1b57 --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessLease_Revoke.sql @@ -0,0 +1,35 @@ +CREATE PROCEDURE [dbo].[AccessLease_Revoke] + @AccessLeaseId UNIQUEIDENTIFIER, + @AccessRequestId UNIQUEIDENTIFIER, + @RevokedBy UNIQUEIDENTIFIER, + @AccessDecisionId UNIQUEIDENTIFIER, + @Reason NVARCHAR(MAX) = NULL, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- Atomically revoke an active lease and capture who/why. The revocation reason has no dedicated column, so it is + -- preserved as a human AccessDecision (Deny) against the lease's originating request, keeping the audit trail + -- without a schema change. The WHERE guard keeps revocation idempotent if two approvers race. + BEGIN TRANSACTION AccessLease_Revoke + + UPDATE [dbo].[AccessLease] + SET [Status] = 2 /* Revoked */, + [RevokedDate] = @Now, + [RevokedBy] = @RevokedBy + WHERE [Id] = @AccessLeaseId AND [Status] = 0 -- Active + + INSERT INTO [dbo].[AccessDecision] + ( + [Id], [AccessRequestId], [DeciderKind], [ApproverId], [ConditionKind], + [Verdict], [Comment], [EvaluationContext], [CreationDate] + ) + VALUES + ( + @AccessDecisionId, @AccessRequestId, 1 /* Human */, @RevokedBy, NULL, + 0 /* Deny */, @Reason, NULL, @Now + ) + + COMMIT TRANSACTION AccessLease_Revoke +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_Cancel.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_Cancel.sql new file mode 100644 index 000000000000..26f65fa8c95a --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_Cancel.sql @@ -0,0 +1,19 @@ +CREATE PROCEDURE [dbo].[AccessRequest_Cancel] + @AccessRequestId UNIQUEIDENTIFIER, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- The requester withdraws their own not-yet-activated request (Pending, or an Approved request they have not + -- activated). Unlike [AccessRequest_CancelWithDecision], no AccessDecision is written: a cancellation is the + -- requester acting on their own request, not an approver verdict. The WHERE guard keeps the write idempotent under + -- a race and refuses a request that has already produced a lease (that access is governed by the lease, which must + -- be revoked instead). + UPDATE [dbo].[AccessRequest] + SET [Status] = 3, -- Cancelled + [ResolvedDate] = @Now + WHERE [Id] = @AccessRequestId + AND [Status] IN (0, 1) -- Pending or Approved + AND NOT EXISTS (SELECT 1 FROM [dbo].[AccessLease] L WHERE L.[AccessRequestId] = @AccessRequestId) +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_CancelWithDecision.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_CancelWithDecision.sql new file mode 100644 index 000000000000..7560a8ccc398 --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_CancelWithDecision.sql @@ -0,0 +1,41 @@ +CREATE PROCEDURE [dbo].[AccessRequest_CancelWithDecision] + @AccessRequestId UNIQUEIDENTIFIER, + @AccessDecisionId UNIQUEIDENTIFIER, + @ApproverId UNIQUEIDENTIFIER, + @Verdict TINYINT, + @Comment NVARCHAR(MAX) = NULL, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- A managing approver retracts a not-yet-activated request (Pending, or an Approved request the requester has not + -- activated): transition it to Denied and record the approver's human decision, mirroring + -- [AccessRequest_ResolveWithDecision] but over the broader cancellable set. The WHERE guard is race-safe and + -- refuses a request that has produced a lease (governed by the lease — revoke instead). The decision is inserted + -- only when the transition actually happened (@@ROWCOUNT > 0), so a no-op never orphans an AccessDecision. + BEGIN TRANSACTION AccessRequest_CancelWithDecision + + UPDATE [dbo].[AccessRequest] + SET [Status] = 2, -- Denied + [ResolvedDate] = @Now + WHERE [Id] = @AccessRequestId + AND [Status] IN (0, 1) -- Pending or Approved + AND NOT EXISTS (SELECT 1 FROM [dbo].[AccessLease] L WHERE L.[AccessRequestId] = @AccessRequestId) + + IF @@ROWCOUNT > 0 + BEGIN + INSERT INTO [dbo].[AccessDecision] + ( + [Id], [AccessRequestId], [DeciderKind], [ApproverId], [ConditionKind], + [Verdict], [Comment], [EvaluationContext], [CreationDate] + ) + VALUES + ( + @AccessDecisionId, @AccessRequestId, 1 /* Human */, @ApproverId, NULL, + @Verdict, @Comment, NULL, @Now + ) + END + + COMMIT TRANSACTION AccessRequest_CancelWithDecision +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_CountExtensionsByLeaseId.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_CountExtensionsByLeaseId.sql new file mode 100644 index 000000000000..ae70378082de --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_CountExtensionsByLeaseId.sql @@ -0,0 +1,12 @@ +CREATE PROCEDURE [dbo].[AccessRequest_CountExtensionsByLeaseId] + @LeaseId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + -- Number of extension requests recorded against the lease. Extensions are always auto-approved, so every such + -- request counts toward the governing rule's per-lease maximum. + SELECT COUNT(*) + FROM [dbo].[AccessRequest] + WHERE [ExtensionOfLeaseId] = @LeaseId +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_Create.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_Create.sql new file mode 100644 index 000000000000..f7a8d26be7e1 --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_Create.sql @@ -0,0 +1,48 @@ +CREATE PROCEDURE [dbo].[AccessRequest_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @ExtensionOfLeaseId UNIQUEIDENTIFIER = NULL, + @OrganizationId UNIQUEIDENTIFIER, + @CollectionId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @RequesterId UNIQUEIDENTIFIER, + @NotBefore DATETIME2(7), + @NotAfter DATETIME2(7), + @Reason NVARCHAR(MAX) = NULL, + @Status TINYINT, + @CreationDate DATETIME2(7), + @ResolvedDate DATETIME2(7) = NULL +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[AccessRequest] + ( + [Id], + [ExtensionOfLeaseId], + [OrganizationId], + [CollectionId], + [CipherId], + [RequesterId], + [NotBefore], + [NotAfter], + [Reason], + [Status], + [CreationDate], + [ResolvedDate] + ) + VALUES + ( + @Id, + @ExtensionOfLeaseId, + @OrganizationId, + @CollectionId, + @CipherId, + @RequesterId, + @NotBefore, + @NotAfter, + @Reason, + @Status, + @CreationDate, + @ResolvedDate + ) +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_CreateApprovedExtension.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_CreateApprovedExtension.sql new file mode 100644 index 000000000000..0a715e0446d1 --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_CreateApprovedExtension.sql @@ -0,0 +1,81 @@ +CREATE PROCEDURE [dbo].[AccessRequest_CreateApprovedExtension] + @AccessRequestId UNIQUEIDENTIFIER, + @AccessDecisionId UNIQUEIDENTIFIER, + @ExtensionOfLeaseId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @CollectionId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @RequesterId UNIQUEIDENTIFIER, + @NotBefore DATETIME2(7), + @NotAfter DATETIME2(7), + @Reason NVARCHAR(MAX) = NULL, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + -- An explicit transaction holds the per-lease range lock until the writes commit, so concurrent extensions of + -- the same lease serialize. XACT_ABORT guarantees rollback (and a clean pooled connection) on any error. + SET XACT_ABORT ON + + BEGIN TRANSACTION + + -- Lock the parent lease row for the life of the transaction. A second concurrent extension of the same lease + -- blocks here until this transaction commits, then re-counts below and sees this extension. The lease must + -- still be active and in-window to be extendable; outcome 0 is distinct from the cap conflict (-1). + IF NOT EXISTS ( + SELECT 1 + FROM [dbo].[AccessLease] WITH (UPDLOCK, HOLDLOCK) + WHERE [Id] = @ExtensionOfLeaseId + AND [RequesterId] = @RequesterId + AND [Status] = 0 /* Active */ + AND [NotAfter] > @Now + ) + BEGIN + ROLLBACK TRANSACTION + SELECT 0 -- LeaseNotActive + RETURN + END + + -- A lease may be extended exactly once. Counted under the lease lock, so it is race-safe against a concurrent + -- extension of the same lease. + IF EXISTS (SELECT 1 FROM [dbo].[AccessRequest] WHERE [ExtensionOfLeaseId] = @ExtensionOfLeaseId) + BEGIN + ROLLBACK TRANSACTION + SELECT -1 -- AlreadyExtended + RETURN + END + + -- Record the auto-approved extension request and its automatic verdict, then push the parent lease's end out in + -- place. No new lease is minted — extending reuses the existing lease, preserving the single-active-lease + -- invariant. The request's window spans the extension ([old lease end] .. [new lease end]); its NotAfter is the + -- lease's new end. + INSERT INTO [dbo].[AccessRequest] + ( + [Id], [ExtensionOfLeaseId], [OrganizationId], [CollectionId], [CipherId], [RequesterId], + [NotBefore], [NotAfter], [Reason], [Status], [CreationDate], [ResolvedDate] + ) + VALUES + ( + @AccessRequestId, @ExtensionOfLeaseId, @OrganizationId, @CollectionId, @CipherId, @RequesterId, + @NotBefore, @NotAfter, @Reason, 1 /* Approved */, @Now, @Now + ) + + INSERT INTO [dbo].[AccessDecision] + ( + [Id], [AccessRequestId], [DeciderKind], [ApproverId], [ConditionKind], + [Verdict], [Comment], [EvaluationContext], [CreationDate] + ) + VALUES + ( + @AccessDecisionId, @AccessRequestId, 0 /* Automatic */, NULL, NULL, + 1 /* Approve */, NULL, NULL, @Now + ) + + UPDATE [dbo].[AccessLease] + SET [NotAfter] = @NotAfter + WHERE [Id] = @ExtensionOfLeaseId + + COMMIT TRANSACTION + + SELECT 1 -- Extended +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_CreateAutoApproved.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_CreateAutoApproved.sql new file mode 100644 index 000000000000..b9d9d3d9eb54 --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_CreateAutoApproved.sql @@ -0,0 +1,47 @@ +CREATE PROCEDURE [dbo].[AccessRequest_CreateAutoApproved] + @AccessRequestId UNIQUEIDENTIFIER, + @AccessDecisionId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @CollectionId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @RequesterId UNIQUEIDENTIFIER, + @NotBefore DATETIME2(7), + @NotAfter DATETIME2(7), + @Reason NVARCHAR(MAX) = NULL, + @ConditionKind TINYINT = NULL, + @CreationDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- Atomically record an auto-approved request and its automatic verdict. No lease is minted here: the requester + -- activates the approved request later via [AccessLease_CreateFromApprovedRequest], exactly like the human path + -- after approval. The per-cipher single-active-lease guard therefore lives entirely on that activation path. + BEGIN TRANSACTION AccessRequest_CreateAutoApproved + + -- The request is created already resolved (Approved). ExtensionOfLeaseId stays NULL: it is reserved for extension + -- requests; provenance for an original lease flows the other way, via AccessLease.AccessRequestId. + INSERT INTO [dbo].[AccessRequest] + ( + [Id], [ExtensionOfLeaseId], [OrganizationId], [CollectionId], [CipherId], [RequesterId], + [NotBefore], [NotAfter], [Reason], [Status], [CreationDate], [ResolvedDate] + ) + VALUES + ( + @AccessRequestId, NULL, @OrganizationId, @CollectionId, @CipherId, @RequesterId, + @NotBefore, @NotAfter, @Reason, 1 /* Approved */, @CreationDate, @CreationDate + ) + + INSERT INTO [dbo].[AccessDecision] + ( + [Id], [AccessRequestId], [DeciderKind], [ApproverId], [ConditionKind], + [Verdict], [Comment], [EvaluationContext], [CreationDate] + ) + VALUES + ( + @AccessDecisionId, @AccessRequestId, 0 /* Automatic */, NULL, @ConditionKind, + 1 /* Approve */, NULL, NULL, @CreationDate + ) + + COMMIT TRANSACTION AccessRequest_CreateAutoApproved +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_ReadActiveApprovedByRequesterIdCipherId.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_ReadActiveApprovedByRequesterIdCipherId.sql new file mode 100644 index 000000000000..a937f33b6275 --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_ReadActiveApprovedByRequesterIdCipherId.sql @@ -0,0 +1,27 @@ +CREATE PROCEDURE [dbo].[AccessRequest_ReadActiveApprovedByRequesterIdCipherId] + @RequesterId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- The caller's approved-but-not-yet-activated request whose window can still produce access. Future windows are + -- included (the client shows the upcoming window); lapsed windows are excluded so the client never offers an + -- activation that the server would reject. A request that has produced a lease is activated, not approved. + -- Extension requests are excluded: an approved extension pushes its parent lease's end out in place and never + -- produces a lease of its own, so it must not surface here as an activatable "Start access" request. + SELECT TOP 1 + AR.* + FROM + [dbo].[AccessRequest] AR + WHERE + AR.[RequesterId] = @RequesterId + AND AR.[CipherId] = @CipherId + AND AR.[Status] = 1 -- Approved + AND AR.[NotAfter] > @Now + AND AR.[ExtensionOfLeaseId] IS NULL + AND NOT EXISTS (SELECT 1 FROM [dbo].[AccessLease] AL WHERE AL.[AccessRequestId] = AR.[Id]) + ORDER BY + AR.[CreationDate] DESC +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_ReadActivePendingByRequesterIdCipherId.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_ReadActivePendingByRequesterIdCipherId.sql new file mode 100644 index 000000000000..23e21e5731c3 --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_ReadActivePendingByRequesterIdCipherId.sql @@ -0,0 +1,18 @@ +CREATE PROCEDURE [dbo].[AccessRequest_ReadActivePendingByRequesterIdCipherId] + @RequesterId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT TOP 1 + * + FROM + [dbo].[AccessRequest] + WHERE + [RequesterId] = @RequesterId + AND [CipherId] = @CipherId + AND [Status] = 0 -- Pending + ORDER BY + [CreationDate] DESC +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_ReadById.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_ReadById.sql new file mode 100644 index 000000000000..fb011b14bfda --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_ReadById.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[AccessRequest_ReadById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[AccessRequest] + WHERE + [Id] = @Id +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_ReadInboxHistoryByCollectionIds.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_ReadInboxHistoryByCollectionIds.sql new file mode 100644 index 000000000000..cd3759d795ee --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_ReadInboxHistoryByCollectionIds.sql @@ -0,0 +1,64 @@ +CREATE PROCEDURE [dbo].[AccessRequest_ReadInboxHistoryByCollectionIds] + @CollectionIds [dbo].[GuidIdArray] READONLY, + @Since DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- The approver history, returned as two result sets so the caller can attach each request's full decision list + -- without an N+1: + -- 1) the resolved requests (anything no longer Pending) created on or after @Since, for the supplied + -- (caller-manageable) collections, with denormalized display fields. Rows that produced a lease carry + -- ProducedLeaseId/ProducedLeaseStatus so the client can target (and gate) the Revoke action. + -- 2) every decision (human or automatic) for those requests, keyed by AccessRequestId and ordered oldest-first; + -- DeciderKind says which, and a human decision's identity is denormalized from [User]. + SELECT + LR.[Id], + LR.[ExtensionOfLeaseId], + LR.[OrganizationId], + LR.[CollectionId], + LR.[CipherId], + LR.[RequesterId], + LR.[NotBefore], + LR.[NotAfter], + LR.[Reason], + LR.[Status], + LR.[CreationDate], + LR.[ResolvedDate], + PL.[Id] AS [ProducedLeaseId], + PL.[Status] AS [ProducedLeaseStatus], + JSON_VALUE(C.[Data], '$.Name') AS [CipherName], + COL.[Name] AS [CollectionName], + U.[Name] AS [RequesterName], + U.[Email] AS [RequesterEmail] + FROM [dbo].[AccessRequest] LR + INNER JOIN @CollectionIds CI ON CI.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[Cipher] C ON C.[Id] = LR.[CipherId] + LEFT JOIN [dbo].[Collection] COL ON COL.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[User] U ON U.[Id] = LR.[RequesterId] + OUTER APPLY ( + SELECT TOP 1 L.[Id], L.[Status] + FROM [dbo].[AccessLease] L + WHERE L.[AccessRequestId] = LR.[Id] + ORDER BY L.[CreationDate] DESC + ) PL + WHERE LR.[Status] <> 0 -- not Pending + AND LR.[CreationDate] >= @Since + + SELECT + AD.[AccessRequestId], + AD.[DeciderKind] AS [DeciderKind], + AD.[ApproverId] AS [Id], + AU.[Name] AS [Name], + AU.[Email] AS [Email], + AD.[Comment] AS [Comment], + AD.[Verdict] AS [Verdict], + AD.[CreationDate] AS [DecidedAt] + FROM [dbo].[AccessDecision] AD + INNER JOIN [dbo].[AccessRequest] LR ON LR.[Id] = AD.[AccessRequestId] + INNER JOIN @CollectionIds CI ON CI.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[User] AU ON AU.[Id] = AD.[ApproverId] + WHERE LR.[Status] <> 0 -- not Pending + AND LR.[CreationDate] >= @Since + ORDER BY AD.[AccessRequestId], AD.[CreationDate] ASC +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_ReadInboxPendingByCollectionIds.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_ReadInboxPendingByCollectionIds.sql new file mode 100644 index 000000000000..3b10c00c9d65 --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_ReadInboxPendingByCollectionIds.sql @@ -0,0 +1,42 @@ +CREATE PROCEDURE [dbo].[AccessRequest_ReadInboxPendingByCollectionIds] + @CollectionIds [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + -- The approver inbox: pending requests for the supplied (caller-manageable) collections, joined with the + -- denormalized display fields the client needs (cipher/collection names, requester identity) so it avoids an N+1. + -- A pending request has not been decided by anyone yet, so it carries no approvers (the caller leaves the + -- request's approvers list empty); only the resolved reads return a second decision result set. + SELECT + LR.[Id], + LR.[ExtensionOfLeaseId], + LR.[OrganizationId], + LR.[CollectionId], + LR.[CipherId], + LR.[RequesterId], + LR.[NotBefore], + LR.[NotAfter], + LR.[Reason], + LR.[Status], + LR.[CreationDate], + LR.[ResolvedDate], + PL.[Id] AS [ProducedLeaseId], + PL.[Status] AS [ProducedLeaseStatus], + JSON_VALUE(C.[Data], '$.Name') AS [CipherName], + COL.[Name] AS [CollectionName], + U.[Name] AS [RequesterName], + U.[Email] AS [RequesterEmail] + FROM [dbo].[AccessRequest] LR + INNER JOIN @CollectionIds CI ON CI.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[Cipher] C ON C.[Id] = LR.[CipherId] + LEFT JOIN [dbo].[Collection] COL ON COL.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[User] U ON U.[Id] = LR.[RequesterId] + OUTER APPLY ( + SELECT TOP 1 L.[Id], L.[Status] + FROM [dbo].[AccessLease] L + WHERE L.[AccessRequestId] = LR.[Id] + ORDER BY L.[CreationDate] DESC + ) PL + WHERE LR.[Status] = 0 -- Pending +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_ReadManyByRequesterId.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_ReadManyByRequesterId.sql new file mode 100644 index 000000000000..6a62b70b201c --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_ReadManyByRequesterId.sql @@ -0,0 +1,53 @@ +CREATE PROCEDURE [dbo].[AccessRequest_ReadManyByRequesterId] + @RequesterId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + -- The caller's own requests, returned as two result sets so the caller can attach each request's decision list + -- without an N+1: + -- 1) the caller's requests (TOP 250 most recent), all statuses. Unlike the approver-inbox reads this is a + -- caller-scoped self-read, so the cipher/collection/requester display-name joins are intentionally omitted + -- (those names come from the caller's local vault, and the requester is the caller). + -- 2) every decision (human or automatic) on the caller's requests, keyed by AccessRequestId and ordered + -- oldest-first; DeciderKind says which, and a human decision's identity is denormalized from [User] -- the + -- requester has no other way to name who decided their request. + SELECT TOP (250) + LR.[Id], + LR.[ExtensionOfLeaseId], + LR.[OrganizationId], + LR.[CollectionId], + LR.[CipherId], + LR.[RequesterId], + LR.[NotBefore], + LR.[NotAfter], + LR.[Reason], + LR.[Status], + LR.[CreationDate], + LR.[ResolvedDate], + PL.[Id] AS [ProducedLeaseId] + FROM [dbo].[AccessRequest] LR + OUTER APPLY ( + SELECT TOP 1 L.[Id] + FROM [dbo].[AccessLease] L + WHERE L.[AccessRequestId] = LR.[Id] + ORDER BY L.[CreationDate] DESC + ) PL + WHERE LR.[RequesterId] = @RequesterId + ORDER BY LR.[CreationDate] DESC + + SELECT + AD.[AccessRequestId], + AD.[DeciderKind] AS [DeciderKind], + AD.[ApproverId] AS [Id], + AU.[Name] AS [Name], + AU.[Email] AS [Email], + AD.[Comment] AS [Comment], + AD.[Verdict] AS [Verdict], + AD.[CreationDate] AS [DecidedAt] + FROM [dbo].[AccessDecision] AD + INNER JOIN [dbo].[AccessRequest] LR ON LR.[Id] = AD.[AccessRequestId] + LEFT JOIN [dbo].[User] AU ON AU.[Id] = AD.[ApproverId] + WHERE LR.[RequesterId] = @RequesterId + ORDER BY AD.[AccessRequestId], AD.[CreationDate] ASC +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_ResolveWithDecision.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_ResolveWithDecision.sql new file mode 100644 index 000000000000..28fda8157b9e --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessRequest_ResolveWithDecision.sql @@ -0,0 +1,39 @@ +CREATE PROCEDURE [dbo].[AccessRequest_ResolveWithDecision] + @AccessRequestId UNIQUEIDENTIFIER, + @Status TINYINT, + @AccessDecisionId UNIQUEIDENTIFIER, + @ApproverId UNIQUEIDENTIFIER, + @Verdict TINYINT, + @Comment NVARCHAR(MAX) = NULL, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- Atomically resolve a pending request and record the human approver's decision. The caller has already verified + -- (and the application enforces) that the request is still Pending; the WHERE guard keeps the write idempotent + -- under a race so a second approver can't move an already-resolved request. + -- + -- Approval does not mint the lease: the requester activates the approved request later via + -- [AccessLease_CreateFromApprovedRequest]. The automatic path ([AccessRequest_CreateAutoApproved]) records the + -- approved request the same way and likewise leaves the lease to be minted at activation. + BEGIN TRANSACTION AccessRequest_Resolve + + UPDATE [dbo].[AccessRequest] + SET [Status] = @Status, + [ResolvedDate] = @Now + WHERE [Id] = @AccessRequestId AND [Status] = 0 -- Pending + + INSERT INTO [dbo].[AccessDecision] + ( + [Id], [AccessRequestId], [DeciderKind], [ApproverId], [ConditionKind], + [Verdict], [Comment], [EvaluationContext], [CreationDate] + ) + VALUES + ( + @AccessDecisionId, @AccessRequestId, 1 /* Human */, @ApproverId, NULL, + @Verdict, @Comment, NULL, @Now + ) + + COMMIT TRANSACTION AccessRequest_Resolve +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessRule_Create.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessRule_Create.sql new file mode 100644 index 000000000000..ef3f8bf7a6de --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessRule_Create.sql @@ -0,0 +1,51 @@ +CREATE PROCEDURE [dbo].[AccessRule_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @Name NVARCHAR(256), + @Description NVARCHAR(MAX) = NULL, + @Conditions NVARCHAR(MAX), + @SingleActiveLease BIT = 0, + @DefaultLeaseDurationSeconds INT = NULL, + @MaxLeaseDurationSeconds INT = NULL, + @Enabled BIT = 1, + @AllowsExtensions BIT = 0, + @MaxExtensionDurationSeconds INT = NULL, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[AccessRule] + ( + [Id], + [OrganizationId], + [Name], + [Description], + [Conditions], + [SingleActiveLease], + [DefaultLeaseDurationSeconds], + [MaxLeaseDurationSeconds], + [Enabled], + [AllowsExtensions], + [MaxExtensionDurationSeconds], + [CreationDate], + [RevisionDate] + ) + VALUES + ( + @Id, + @OrganizationId, + @Name, + @Description, + @Conditions, + @SingleActiveLease, + @DefaultLeaseDurationSeconds, + @MaxLeaseDurationSeconds, + @Enabled, + @AllowsExtensions, + @MaxExtensionDurationSeconds, + @CreationDate, + @RevisionDate + ) +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessRule_DeleteById.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessRule_DeleteById.sql new file mode 100644 index 000000000000..50ba4eb8db4f --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessRule_DeleteById.sql @@ -0,0 +1,32 @@ +CREATE PROCEDURE [dbo].[AccessRule_DeleteById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DECLARE @RevisionDate DATETIME2(7) = SYSUTCDATETIME() + DECLARE @OrganizationId UNIQUEIDENTIFIER + + SELECT @OrganizationId = [OrganizationId] + FROM [dbo].[AccessRule] + WHERE [Id] = @Id + + BEGIN TRANSACTION + + UPDATE + [dbo].[Collection] + SET + [AccessRuleId] = NULL, + [RevisionDate] = @RevisionDate + WHERE + [AccessRuleId] = @Id + + DELETE FROM [dbo].[AccessRule] WHERE [Id] = @Id + + COMMIT TRANSACTION + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId + END +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessRule_ReadById.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessRule_ReadById.sql new file mode 100644 index 000000000000..1851baead921 --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessRule_ReadById.sql @@ -0,0 +1,10 @@ +CREATE PROCEDURE [dbo].[AccessRule_ReadById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT * + FROM [dbo].[AccessRule] + WHERE [Id] = @Id +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessRule_ReadByOrganizationId.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessRule_ReadByOrganizationId.sql new file mode 100644 index 000000000000..60c001940650 --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessRule_ReadByOrganizationId.sql @@ -0,0 +1,10 @@ +CREATE PROCEDURE [dbo].[AccessRule_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT * + FROM [dbo].[AccessRule] + WHERE [OrganizationId] = @OrganizationId +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessRule_ReadDetailsById.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessRule_ReadDetailsById.sql new file mode 100644 index 000000000000..f9ee5eec5992 --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessRule_ReadDetailsById.sql @@ -0,0 +1,14 @@ +CREATE PROCEDURE [dbo].[AccessRule_ReadDetailsById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT * + FROM [dbo].[AccessRule] + WHERE [Id] = @Id + + SELECT [Id] + FROM [dbo].[Collection] + WHERE [AccessRuleId] = @Id +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessRule_ReadDetailsByOrganizationId.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessRule_ReadDetailsByOrganizationId.sql new file mode 100644 index 000000000000..6a4cb6d4bd40 --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessRule_ReadDetailsByOrganizationId.sql @@ -0,0 +1,17 @@ +CREATE PROCEDURE [dbo].[AccessRule_ReadDetailsByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT * + FROM [dbo].[AccessRule] + WHERE [OrganizationId] = @OrganizationId + + SELECT + [AccessRuleId], + [Id] AS [CollectionId] + FROM [dbo].[Collection] + WHERE [OrganizationId] = @OrganizationId + AND [AccessRuleId] IS NOT NULL +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/AccessRule_Update.sql b/src/Sql/dbo/Pam/Stored Procedures/AccessRule_Update.sql new file mode 100644 index 000000000000..787d0edac456 --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/AccessRule_Update.sql @@ -0,0 +1,36 @@ +CREATE PROCEDURE [dbo].[AccessRule_Update] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Name NVARCHAR(256), + @Description NVARCHAR(MAX) = NULL, + @Conditions NVARCHAR(MAX), + @SingleActiveLease BIT = 0, + @DefaultLeaseDurationSeconds INT = NULL, + @MaxLeaseDurationSeconds INT = NULL, + @Enabled BIT = 1, + @AllowsExtensions BIT = 0, + @MaxExtensionDurationSeconds INT = NULL, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[AccessRule] + SET + [OrganizationId] = @OrganizationId, + [Name] = @Name, + [Description] = @Description, + [Conditions] = @Conditions, + [SingleActiveLease] = @SingleActiveLease, + [DefaultLeaseDurationSeconds] = @DefaultLeaseDurationSeconds, + [MaxLeaseDurationSeconds] = @MaxLeaseDurationSeconds, + [Enabled] = @Enabled, + [AllowsExtensions] = @AllowsExtensions, + [MaxExtensionDurationSeconds] = @MaxExtensionDurationSeconds, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate + WHERE + [Id] = @Id +END diff --git a/src/Sql/dbo/Pam/Stored Procedures/Collection_SetAccessRuleAssociations.sql b/src/Sql/dbo/Pam/Stored Procedures/Collection_SetAccessRuleAssociations.sql new file mode 100644 index 000000000000..9a52130f076b --- /dev/null +++ b/src/Sql/dbo/Pam/Stored Procedures/Collection_SetAccessRuleAssociations.sql @@ -0,0 +1,42 @@ +CREATE PROCEDURE [dbo].[Collection_SetAccessRuleAssociations] + @AccessRuleId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @ToAssign AS [dbo].[GuidIdArray] READONLY, + @ToClear AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + DECLARE @RevisionDate DATETIME2(7) = SYSUTCDATETIME() + + BEGIN TRANSACTION + + UPDATE + C + SET + C.[AccessRuleId] = NULL, + C.[RevisionDate] = @RevisionDate + FROM + [dbo].[Collection] C + INNER JOIN + @ToClear T ON T.[Id] = C.[Id] + WHERE + C.[OrganizationId] = @OrganizationId + AND C.[AccessRuleId] = @AccessRuleId + + UPDATE + C + SET + C.[AccessRuleId] = @AccessRuleId, + C.[RevisionDate] = @RevisionDate + FROM + [dbo].[Collection] C + INNER JOIN + @ToAssign T ON T.[Id] = C.[Id] + WHERE + C.[OrganizationId] = @OrganizationId + + COMMIT TRANSACTION + + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId +END diff --git a/src/Sql/dbo/Pam/Tables/AccessDecision.sql b/src/Sql/dbo/Pam/Tables/AccessDecision.sql new file mode 100644 index 000000000000..9ccfce831f92 --- /dev/null +++ b/src/Sql/dbo/Pam/Tables/AccessDecision.sql @@ -0,0 +1,18 @@ +CREATE TABLE [dbo].[AccessDecision] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [AccessRequestId] UNIQUEIDENTIFIER NOT NULL, + [DeciderKind] TINYINT NOT NULL, + [ApproverId] UNIQUEIDENTIFIER NULL, + [ConditionKind] TINYINT NULL, + [Verdict] TINYINT NOT NULL, + [Comment] NVARCHAR(MAX) NULL, + [EvaluationContext] NVARCHAR(MAX) NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + CONSTRAINT [PK_AccessDecision] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_AccessDecision_AccessRequest] FOREIGN KEY ([AccessRequestId]) REFERENCES [dbo].[AccessRequest] ([Id]) ON DELETE CASCADE +); +GO + +CREATE NONCLUSTERED INDEX [IX_AccessDecision_AccessRequestId] + ON [dbo].[AccessDecision] ([AccessRequestId] ASC); +GO diff --git a/src/Sql/dbo/Pam/Tables/AccessLease.sql b/src/Sql/dbo/Pam/Tables/AccessLease.sql new file mode 100644 index 000000000000..0b81fe226296 --- /dev/null +++ b/src/Sql/dbo/Pam/Tables/AccessLease.sql @@ -0,0 +1,38 @@ +CREATE TABLE [dbo].[AccessLease] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [AccessRequestId] UNIQUEIDENTIFIER NOT NULL, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [CollectionId] UNIQUEIDENTIFIER NOT NULL, + [CipherId] UNIQUEIDENTIFIER NOT NULL, + [RequesterId] UNIQUEIDENTIFIER NOT NULL, + [Status] TINYINT NOT NULL, + [NotBefore] DATETIME2 (7) NOT NULL, + [NotAfter] DATETIME2 (7) NOT NULL, + [RevokedDate] DATETIME2 (7) NULL, + [RevokedBy] UNIQUEIDENTIFIER NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + CONSTRAINT [PK_AccessLease] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_AccessLease_AccessRequest] FOREIGN KEY ([AccessRequestId]) REFERENCES [dbo].[AccessRequest] ([Id]), + CONSTRAINT [FK_AccessLease_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE +); +GO + +CREATE NONCLUSTERED INDEX [IX_AccessLease_RequesterId_CipherId_Status] + ON [dbo].[AccessLease] ([RequesterId] ASC, [CipherId] ASC, [Status] ASC); +GO + +CREATE NONCLUSTERED INDEX [IX_AccessLease_NotAfter_Status] + ON [dbo].[AccessLease] ([NotAfter] ASC, [Status] ASC); +GO + +-- Supports the governance lease lists (AccessLease_ReadManyActiveByCollectionIds / +-- AccessLease_ReadManyEndedByCollectionIds), which filter by the caller's manageable collection ids. +CREATE NONCLUSTERED INDEX [IX_AccessLease_CollectionId_Status] + ON [dbo].[AccessLease] ([CollectionId] ASC, [Status] ASC); +GO + +-- A request produces at most one lease, ever: activating an approved request and the automatic path each insert +-- exactly one. Unique to backstop racing activations that pass the application-level checks simultaneously. +CREATE UNIQUE NONCLUSTERED INDEX [IX_AccessLease_AccessRequestId] + ON [dbo].[AccessLease] ([AccessRequestId] ASC); +GO diff --git a/src/Sql/dbo/Pam/Tables/AccessRequest.sql b/src/Sql/dbo/Pam/Tables/AccessRequest.sql new file mode 100644 index 000000000000..57b92eff3e57 --- /dev/null +++ b/src/Sql/dbo/Pam/Tables/AccessRequest.sql @@ -0,0 +1,26 @@ +CREATE TABLE [dbo].[AccessRequest] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [ExtensionOfLeaseId] UNIQUEIDENTIFIER NULL, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [CollectionId] UNIQUEIDENTIFIER NOT NULL, + [CipherId] UNIQUEIDENTIFIER NOT NULL, + [RequesterId] UNIQUEIDENTIFIER NOT NULL, + [NotBefore] DATETIME2 (7) NOT NULL, + [NotAfter] DATETIME2 (7) NOT NULL, + [Reason] NVARCHAR(MAX) NULL, + [Status] TINYINT NOT NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + [ResolvedDate] DATETIME2 (7) NULL, + CONSTRAINT [PK_AccessRequest] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_AccessRequest_AccessLease] FOREIGN KEY ([ExtensionOfLeaseId]) REFERENCES [dbo].[AccessLease] ([Id]), + CONSTRAINT [FK_AccessRequest_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE +); +GO + +CREATE NONCLUSTERED INDEX [IX_AccessRequest_RequesterId_CipherId_Status] + ON [dbo].[AccessRequest] ([RequesterId] ASC, [CipherId] ASC, [Status] ASC); +GO + +CREATE NONCLUSTERED INDEX [IX_AccessRequest_OrganizationId_Status] + ON [dbo].[AccessRequest] ([OrganizationId] ASC, [Status] ASC); +GO diff --git a/src/Sql/dbo/Pam/Tables/AccessRule.sql b/src/Sql/dbo/Pam/Tables/AccessRule.sql new file mode 100644 index 000000000000..4c636e86df32 --- /dev/null +++ b/src/Sql/dbo/Pam/Tables/AccessRule.sql @@ -0,0 +1,23 @@ +CREATE TABLE [dbo].[AccessRule] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [Name] NVARCHAR(256) NOT NULL, + [Description] NVARCHAR(MAX) NULL, + [Conditions] NVARCHAR(MAX) NOT NULL, + [SingleActiveLease] BIT NOT NULL CONSTRAINT [DF_AccessRule_SingleActiveLease] DEFAULT (0), + [DefaultLeaseDurationSeconds] INT NULL, + [MaxLeaseDurationSeconds] INT NULL, + [Enabled] BIT NOT NULL CONSTRAINT [DF_AccessRule_Enabled] DEFAULT (1), + [AllowsExtensions] BIT NOT NULL CONSTRAINT [DF_AccessRule_AllowsExtensions] DEFAULT (0), + [MaxExtensionDurationSeconds] INT NULL, + [CreationDate] DATETIME2(7) NOT NULL, + [RevisionDate] DATETIME2(7) NOT NULL, + CONSTRAINT [PK_AccessRule] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_AccessRule_Organization] FOREIGN KEY ([OrganizationId]) + REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE +); +GO + +CREATE UNIQUE NONCLUSTERED INDEX [IX_AccessRule_OrganizationId_Name] + ON [dbo].[AccessRule] ([OrganizationId] ASC, [Name] ASC); +GO diff --git a/test/Admin.Test/packages.lock.json b/test/Admin.Test/packages.lock.json index f0b42fba5bf1..4cd83634c541 100644 --- a/test/Admin.Test/packages.lock.json +++ b/test/Admin.Test/packages.lock.json @@ -1607,11 +1607,11 @@ "admin": { "type": "Project", "dependencies": { - "Commercial.Core": "[2026.6.0, )", - "Commercial.Infrastructure.EntityFramework": "[2026.6.0, )", - "Core": "[2026.6.0, )", - "Migrator": "[2026.6.0, )", - "MySqlMigrations": "[2026.6.0, )", + "Commercial.Core": "[2026.6.1, )", + "Commercial.Infrastructure.EntityFramework": "[2026.6.1, )", + "Core": "[2026.6.1, )", + "Migrator": "[2026.6.1, )", + "MySqlMigrations": "[2026.6.1, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", @@ -1619,15 +1619,15 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "PostgresMigrations": "[2026.6.0, )", - "SharedWeb": "[2026.6.0, )", - "SqliteMigrations": "[2026.6.0, )" + "PostgresMigrations": "[2026.6.1, )", + "SharedWeb": "[2026.6.1, )", + "SqliteMigrations": "[2026.6.1, )" } }, "commercial.core": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "CsvHelper": "[33.1.0, 33.1.0]" } }, @@ -1635,8 +1635,8 @@ "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" } }, "common": { @@ -1644,7 +1644,7 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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, )", @@ -1668,6 +1668,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1692,6 +1693,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1705,22 +1707,27 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" @@ -1729,7 +1736,7 @@ "migrator": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "Microsoft.Extensions.Logging": "[10.0.8, 10.0.8]", "dbup-sqlserver": "[7.2.0, 7.2.0]" } @@ -1737,23 +1744,29 @@ "mysqlmigrations": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" + } + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" } }, "postgresmigrations": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" } }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } @@ -1761,8 +1774,8 @@ "sqlitemigrations": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" } } } diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationInviteLinksControllerTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationInviteLinksControllerTests.cs index 385577bcd968..2f6e79fa3813 100644 --- a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationInviteLinksControllerTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationInviteLinksControllerTests.cs @@ -3,7 +3,6 @@ using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.Helpers; -using Bit.Api.Models.Response; using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerBulkAutoConfirmTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerBulkAutoConfirmTests.cs index 7e55f9502ebb..f50202baec8f 100644 --- a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerBulkAutoConfirmTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerBulkAutoConfirmTests.cs @@ -3,7 +3,6 @@ using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.Helpers; -using Bit.Api.Models.Response; using Bit.Core; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Repositories; diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerBulkRevokeTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerBulkRevokeTests.cs index 3af91a76b024..5e80755eb510 100644 --- a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerBulkRevokeTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerBulkRevokeTests.cs @@ -3,7 +3,6 @@ using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.Helpers; -using Bit.Api.Models.Response; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; @@ -13,6 +12,7 @@ using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Repositories; +using Bit.SharedWeb.Models.Response; using Xunit; namespace Bit.Api.IntegrationTest.AdminConsole.Controllers; diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs index 343178e7a22c..663f85c73e2b 100644 --- a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs @@ -4,7 +4,6 @@ using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.Helpers; using Bit.Api.Models.Request; -using Bit.Api.Models.Response; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; using Bit.Core.AdminConsole.Repositories; @@ -13,6 +12,7 @@ using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Repositories; +using Bit.SharedWeb.Models.Response; using Xunit; namespace Bit.Api.IntegrationTest.AdminConsole.Controllers; diff --git a/test/Api.IntegrationTest/NotificationCenter/Controllers/NotificationsControllerTests.cs b/test/Api.IntegrationTest/NotificationCenter/Controllers/NotificationsControllerTests.cs index ca04c9775d8e..1d540b82da25 100644 --- a/test/Api.IntegrationTest/NotificationCenter/Controllers/NotificationsControllerTests.cs +++ b/test/Api.IntegrationTest/NotificationCenter/Controllers/NotificationsControllerTests.cs @@ -1,7 +1,6 @@ using System.Net; using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.Helpers; -using Bit.Api.Models.Response; using Bit.Api.NotificationCenter.Models.Response; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; @@ -12,6 +11,7 @@ using Bit.Core.NotificationCenter.Enums; using Bit.Core.NotificationCenter.Repositories; using Bit.Core.Repositories; +using Bit.SharedWeb.Models.Response; using Xunit; namespace Bit.Api.IntegrationTest.NotificationCenter.Controllers; diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs index 77614574c178..415222cc6d2d 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs @@ -2,7 +2,6 @@ using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.SecretsManager.Enums; using Bit.Api.IntegrationTest.SecretsManager.Helpers; -using Bit.Api.Models.Response; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; using Bit.Core.AdminConsole.Entities; @@ -11,6 +10,7 @@ using Bit.Core.Enums; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; +using Bit.SharedWeb.Models.Response; using Xunit; namespace Bit.Api.IntegrationTest.SecretsManager.Controllers; diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTests.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTests.cs index 099dde512770..67280b1e2df7 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTests.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTests.cs @@ -2,13 +2,13 @@ using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.SecretsManager.Enums; using Bit.Api.IntegrationTest.SecretsManager.Helpers; -using Bit.Api.Models.Response; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; using Bit.Core.AdminConsole.Entities; using Bit.Core.Enums; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; +using Bit.SharedWeb.Models.Response; using Bit.Test.Common.Helpers; using Xunit; diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/SecretVersionsControllerTests.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/SecretVersionsControllerTests.cs index 9393795e551f..fe9e07b39176 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/SecretVersionsControllerTests.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/SecretVersionsControllerTests.cs @@ -2,12 +2,12 @@ using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.SecretsManager.Enums; using Bit.Api.IntegrationTest.SecretsManager.Helpers; -using Bit.Api.Models.Response; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; using Bit.Core.Enums; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; +using Bit.SharedWeb.Models.Response; using Xunit; namespace Bit.Api.IntegrationTest.SecretsManager.Controllers; diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsControllerTests.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsControllerTests.cs index be95c0dc1ece..3450fa28351d 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsControllerTests.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsControllerTests.cs @@ -2,13 +2,13 @@ using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.SecretsManager.Enums; using Bit.Api.IntegrationTest.SecretsManager.Helpers; -using Bit.Api.Models.Response; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; +using Bit.SharedWeb.Models.Response; using Bit.Test.Common.Helpers; using Xunit; diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/ServiceAccountsControllerTests.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/ServiceAccountsControllerTests.cs index 6884de5b2665..ffd9092e56c8 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/ServiceAccountsControllerTests.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/ServiceAccountsControllerTests.cs @@ -3,13 +3,13 @@ using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.SecretsManager.Enums; using Bit.Api.IntegrationTest.SecretsManager.Helpers; -using Bit.Api.Models.Response; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; using Bit.Core.AdminConsole.Entities; using Bit.Core.Enums; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; +using Bit.SharedWeb.Models.Response; using Bit.Test.Common.Helpers; using Xunit; diff --git a/test/Api.IntegrationTest/packages.lock.json b/test/Api.IntegrationTest/packages.lock.json index 6443db448851..98ed19b0cdba 100644 --- a/test/Api.IntegrationTest/packages.lock.json +++ b/test/Api.IntegrationTest/packages.lock.json @@ -1349,9 +1349,10 @@ "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]", - "Commercial.Core": "[2026.6.0, )", - "Commercial.Infrastructure.EntityFramework": "[2026.6.0, )", - "Core": "[2026.6.0, )", + "Commercial.Core": "[2026.6.1, )", + "Commercial.Infrastructure.EntityFramework": "[2026.6.1, )", + "Commercial.Pam": "[2026.6.1, )", + "Core": "[2026.6.1, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", @@ -1359,14 +1360,15 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )", + "Pam.Domain": "[2026.6.1, )", + "SharedWeb": "[2026.6.1, )", "Swashbuckle.AspNetCore": "[10.1.7, 10.1.7]" } }, "commercial.core": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "CsvHelper": "[33.1.0, 33.1.0]" } }, @@ -1374,8 +1376,16 @@ "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" + } + }, + "commercial.pam": { + "type": "Project", + "dependencies": { + "Core": "[2026.6.1, )", + "Pam.Domain": "[2026.6.1, )", + "SharedWeb": "[2026.6.1, )" } }, "common": { @@ -1383,7 +1393,7 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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, )", @@ -1407,6 +1417,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1431,6 +1442,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1444,10 +1456,13 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "identity": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", @@ -1455,25 +1470,27 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )" + "SharedWeb": "[2026.6.1, )" } }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" @@ -1482,21 +1499,27 @@ "integrationtestcommon": { "type": "Project", "dependencies": { - "Common": "[2026.6.0, )", - "Identity": "[2026.6.0, )", + "Common": "[2026.6.1, )", + "Identity": "[2026.6.1, )", "Microsoft.AspNetCore.Mvc.Testing": "[10.0.8, 10.0.8]", - "Migrator": "[2026.6.0, )", - "Seeder": "[2026.6.0, )" + "Migrator": "[2026.6.1, )", + "Seeder": "[2026.6.1, )" } }, "migrator": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "Microsoft.Extensions.Logging": "[10.0.8, 10.0.8]", "dbup-sqlserver": "[7.2.0, 7.2.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "rustsdk": { "type": "Project" }, @@ -1504,18 +1527,18 @@ "type": "Project", "dependencies": { "Bogus": "[35.6.5, 35.6.5]", - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", - "RustSdk": "[2026.6.0, )", - "SharedWeb": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", + "RustSdk": "[2026.6.1, )", + "SharedWeb": "[2026.6.1, )" } }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationDomainControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationDomainControllerTests.cs index 738224727ac6..32495c85cc48 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationDomainControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationDomainControllerTests.cs @@ -2,7 +2,6 @@ using Bit.Api.AdminConsole.Models.Request; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; -using Bit.Api.Models.Response; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.Context; @@ -10,6 +9,7 @@ using Bit.Core.Exceptions; using Bit.Core.Models.Data.Organizations; using Bit.Core.Repositories; +using Bit.SharedWeb.Models.Response; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationInviteLinksControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationInviteLinksControllerTests.cs index f89c7b4cda1c..49ef1f4f1a0b 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationInviteLinksControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationInviteLinksControllerTests.cs @@ -1,7 +1,6 @@ using Bit.Api.AdminConsole.Controllers; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; -using Bit.Api.Models.Response; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.OrganizationFeatures.InviteLinks; using Bit.Core.AdminConsole.OrganizationFeatures.InviteLinks.Interfaces; diff --git a/test/Api.Test/Api.Test.csproj b/test/Api.Test/Api.Test.csproj index da9cdcff060d..1887e11c05b5 100644 --- a/test/Api.Test/Api.Test.csproj +++ b/test/Api.Test/Api.Test.csproj @@ -24,6 +24,7 @@ + diff --git a/test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs b/test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs index ca9613f09997..84d5b8b1b00b 100644 --- a/test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs @@ -1,7 +1,6 @@ using System.Security.Claims; using Bit.Api.Auth.Controllers; using Bit.Api.Auth.Models.Response; -using Bit.Api.Models.Response; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Api.Request.AuthRequest; @@ -12,6 +11,7 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; +using Bit.SharedWeb.Models.Response; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; diff --git a/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs b/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs index bed483f83af0..37c2a6eac1fb 100644 --- a/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs @@ -1,5 +1,4 @@ using Bit.Api.Controllers; -using Bit.Api.Models.Response; using Bit.Core.Auth.Models.Api.Response; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.UserFeatures.DeviceTrust; @@ -8,6 +7,7 @@ using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.SharedWeb.Models.Response; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; diff --git a/test/Api.Test/Auth/Controllers/EmergencyAccessControllerTests.cs b/test/Api.Test/Auth/Controllers/EmergencyAccessControllerTests.cs index e5cacb3f163c..283f7531af7d 100644 --- a/test/Api.Test/Auth/Controllers/EmergencyAccessControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/EmergencyAccessControllerTests.cs @@ -2,7 +2,6 @@ using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Auth.Controllers; using Bit.Api.Auth.Models.Response; -using Bit.Api.Models.Response; using Bit.Api.Vault.Models.Response; using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; @@ -14,6 +13,7 @@ using Bit.Core.Services; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Models.Data; +using Bit.SharedWeb.Models.Response; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -23,6 +23,7 @@ namespace Bit.Api.Test.Auth.Controllers; [ControllerCustomize(typeof(EmergencyAccessController))] [SutProviderCustomize] +[Bit.Api.Test.Vault.AutoFixture.CipherLeaseGateBypassCustomize] public class EmergencyAccessControllerTests { [Theory, BitAutoData] diff --git a/test/Api.Test/Tools/Controllers/SendsControllerTests.cs b/test/Api.Test/Tools/Controllers/SendsControllerTests.cs index 07c2d8a65602..d26b3ee14537 100644 --- a/test/Api.Test/Tools/Controllers/SendsControllerTests.cs +++ b/test/Api.Test/Tools/Controllers/SendsControllerTests.cs @@ -2,7 +2,6 @@ using System.Text; using System.Text.Json; using AutoFixture.Xunit2; -using Bit.Api.Models.Response; using Bit.Api.Tools.Controllers; using Bit.Api.Tools.Models; using Bit.Api.Tools.Models.Request; @@ -22,6 +21,7 @@ using Bit.Core.Tools.SendFeatures.Queries.Interfaces; using Bit.Core.Tools.Services; using Bit.Core.Utilities; +using Bit.SharedWeb.Models.Response; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; diff --git a/test/Api.Test/Vault/AutoFixture/CipherLeaseGateBypassCustomization.cs b/test/Api.Test/Vault/AutoFixture/CipherLeaseGateBypassCustomization.cs new file mode 100644 index 000000000000..92c1f1599b9d --- /dev/null +++ b/test/Api.Test/Vault/AutoFixture/CipherLeaseGateBypassCustomization.cs @@ -0,0 +1,42 @@ +using AutoFixture; +using Bit.Core.Entities; +using Bit.Core.Models.Data; +using Bit.Core.Vault.Authorization; +using Bit.Core.Vault.Entities; +using Bit.Pam.Services; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; + +namespace Bit.Api.Test.Vault.AutoFixture; + +/// +/// Injects an substitute pre-configured to authorize full data for every +/// cipher — the flag-off / not-gated behaviour. This lets leasing-agnostic controller tests assert their +/// existing full-data expectations without each one having to stub the gate. Tests that exercise gating +/// re-stub the dependency (e.g. make AuthorizeReadAsync return null) after building the SUT. +/// +public class CipherLeaseGateBypassCustomization : ICustomization +{ + public void Customize(IFixture fixture) + { + var gate = Substitute.For(); + var unrestricted = FullCipherAccess.Unrestricted(); + + gate.Unrestricted().Returns(unrestricted); + gate.AuthorizeReadAsync(Arg.Any(), Arg.Any()).Returns(unrestricted); + gate.AuthorizeReadManyAsync(Arg.Any(), Arg.Any>()).Returns(unrestricted); + gate.AuthorizeReadManyAsync(Arg.Any(), Arg.Any>(), + Arg.Any>(), + Arg.Any>>()) + .Returns(unrestricted); + gate.EnsureCanMutateAsync(Arg.Any(), Arg.Any()).Returns(unrestricted); + gate.EnsureCanMutateManyAsync(Arg.Any(), Arg.Any>()).Returns(unrestricted); + + fixture.Inject(gate); + } +} + +public class CipherLeaseGateBypassCustomizeAttribute : BitCustomizeAttribute +{ + public override ICustomization GetCustomization() => new CipherLeaseGateBypassCustomization(); +} diff --git a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs index 849036bec930..2141e6c49770 100644 --- a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs @@ -1,6 +1,7 @@ using System.Security.Claims; using System.Text.Json; using Bit.Api.Auth.Models.Request.Accounts; +using Bit.Api.Test.Vault.AutoFixture; using Bit.Api.Vault.Controllers; using Bit.Api.Vault.Models; using Bit.Api.Vault.Models.Request; @@ -30,6 +31,7 @@ namespace Bit.Api.Test.Controllers; [ControllerCustomize(typeof(CiphersController))] [SutProviderCustomize] +[CipherLeaseGateBypassCustomize] public class CiphersControllerTests { [Theory, BitAutoData] @@ -1177,7 +1179,7 @@ public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithManagePermission_Restores var result = await sutProvider.Sut.PutRestoreAdmin(cipherOrgDetails.Id); - Assert.IsType(result); + Assert.IsAssignableFrom(result); await sutProvider.GetDependency().Received(1).RestoreAsync(Arg.Is( (cd) => cd.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true); } @@ -1246,7 +1248,7 @@ public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithAccessToUnassignedCipher_ var result = await sutProvider.Sut.PutRestoreAdmin(cipherOrgDetails.Id); - Assert.IsType(result); + Assert.IsAssignableFrom(result); await sutProvider.GetDependency().Received(1).RestoreAsync(Arg.Is( (cd) => cd.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true); } @@ -1275,7 +1277,7 @@ public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithAccessToAllCollectionItem var result = await sutProvider.Sut.PutRestoreAdmin(cipherOrgDetails.Id); - Assert.IsType(result); + Assert.IsAssignableFrom(result); await sutProvider.GetDependency().Received(1).RestoreAsync(Arg.Is( (cd) => cd.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true); } @@ -1299,7 +1301,7 @@ public async Task PutRestoreAdmin_WithCustomUser_WithEditAnyCollectionTrue_Resto var result = await sutProvider.Sut.PutRestoreAdmin(cipherOrgDetails.Id); - Assert.IsType(result); + Assert.IsAssignableFrom(result); await sutProvider.GetDependency().Received(1).RestoreAsync(Arg.Is( (cd) => cd.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true); } @@ -1339,7 +1341,7 @@ public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithEditPermission_LimitItemD var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id); - Assert.IsType(result); + Assert.IsAssignableFrom(result); await sutProvider.GetDependency().Received(1).RestoreAsync(Arg.Is( (cd) => cd.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true); } diff --git a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs index 4d3458b2f08f..5e6267e6e761 100644 --- a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs @@ -17,16 +17,19 @@ using Bit.Core.KeyManagement.Models.Data; using Bit.Core.KeyManagement.Queries.Interfaces; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Test.Billing.Mocks; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Repositories; +using Bit.Core.Vault.Authorization; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Enums; using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Repositories; +using Bit.Pam.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -37,6 +40,7 @@ namespace Bit.Api.Test.Controllers; [ControllerCustomize(typeof(SyncController))] [SutProviderCustomize] +[Bit.Api.Test.Vault.AutoFixture.CipherLeaseGateBypassCustomize] public class SyncControllerTests { [Theory] @@ -695,6 +699,172 @@ public async Task Get_PoliciesInAcceptedState_FlagDisabled_DoesNotCallNewReposit Assert.Null(result.Profile.OrganizationsNew); } + [Theory] + [BitAutoData] + public async Task Get_Leasing_FlagEnabled_CipherOnlyInLeasingCollection_ReturnsPartialData( + User user, SutProvider sutProvider) + { + var orgId = Guid.NewGuid(); + var leasingCollectionId = Guid.NewGuid(); + var cipherId = Guid.NewGuid(); + + var cipher = new CipherDetails + { + Id = cipherId, + Type = CipherType.Login, + OrganizationId = orgId, + Data = JsonSerializer.Serialize(new CipherLoginData + { + Name = "2.name|encrypted", + Username = "2.SENTINEL-username|encrypted", + Password = "2.SENTINEL-password|encrypted", + Uris = new[] { new CipherLoginData.CipherLoginUriData { Uri = "2.uri|encrypted" } }, + }), + }; + + SetupLeasingSync(sutProvider, user, orgId, + ciphers: new List { cipher }, + collections: new List { new() { Id = leasingCollectionId, OrganizationId = orgId, AccessRuleId = Guid.NewGuid() } }, + collectionCiphers: new List { new() { CipherId = cipherId, CollectionId = leasingCollectionId } }, + pamFlagEnabled: true); + // The gate withholds this leasing-gated cipher: its witness authorizes nothing, so sync delivers partial data. + sutProvider.GetDependency() + .AuthorizeReadManyAsync(Arg.Any(), Arg.Any>(), + Arg.Any>(), + Arg.Any>>()) + .Returns(FullCipherAccess.ForCiphers(Array.Empty())); + + var result = await sutProvider.Sut.Get(); + + var synced = Assert.Single(result.Ciphers, c => c.Id == cipherId); + Assert.Null(synced.Data); + Assert.NotNull(synced.PartialData); + Assert.DoesNotContain("SENTINEL", synced.PartialData); + Assert.Contains("2.name|encrypted", synced.PartialData); + Assert.Contains("2.uri|encrypted", synced.PartialData); + } + + [Theory] + [BitAutoData] + public async Task Get_Leasing_FlagEnabled_CipherInMixedCollections_ReturnsFullData( + User user, SutProvider sutProvider) + { + var orgId = Guid.NewGuid(); + var leasingCollectionId = Guid.NewGuid(); + var normalCollectionId = Guid.NewGuid(); + var cipherId = Guid.NewGuid(); + + var cipher = new CipherDetails + { + Id = cipherId, + Type = CipherType.Login, + OrganizationId = orgId, + Data = JsonSerializer.Serialize(new CipherLoginData + { + Name = "2.name|encrypted", + Username = "2.SENTINEL-username|encrypted", + }), + }; + + SetupLeasingSync(sutProvider, user, orgId, + ciphers: new List { cipher }, + collections: new List + { + new() { Id = leasingCollectionId, OrganizationId = orgId, AccessRuleId = Guid.NewGuid() }, + new() { Id = normalCollectionId, OrganizationId = orgId, AccessRuleId = null }, + }, + collectionCiphers: new List + { + new() { CipherId = cipherId, CollectionId = leasingCollectionId }, + new() { CipherId = cipherId, CollectionId = normalCollectionId }, + }, + pamFlagEnabled: true); + + var result = await sutProvider.Sut.Get(); + + var synced = Assert.Single(result.Ciphers, c => c.Id == cipherId); + Assert.Contains("SENTINEL-username|encrypted", synced.Data); + Assert.Null(synced.PartialData); + } + + [Theory] + [BitAutoData] + public async Task Get_Leasing_FlagDisabled_CipherOnlyInLeasingCollection_ReturnsFullData( + User user, SutProvider sutProvider) + { + var orgId = Guid.NewGuid(); + var leasingCollectionId = Guid.NewGuid(); + var cipherId = Guid.NewGuid(); + + var cipher = new CipherDetails + { + Id = cipherId, + Type = CipherType.Login, + OrganizationId = orgId, + Data = JsonSerializer.Serialize(new CipherLoginData + { + Name = "2.name|encrypted", + Username = "2.SENTINEL-username|encrypted", + }), + }; + + SetupLeasingSync(sutProvider, user, orgId, + ciphers: new List { cipher }, + collections: new List { new() { Id = leasingCollectionId, OrganizationId = orgId, AccessRuleId = Guid.NewGuid() } }, + collectionCiphers: new List { new() { CipherId = cipherId, CollectionId = leasingCollectionId } }, + pamFlagEnabled: false); + + var result = await sutProvider.Sut.Get(); + + var synced = Assert.Single(result.Ciphers, c => c.Id == cipherId); + Assert.Contains("SENTINEL-username|encrypted", synced.Data); + Assert.Null(synced.PartialData); + } + + private static void SetupLeasingSync( + SutProvider sutProvider, + User user, + Guid orgId, + List ciphers, + List collections, + List collectionCiphers, + bool pamFlagEnabled) + { + user.EquivalentDomains = null; + user.ExcludedGlobalEquivalentDomains = null; + + var userService = sutProvider.GetDependency(); + userService.GetUserByPrincipalAsync(Arg.Any()).ReturnsForAnyArgs(user); + userService.HasPremiumFromOrganization(user).Returns(false); + + sutProvider.GetDependency().Run(user).Returns(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(), + SignatureKeyPairData = null, + }); + + var enabledOrg = new Fixture().Create(); + enabledOrg.Enabled = true; + enabledOrg.OrganizationId = orgId; + sutProvider.GetDependency() + .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed) + .Returns(new List { enabledOrg }); + + sutProvider.GetDependency() + .GetManyByUserIdAsync(user.Id, Arg.Any()).Returns(ciphers); + sutProvider.GetDependency() + .GetManyByUserIdAsync(user.Id).Returns(collections); + sutProvider.GetDependency() + .GetManyByUserIdAsync(user.Id).Returns(collectionCiphers); + + sutProvider.GetDependency() + .GetOrganizationAbilitiesAsync(Arg.Any>()) + .Returns(new Dictionary { { orgId, new OrganizationAbility { Id = orgId } } }); + + sutProvider.GetDependency().TwoFactorIsEnabledAsync(user).Returns(false); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.Pam).Returns(pamFlagEnabled); + } + private async Task AssertMethodsCalledAsync(IUserService userService, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IOrganizationUserRepository organizationUserRepository, diff --git a/test/Api.Test/Vault/Models/Response/CipherLeaseFilterEnforcementTests.cs b/test/Api.Test/Vault/Models/Response/CipherLeaseFilterEnforcementTests.cs new file mode 100644 index 000000000000..b9e60dbabef0 --- /dev/null +++ b/test/Api.Test/Vault/Models/Response/CipherLeaseFilterEnforcementTests.cs @@ -0,0 +1,76 @@ +using System.Reflection; +using Bit.Api.Vault.Models.Response; +using Bit.Core.Vault.Authorization; +using Xunit; + +namespace Bit.Api.Test.Vault.Models.Response; + +/// +/// Architecture (fitness) tests for PAM credential-leasing filtering. The primary guarantee is enforced +/// by the type system: secret data on a cipher response can only be populated through a +/// witness that the leasing gate alone mints. These tests lock that +/// invariant in place so a future change that re-opens a public setter — or makes the witness publicly +/// mintable — fails CI rather than silently leaking secrets. +/// +public class CipherLeaseFilterEnforcementTests +{ + // The secret members carried by the cipher response models. None may be settable by external code + // (public setter / object initializer); they are populated only inside the witness-gated full path. + public static readonly TheoryData SecretProperties = new() + { + nameof(CipherMiniResponseModel.Data), + nameof(CipherMiniResponseModel.Name), + nameof(CipherMiniResponseModel.Notes), + nameof(CipherMiniResponseModel.Login), + nameof(CipherMiniResponseModel.Card), + nameof(CipherMiniResponseModel.Identity), + nameof(CipherMiniResponseModel.SecureNote), + nameof(CipherMiniResponseModel.SSHKey), + nameof(CipherMiniResponseModel.BankAccount), + nameof(CipherMiniResponseModel.DriversLicense), + nameof(CipherMiniResponseModel.Passport), + nameof(CipherMiniResponseModel.Fields), + nameof(CipherMiniResponseModel.PasswordHistory), + }; + + [Theory] + [MemberData(nameof(SecretProperties))] + public void SecretProperties_HaveNoPublicSetter(string propertyName) + { + var property = typeof(CipherMiniResponseModel).GetProperty(propertyName); + + Assert.NotNull(property); + var setter = property!.SetMethod; + Assert.True(setter is null || !setter.IsPublic, + $"{propertyName} must not have a public setter — secret data may only be populated through the " + + "witness-gated full-data path, never via a public constructor or object initializer."); + } + + [Fact] + public void FullCipherAccess_CannotBeMintedByPublicApi() + { + // The witness has no public constructor; only the leasing gate (in Bit.Core) mints one via the + // internal factory methods. This keeps emitting full secret data a deliberate, gate-mediated act. + var publicConstructors = typeof(FullCipherAccess) + .GetConstructors(BindingFlags.Public | BindingFlags.Instance); + + Assert.Empty(publicConstructors); + + var publicFactories = typeof(FullCipherAccess) + .GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(m => m.ReturnType == typeof(FullCipherAccess)); + + Assert.Empty(publicFactories); + } + + [Fact] + public void FullResponseTypes_DeriveFromTheirPartialCounterpart() + { + // The Full* types are the only producers of secret data and are siblings of the safe (partial) + // types, so a list of the base type holds a polymorphic mix without a separate wire contract. + Assert.True(typeof(CipherResponseModel).IsAssignableFrom(typeof(FullCipherResponseModel))); + Assert.True(typeof(CipherDetailsResponseModel).IsAssignableFrom(typeof(FullCipherDetailsResponseModel))); + Assert.True(typeof(CipherMiniResponseModel).IsAssignableFrom(typeof(FullCipherMiniResponseModel))); + Assert.True(typeof(CipherMiniDetailsResponseModel).IsAssignableFrom(typeof(FullCipherMiniDetailsResponseModel))); + } +} diff --git a/test/Api.Test/Vault/Models/Response/CipherResponseModelTests.cs b/test/Api.Test/Vault/Models/Response/CipherResponseModelTests.cs index f0f29ddc5324..110a4207a03c 100644 --- a/test/Api.Test/Vault/Models/Response/CipherResponseModelTests.cs +++ b/test/Api.Test/Vault/Models/Response/CipherResponseModelTests.cs @@ -1,6 +1,7 @@ using System.Text.Json; using Bit.Api.Vault.Models.Response; using Bit.Core.Settings; +using Bit.Core.Vault.Authorization; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Enums; using Bit.Core.Vault.Models.Data; @@ -46,7 +47,7 @@ public void Constructor_DriversLicense_DeserializesAllFields() CreationDate = DateTime.UtcNow, }; - var response = new CipherMiniResponseModel(cipher, _globalSettings, false); + var response = new FullCipherMiniResponseModel(FullCipherAccess.Unrestricted(), cipher, _globalSettings, false); Assert.Equal(CipherType.DriversLicense, response.Type); Assert.Equal("2.name|encrypted", response.Name); @@ -80,7 +81,7 @@ public void Constructor_DriversLicense_WithMinimalData_DeserializesSuccessfully( CreationDate = DateTime.UtcNow, }; - var response = new CipherMiniResponseModel(cipher, _globalSettings, false); + var response = new FullCipherMiniResponseModel(FullCipherAccess.Unrestricted(), cipher, _globalSettings, false); Assert.Equal(CipherType.DriversLicense, response.Type); Assert.NotNull(response.DriversLicense); @@ -118,7 +119,7 @@ public void Constructor_Passport_DeserializesAllFields() CreationDate = DateTime.UtcNow, }; - var response = new CipherMiniResponseModel(cipher, _globalSettings, false); + var response = new FullCipherMiniResponseModel(FullCipherAccess.Unrestricted(), cipher, _globalSettings, false); Assert.Equal(CipherType.Passport, response.Type); Assert.Equal("2.name|encrypted", response.Name); @@ -154,7 +155,7 @@ public void Constructor_Passport_WithMinimalData_DeserializesSuccessfully() CreationDate = DateTime.UtcNow, }; - var response = new CipherMiniResponseModel(cipher, _globalSettings, false); + var response = new FullCipherMiniResponseModel(FullCipherAccess.Unrestricted(), cipher, _globalSettings, false); Assert.Equal(CipherType.Passport, response.Type); Assert.NotNull(response.Passport); @@ -186,7 +187,7 @@ public void Constructor_DriversLicense_WithCustomFields_IncludesFields() CreationDate = DateTime.UtcNow, }; - var response = new CipherMiniResponseModel(cipher, _globalSettings, false); + var response = new FullCipherMiniResponseModel(FullCipherAccess.Unrestricted(), cipher, _globalSettings, false); Assert.NotNull(response.Fields); Assert.Single(response.Fields); @@ -215,7 +216,7 @@ public void Constructor_Passport_WithCustomFields_IncludesFields() CreationDate = DateTime.UtcNow, }; - var response = new CipherMiniResponseModel(cipher, _globalSettings, false); + var response = new FullCipherMiniResponseModel(FullCipherAccess.Unrestricted(), cipher, _globalSettings, false); Assert.NotNull(response.Fields); Assert.Single(response.Fields); @@ -241,7 +242,7 @@ public void Constructor_DriversLicense_PreservesRawDataField() CreationDate = DateTime.UtcNow, }; - var response = new CipherMiniResponseModel(cipher, _globalSettings, false); + var response = new FullCipherMiniResponseModel(FullCipherAccess.Unrestricted(), cipher, _globalSettings, false); Assert.Equal(serializedData, response.Data); } @@ -265,9 +266,141 @@ public void Constructor_Passport_PreservesRawDataField() CreationDate = DateTime.UtcNow, }; + var response = new FullCipherMiniResponseModel(FullCipherAccess.Unrestricted(), cipher, _globalSettings, false); + + Assert.Equal(serializedData, response.Data); + } + + [Fact] + public void Constructor_Partial_Login_KeepsNameAndUrisAndStripsSecrets() + { + var loginData = new CipherLoginData + { + Name = "2.name|encrypted", + Notes = "2.notes|encrypted", + Username = "2.username|encrypted", + Password = "2.password|encrypted", + Totp = "2.totp|encrypted", + Uris = new[] + { + new CipherLoginData.CipherLoginUriData { Uri = "2.uri|encrypted", UriChecksum = "2.checksum|encrypted" }, + }, + }; + + var cipher = new Cipher + { + Id = Guid.NewGuid(), + Type = CipherType.Login, + Data = JsonSerializer.Serialize(loginData), + RevisionDate = DateTime.UtcNow, + CreationDate = DateTime.UtcNow, + }; + var response = new CipherMiniResponseModel(cipher, _globalSettings, false); + // Full data is withheld; the reduced blob is returned only in the separate PartialData field. + Assert.Null(response.Data); + Assert.NotNull(response.PartialData); + Assert.Contains("2.name|encrypted", response.PartialData); + Assert.Contains("2.uri|encrypted", response.PartialData); + + // The obsolete typed fields are withheld entirely — partial content lives only in PartialData. + Assert.Null(response.Name); + Assert.Null(response.Login); + Assert.Null(response.Notes); + + // The reduced blob and the serialized model may not carry the secrets anywhere. + Assert.DoesNotContain("username|encrypted", response.PartialData); + Assert.DoesNotContain("password|encrypted", response.PartialData); + Assert.DoesNotContain("totp|encrypted", response.PartialData); + var serialized = JsonSerializer.Serialize(response); + Assert.DoesNotContain("username|encrypted", serialized); + Assert.DoesNotContain("password|encrypted", serialized); + Assert.DoesNotContain("totp|encrypted", serialized); + } + + [Fact] + public void Constructor_Partial_NonLogin_KeepsOnlyName() + { + var cardData = new CipherCardData + { + Name = "2.name|encrypted", + Notes = "2.notes|encrypted", + Number = "2.number|encrypted", + Code = "2.code|encrypted", + }; + + var cipher = new Cipher + { + Id = Guid.NewGuid(), + Type = CipherType.Card, + Data = JsonSerializer.Serialize(cardData), + RevisionDate = DateTime.UtcNow, + CreationDate = DateTime.UtcNow, + }; + + var response = new CipherMiniResponseModel(cipher, _globalSettings, false); + + Assert.Null(response.Data); + Assert.NotNull(response.PartialData); + Assert.Contains("2.name|encrypted", response.PartialData); + // Obsolete typed fields withheld; partial content lives only in PartialData. + Assert.Null(response.Name); + Assert.Null(response.Card); + Assert.Null(response.Notes); + Assert.DoesNotContain("number|encrypted", response.PartialData); + Assert.DoesNotContain("code|encrypted", response.PartialData); + } + + [Fact] + public void Constructor_Partial_BlobEncryptedData_WithholdsData() + { + const string opaque = "2.iv|ct|mac"; + var cipher = new Cipher + { + Id = Guid.NewGuid(), + Type = CipherType.Login, + Data = opaque, + RevisionDate = DateTime.UtcNow, + CreationDate = DateTime.UtcNow, + }; + + var response = new CipherMiniResponseModel(cipher, _globalSettings, false); + + // An opaque blob can't be reshaped, so neither full nor partial data is returned. + Assert.Null(response.Data); + Assert.Null(response.PartialData); + Assert.Null(response.Login); + Assert.Null(response.Name); + } + + [Fact] + public void Constructor_NotPartial_PreservesFullData() + { + var loginData = new CipherLoginData + { + Name = "2.name|encrypted", + Username = "2.username|encrypted", + Password = "2.password|encrypted", + }; + + var serializedData = JsonSerializer.Serialize(loginData); + var cipher = new Cipher + { + Id = Guid.NewGuid(), + Type = CipherType.Login, + Data = serializedData, + RevisionDate = DateTime.UtcNow, + CreationDate = DateTime.UtcNow, + }; + + // The full-data path requires a gate-minted witness; the default-named type is partial only. + var response = new FullCipherMiniResponseModel(FullCipherAccess.Unrestricted(), cipher, _globalSettings, false); + Assert.Equal(serializedData, response.Data); + Assert.Null(response.PartialData); + Assert.Equal("2.username|encrypted", response.Login.Username); + Assert.Equal("2.password|encrypted", response.Login.Password); } [Theory] @@ -291,7 +424,7 @@ public void Constructor_OpaqueData_DoesNotThrowAndSkipsLegacyFields(CipherType t CreationDate = DateTime.UtcNow, }; - var response = new CipherMiniResponseModel(cipher, _globalSettings, false); + var response = new FullCipherMiniResponseModel(FullCipherAccess.Unrestricted(), cipher, _globalSettings, false); Assert.Equal(type, response.Type); Assert.Equal(opaque, response.Data); diff --git a/test/Api.Test/packages.lock.json b/test/Api.Test/packages.lock.json index f9711ce6014d..7a2514868f2b 100644 --- a/test/Api.Test/packages.lock.json +++ b/test/Api.Test/packages.lock.json @@ -1702,9 +1702,10 @@ "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]", - "Commercial.Core": "[2026.6.0, )", - "Commercial.Infrastructure.EntityFramework": "[2026.6.0, )", - "Core": "[2026.6.0, )", + "Commercial.Core": "[2026.6.1, )", + "Commercial.Infrastructure.EntityFramework": "[2026.6.1, )", + "Commercial.Pam": "[2026.6.1, )", + "Core": "[2026.6.1, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", @@ -1712,14 +1713,15 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )", + "Pam.Domain": "[2026.6.1, )", + "SharedWeb": "[2026.6.1, )", "Swashbuckle.AspNetCore": "[10.1.7, 10.1.7]" } }, "commercial.core": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "CsvHelper": "[33.1.0, 33.1.0]" } }, @@ -1727,8 +1729,16 @@ "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" + } + }, + "commercial.pam": { + "type": "Project", + "dependencies": { + "Core": "[2026.6.1, )", + "Pam.Domain": "[2026.6.1, )", + "SharedWeb": "[2026.6.1, )" } }, "common": { @@ -1736,7 +1746,7 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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, )", @@ -1760,6 +1770,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1784,6 +1795,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1802,42 +1814,54 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Common": "[2026.6.0, )", - "Core": "[2026.6.0, )", + "Common": "[2026.6.1, )", + "Core": "[2026.6.1, )", "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, )", + "Pam.Domain": "[2026.6.1, )", "xunit": "[2.6.6, )" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/test/Billing.Test/packages.lock.json b/test/Billing.Test/packages.lock.json index 8c8a1ca5f2e2..6e0671e6b552 100644 --- a/test/Billing.Test/packages.lock.json +++ b/test/Billing.Test/packages.lock.json @@ -1664,8 +1664,8 @@ "billing": { "type": "Project", "dependencies": { - "Commercial.Core": "[2026.6.0, )", - "Core": "[2026.6.0, )", + "Commercial.Core": "[2026.6.1, )", + "Core": "[2026.6.1, )", "MarkDig": "[1.1.0, 1.1.0]", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", @@ -1674,14 +1674,14 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )", + "SharedWeb": "[2026.6.1, )", "Swashbuckle.AspNetCore": "[10.1.7, 10.1.7]" } }, "commercial.core": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "CsvHelper": "[33.1.0, 33.1.0]" } }, @@ -1690,7 +1690,7 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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, )", @@ -1714,6 +1714,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1738,6 +1739,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1756,42 +1758,54 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Common": "[2026.6.0, )", - "Core": "[2026.6.0, )", + "Common": "[2026.6.1, )", + "Core": "[2026.6.1, )", "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, )", + "Pam.Domain": "[2026.6.1, )", "xunit": "[2.6.6, )" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/test/Common/packages.lock.json b/test/Common/packages.lock.json index fd6d75d51e5b..802563192b84 100644 --- a/test/Common/packages.lock.json +++ b/test/Common/packages.lock.json @@ -1318,6 +1318,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1342,6 +1343,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1354,6 +1356,15 @@ "ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis": "[2.0.2, 2.0.2]", "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } + }, + "data": { + "type": "Project" + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } } } } diff --git a/test/Core.IntegrationTest/packages.lock.json b/test/Core.IntegrationTest/packages.lock.json index ae3c95924d0e..81fbda065599 100644 --- a/test/Core.IntegrationTest/packages.lock.json +++ b/test/Core.IntegrationTest/packages.lock.json @@ -1293,6 +1293,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1317,6 +1318,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1329,6 +1331,15 @@ "ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis": "[2.0.2, 2.0.2]", "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } + }, + "data": { + "type": "Project" + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } } } } diff --git a/test/Core.Test/Core.Test.csproj b/test/Core.Test/Core.Test.csproj index bb6034e546b9..13359519504e 100644 --- a/test/Core.Test/Core.Test.csproj +++ b/test/Core.Test/Core.Test.csproj @@ -24,6 +24,7 @@ + diff --git a/test/Core.Test/Services/HandlebarsMailServiceTests.cs b/test/Core.Test/Services/HandlebarsMailServiceTests.cs index 5dae4f5d17d4..3e96f8ac26f9 100644 --- a/test/Core.Test/Services/HandlebarsMailServiceTests.cs +++ b/test/Core.Test/Services/HandlebarsMailServiceTests.cs @@ -275,6 +275,43 @@ await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Is(m => m.Category == "Welcome")); } + [Fact] + public async Task SendPamPendingAccessRequestEmailsAsync_SendsOneRenderedEmailPerRecipient() + { + // Arrange + var managerEmails = new[] { "approver1@example.com", "approver2@example.com" }; + var notBefore = new DateTime(2026, 6, 16, 9, 0, 0, DateTimeKind.Utc); + var notAfter = new DateTime(2026, 6, 16, 17, 0, 0, DateTimeKind.Utc); + // The messages are enqueued; run the fallback inline for each so the template renders and emails are delivered. + _mailEnqueuingService + .EnqueueManyAsync(Arg.Any>(), Arg.Any>()) + .Returns(call => Task.WhenAll( + call.Arg>().Select(call.Arg>()))); + + // Act + await _sut.SendPamPendingAccessRequestEmailsAsync(managerEmails, "Acme Corp", + "Alice Smith", "alice@example.com", notBefore, notAfter, "Rotate prod DB password"); + + // Assert — one message per recipient (approvers are not disclosed to one another), each carrying the + // server-readable request fields with the requester's email anti-phishing sanitized. + await _mailDeliveryService.Received(2).SendEmailAsync(Arg.Any()); + foreach (var recipient in managerEmails) + { + await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Is(m => + m.ToEmails.Count() == 1 && + m.ToEmails.Contains(recipient) && + m.Subject == "New access request awaiting your approval" && + m.Category == "PamPendingAccessRequest" && + m.HtmlContent.Contains("Acme Corp") && + m.HtmlContent.Contains("Alice Smith") && + m.HtmlContent.Contains("alice[at]example[dot]com") && + m.HtmlContent.Contains("Rotate prod DB password") && + m.HtmlContent.Contains("2026") && + m.HtmlContent.Contains("UTC") && + m.HtmlContent.Contains("/pam/approver-inbox/approvals"))); + } + } + [Fact] public async Task SendOrganizationUserWelcomeEmailAsync_SendsCorrectEmailWithOrganizationName() { diff --git a/test/Core.Test/Vault/Models/Data/PartialCipherDataTests.cs b/test/Core.Test/Vault/Models/Data/PartialCipherDataTests.cs new file mode 100644 index 000000000000..da0e9073e9c4 --- /dev/null +++ b/test/Core.Test/Vault/Models/Data/PartialCipherDataTests.cs @@ -0,0 +1,131 @@ +using System.Text.Json; +using Bit.Core.Enums; +using Bit.Core.Vault.Enums; +using Bit.Core.Vault.Models.Data; +using Xunit; + +namespace Bit.Core.Test.Vault.Models.Data; + +public class PartialCipherDataTests +{ + private const string EncryptedName = "2.name|encrypted"; + private const string SecretUsername = "2.SENTINEL-username|encrypted"; + private const string SecretPassword = "2.SENTINEL-password|encrypted"; + private const string SecretTotp = "2.SENTINEL-totp|encrypted"; + + [Fact] + public void Strip_Login_KeepsNameAndUrisAndDropsSecrets() + { + var loginData = new CipherLoginData + { + Name = EncryptedName, + Notes = "2.SENTINEL-notes|encrypted", + Username = SecretUsername, + Password = SecretPassword, + PasswordRevisionDate = DateTime.UtcNow, + Totp = SecretTotp, + AutofillOnPageLoad = true, + Uris = new[] + { + new CipherLoginData.CipherLoginUriData { Uri = "2.uri1|encrypted", UriChecksum = "2.checksum1|encrypted", Match = UriMatchType.Domain }, + new CipherLoginData.CipherLoginUriData { Uri = "2.uri2|encrypted" }, + }, + Fido2Credentials = new[] { new CipherLoginFido2CredentialData { CredentialId = "2.SENTINEL-fido2|encrypted" } }, + Fields = new[] { new CipherFieldData { Name = "2.SENTINEL-field|encrypted", Value = "2.SENTINEL-fieldvalue|encrypted", Type = FieldType.Text } }, + PasswordHistory = new[] { new CipherPasswordHistoryData { Password = "2.SENTINEL-history|encrypted", LastUsedDate = DateTime.UtcNow } }, + }; + + var stripped = PartialCipherData.Strip(CipherType.Login, JsonSerializer.Serialize(loginData)); + + var result = JsonSerializer.Deserialize(stripped); + Assert.Equal(EncryptedName, result.Name); + Assert.Equal(2, result.Uris.Count()); + Assert.Equal("2.uri1|encrypted", result.Uris.First().Uri); + Assert.Equal("2.checksum1|encrypted", result.Uris.First().UriChecksum); + Assert.Equal(UriMatchType.Domain, result.Uris.First().Match); + Assert.Null(result.Username); + Assert.Null(result.Password); + Assert.Null(result.PasswordRevisionDate); + Assert.Null(result.Totp); + Assert.Null(result.AutofillOnPageLoad); + Assert.Null(result.Fido2Credentials); + Assert.Null(result.Notes); + Assert.Null(result.Fields); + Assert.Null(result.PasswordHistory); + + // No secret value may appear anywhere in the serialized envelope. + Assert.DoesNotContain("SENTINEL", stripped); + } + + [Theory] + [InlineData(CipherType.SecureNote)] + [InlineData(CipherType.Card)] + [InlineData(CipherType.Identity)] + [InlineData(CipherType.SSHKey)] + [InlineData(CipherType.BankAccount)] + [InlineData(CipherType.DriversLicense)] + [InlineData(CipherType.Passport)] + public void Strip_NonLogin_KeepsOnlyName(CipherType type) + { + // A superset blob containing fields specific to several types plus base/secret fields. + var data = JsonSerializer.Serialize(new + { + Name = EncryptedName, + Notes = "2.SENTINEL-notes|encrypted", + Number = "2.SENTINEL-cardnumber|encrypted", + Code = "2.SENTINEL-cvv|encrypted", + LicenseNumber = "2.SENTINEL-license|encrypted", + PassportNumber = "2.SENTINEL-passport|encrypted", + PrivateKey = "2.SENTINEL-sshkey|encrypted", + Fields = new[] { new { Name = "2.SENTINEL-field|encrypted", Value = "2.SENTINEL-fieldvalue|encrypted" } }, + }); + + var stripped = PartialCipherData.Strip(type, data); + + var result = JsonSerializer.Deserialize(stripped); + Assert.Equal(EncryptedName, result.Name); + Assert.Null(result.Notes); + Assert.Null(result.Number); + Assert.Null(result.Code); + Assert.Null(result.Fields); + Assert.DoesNotContain("SENTINEL", stripped); + } + + [Theory] + [InlineData(CipherType.Login)] + [InlineData(CipherType.Card)] + public void Strip_NullOrEmptyData_ReturnedUnchanged(CipherType type) + { + Assert.Null(PartialCipherData.Strip(type, null)); + Assert.Equal("", PartialCipherData.Strip(type, "")); + Assert.Equal(" ", PartialCipherData.Strip(type, " ")); + } + + [Fact] + public void Strip_NullName_ProducesValidJsonWithoutThrowing() + { + var loginData = new CipherLoginData { Username = SecretUsername }; + + var stripped = PartialCipherData.Strip(CipherType.Login, JsonSerializer.Serialize(loginData)); + + var result = JsonSerializer.Deserialize(stripped); + Assert.Null(result.Name); + Assert.DoesNotContain("SENTINEL", stripped); + } + + [Fact] + public void Strip_IsIdempotent() + { + var loginData = new CipherLoginData + { + Name = EncryptedName, + Username = SecretUsername, + Uris = new[] { new CipherLoginData.CipherLoginUriData { Uri = "2.uri|encrypted" } }, + }; + + var once = PartialCipherData.Strip(CipherType.Login, JsonSerializer.Serialize(loginData)); + var twice = PartialCipherData.Strip(CipherType.Login, once); + + Assert.Equal(once, twice); + } +} diff --git a/test/Core.Test/Vault/Services/CipherServiceTests.cs b/test/Core.Test/Vault/Services/CipherServiceTests.cs index 010ae4fa33af..d3aa4ad3466a 100644 --- a/test/Core.Test/Vault/Services/CipherServiceTests.cs +++ b/test/Core.Test/Vault/Services/CipherServiceTests.cs @@ -20,9 +20,11 @@ using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Repositories; using Bit.Core.Vault.Services; +using Bit.Pam.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; +using NSubstitute.ExceptionExtensions; using Xunit; namespace Bit.Core.Test.Services; @@ -2493,4 +2495,45 @@ await sutProvider.GetDependency() .DeleteAttachmentsForCipherAsync(cipher.Id); } } + + [Theory, BitAutoData] + public async Task SaveDetailsAsync_ExistingLeasingGatedCipher_ThrowsAndDoesNotPersist( + SutProvider sutProvider, CipherDetails cipher) + { + // Personal cipher so the edit-permission check passes and we reach the lease gate. + var savingUserId = cipher.UserId!.Value; + sutProvider.GetDependency() + .EnsureCanMutateAsync(savingUserId, cipher) + .ThrowsAsync(new NotFoundException()); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, cipher.RevisionDate)); + + await sutProvider.GetDependency().DidNotReceive().ReplaceAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task SaveAsync_NewCipher_DoesNotInvokeLeaseGate( + SutProvider sutProvider, Cipher cipher) + { + // A new cipher has no collection paths yet, so it is never leasing-gated. + cipher.Id = default; + var savingUserId = cipher.UserId!.Value; + + await sutProvider.Sut.SaveAsync(cipher, savingUserId, null); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs().EnsureCanMutateAsync(default, default!); + } + + [Theory, BitAutoData] + public async Task DeleteAsync_OrgAdmin_DoesNotInvokeLeaseGate( + SutProvider sutProvider, CipherDetails cipher, Guid deletingUserId) + { + // Admins act through org-wide permissions with no collection-membership path, so the gate is skipped. + await sutProvider.Sut.DeleteAsync(cipher, deletingUserId, orgAdmin: true); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs().EnsureCanMutateAsync(default, default!); + } } diff --git a/test/Core.Test/packages.lock.json b/test/Core.Test/packages.lock.json index 2dfc1673aebd..d88f35325ddc 100644 --- a/test/Core.Test/packages.lock.json +++ b/test/Core.Test/packages.lock.json @@ -1348,7 +1348,7 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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, )", @@ -1372,6 +1372,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1396,6 +1397,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1408,6 +1410,15 @@ "ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis": "[2.0.2, 2.0.2]", "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } + }, + "data": { + "type": "Project" + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } } } } diff --git a/test/Events.IntegrationTest/packages.lock.json b/test/Events.IntegrationTest/packages.lock.json index ab16d1cadb7e..bbdd5faca7d8 100644 --- a/test/Events.IntegrationTest/packages.lock.json +++ b/test/Events.IntegrationTest/packages.lock.json @@ -1713,7 +1713,7 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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, )", @@ -1737,6 +1737,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1761,6 +1762,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1774,10 +1776,13 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "events": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", @@ -1785,13 +1790,13 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )" + "SharedWeb": "[2026.6.1, )" } }, "identity": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", @@ -1799,25 +1804,27 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )" + "SharedWeb": "[2026.6.1, )" } }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" @@ -1826,21 +1833,27 @@ "integrationtestcommon": { "type": "Project", "dependencies": { - "Common": "[2026.6.0, )", - "Identity": "[2026.6.0, )", + "Common": "[2026.6.1, )", + "Identity": "[2026.6.1, )", "Microsoft.AspNetCore.Mvc.Testing": "[10.0.8, 10.0.8]", - "Migrator": "[2026.6.0, )", - "Seeder": "[2026.6.0, )" + "Migrator": "[2026.6.1, )", + "Seeder": "[2026.6.1, )" } }, "migrator": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "Microsoft.Extensions.Logging": "[10.0.8, 10.0.8]", "dbup-sqlserver": "[7.2.0, 7.2.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "rustsdk": { "type": "Project" }, @@ -1848,18 +1861,18 @@ "type": "Project", "dependencies": { "Bogus": "[35.6.5, 35.6.5]", - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", - "RustSdk": "[2026.6.0, )", - "SharedWeb": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", + "RustSdk": "[2026.6.1, )", + "SharedWeb": "[2026.6.1, )" } }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/test/Events.Test/packages.lock.json b/test/Events.Test/packages.lock.json index d88d83566955..07d0b76e5700 100644 --- a/test/Events.Test/packages.lock.json +++ b/test/Events.Test/packages.lock.json @@ -1594,7 +1594,7 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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, )", @@ -1618,6 +1618,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1642,6 +1643,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1655,10 +1657,13 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "events": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", @@ -1666,36 +1671,44 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )" + "SharedWeb": "[2026.6.1, )" } }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/test/EventsProcessor.Test/packages.lock.json b/test/EventsProcessor.Test/packages.lock.json index ebd40df77f5b..6c11c3024ccf 100644 --- a/test/EventsProcessor.Test/packages.lock.json +++ b/test/EventsProcessor.Test/packages.lock.json @@ -1534,6 +1534,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1558,6 +1559,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1571,10 +1573,13 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "eventsprocessor": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", @@ -1582,36 +1587,44 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )" + "SharedWeb": "[2026.6.1, )" } }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/test/Icons.Test/packages.lock.json b/test/Icons.Test/packages.lock.json index fe6e418d4fc9..ef4004b29b0c 100644 --- a/test/Icons.Test/packages.lock.json +++ b/test/Icons.Test/packages.lock.json @@ -1598,7 +1598,7 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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, )", @@ -1622,6 +1622,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1646,6 +1647,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1659,11 +1661,14 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "icons": { "type": "Project", "dependencies": { "AngleSharp": "[1.4.0, 1.4.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", @@ -1671,36 +1676,44 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )" + "SharedWeb": "[2026.6.1, )" } }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/test/Identity.IntegrationTest/packages.lock.json b/test/Identity.IntegrationTest/packages.lock.json index 5f1e34051a7c..a4772c536e20 100644 --- a/test/Identity.IntegrationTest/packages.lock.json +++ b/test/Identity.IntegrationTest/packages.lock.json @@ -1299,7 +1299,7 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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, )", @@ -1323,6 +1323,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1347,6 +1348,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1365,19 +1367,23 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Common": "[2026.6.0, )", - "Core": "[2026.6.0, )", + "Common": "[2026.6.1, )", + "Core": "[2026.6.1, )", "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, )", + "Pam.Domain": "[2026.6.1, )", "xunit": "[2.6.6, )" } }, + "data": { + "type": "Project" + }, "identity": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", @@ -1385,25 +1391,27 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )" + "SharedWeb": "[2026.6.1, )" } }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" @@ -1412,21 +1420,27 @@ "integrationtestcommon": { "type": "Project", "dependencies": { - "Common": "[2026.6.0, )", - "Identity": "[2026.6.0, )", + "Common": "[2026.6.1, )", + "Identity": "[2026.6.1, )", "Microsoft.AspNetCore.Mvc.Testing": "[10.0.8, 10.0.8]", - "Migrator": "[2026.6.0, )", - "Seeder": "[2026.6.0, )" + "Migrator": "[2026.6.1, )", + "Seeder": "[2026.6.1, )" } }, "migrator": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "Microsoft.Extensions.Logging": "[10.0.8, 10.0.8]", "dbup-sqlserver": "[7.2.0, 7.2.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "rustsdk": { "type": "Project" }, @@ -1434,18 +1448,18 @@ "type": "Project", "dependencies": { "Bogus": "[35.6.5, 35.6.5]", - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", - "RustSdk": "[2026.6.0, )", - "SharedWeb": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", + "RustSdk": "[2026.6.1, )", + "SharedWeb": "[2026.6.1, )" } }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/test/Identity.Test/packages.lock.json b/test/Identity.Test/packages.lock.json index 4ed34d07af85..733b1b1f4882 100644 --- a/test/Identity.Test/packages.lock.json +++ b/test/Identity.Test/packages.lock.json @@ -1630,7 +1630,7 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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, )", @@ -1654,6 +1654,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1678,6 +1679,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1691,10 +1693,13 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "identity": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", @@ -1702,36 +1707,44 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )" + "SharedWeb": "[2026.6.1, )" } }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/test/Infrastructure.Dapper.Test/packages.lock.json b/test/Infrastructure.Dapper.Test/packages.lock.json index d84767d2853b..c36ae86720b0 100644 --- a/test/Infrastructure.Dapper.Test/packages.lock.json +++ b/test/Infrastructure.Dapper.Test/packages.lock.json @@ -1255,6 +1255,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1279,6 +1280,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1292,11 +1294,21 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" + } + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" } } } diff --git a/test/Infrastructure.EFIntegration.Test/packages.lock.json b/test/Infrastructure.EFIntegration.Test/packages.lock.json index 128e17d3345e..573813461386 100644 --- a/test/Infrastructure.EFIntegration.Test/packages.lock.json +++ b/test/Infrastructure.EFIntegration.Test/packages.lock.json @@ -1507,7 +1507,7 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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, )", @@ -1531,6 +1531,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1555,6 +1556,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1573,31 +1575,37 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Common": "[2026.6.0, )", - "Core": "[2026.6.0, )", + "Common": "[2026.6.1, )", + "Core": "[2026.6.1, )", "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, )", + "Pam.Domain": "[2026.6.1, )", "xunit": "[2.6.6, )" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" @@ -1606,22 +1614,28 @@ "mysqlmigrations": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" + } + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" } }, "postgresmigrations": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" } }, "sqlitemigrations": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" } } } diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryGetManagingUserIdsTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryGetManagingUserIdsTests.cs new file mode 100644 index 000000000000..a4250fdcded0 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryGetManagingUserIdsTests.cs @@ -0,0 +1,132 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository; + +public class CollectionRepositoryGetManagingUserIdsTests +{ + [DatabaseTheory, DatabaseData] + public async Task GetManagingUserIdsAsync_DirectManageUser_Included_NonManageExcluded( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + + var manager = await userRepository.CreateTestUserAsync("manager"); + var managerOrgUser = await CreateConfirmedUserAsync(organizationUserRepository, organization, manager); + + var viewer = await userRepository.CreateTestUserAsync("viewer"); + var viewerOrgUser = await CreateConfirmedUserAsync(organizationUserRepository, organization, viewer); + + var collection = new Collection { Name = "Leased", OrganizationId = organization.Id }; + await collectionRepository.CreateAsync(collection, groups: [], users: + [ + new CollectionAccessSelection { Id = managerOrgUser.Id, Manage = true }, + new CollectionAccessSelection { Id = viewerOrgUser.Id, Manage = false, ReadOnly = true }, + ]); + + var userIds = await collectionRepository.GetManagingUserIdsAsync(collection.Id); + + Assert.Contains(manager.Id, userIds); + Assert.DoesNotContain(viewer.Id, userIds); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManagingUserIdsAsync_GroupManageMember_Included( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository, + IGroupRepository groupRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var member = await userRepository.CreateTestUserAsync("groupmember"); + var memberOrgUser = await CreateConfirmedUserAsync(organizationUserRepository, organization, member); + + var group = await groupRepository.CreateTestGroupAsync(organization); + await groupRepository.UpdateUsersAsync(group.Id, [memberOrgUser.Id], DateTime.UtcNow); + + var collection = new Collection { Name = "Leased", OrganizationId = organization.Id }; + await collectionRepository.CreateAsync(collection, groups: + [ + new CollectionAccessSelection { Id = group.Id, Manage = true }, + ], users: []); + + var userIds = await collectionRepository.GetManagingUserIdsAsync(collection.Id); + + Assert.Contains(member.Id, userIds); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManagingUserIdsAsync_OwnerWithAdminAccess_Included( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + organization.AllowAdminAccessToAllCollectionItems = true; + await organizationRepository.ReplaceAsync(organization); + + var owner = await userRepository.CreateTestUserAsync("owner"); + await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = owner.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Owner, + }); + + // A collection the owner is not directly assigned to. + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + + var userIds = await collectionRepository.GetManagingUserIdsAsync(collection.Id); + + Assert.Contains(owner.Id, userIds); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManagingUserIdsAsync_OwnerWithoutAdminAccess_Excluded( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + // The test-org helper enables admin access by default, so turn it off for this case. + organization.AllowAdminAccessToAllCollectionItems = false; + await organizationRepository.ReplaceAsync(organization); + + var owner = await userRepository.CreateTestUserAsync("owner"); + await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = owner.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Owner, + }); + + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + + var userIds = await collectionRepository.GetManagingUserIdsAsync(collection.Id); + + Assert.DoesNotContain(owner.Id, userIds); + } + + private static Task CreateConfirmedUserAsync( + IOrganizationUserRepository organizationUserRepository, Organization organization, User user) + => organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.User, + }); +} diff --git a/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj b/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj index eb9d149a70c1..0c241b58f559 100644 --- a/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj +++ b/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj @@ -27,6 +27,7 @@ + diff --git a/test/Infrastructure.IntegrationTest/Pam/Repositories/AccessLeaseRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Pam/Repositories/AccessLeaseRepositoryTests.cs new file mode 100644 index 000000000000..2631cba9385a --- /dev/null +++ b/test/Infrastructure.IntegrationTest/Pam/Repositories/AccessLeaseRepositoryTests.cs @@ -0,0 +1,476 @@ +using Bit.Core.Repositories; +using Bit.Core.Utilities; +using Bit.Infrastructure.IntegrationTest.AdminConsole; +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Pam.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.Pam.Repositories; + +public class LeaseRepositoryTests +{ + [DatabaseTheory, DatabaseData] + public async Task CreateAutoApprovedAsync_PersistsApprovedRequestAndDecisionWithoutLease( + IOrganizationRepository organizationRepository, + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var now = DateTime.UtcNow; + var cipherId = Guid.NewGuid(); + var requesterId = Guid.NewGuid(); + + var (request, decision, _) = BuildAutoApproved(organization.Id, cipherId, requesterId, now, now.AddHours(1)); + // Exercise the TINYINT ConditionKind column end-to-end: the INSERT throws if the sproc param / column type + // does not accept the byte-backed enum value. + decision.ConditionKind = AccessConditionKind.IpAllowlist; + + await accessRequestRepository.CreateAutoApprovedAsync(request, decision); + + // The request is persisted already resolved as Approved... + var persistedRequest = await accessRequestRepository.GetByIdAsync(request.Id); + Assert.NotNull(persistedRequest); + Assert.Equal(AccessRequestStatus.Approved, persistedRequest!.Status); + Assert.NotNull(persistedRequest.ResolvedDate); + + // ...but no lease is minted at submit: the requester activates the approved request to start one. + Assert.Null(await accessLeaseRepository.GetByAccessRequestIdAsync(request.Id)); + } + + [DatabaseTheory, DatabaseData] + public async Task GetActiveByRequesterIdCipherIdAsync_WithinWindow_ReturnsLease( + IOrganizationRepository organizationRepository, + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var now = DateTime.UtcNow; + var cipherId = Guid.NewGuid(); + var requesterId = Guid.NewGuid(); + + var (request, decision, lease) = BuildAutoApproved( + organization.Id, cipherId, requesterId, now.AddMinutes(-5), now.AddHours(1)); + await SeedActiveLeaseAsync(accessRequestRepository, accessLeaseRepository, request, decision, lease, now); + + var active = await accessLeaseRepository.GetActiveByRequesterIdCipherIdAsync(requesterId, cipherId, now); + + Assert.NotNull(active); + Assert.Equal(lease.Id, active!.Id); + } + + [DatabaseTheory, DatabaseData] + public async Task GetActiveByRequesterIdCipherIdAsync_OutsideWindow_ReturnsNull( + IOrganizationRepository organizationRepository, + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var now = DateTime.UtcNow; + var cipherId = Guid.NewGuid(); + var requesterId = Guid.NewGuid(); + + // A lease whose window has already elapsed. It is minted while the window was still open (now - 2h), then + // read back at now, by which point it has expired. + var (request, decision, lease) = BuildAutoApproved( + organization.Id, cipherId, requesterId, now.AddHours(-2), now.AddHours(-1)); + await SeedActiveLeaseAsync( + accessRequestRepository, accessLeaseRepository, request, decision, lease, now.AddHours(-2)); + + var active = await accessLeaseRepository.GetActiveByRequesterIdCipherIdAsync(requesterId, cipherId, now); + + Assert.Null(active); + } + + [DatabaseTheory, DatabaseData] + public async Task GetActivePendingByRequesterIdCipherIdAsync_ReturnsPendingRequest( + IOrganizationRepository organizationRepository, + IAccessRequestRepository accessRequestRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var now = DateTime.UtcNow; + var cipherId = Guid.NewGuid(); + var requesterId = Guid.NewGuid(); + + var request = await accessRequestRepository.CreateAsync(new AccessRequest + { + OrganizationId = organization.Id, + CollectionId = Guid.NewGuid(), + CipherId = cipherId, + RequesterId = requesterId, + NotBefore = now.AddHours(1), + NotAfter = now.AddHours(2), + Reason = "audit", + Status = AccessRequestStatus.Pending, + CreationDate = now, + }); + + var pending = await accessRequestRepository.GetActivePendingByRequesterIdCipherIdAsync(requesterId, cipherId); + + Assert.NotNull(pending); + Assert.Equal(request.Id, pending!.Id); + Assert.Equal("audit", pending.Reason); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManyActiveByRequesterIdAsync_ReturnsOnlyActiveLeasesInWindow( + IOrganizationRepository organizationRepository, + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var now = DateTime.UtcNow; + var requesterId = Guid.NewGuid(); + + // Active, in-window lease for the requester. + var (activeReq, activeDec, activeLease) = BuildAutoApproved( + organization.Id, Guid.NewGuid(), requesterId, now.AddMinutes(-5), now.AddHours(1)); + await SeedActiveLeaseAsync(accessRequestRepository, accessLeaseRepository, activeReq, activeDec, activeLease, now); + + // Expired lease for the same requester — must be excluded. + var (expiredReq, expiredDec, expiredLease) = BuildAutoApproved( + organization.Id, Guid.NewGuid(), requesterId, now.AddHours(-2), now.AddHours(-1)); + await SeedActiveLeaseAsync( + accessRequestRepository, accessLeaseRepository, expiredReq, expiredDec, expiredLease, now.AddHours(-2)); + + // Active lease for a different requester — must be excluded. + var (otherReq, otherDec, otherLease) = BuildAutoApproved( + organization.Id, Guid.NewGuid(), Guid.NewGuid(), now.AddMinutes(-5), now.AddHours(1)); + await SeedActiveLeaseAsync(accessRequestRepository, accessLeaseRepository, otherReq, otherDec, otherLease, now); + + var result = await accessLeaseRepository.GetManyActiveByRequesterIdAsync(requesterId, now); + + Assert.Single(result); + Assert.Equal(activeLease.Id, result.First().Id); + } + + [DatabaseTheory, DatabaseData] + public async Task RevokeAsync_RevokesLeaseAndRecordsAuditDecision( + IOrganizationRepository organizationRepository, + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var now = DateTime.UtcNow; + var cipherId = Guid.NewGuid(); + var requesterId = Guid.NewGuid(); + var revokerId = Guid.NewGuid(); + + var (request, decision, lease) = BuildAutoApproved( + organization.Id, cipherId, requesterId, now.AddMinutes(-5), now.AddHours(1)); + await SeedActiveLeaseAsync(accessRequestRepository, accessLeaseRepository, request, decision, lease, now); + + var auditDecision = new AccessDecision + { + Id = CoreHelpers.GenerateComb(), + AccessRequestId = lease.AccessRequestId, + DeciderKind = AccessDeciderKind.Human, + ApproverId = revokerId, + Verdict = AccessDecisionVerdict.Deny, + Comment = "policy change", + CreationDate = now, + }; + + await accessLeaseRepository.RevokeAsync(lease, auditDecision, now); + + var persisted = await accessLeaseRepository.GetByIdAsync(lease.Id); + Assert.NotNull(persisted); + Assert.Equal(AccessLeaseStatus.Revoked, persisted!.Status); + Assert.Equal(revokerId, persisted.RevokedBy); + Assert.NotNull(persisted.RevokedDate); + } + + [DatabaseTheory, DatabaseData] + public async Task CreateFromApprovedRequestAsync_ApprovedOpenWindow_MintsActiveLease( + IOrganizationRepository organizationRepository, + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var now = DateTime.UtcNow; + var request = await CreateApprovedRequestAsync( + accessRequestRepository, organization.Id, now.AddHours(-1), now.AddHours(1)); + + // Activation has not happened yet, so the request has produced nothing. + Assert.Null(await accessLeaseRepository.GetByAccessRequestIdAsync(request.Id)); + + var lease = BuildLeaseFor(request, now); + Assert.Equal(AccessLeaseMintOutcome.Minted, + await accessLeaseRepository.CreateFromApprovedRequestAsync(lease, now, false)); + + var produced = await accessLeaseRepository.GetByAccessRequestIdAsync(request.Id); + Assert.NotNull(produced); + Assert.Equal(lease.Id, produced!.Id); + Assert.Equal(AccessLeaseStatus.Active, produced.Status); + // The minted lease spans the request's approved window exactly — compare against the persisted request, + // since the in-memory entity keeps tick precision the driver's datetime parameters do not. + var persistedRequest = await accessRequestRepository.GetByIdAsync(request.Id); + Assert.Equal(persistedRequest!.NotBefore, produced.NotBefore); + Assert.Equal(persistedRequest.NotAfter, produced.NotAfter); + + // The requester now holds access through the standard active-lease read. + var active = await accessLeaseRepository.GetActiveByRequesterIdCipherIdAsync( + request.RequesterId, request.CipherId, now); + Assert.NotNull(active); + Assert.Equal(lease.Id, active!.Id); + } + + [DatabaseTheory, DatabaseData] + public async Task CreateFromApprovedRequestAsync_SecondActivation_PreconditionFailedAndKeepsFirstLease( + IOrganizationRepository organizationRepository, + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var now = DateTime.UtcNow; + var request = await CreateApprovedRequestAsync( + accessRequestRepository, organization.Id, now.AddHours(-1), now.AddHours(1)); + + var first = BuildLeaseFor(request, now); + Assert.Equal(AccessLeaseMintOutcome.Minted, + await accessLeaseRepository.CreateFromApprovedRequestAsync(first, now, false)); + + // A request authorizes access at most once: the second insert is refused by the guard (and would be by the + // unique index even if the guard raced). + var second = BuildLeaseFor(request, now); + Assert.Equal(AccessLeaseMintOutcome.PreconditionFailed, + await accessLeaseRepository.CreateFromApprovedRequestAsync(second, now, false)); + + var produced = await accessLeaseRepository.GetByAccessRequestIdAsync(request.Id); + Assert.Equal(first.Id, produced!.Id); + } + + [DatabaseTheory, DatabaseData] + public async Task CreateFromApprovedRequestAsync_PreconditionNoLongerHolds_PreconditionFailed( + IOrganizationRepository organizationRepository, + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var now = DateTime.UtcNow; + + // Still pending: not an approval. + var pending = await CreateApprovedRequestAsync( + accessRequestRepository, organization.Id, now.AddHours(-1), now.AddHours(1), AccessRequestStatus.Pending); + Assert.Equal(AccessLeaseMintOutcome.PreconditionFailed, + await accessLeaseRepository.CreateFromApprovedRequestAsync(BuildLeaseFor(pending, now), now, false)); + + // Someone else's request: the requester filter refuses it. + var approved = await CreateApprovedRequestAsync( + accessRequestRepository, organization.Id, now.AddHours(-1), now.AddHours(1)); + var foreign = BuildLeaseFor(approved, now); + foreign.RequesterId = Guid.NewGuid(); + Assert.Equal(AccessLeaseMintOutcome.PreconditionFailed, + await accessLeaseRepository.CreateFromApprovedRequestAsync(foreign, now, false)); + + // Window not started yet. + var future = await CreateApprovedRequestAsync( + accessRequestRepository, organization.Id, now.AddHours(1), now.AddHours(2)); + Assert.Equal(AccessLeaseMintOutcome.PreconditionFailed, + await accessLeaseRepository.CreateFromApprovedRequestAsync(BuildLeaseFor(future, now), now, false)); + + // Window already ended. + var lapsed = await CreateApprovedRequestAsync( + accessRequestRepository, organization.Id, now.AddHours(-2), now.AddHours(-1)); + Assert.Equal(AccessLeaseMintOutcome.PreconditionFailed, + await accessLeaseRepository.CreateFromApprovedRequestAsync(BuildLeaseFor(lapsed, now), now, false)); + + // None of the refused activations left a lease behind. + foreach (var requestId in new[] { pending.Id, approved.Id, future.Id, lapsed.Id }) + { + Assert.Null(await accessLeaseRepository.GetByAccessRequestIdAsync(requestId)); + } + } + + [DatabaseTheory, DatabaseData] + public async Task CreateFromApprovedRequestAsync_EnforceSingleActiveLease_SecondCipherActivationConflicts( + IOrganizationRepository organizationRepository, + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var now = DateTime.UtcNow; + var cipherId = Guid.NewGuid(); + + // Two different users each hold an approved request for the SAME cipher. With enforcement on, only one of them + // may mint an active lease — contention is purely per-cipher across all users. + var first = await CreateApprovedRequestAsync( + accessRequestRepository, organization.Id, now.AddHours(-1), now.AddHours(1), cipherId: cipherId); + var second = await CreateApprovedRequestAsync( + accessRequestRepository, organization.Id, now.AddHours(-1), now.AddHours(1), cipherId: cipherId); + + Assert.Equal(AccessLeaseMintOutcome.Minted, + await accessLeaseRepository.CreateFromApprovedRequestAsync(BuildLeaseFor(first, now), now, true)); + + // The cipher already has an active in-window lease, so the second activation is refused as a conflict. + Assert.Equal(AccessLeaseMintOutcome.SingleActiveLeaseConflict, + await accessLeaseRepository.CreateFromApprovedRequestAsync(BuildLeaseFor(second, now), now, true)); + + // The conflict left no lease behind for the second request. + Assert.Null(await accessLeaseRepository.GetByAccessRequestIdAsync(second.Id)); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManyActiveByCollectionIdsAsync_ReturnsActiveInWindowLeasesOnGivenCollections( + IOrganizationRepository organizationRepository, + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var now = DateTime.UtcNow; + + // Two active, in-window leases on distinct collections — both visible to a manager of those collections. + var (req1, dec1, lease1) = BuildAutoApproved( + organization.Id, Guid.NewGuid(), Guid.NewGuid(), now.AddMinutes(-5), now.AddHours(1)); + await SeedActiveLeaseAsync(accessRequestRepository, accessLeaseRepository, req1, dec1, lease1, now); + var (req2, dec2, lease2) = BuildAutoApproved( + organization.Id, Guid.NewGuid(), Guid.NewGuid(), now.AddMinutes(-5), now.AddHours(1)); + await SeedActiveLeaseAsync(accessRequestRepository, accessLeaseRepository, req2, dec2, lease2, now); + + // Active but already out of window (minted in a past window) on a third collection — excluded by the window. + var (req3, dec3, lease3) = BuildAutoApproved( + organization.Id, Guid.NewGuid(), Guid.NewGuid(), now.AddHours(-2), now.AddHours(-1)); + await SeedActiveLeaseAsync(accessRequestRepository, accessLeaseRepository, req3, dec3, lease3, now.AddHours(-2)); + + var all = await accessLeaseRepository.GetManyActiveByCollectionIdsAsync( + new[] { lease1.CollectionId, lease2.CollectionId, lease3.CollectionId }, now); + + Assert.Equal(2, all.Count); + Assert.Contains(all, l => l.Id == lease1.Id); + Assert.Contains(all, l => l.Id == lease2.Id); + + // Collection scoping: querying a subset returns only that collection's leases. + var scoped = await accessLeaseRepository.GetManyActiveByCollectionIdsAsync(new[] { lease1.CollectionId }, now); + Assert.Single(scoped); + Assert.Equal(lease1.Id, scoped.First().Id); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManyEndedByCollectionIdsAsync_ReturnsRecentlyEndedLeasesOnGivenCollections( + IOrganizationRepository organizationRepository, + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var now = DateTime.UtcNow; + var since = now.AddDays(-90); + + // Active lease — not ended, excluded. + var (activeReq, activeDec, activeLease) = BuildAutoApproved( + organization.Id, Guid.NewGuid(), Guid.NewGuid(), now.AddMinutes(-5), now.AddHours(1)); + await SeedActiveLeaseAsync(accessRequestRepository, accessLeaseRepository, activeReq, activeDec, activeLease, now); + + // Revoked within the window — included. + var (revReq, revDec, revLease) = BuildAutoApproved( + organization.Id, Guid.NewGuid(), Guid.NewGuid(), now.AddMinutes(-5), now.AddHours(1)); + await SeedActiveLeaseAsync(accessRequestRepository, accessLeaseRepository, revReq, revDec, revLease, now); + await accessLeaseRepository.RevokeAsync(revLease, BuildAuditDecision(revLease, now), now); + + // Revoked long before the window — excluded by @Since. + var (oldReq, oldDec, oldLease) = BuildAutoApproved( + organization.Id, Guid.NewGuid(), Guid.NewGuid(), now.AddDays(-200), now.AddDays(-100)); + await SeedActiveLeaseAsync( + accessRequestRepository, accessLeaseRepository, oldReq, oldDec, oldLease, now.AddDays(-200)); + await accessLeaseRepository.RevokeAsync(oldLease, BuildAuditDecision(oldLease, now.AddDays(-150)), now.AddDays(-150)); + + var result = await accessLeaseRepository.GetManyEndedByCollectionIdsAsync( + new[] { activeLease.CollectionId, revLease.CollectionId, oldLease.CollectionId }, since); + + Assert.Single(result); + Assert.Equal(revLease.Id, result.First().Id); + Assert.Equal(AccessLeaseStatus.Revoked, result.First().Status); + } + + private static AccessDecision BuildAuditDecision(AccessLease lease, DateTime now) + => new() + { + Id = CoreHelpers.GenerateComb(), + AccessRequestId = lease.AccessRequestId, + DeciderKind = AccessDeciderKind.Human, + ApproverId = Guid.NewGuid(), + Verdict = AccessDecisionVerdict.Deny, + Comment = "ended for test", + CreationDate = now, + }; + + private static async Task CreateApprovedRequestAsync( + IAccessRequestRepository accessRequestRepository, Guid organizationId, DateTime notBefore, DateTime notAfter, + AccessRequestStatus status = AccessRequestStatus.Approved, Guid? cipherId = null) + => await accessRequestRepository.CreateAsync(new AccessRequest + { + OrganizationId = organizationId, + CollectionId = Guid.NewGuid(), + CipherId = cipherId ?? Guid.NewGuid(), + RequesterId = Guid.NewGuid(), + NotBefore = notBefore, + NotAfter = notAfter, + Reason = "audit", + Status = status, + CreationDate = DateTime.UtcNow, + ResolvedDate = status == AccessRequestStatus.Pending ? null : DateTime.UtcNow, + }); + + // Seeds an active lease the way production now does: record the approved request, then mint the lease by + // activating it. The mint time sits inside the request's window (it can be in the past), so leases whose windows + // have already elapsed by read time can still be seeded for the read-path tests. + private static async Task SeedActiveLeaseAsync( + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository, + AccessRequest request, AccessDecision decision, AccessLease lease, DateTime mintTime) + { + await accessRequestRepository.CreateAutoApprovedAsync(request, decision); + await accessLeaseRepository.CreateFromApprovedRequestAsync(lease, mintTime, false); + } + + private static AccessLease BuildLeaseFor(AccessRequest request, DateTime now) + => new() + { + Id = CoreHelpers.GenerateComb(), + AccessRequestId = request.Id, + OrganizationId = request.OrganizationId, + CollectionId = request.CollectionId, + CipherId = request.CipherId, + RequesterId = request.RequesterId, + Status = AccessLeaseStatus.Active, + NotBefore = request.NotBefore, + NotAfter = request.NotAfter, + CreationDate = now, + }; + + private static (AccessRequest, AccessDecision, AccessLease) BuildAutoApproved( + Guid organizationId, Guid cipherId, Guid requesterId, DateTime notBefore, DateTime notAfter) + { + var collectionId = Guid.NewGuid(); + var request = new AccessRequest + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organizationId, + CollectionId = collectionId, + CipherId = cipherId, + RequesterId = requesterId, + NotBefore = notBefore, + NotAfter = notAfter, + Status = AccessRequestStatus.Approved, + }; + var decision = new AccessDecision + { + Id = CoreHelpers.GenerateComb(), + AccessRequestId = request.Id, + DeciderKind = AccessDeciderKind.Automatic, + Verdict = AccessDecisionVerdict.Approve, + }; + var lease = new AccessLease + { + Id = CoreHelpers.GenerateComb(), + AccessRequestId = request.Id, + OrganizationId = organizationId, + CollectionId = collectionId, + CipherId = cipherId, + RequesterId = requesterId, + Status = AccessLeaseStatus.Active, + NotBefore = notBefore, + NotAfter = notAfter, + }; + return (request, decision, lease); + } +} diff --git a/test/Infrastructure.IntegrationTest/Pam/Repositories/AccessRequestExtensionRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Pam/Repositories/AccessRequestExtensionRepositoryTests.cs new file mode 100644 index 000000000000..ab2b8bca8bf8 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/Pam/Repositories/AccessRequestExtensionRepositoryTests.cs @@ -0,0 +1,208 @@ +using Bit.Core.Repositories; +using Bit.Core.Utilities; +using Bit.Infrastructure.IntegrationTest.AdminConsole; +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Pam.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.Pam.Repositories; + +public class AccessRequestExtensionRepositoryTests +{ + [DatabaseTheory, DatabaseData] + public async Task CreateApprovedExtensionAsync_ExtendsLeaseInPlaceAndRecordsRequest( + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository, + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + var now = DateTime.UtcNow; + var requesterId = Guid.NewGuid(); + + var lease = await CreateActiveLeaseAsync( + accessRequestRepository, accessLeaseRepository, organization.Id, collection.Id, requesterId, now); + var newNotAfter = lease.NotAfter.AddHours(1); + + var outcome = await accessRequestRepository.CreateApprovedExtensionAsync( + BuildExtension(lease, newNotAfter, now), BuildAutoDecision(now), now); + + Assert.Equal(AccessLeaseExtendOutcome.Extended, outcome); + + // The parent lease's end is pushed out in place; no new lease is minted. + var updatedLease = await accessLeaseRepository.GetByIdAsync(lease.Id); + Assert.NotNull(updatedLease); + Assert.Equal(newNotAfter, updatedLease!.NotAfter); + Assert.Equal(AccessLeaseStatus.Active, updatedLease.Status); + + // The extension is recorded as an approved request pointing at the parent lease. + Assert.Equal(1, await accessRequestRepository.CountExtensionsByLeaseIdAsync(lease.Id)); + + // An approved extension produces no lease of its own, so it must not surface as a startable approval. + Assert.Null(await accessRequestRepository.GetActiveApprovedByRequesterIdCipherIdAsync( + requesterId, lease.CipherId, now)); + } + + [DatabaseTheory, DatabaseData] + public async Task CreateApprovedExtensionAsync_SecondExtension_ReturnsAlreadyExtended( + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository, + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + var now = DateTime.UtcNow; + var requesterId = Guid.NewGuid(); + + var lease = await CreateActiveLeaseAsync( + accessRequestRepository, accessLeaseRepository, organization.Id, collection.Id, requesterId, now); + + var firstNotAfter = lease.NotAfter.AddHours(1); + Assert.Equal(AccessLeaseExtendOutcome.Extended, await accessRequestRepository.CreateApprovedExtensionAsync( + BuildExtension(lease, firstNotAfter, now), BuildAutoDecision(now), now)); + + // A lease may be extended exactly once, so a second extension is rejected and nothing is written. + var rejected = await accessRequestRepository.CreateApprovedExtensionAsync( + BuildExtension(lease, firstNotAfter.AddHours(1), now), BuildAutoDecision(now), now); + + Assert.Equal(AccessLeaseExtendOutcome.AlreadyExtended, rejected); + Assert.Equal(1, await accessRequestRepository.CountExtensionsByLeaseIdAsync(lease.Id)); + var updatedLease = await accessLeaseRepository.GetByIdAsync(lease.Id); + Assert.Equal(firstNotAfter, updatedLease!.NotAfter); + } + + [DatabaseTheory, DatabaseData] + public async Task CreateApprovedExtensionAsync_LeaseNotActive_ReturnsLeaseNotActiveAndWritesNothing( + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository, + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + var now = DateTime.UtcNow; + var requesterId = Guid.NewGuid(); + + var lease = await CreateActiveLeaseAsync( + accessRequestRepository, accessLeaseRepository, organization.Id, collection.Id, requesterId, now); + // Revoke the lease so it is no longer active. + await accessLeaseRepository.RevokeAsync(lease, BuildHumanDecision(lease.AccessRequestId, now), now); + + var extension = BuildExtension(lease, lease.NotAfter.AddHours(1), now); + var outcome = await accessRequestRepository.CreateApprovedExtensionAsync( + extension, BuildAutoDecision(now), now); + + Assert.Equal(AccessLeaseExtendOutcome.LeaseNotActive, outcome); + Assert.Equal(0, await accessRequestRepository.CountExtensionsByLeaseIdAsync(lease.Id)); + Assert.Null(await accessRequestRepository.GetByIdAsync(extension.Id)); + } + + [DatabaseTheory, DatabaseData] + public async Task CountExtensionsByLeaseIdAsync_CountsOnlyThatLease( + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository, + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + var now = DateTime.UtcNow; + + var leaseA = await CreateActiveLeaseAsync( + accessRequestRepository, accessLeaseRepository, organization.Id, collection.Id, Guid.NewGuid(), now); + var leaseB = await CreateActiveLeaseAsync( + accessRequestRepository, accessLeaseRepository, organization.Id, collection.Id, Guid.NewGuid(), now); + + // Extend only leaseA (a lease may be extended once); the count is scoped to its own lease. + await accessRequestRepository.CreateApprovedExtensionAsync( + BuildExtension(leaseA, leaseA.NotAfter.AddHours(1), now), BuildAutoDecision(now), now); + + Assert.Equal(1, await accessRequestRepository.CountExtensionsByLeaseIdAsync(leaseA.Id)); + Assert.Equal(0, await accessRequestRepository.CountExtensionsByLeaseIdAsync(leaseB.Id)); + } + + private static async Task CreateActiveLeaseAsync( + IAccessRequestRepository accessRequestRepository, IAccessLeaseRepository accessLeaseRepository, + Guid organizationId, Guid collectionId, Guid requesterId, DateTime now) + { + var approved = await accessRequestRepository.CreateAsync(new AccessRequest + { + OrganizationId = organizationId, + CollectionId = collectionId, + CipherId = Guid.NewGuid(), + RequesterId = requesterId, + NotBefore = now.AddMinutes(-5), + NotAfter = now.AddHours(1), + Reason = "audit", + Status = AccessRequestStatus.Approved, + CreationDate = now, + ResolvedDate = now, + }); + + var lease = new AccessLease + { + Id = CoreHelpers.GenerateComb(), + AccessRequestId = approved.Id, + OrganizationId = approved.OrganizationId, + CollectionId = approved.CollectionId, + CipherId = approved.CipherId, + RequesterId = approved.RequesterId, + Status = AccessLeaseStatus.Active, + NotBefore = approved.NotBefore, + NotAfter = approved.NotAfter, + CreationDate = now, + }; + Assert.Equal(AccessLeaseMintOutcome.Minted, + await accessLeaseRepository.CreateFromApprovedRequestAsync(lease, now, false)); + return lease; + } + + private static AccessRequest BuildExtension(AccessLease lease, DateTime newNotAfter, DateTime now) + { + var extension = new AccessRequest + { + ExtensionOfLeaseId = lease.Id, + OrganizationId = lease.OrganizationId, + CollectionId = lease.CollectionId, + CipherId = lease.CipherId, + RequesterId = lease.RequesterId, + NotBefore = lease.NotAfter, + NotAfter = newNotAfter, + Reason = "need more time", + Status = AccessRequestStatus.Approved, + CreationDate = now, + ResolvedDate = now, + }; + extension.SetNewId(); + return extension; + } + + private static AccessDecision BuildAutoDecision(DateTime now) + { + var decision = new AccessDecision + { + DeciderKind = AccessDeciderKind.Automatic, + Verdict = AccessDecisionVerdict.Approve, + CreationDate = now, + }; + decision.SetNewId(); + return decision; + } + + private static AccessDecision BuildHumanDecision(Guid accessRequestId, DateTime now) + { + var decision = new AccessDecision + { + AccessRequestId = accessRequestId, + DeciderKind = AccessDeciderKind.Human, + ApproverId = Guid.NewGuid(), + Verdict = AccessDecisionVerdict.Deny, + CreationDate = now, + }; + decision.SetNewId(); + return decision; + } +} diff --git a/test/Infrastructure.IntegrationTest/Pam/Repositories/AccessRequestRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Pam/Repositories/AccessRequestRepositoryTests.cs new file mode 100644 index 000000000000..d631873c7137 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/Pam/Repositories/AccessRequestRepositoryTests.cs @@ -0,0 +1,532 @@ +using Bit.Core.Repositories; +using Bit.Core.Utilities; +using Bit.Infrastructure.IntegrationTest.AdminConsole; +using Bit.Pam.Entities; +using Bit.Pam.Enums; +using Bit.Pam.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.Pam.Repositories; + +public class AccessRequestRepositoryTests +{ + [DatabaseTheory, DatabaseData] + public async Task GetManyInboxPendingByCollectionIdsAsync_ReturnsPendingWithDenormalizedFields( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository, + IAccessRequestRepository accessRequestRepository) + { + var requester = await userRepository.CreateTestUserAsync("requester"); + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + var now = DateTime.UtcNow; + + var pending = await accessRequestRepository.CreateAsync(BuildRequest( + organization.Id, collection.Id, requester.Id, AccessRequestStatus.Pending, now)); + // A resolved request on the same collection must NOT appear in the pending inbox. + await accessRequestRepository.CreateAsync(BuildRequest( + organization.Id, collection.Id, requester.Id, AccessRequestStatus.Denied, now)); + + var pendingRows = await accessRequestRepository.GetManyInboxPendingByCollectionIdsAsync([collection.Id]); + + var row = Assert.Single(pendingRows); + Assert.Equal(pending.Id, row.Id); + Assert.Equal(AccessRequestStatus.Pending, row.Status); + Assert.Equal(collection.Name, row.CollectionName); + Assert.Equal(requester.Email, row.RequesterEmail); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManyInboxPendingByCollectionIdsAsync_OtherCollection_NotReturned( + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository, + IAccessRequestRepository accessRequestRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + var now = DateTime.UtcNow; + await accessRequestRepository.CreateAsync(BuildRequest( + organization.Id, collection.Id, Guid.NewGuid(), AccessRequestStatus.Pending, now)); + + var rows = await accessRequestRepository.GetManyInboxPendingByCollectionIdsAsync([Guid.NewGuid()]); + + Assert.Empty(rows); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManyInboxHistoryByCollectionIdsAsync_RespectsStatusAndWindow( + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository, + IAccessRequestRepository accessRequestRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + var now = DateTime.UtcNow; + + var resolved = await accessRequestRepository.CreateAsync(BuildRequest( + organization.Id, collection.Id, Guid.NewGuid(), AccessRequestStatus.Approved, now)); + // Pending requests are excluded from history. + await accessRequestRepository.CreateAsync(BuildRequest( + organization.Id, collection.Id, Guid.NewGuid(), AccessRequestStatus.Pending, now)); + // A resolved request older than the window is excluded. + await accessRequestRepository.CreateAsync(BuildRequest( + organization.Id, collection.Id, Guid.NewGuid(), AccessRequestStatus.Denied, now.AddDays(-120))); + + var history = await accessRequestRepository.GetManyInboxHistoryByCollectionIdsAsync( + [collection.Id], now.AddDays(-90)); + + var row = Assert.Single(history); + Assert.Equal(resolved.Id, row.Id); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManyInboxHistoryByCollectionIdsAsync_SurfacesProducedLeaseStatus( + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository, + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + var now = DateTime.UtcNow; + + var approved = BuildRequest(organization.Id, collection.Id, Guid.NewGuid(), AccessRequestStatus.Approved, now); + approved.NotBefore = now.AddHours(-1); + approved.NotAfter = now.AddHours(1); + approved = await accessRequestRepository.CreateAsync(approved); + + var lease = new AccessLease + { + Id = CoreHelpers.GenerateComb(), + AccessRequestId = approved.Id, + OrganizationId = approved.OrganizationId, + CollectionId = approved.CollectionId, + CipherId = approved.CipherId, + RequesterId = approved.RequesterId, + Status = AccessLeaseStatus.Active, + NotBefore = approved.NotBefore, + NotAfter = approved.NotAfter, + CreationDate = now, + }; + Assert.Equal(AccessLeaseMintOutcome.Minted, + await accessLeaseRepository.CreateFromApprovedRequestAsync(lease, now, false)); + + // While the lease is active the inbox sees its Active status, so the client offers Revoke. + var active = Assert.Single(await accessRequestRepository.GetManyInboxHistoryByCollectionIdsAsync( + [collection.Id], now.AddDays(-1))); + Assert.Equal(lease.Id, active.ProducedLeaseId); + Assert.Equal(AccessLeaseStatus.Active, active.ProducedLeaseStatus); + + // After the lease ends the inbox sees the Revoked status (the window is unchanged), so the client can keep + // the row out of the Active group and stop offering a Revoke that the server would now reject. + var auditDecision = new AccessDecision + { + Id = CoreHelpers.GenerateComb(), + AccessRequestId = approved.Id, + DeciderKind = AccessDeciderKind.Human, + ApproverId = Guid.NewGuid(), + Verdict = AccessDecisionVerdict.Deny, + CreationDate = now, + }; + await accessLeaseRepository.RevokeAsync(lease, auditDecision, now); + + var revoked = Assert.Single(await accessRequestRepository.GetManyInboxHistoryByCollectionIdsAsync( + [collection.Id], now.AddDays(-1))); + Assert.Equal(lease.Id, revoked.ProducedLeaseId); + Assert.Equal(AccessLeaseStatus.Revoked, revoked.ProducedLeaseStatus); + } + + [DatabaseTheory, DatabaseData] + public async Task ResolveWithDecisionAsync_Approve_ResolvesRequestAndRecordsDecisionWithoutMintingLease( + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository, + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + var now = DateTime.UtcNow; + var approverId = Guid.NewGuid(); + + var request = await accessRequestRepository.CreateAsync(new AccessRequest + { + OrganizationId = organization.Id, + CollectionId = collection.Id, + CipherId = Guid.NewGuid(), + RequesterId = Guid.NewGuid(), + NotBefore = now.AddHours(-1), + NotAfter = now.AddHours(1), + Reason = "audit", + Status = AccessRequestStatus.Pending, + CreationDate = now, + }); + + var decision = new AccessDecision + { + Id = CoreHelpers.GenerateComb(), + AccessRequestId = request.Id, + DeciderKind = AccessDeciderKind.Human, + ApproverId = approverId, + Verdict = AccessDecisionVerdict.Approve, + Comment = "approved for audit", + CreationDate = now, + }; + + await accessRequestRepository.ResolveWithDecisionAsync(request, decision, AccessRequestStatus.Approved, now); + + var persisted = await accessRequestRepository.GetByIdAsync(request.Id); + Assert.NotNull(persisted); + Assert.Equal(AccessRequestStatus.Approved, persisted!.Status); + Assert.NotNull(persisted.ResolvedDate); + + // The human decision surfaces as a single element of the inbox projection's decision log. + var history = await accessRequestRepository.GetManyInboxHistoryByCollectionIdsAsync( + [collection.Id], now.AddDays(-1)); + var row = Assert.Single(history); + var recorded = Assert.Single(row.Decisions); + Assert.Equal(AccessDeciderKind.Human, recorded.DeciderKind); + Assert.Equal(approverId, recorded.Id!.Value); + Assert.Equal("approved for audit", recorded.Comment); + // Verdict and decision timestamp come straight from the AccessDecision row, so the contract exposes what each + // approver decided and when. + Assert.Equal(AccessDecisionVerdict.Approve, recorded.Verdict); + Assert.Equal(now, recorded.DecidedAt); + // The approver id here belongs to no User row, so the identity join yields null name/email and the client + // falls back to the id. Identity resolution against a real User is covered by the My Requests read test. + Assert.Null(recorded.Name); + Assert.Null(recorded.Email); + + // Approval records the verdict only: no lease exists until the requester activates the approved request, + // so the requester does not yet hold access and the inbox row carries no produced lease. + Assert.Null(row.ProducedLeaseId); + Assert.Null(row.ProducedLeaseStatus); + Assert.Null(await accessLeaseRepository.GetByAccessRequestIdAsync(request.Id)); + Assert.Null(await accessLeaseRepository.GetActiveByRequesterIdCipherIdAsync( + request.RequesterId, request.CipherId, now)); + + // The approved request is now the requester's startable approval for this cipher. + var approved = await accessRequestRepository.GetActiveApprovedByRequesterIdCipherIdAsync( + request.RequesterId, request.CipherId, now); + Assert.NotNull(approved); + Assert.Equal(request.Id, approved!.Id); + } + + [DatabaseTheory, DatabaseData] + public async Task ResolveWithDecisionAsync_Deny_ResolvesWithoutLease( + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository, + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + var now = DateTime.UtcNow; + + var request = await accessRequestRepository.CreateAsync(new AccessRequest + { + OrganizationId = organization.Id, + CollectionId = collection.Id, + CipherId = Guid.NewGuid(), + RequesterId = Guid.NewGuid(), + NotBefore = now.AddHours(-1), + NotAfter = now.AddHours(1), + Reason = "audit", + Status = AccessRequestStatus.Pending, + CreationDate = now, + }); + + var decision = new AccessDecision + { + Id = CoreHelpers.GenerateComb(), + AccessRequestId = request.Id, + DeciderKind = AccessDeciderKind.Human, + ApproverId = Guid.NewGuid(), + Verdict = AccessDecisionVerdict.Deny, + CreationDate = now, + }; + + await accessRequestRepository.ResolveWithDecisionAsync(request, decision, AccessRequestStatus.Denied, now); + + var persisted = await accessRequestRepository.GetByIdAsync(request.Id); + Assert.Equal(AccessRequestStatus.Denied, persisted!.Status); + + // A denial grants nothing: no active lease exists for the requester. + var active = await accessLeaseRepository.GetActiveByRequesterIdCipherIdAsync(request.RequesterId, request.CipherId, now); + Assert.Null(active); + } + + [DatabaseTheory, DatabaseData] + public async Task GetActiveApprovedByRequesterIdCipherIdAsync_ReturnsStartableApprovalsOnly( + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository, + IAccessRequestRepository accessRequestRepository, + IAccessLeaseRepository accessLeaseRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + var now = DateTime.UtcNow; + var requesterId = Guid.NewGuid(); + + // Approved with an open window: the startable approval the query must return. + var openWindow = BuildRequest(organization.Id, collection.Id, requesterId, AccessRequestStatus.Approved, now); + openWindow.NotBefore = now.AddHours(-1); + openWindow.NotAfter = now.AddHours(1); + var startable = await accessRequestRepository.CreateAsync(openWindow); + + var found = await accessRequestRepository.GetActiveApprovedByRequesterIdCipherIdAsync( + requesterId, startable.CipherId, now); + Assert.NotNull(found); + Assert.Equal(startable.Id, found!.Id); + + // Approved with a future window is included — the client shows the upcoming window. + var future = await accessRequestRepository.CreateAsync( + BuildRequest(organization.Id, collection.Id, requesterId, AccessRequestStatus.Approved, now)); + Assert.NotNull(await accessRequestRepository.GetActiveApprovedByRequesterIdCipherIdAsync( + requesterId, future.CipherId, now)); + + // Approved with a lapsed window is excluded — it can never be activated. + var lapsed = BuildRequest(organization.Id, collection.Id, requesterId, AccessRequestStatus.Approved, now); + lapsed.NotBefore = now.AddHours(-2); + lapsed.NotAfter = now.AddHours(-1); + lapsed = await accessRequestRepository.CreateAsync(lapsed); + Assert.Null(await accessRequestRepository.GetActiveApprovedByRequesterIdCipherIdAsync( + requesterId, lapsed.CipherId, now)); + + // Pending and denied requests are not approvals. + var pending = await accessRequestRepository.CreateAsync( + BuildRequest(organization.Id, collection.Id, requesterId, AccessRequestStatus.Pending, now)); + Assert.Null(await accessRequestRepository.GetActiveApprovedByRequesterIdCipherIdAsync( + requesterId, pending.CipherId, now)); + var denied = await accessRequestRepository.CreateAsync( + BuildRequest(organization.Id, collection.Id, requesterId, AccessRequestStatus.Denied, now)); + Assert.Null(await accessRequestRepository.GetActiveApprovedByRequesterIdCipherIdAsync( + requesterId, denied.CipherId, now)); + + // Another user's approval for the same cipher is not the caller's. + Assert.Null(await accessRequestRepository.GetActiveApprovedByRequesterIdCipherIdAsync( + Guid.NewGuid(), startable.CipherId, now)); + + // Once the approval produces a lease it is activated, not approved, and leaves this read. + var lease = new AccessLease + { + Id = CoreHelpers.GenerateComb(), + AccessRequestId = startable.Id, + OrganizationId = startable.OrganizationId, + CollectionId = startable.CollectionId, + CipherId = startable.CipherId, + RequesterId = startable.RequesterId, + Status = AccessLeaseStatus.Active, + NotBefore = startable.NotBefore, + NotAfter = startable.NotAfter, + CreationDate = now, + }; + Assert.Equal(AccessLeaseMintOutcome.Minted, + await accessLeaseRepository.CreateFromApprovedRequestAsync(lease, now, false)); + Assert.Null(await accessRequestRepository.GetActiveApprovedByRequesterIdCipherIdAsync( + requesterId, startable.CipherId, now)); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManyByRequesterIdAsync_ReturnsOwnRequestsRegardlessOfStatus( + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository, + IAccessRequestRepository accessRequestRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + var now = DateTime.UtcNow; + var requesterId = Guid.NewGuid(); + + var pending = await accessRequestRepository.CreateAsync(BuildRequest( + organization.Id, collection.Id, requesterId, AccessRequestStatus.Pending, now)); + var denied = await accessRequestRepository.CreateAsync(BuildRequest( + organization.Id, collection.Id, requesterId, AccessRequestStatus.Denied, now.AddMinutes(-1))); + // A different user's request on the same collection must not appear. + await accessRequestRepository.CreateAsync(BuildRequest( + organization.Id, collection.Id, Guid.NewGuid(), AccessRequestStatus.Pending, now)); + + var mine = await accessRequestRepository.GetManyByRequesterIdAsync(requesterId); + + Assert.Equal(2, mine.Count); + Assert.Contains(mine, r => r.Id == pending.Id); + Assert.Contains(mine, r => r.Id == denied.Id); + // Caller-scoped self-read omits the display-name joins. + Assert.All(mine, r => Assert.Null(r.CollectionName)); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManyByRequesterIdAsync_ResolvesHumanApproverIdentity( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository, + IAccessRequestRepository accessRequestRepository) + { + // The requester's own list names who decided the request. The collection/cipher/requester joins stay + // omitted (those names come from the caller's vault), but the approver identity must resolve from the + // human decision's User so the client shows a name instead of a raw id. + var approver = await userRepository.CreateTestUserAsync("approver"); + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + var now = DateTime.UtcNow; + var requesterId = Guid.NewGuid(); + + var request = await accessRequestRepository.CreateAsync(BuildRequest( + organization.Id, collection.Id, requesterId, AccessRequestStatus.Pending, now)); + + var decision = new AccessDecision + { + Id = CoreHelpers.GenerateComb(), + AccessRequestId = request.Id, + DeciderKind = AccessDeciderKind.Human, + ApproverId = approver.Id, + Verdict = AccessDecisionVerdict.Deny, + Comment = "not now", + CreationDate = now, + }; + await accessRequestRepository.ResolveWithDecisionAsync(request, decision, AccessRequestStatus.Denied, now); + + var mine = await accessRequestRepository.GetManyByRequesterIdAsync(requesterId); + + var row = Assert.Single(mine); + var resolver = Assert.Single(row.Decisions); + Assert.Equal(AccessDeciderKind.Human, resolver.DeciderKind); + Assert.Equal(approver.Id, resolver.Id!.Value); + Assert.Equal(approver.Name, resolver.Name); + Assert.Equal(approver.Email, resolver.Email); + Assert.Equal("not now", resolver.Comment); + Assert.Equal(AccessDecisionVerdict.Deny, resolver.Verdict); + Assert.Equal(now, resolver.DecidedAt); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManyInboxHistoryByCollectionIdsAsync_MultipleHumanDecisions_ProjectsFullHistoryOldestFirst( + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository, + IAccessRequestRepository accessRequestRepository) + { + // AccessDecision is 1-to-many with AccessRequest, so the approvers array carries every human decision oldest + // first: an approval followed by a managing approver retracting the unactivated approval surfaces both, rather + // than collapsing to a single resolver. + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + var now = DateTime.UtcNow; + var firstApproverId = Guid.NewGuid(); + var secondApproverId = Guid.NewGuid(); + + var request = await accessRequestRepository.CreateAsync(new AccessRequest + { + OrganizationId = organization.Id, + CollectionId = collection.Id, + CipherId = Guid.NewGuid(), + RequesterId = Guid.NewGuid(), + NotBefore = now.AddHours(-1), + NotAfter = now.AddHours(1), + Reason = "audit", + Status = AccessRequestStatus.Pending, + CreationDate = now, + }); + + // First decision: approve. + await accessRequestRepository.ResolveWithDecisionAsync( + request, + new AccessDecision + { + Id = CoreHelpers.GenerateComb(), + AccessRequestId = request.Id, + DeciderKind = AccessDeciderKind.Human, + ApproverId = firstApproverId, + Verdict = AccessDecisionVerdict.Approve, + Comment = "approved", + CreationDate = now, + }, + AccessRequestStatus.Approved, + now); + + // Second decision: a managing approver retracts the still-unactivated approval (records a Deny). + await accessRequestRepository.CancelWithDecisionAsync( + request, + new AccessDecision + { + Id = CoreHelpers.GenerateComb(), + AccessRequestId = request.Id, + DeciderKind = AccessDeciderKind.Human, + ApproverId = secondApproverId, + Verdict = AccessDecisionVerdict.Deny, + Comment = "retracted", + CreationDate = now.AddMinutes(1), + }, + now.AddMinutes(1)); + + var history = await accessRequestRepository.GetManyInboxHistoryByCollectionIdsAsync( + [collection.Id], now.AddDays(-1)); + var row = Assert.Single(history); + Assert.Equal(2, row.Decisions.Count); + Assert.Equal(AccessDeciderKind.Human, row.Decisions[0].DeciderKind); + Assert.Equal(firstApproverId, row.Decisions[0].Id!.Value); + Assert.Equal(AccessDecisionVerdict.Approve, row.Decisions[0].Verdict); + Assert.Equal("approved", row.Decisions[0].Comment); + Assert.Equal(secondApproverId, row.Decisions[1].Id!.Value); + Assert.Equal(AccessDecisionVerdict.Deny, row.Decisions[1].Verdict); + Assert.Equal("retracted", row.Decisions[1].Comment); + } + + [DatabaseTheory, DatabaseData] + public async Task CancelAsync_PendingRequest_TransitionsToCancelledAndStampsResolvedDate( + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository, + IAccessRequestRepository accessRequestRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + var now = DateTime.UtcNow; + + var request = await accessRequestRepository.CreateAsync(BuildRequest( + organization.Id, collection.Id, Guid.NewGuid(), AccessRequestStatus.Pending, now)); + + var resolvedAt = now.AddMinutes(5); + await accessRequestRepository.CancelAsync(request.Id, resolvedAt); + + var persisted = await accessRequestRepository.GetByIdAsync(request.Id); + Assert.NotNull(persisted); + Assert.Equal(AccessRequestStatus.Cancelled, persisted!.Status); + Assert.NotNull(persisted.ResolvedDate); + } + + [DatabaseTheory, DatabaseData] + public async Task CancelAsync_AlreadyResolvedRequest_LeavesItUntouched( + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository, + IAccessRequestRepository accessRequestRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + var now = DateTime.UtcNow; + + // The proc only acts on Pending rows, so a request that already left Pending (e.g. approved) is never + // clobbered into Cancelled by a stray/raced cancel. + var approved = await accessRequestRepository.CreateAsync(BuildRequest( + organization.Id, collection.Id, Guid.NewGuid(), AccessRequestStatus.Approved, now)); + + await accessRequestRepository.CancelAsync(approved.Id, now.AddMinutes(5)); + + var persisted = await accessRequestRepository.GetByIdAsync(approved.Id); + Assert.Equal(AccessRequestStatus.Approved, persisted!.Status); + } + + private static AccessRequest BuildRequest( + Guid organizationId, Guid collectionId, Guid requesterId, AccessRequestStatus status, DateTime creationDate) + => new() + { + OrganizationId = organizationId, + CollectionId = collectionId, + CipherId = Guid.NewGuid(), + RequesterId = requesterId, + NotBefore = creationDate.AddHours(1), + NotAfter = creationDate.AddHours(2), + Reason = "audit", + Status = status, + CreationDate = creationDate, + ResolvedDate = status == AccessRequestStatus.Pending ? null : creationDate, + }; +} diff --git a/test/Infrastructure.IntegrationTest/Pam/Repositories/AccessRuleRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Pam/Repositories/AccessRuleRepositoryTests.cs new file mode 100644 index 000000000000..ca7768b2b9ae --- /dev/null +++ b/test/Infrastructure.IntegrationTest/Pam/Repositories/AccessRuleRepositoryTests.cs @@ -0,0 +1,53 @@ +using Bit.Core.Entities; +using Bit.Core.Repositories; +using Bit.Infrastructure.IntegrationTest.AdminConsole; +using Bit.Pam.Entities; +using Bit.Pam.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.Pam.Repositories; + +public class AccessRuleRepositoryTests +{ + [DatabaseTheory, DatabaseData] + public async Task DeleteAsync_WithGovernedCollections_ClearsAssociationsAndKeepsCollections( + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository, + IAccessRuleRepository accessRuleRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + + var rule = await accessRuleRepository.CreateAsync(new AccessRule + { + OrganizationId = organization.Id, + Name = "Test Rule", + Conditions = """{"kind":"human_approval"}""", + }); + + var collection = new Collection + { + Name = "Governed Collection", + OrganizationId = organization.Id, + }; + await collectionRepository.CreateAsync(collection, [], []); + + await accessRuleRepository.SetCollectionAssociationsAsync( + organization.Id, rule.Id, [collection.Id], []); + + // Sanity check: the collection is governed by the rule before deletion. + var details = await accessRuleRepository.GetDetailsByIdAsync(rule.Id); + Assert.NotNull(details); + Assert.Contains(collection.Id, details.CollectionIds); + + // Act + await accessRuleRepository.DeleteAsync(rule); + + // Assert: the rule is gone, but the collection survives with its association cleared. + Assert.Null(await accessRuleRepository.GetByIdAsync(rule.Id)); + + var actualCollection = await collectionRepository.GetByIdAsync(collection.Id); + Assert.NotNull(actualCollection); + Assert.Null(actualCollection.AccessRuleId); + } +} diff --git a/test/Infrastructure.IntegrationTest/packages.lock.json b/test/Infrastructure.IntegrationTest/packages.lock.json index 32f32f7fdf03..93cd14f2a077 100644 --- a/test/Infrastructure.IntegrationTest/packages.lock.json +++ b/test/Infrastructure.IntegrationTest/packages.lock.json @@ -18,6 +18,17 @@ "Microsoft.Extensions.Primitives": "10.0.8" } }, + "Microsoft.Extensions.Diagnostics.Testing": { + "type": "Direct", + "requested": "[10.5.0, 10.5.0]", + "resolved": "10.5.0", + "contentHash": "EiERK24AmaxMa7hkgV5UqdfIlrMxg3n+7XdkaV3FWLoyEzca6gvGiaRZofl6KzAtvMBJoZvb5a+EyYqq+M9CoQ==", + "dependencies": { + "Microsoft.Extensions.Logging": "10.0.6", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.6", + "Microsoft.Extensions.Telemetry.Abstractions": "10.5.0" + } + }, "Microsoft.Extensions.Logging": { "type": "Direct", "requested": "[10.0.8, 10.0.8]", @@ -731,6 +742,15 @@ "StackExchange.Redis": "2.7.27" } }, + "Microsoft.Extensions.Compliance.Abstractions": { + "type": "Transitive", + "resolved": "10.5.0", + "contentHash": "xbWZji13Vb2jDJNtwVrKpI09jd8x3n3fL+GzhiLK+8O5Wc2A+GyqCZalST2fV46Pf0QfCwkXf83y+3/rDkCd7A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.6", + "Microsoft.Extensions.ObjectPool": "10.0.6" + } + }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "10.0.8", @@ -905,6 +925,11 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8" } }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "10.0.6", + "contentHash": "2Jafd4fdxxiwiQ08mcF+Lf3vqikkQZusGVThOKZNSmPDceGk4IwkjeHL7OEb9Ov8q9ICY5wofL98CS153K5VvQ==" + }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "10.0.8", @@ -931,6 +956,17 @@ "resolved": "10.0.8", "contentHash": "OBPo4nYhMyIbtueoC10CBm6AGAbo/A9IV8QQ/6ryZS7VvmqpGT7hunazeHLxFawRzn3oLOq4jhqhpBX4tfswWQ==" }, + "Microsoft.Extensions.Telemetry.Abstractions": { + "type": "Transitive", + "resolved": "10.5.0", + "contentHash": "VmU7e6xHqoubWKl7y9MtWyQAjlDpvbds3gY8ZKMS/1GxY2+U1/aMNnMj09aOXAa3p5qhHSSkBzDJvyokCjVkPg==", + "dependencies": { + "Microsoft.Extensions.Compliance.Abstractions": "10.5.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.6", + "Microsoft.Extensions.ObjectPool": "10.0.6", + "Microsoft.Extensions.Options": "10.0.6" + } + }, "Microsoft.Identity.Client": { "type": "Transitive", "resolved": "4.66.1", @@ -1478,6 +1514,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1502,6 +1539,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1515,22 +1553,27 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" @@ -1539,7 +1582,7 @@ "migrator": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "Microsoft.Extensions.Logging": "[10.0.8, 10.0.8]", "dbup-sqlserver": "[7.2.0, 7.2.0]" } @@ -1547,22 +1590,28 @@ "mysqlmigrations": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" + } + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" } }, "postgresmigrations": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" } }, "sqlitemigrations": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" } } } diff --git a/test/IntegrationTestCommon/packages.lock.json b/test/IntegrationTestCommon/packages.lock.json index 162fbede3ae2..d5af571cc619 100644 --- a/test/IntegrationTestCommon/packages.lock.json +++ b/test/IntegrationTestCommon/packages.lock.json @@ -1700,7 +1700,7 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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, )", @@ -1724,6 +1724,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1748,6 +1749,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1761,10 +1763,13 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "identity": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", @@ -1772,25 +1777,27 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )" + "SharedWeb": "[2026.6.1, )" } }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" @@ -1799,11 +1806,17 @@ "migrator": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "Microsoft.Extensions.Logging": "[10.0.8, 10.0.8]", "dbup-sqlserver": "[7.2.0, 7.2.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "rustsdk": { "type": "Project" }, @@ -1811,18 +1824,18 @@ "type": "Project", "dependencies": { "Bogus": "[35.6.5, 35.6.5]", - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", - "RustSdk": "[2026.6.0, )", - "SharedWeb": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", + "RustSdk": "[2026.6.1, )", + "SharedWeb": "[2026.6.1, )" } }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/test/Notifications.Test/packages.lock.json b/test/Notifications.Test/packages.lock.json index 36524d1c1af9..33215360611c 100644 --- a/test/Notifications.Test/packages.lock.json +++ b/test/Notifications.Test/packages.lock.json @@ -1693,7 +1693,7 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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, )", @@ -1717,6 +1717,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1741,6 +1742,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1759,31 +1761,37 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Common": "[2026.6.0, )", - "Core": "[2026.6.0, )", + "Common": "[2026.6.1, )", + "Core": "[2026.6.1, )", "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, )", + "Pam.Domain": "[2026.6.1, )", "xunit": "[2.6.6, )" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" @@ -1792,7 +1800,7 @@ "notifications": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "MessagePack": "[3.1.7, 3.1.7]", "Microsoft.AspNetCore.SignalR.Protocols.MessagePack": "[10.0.8, 10.0.8]", "Microsoft.AspNetCore.SignalR.StackExchangeRedis": "[10.0.8, 10.0.8]", @@ -1803,15 +1811,21 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )" + "SharedWeb": "[2026.6.1, )" + } + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" } }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/test/SeederApi.IntegrationTest/packages.lock.json b/test/SeederApi.IntegrationTest/packages.lock.json index b847360b1cf6..505efd8c8a48 100644 --- a/test/SeederApi.IntegrationTest/packages.lock.json +++ b/test/SeederApi.IntegrationTest/packages.lock.json @@ -1275,7 +1275,7 @@ "dependencies": { "AutoFixture.AutoNSubstitute": "[4.18.1, )", "AutoFixture.Xunit2": "[4.18.1, )", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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, )", @@ -1299,6 +1299,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1323,6 +1324,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1336,10 +1338,13 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "identity": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", @@ -1347,25 +1352,27 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )" + "SharedWeb": "[2026.6.1, )" } }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" @@ -1374,21 +1381,27 @@ "integrationtestcommon": { "type": "Project", "dependencies": { - "Common": "[2026.6.0, )", - "Identity": "[2026.6.0, )", + "Common": "[2026.6.1, )", + "Identity": "[2026.6.1, )", "Microsoft.AspNetCore.Mvc.Testing": "[10.0.8, 10.0.8]", - "Migrator": "[2026.6.0, )", - "Seeder": "[2026.6.0, )" + "Migrator": "[2026.6.1, )", + "Seeder": "[2026.6.1, )" } }, "migrator": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "Microsoft.Extensions.Logging": "[10.0.8, 10.0.8]", "dbup-sqlserver": "[7.2.0, 7.2.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "rustsdk": { "type": "Project" }, @@ -1396,26 +1409,26 @@ "type": "Project", "dependencies": { "Bogus": "[35.6.5, 35.6.5]", - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", - "RustSdk": "[2026.6.0, )", - "SharedWeb": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", + "RustSdk": "[2026.6.1, )", + "SharedWeb": "[2026.6.1, )" } }, "seederapi": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Seeder": "[2026.6.0, )", - "SharedWeb": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Seeder": "[2026.6.1, )", + "SharedWeb": "[2026.6.1, )" } }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/test/Setup.Test/packages.lock.json b/test/Setup.Test/packages.lock.json index 0f05575946e7..c21d7bb67c57 100644 --- a/test/Setup.Test/packages.lock.json +++ b/test/Setup.Test/packages.lock.json @@ -1379,6 +1379,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1403,6 +1404,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1416,21 +1418,30 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "migrator": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "Microsoft.Extensions.Logging": "[10.0.8, 10.0.8]", "dbup-sqlserver": "[7.2.0, 7.2.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "setup": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "Handlebars.Net": "[2.1.6, 2.1.6]", "Microsoft.Extensions.Logging": "[10.0.8, 10.0.8]", - "Migrator": "[2026.6.0, )", + "Migrator": "[2026.6.1, )", "YamlDotNet": "[11.2.1, 11.2.1]" } } diff --git a/test/SharedWeb.Test/packages.lock.json b/test/SharedWeb.Test/packages.lock.json index abcb038d1383..17d9d38886a8 100644 --- a/test/SharedWeb.Test/packages.lock.json +++ b/test/SharedWeb.Test/packages.lock.json @@ -1492,6 +1492,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1516,6 +1517,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1529,33 +1531,44 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/util/Migrator/DbScripts/2026-05-21_00_AddAccessRule.sql b/util/Migrator/DbScripts/2026-05-21_00_AddAccessRule.sql new file mode 100644 index 000000000000..ced250000f83 --- /dev/null +++ b/util/Migrator/DbScripts/2026-05-21_00_AddAccessRule.sql @@ -0,0 +1,871 @@ +-- Create the AccessRule table +IF OBJECT_ID('[dbo].[AccessRule]') IS NULL +BEGIN + CREATE TABLE [dbo].[AccessRule] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [Name] NVARCHAR(256) NOT NULL, + [Description] NVARCHAR(MAX) NULL, + [Conditions] NVARCHAR(MAX) NOT NULL, + [CreationDate] DATETIME2(7) NOT NULL, + [RevisionDate] DATETIME2(7) NOT NULL, + CONSTRAINT [PK_AccessRule] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_AccessRule_Organization] FOREIGN KEY ([OrganizationId]) + REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE + ); + + CREATE UNIQUE NONCLUSTERED INDEX [IX_AccessRule_OrganizationId_Name] + ON [dbo].[AccessRule] ([OrganizationId] ASC, [Name] ASC); +END +GO + +-- Drop the previous iteration's inline leasing columns from Collection (if present) +IF EXISTS ( + SELECT 1 FROM sys.default_constraints WHERE name = 'DF_Collection_LeasingEnabled' +) +BEGIN + ALTER TABLE [dbo].[Collection] DROP CONSTRAINT [DF_Collection_LeasingEnabled]; +END +GO + +IF COL_LENGTH('[dbo].[Collection]', 'LeasingEnabled') IS NOT NULL +BEGIN + ALTER TABLE [dbo].[Collection] DROP COLUMN [LeasingEnabled]; +END +GO + +IF COL_LENGTH('[dbo].[Collection]', 'LeasingPolicy') IS NOT NULL +BEGIN + ALTER TABLE [dbo].[Collection] DROP COLUMN [LeasingPolicy]; +END +GO + +-- Add AccessRuleId FK column to Collection +IF COL_LENGTH('[dbo].[Collection]', 'AccessRuleId') IS NULL +BEGIN + ALTER TABLE [dbo].[Collection] + ADD [AccessRuleId] UNIQUEIDENTIFIER NULL + CONSTRAINT [FK_Collection_AccessRule] REFERENCES [dbo].[AccessRule] ([Id]) ON DELETE NO ACTION; +END +GO + +IF NOT EXISTS ( + SELECT 1 FROM sys.indexes WHERE name = 'IX_Collection_AccessRuleId' AND object_id = OBJECT_ID('[dbo].[Collection]') +) +BEGIN + CREATE NONCLUSTERED INDEX [IX_Collection_AccessRuleId] + ON [dbo].[Collection] ([AccessRuleId] ASC); +END +GO + +-- Refresh modules that depend on the Collection schema +IF OBJECT_ID('[dbo].[CollectionView]') IS NOT NULL +BEGIN + EXECUTE sp_refreshsqlmodule N'[dbo].[CollectionView]'; +END +GO + +IF OBJECT_ID('[dbo].[Collection_ReadById]') IS NOT NULL +BEGIN + EXECUTE sp_refreshsqlmodule N'[dbo].[Collection_ReadById]'; +END +GO + +IF OBJECT_ID('[dbo].[Collection_ReadByIds]') IS NOT NULL +BEGIN + EXECUTE sp_refreshsqlmodule N'[dbo].[Collection_ReadByIds]'; +END +GO + +IF OBJECT_ID('[dbo].[Collection_ReadByOrganizationId]') IS NOT NULL +BEGIN + EXECUTE sp_refreshsqlmodule N'[dbo].[Collection_ReadByOrganizationId]'; +END +GO + +IF OBJECT_ID('[dbo].[UserCollectionDetails]') IS NOT NULL +BEGIN + EXECUTE sp_refreshsqlmodule N'[dbo].[UserCollectionDetails]'; +END +GO + +-- AccessRule CRUD stored procedures +CREATE OR ALTER PROCEDURE [dbo].[AccessRule_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @Name NVARCHAR(256), + @Description NVARCHAR(MAX) = NULL, + @Conditions NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[AccessRule] + ( + [Id], + [OrganizationId], + [Name], + [Description], + [Conditions], + [CreationDate], + [RevisionDate] + ) + VALUES + ( + @Id, + @OrganizationId, + @Name, + @Description, + @Conditions, + @CreationDate, + @RevisionDate + ) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRule_Update] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Name NVARCHAR(256), + @Description NVARCHAR(MAX) = NULL, + @Conditions NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[AccessRule] + SET + [OrganizationId] = @OrganizationId, + [Name] = @Name, + [Description] = @Description, + [Conditions] = @Conditions, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate + WHERE + [Id] = @Id +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRule_DeleteById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DELETE FROM [dbo].[AccessRule] WHERE [Id] = @Id +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRule_ReadById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT * + FROM [dbo].[AccessRule] + WHERE [Id] = @Id +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRule_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT * + FROM [dbo].[AccessRule] + WHERE [OrganizationId] = @OrganizationId +END +GO + +-- Update Collection_Create to accept AccessRuleId +CREATE OR ALTER PROCEDURE [dbo].[Collection_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @Name VARCHAR(MAX), + @ExternalId NVARCHAR(300), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @DefaultUserCollectionEmail NVARCHAR(256) = NULL, + @Type TINYINT = 0, + @AccessRuleId UNIQUEIDENTIFIER = NULL +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[Collection] + ( + [Id], + [OrganizationId], + [Name], + [ExternalId], + [CreationDate], + [RevisionDate], + [DefaultUserCollectionEmail], + [Type], + [AccessRuleId] + ) + VALUES + ( + @Id, + @OrganizationId, + @Name, + @ExternalId, + @CreationDate, + @RevisionDate, + @DefaultUserCollectionEmail, + @Type, + @AccessRuleId + ) + + EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId +END +GO + +-- Update Collection_Update to accept AccessRuleId +CREATE OR ALTER PROCEDURE [dbo].[Collection_Update] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Name VARCHAR(MAX), + @ExternalId NVARCHAR(300), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @DefaultUserCollectionEmail NVARCHAR(256) = NULL, + @Type TINYINT = 0, + @AccessRuleId UNIQUEIDENTIFIER = NULL +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[Collection] + SET + [OrganizationId] = @OrganizationId, + [Name] = @Name, + [ExternalId] = @ExternalId, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [DefaultUserCollectionEmail] = @DefaultUserCollectionEmail, + [Type] = @Type, + [AccessRuleId] = @AccessRuleId + WHERE + [Id] = @Id + + EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId +END +GO + +-- Update Collection_CreateWithGroupsAndUsers to forward AccessRuleId +CREATE OR ALTER PROCEDURE [dbo].[Collection_CreateWithGroupsAndUsers] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Name VARCHAR(MAX), + @ExternalId NVARCHAR(300), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @Groups AS [dbo].[CollectionAccessSelectionType] READONLY, + @Users AS [dbo].[CollectionAccessSelectionType] READONLY, + @DefaultUserCollectionEmail NVARCHAR(256) = NULL, + @Type TINYINT = 0, + @AccessRuleId UNIQUEIDENTIFIER = NULL +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[Collection_Create] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type, @AccessRuleId + + -- Groups + ;WITH [AvailableGroupsCTE] AS( + SELECT + [Id] + FROM + [dbo].[Group] + WHERE + [OrganizationId] = @OrganizationId + ) + INSERT INTO [dbo].[CollectionGroup] + ( + [CollectionId], + [GroupId], + [ReadOnly], + [HidePasswords], + [Manage] + ) + SELECT + @Id, + [Id], + [ReadOnly], + [HidePasswords], + [Manage] + FROM + @Groups + WHERE + [Id] IN (SELECT [Id] FROM [AvailableGroupsCTE]) + + -- Users + ;WITH [AvailableUsersCTE] AS( + SELECT + [Id] + FROM + [dbo].[OrganizationUser] + WHERE + [OrganizationId] = @OrganizationId + ) + INSERT INTO [dbo].[CollectionUser] + ( + [CollectionId], + [OrganizationUserId], + [ReadOnly], + [HidePasswords], + [Manage] + ) + SELECT + @Id, + [Id], + [ReadOnly], + [HidePasswords], + [Manage] + FROM + @Users + WHERE + [Id] IN (SELECT [Id] FROM [AvailableUsersCTE]) + + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId +END +GO + +-- Update Collection_UpdateWithGroupsAndUsers to forward AccessRuleId +CREATE OR ALTER PROCEDURE [dbo].[Collection_UpdateWithGroupsAndUsers] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Name VARCHAR(MAX), + @ExternalId NVARCHAR(300), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @Groups AS [dbo].[CollectionAccessSelectionType] READONLY, + @Users AS [dbo].[CollectionAccessSelectionType] READONLY, + @DefaultUserCollectionEmail NVARCHAR(256) = NULL, + @Type TINYINT = 0, + @AccessRuleId UNIQUEIDENTIFIER = NULL +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type, @AccessRuleId + + -- Bump RevisionDate on all affected groups (old + new) before modifying CollectionGroup + ;WITH [AffectedGroupsCTE] AS ( + SELECT + g.[Id] + FROM + @Groups g + + UNION + + SELECT + CG.[GroupId] + FROM + [dbo].[CollectionGroup] CG + WHERE + CG.[CollectionId] = @Id + ) + UPDATE + G + SET + G.[RevisionDate] = @RevisionDate + FROM + [dbo].[Group] G + WHERE + G.[OrganizationId] = @OrganizationId + AND G.[Id] IN (SELECT [Id] FROM [AffectedGroupsCTE]) + + -- Groups + -- Delete groups that are no longer in source + DELETE cg + FROM [dbo].[CollectionGroup] cg + LEFT JOIN @Groups g ON cg.GroupId = g.Id + WHERE cg.CollectionId = @Id + AND g.Id IS NULL; + + -- Update existing groups + UPDATE cg + SET cg.ReadOnly = g.ReadOnly, + cg.HidePasswords = g.HidePasswords, + cg.Manage = g.Manage + FROM [dbo].[CollectionGroup] cg + INNER JOIN @Groups g ON cg.GroupId = g.Id + WHERE cg.CollectionId = @Id + AND (cg.ReadOnly != g.ReadOnly + OR cg.HidePasswords != g.HidePasswords + OR cg.Manage != g.Manage); + + -- Insert new groups + INSERT INTO [dbo].[CollectionGroup] + ( + [CollectionId], + [GroupId], + [ReadOnly], + [HidePasswords], + [Manage] + ) + SELECT + @Id, + g.Id, + g.ReadOnly, + g.HidePasswords, + g.Manage + FROM @Groups g + INNER JOIN [dbo].[Group] grp ON grp.Id = g.Id + LEFT JOIN [dbo].[CollectionGroup] cg + ON cg.CollectionId = @Id AND cg.GroupId = g.Id + WHERE grp.OrganizationId = @OrganizationId + AND cg.CollectionId IS NULL; + + -- Users + -- Delete users that are no longer in source + DELETE cu + FROM [dbo].[CollectionUser] cu + LEFT JOIN @Users u ON cu.OrganizationUserId = u.Id + WHERE cu.CollectionId = @Id + AND u.Id IS NULL; + + -- Update existing users + UPDATE cu + SET cu.ReadOnly = u.ReadOnly, + cu.HidePasswords = u.HidePasswords, + cu.Manage = u.Manage + FROM [dbo].[CollectionUser] cu + INNER JOIN @Users u ON cu.OrganizationUserId = u.Id + WHERE cu.CollectionId = @Id + AND (cu.ReadOnly != u.ReadOnly + OR cu.HidePasswords != u.HidePasswords + OR cu.Manage != u.Manage); + + -- Insert new users + INSERT INTO [dbo].[CollectionUser] + ( + [CollectionId], + [OrganizationUserId], + [ReadOnly], + [HidePasswords], + [Manage] + ) + SELECT + @Id, + u.Id, + u.ReadOnly, + u.HidePasswords, + u.Manage + FROM @Users u + INNER JOIN [dbo].[OrganizationUser] ou ON ou.Id = u.Id + LEFT JOIN [dbo].[CollectionUser] cu + ON cu.CollectionId = @Id AND cu.OrganizationUserId = u.Id + WHERE ou.OrganizationId = @OrganizationId + AND cu.CollectionId IS NULL; + + EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId +END +GO + +-- Update Collection_UpdateWithGroups to forward AccessRuleId +CREATE OR ALTER PROCEDURE [dbo].[Collection_UpdateWithGroups] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Name VARCHAR(MAX), + @ExternalId NVARCHAR(300), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @Groups AS [dbo].[CollectionAccessSelectionType] READONLY, + @DefaultUserCollectionEmail NVARCHAR(256) = NULL, + @Type TINYINT = 0, + @AccessRuleId UNIQUEIDENTIFIER = NULL +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type, @AccessRuleId + + -- Bump RevisionDate on all affected groups (old + new) before modifying CollectionGroup + ;WITH [AffectedGroupsCTE] AS ( + SELECT + g.[Id] + FROM + @Groups g + + UNION + + SELECT + CG.[GroupId] + FROM + [dbo].[CollectionGroup] CG + WHERE + CG.[CollectionId] = @Id + ) + UPDATE + G + SET + G.[RevisionDate] = @RevisionDate + FROM + [dbo].[Group] G + WHERE + G.[OrganizationId] = @OrganizationId + AND G.[Id] IN (SELECT [Id] FROM [AffectedGroupsCTE]) + + -- Groups + -- Delete groups that are no longer in source + DELETE + cg + FROM + [dbo].[CollectionGroup] cg + LEFT JOIN + @Groups g ON cg.GroupId = g.Id + WHERE + cg.CollectionId = @Id + AND g.Id IS NULL; + + -- Update existing groups + UPDATE + cg + SET + cg.ReadOnly = g.ReadOnly, + cg.HidePasswords = g.HidePasswords, + cg.Manage = g.Manage + FROM + [dbo].[CollectionGroup] cg + INNER JOIN + @Groups g ON cg.GroupId = g.Id + WHERE + cg.CollectionId = @Id + AND ( + cg.ReadOnly != g.ReadOnly + OR cg.HidePasswords != g.HidePasswords + OR cg.Manage != g.Manage + ); + + -- Insert new groups + INSERT INTO [dbo].[CollectionGroup] + ( + [CollectionId], + [GroupId], + [ReadOnly], + [HidePasswords], + [Manage] + ) + SELECT + @Id, + g.Id, + g.ReadOnly, + g.HidePasswords, + g.Manage + FROM + @Groups g + INNER JOIN + [dbo].[Group] grp ON grp.Id = g.Id + LEFT JOIN + [dbo].[CollectionGroup] cg ON cg.CollectionId = @Id AND cg.GroupId = g.Id + WHERE + grp.OrganizationId = @OrganizationId + AND cg.CollectionId IS NULL; + + EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId +END +GO + +-- Update Collection_UpdateWithUsers to forward AccessRuleId +CREATE OR ALTER PROCEDURE [dbo].[Collection_UpdateWithUsers] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Name VARCHAR(MAX), + @ExternalId NVARCHAR(300), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @Users AS [dbo].[CollectionAccessSelectionType] READONLY, + @DefaultUserCollectionEmail NVARCHAR(256) = NULL, + @Type TINYINT = 0, + @AccessRuleId UNIQUEIDENTIFIER = NULL +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type, @AccessRuleId + + -- Users + -- Delete users that are no longer in source + DELETE + cu + FROM + [dbo].[CollectionUser] cu + LEFT JOIN + @Users u ON cu.OrganizationUserId = u.Id + WHERE + cu.CollectionId = @Id + AND u.Id IS NULL; + + -- Update existing users + UPDATE + cu + SET + cu.ReadOnly = u.ReadOnly, + cu.HidePasswords = u.HidePasswords, + cu.Manage = u.Manage + FROM + [dbo].[CollectionUser] cu + INNER JOIN + @Users u ON cu.OrganizationUserId = u.Id + WHERE + cu.CollectionId = @Id + AND ( + cu.ReadOnly != u.ReadOnly + OR cu.HidePasswords != u.HidePasswords + OR cu.Manage != u.Manage + ); + + -- Insert new users + INSERT INTO [dbo].[CollectionUser] + ( + [CollectionId], + [OrganizationUserId], + [ReadOnly], + [HidePasswords], + [Manage] + ) + SELECT + @Id, + u.Id, + u.ReadOnly, + u.HidePasswords, + u.Manage + FROM + @Users u + INNER JOIN + [dbo].[OrganizationUser] ou ON ou.Id = u.Id + LEFT JOIN + [dbo].[CollectionUser] cu ON cu.CollectionId = @Id AND cu.OrganizationUserId = u.Id + WHERE + ou.OrganizationId = @OrganizationId + AND cu.CollectionId IS NULL; + + EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId +END +GO + +-- Update Collection_ReadByUserId to project AccessRuleId +CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadByUserId] + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + Id, + OrganizationId, + [Name], + CreationDate, + RevisionDate, + ExternalId, + MIN([ReadOnly]) AS [ReadOnly], + MIN([HidePasswords]) AS [HidePasswords], + MAX([Manage]) AS [Manage], + [DefaultUserCollectionEmail], + [Type], + [AccessRuleId] + FROM + [dbo].[UserCollectionDetails](@UserId) + GROUP BY + Id, + OrganizationId, + [Name], + CreationDate, + RevisionDate, + ExternalId, + [DefaultUserCollectionEmail], + [Type], + [AccessRuleId] +END +GO + +-- Update Collection_ReadByIdWithPermissions to GROUP BY AccessRuleId +CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadByIdWithPermissions] + @CollectionId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @IncludeAccessRelationships BIT +AS +BEGIN + SET NOCOUNT ON + + SELECT + C.*, + MIN(CASE + WHEN + COALESCE(CU.[ReadOnly], CG.[ReadOnly], 0) = 0 + THEN 0 + ELSE 1 + END) AS [ReadOnly], + MIN (CASE + WHEN + COALESCE(CU.[HidePasswords], CG.[HidePasswords], 0) = 0 + THEN 0 + ELSE 1 + END) AS [HidePasswords], + MAX(CASE + WHEN + COALESCE(CU.[Manage], CG.[Manage], 0) = 0 + THEN 0 + ELSE 1 + END) AS [Manage], + MAX(CASE + WHEN + CU.[CollectionId] IS NULL AND CG.[CollectionId] IS NULL + THEN 0 + ELSE 1 + END) AS [Assigned], + CASE + WHEN + -- No user or group has manage rights + NOT EXISTS( + SELECT 1 + FROM [dbo].[CollectionUser] CU2 + JOIN [dbo].[OrganizationUser] OU2 ON CU2.[OrganizationUserId] = OU2.[Id] + WHERE + CU2.[CollectionId] = C.[Id] AND + CU2.[Manage] = 1 + ) + AND NOT EXISTS ( + SELECT 1 + FROM [dbo].[CollectionGroup] CG2 + WHERE + CG2.[CollectionId] = C.[Id] AND + CG2.[Manage] = 1 + ) + THEN 1 + ELSE 0 + END AS [Unmanaged] + FROM + [dbo].[CollectionView] C + LEFT JOIN + [dbo].[OrganizationUser] OU ON C.[OrganizationId] = OU.[OrganizationId] AND OU.[UserId] = @UserId + LEFT JOIN + [dbo].[CollectionUser] CU ON CU.[CollectionId] = C.[Id] AND CU.[OrganizationUserId] = [OU].[Id] + LEFT JOIN + [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id] + LEFT JOIN + [dbo].[Group] G ON G.[Id] = GU.[GroupId] + LEFT JOIN + [dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId] + WHERE + C.[Id] = @CollectionId + GROUP BY + C.[Id], + C.[OrganizationId], + C.[Name], + C.[CreationDate], + C.[RevisionDate], + C.[ExternalId], + C.[DefaultUserCollectionEmail], + C.[Type], + C.[AccessRuleId] + + IF (@IncludeAccessRelationships = 1) + BEGIN + EXEC [dbo].[CollectionGroup_ReadByCollectionId] @CollectionId + EXEC [dbo].[CollectionUser_ReadByCollectionId] @CollectionId + END +END +GO + +-- Update Collection_ReadSharedCollectionsByOrganizationIdWithPermissions to GROUP BY AccessRuleId +CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadSharedCollectionsByOrganizationIdWithPermissions] + @OrganizationId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @IncludeAccessRelationships BIT +AS +BEGIN + SET NOCOUNT ON + + SELECT + C.*, + MIN(CASE + WHEN + COALESCE(CU.[ReadOnly], CG.[ReadOnly], 0) = 0 + THEN 0 + ELSE 1 + END) AS [ReadOnly], + MIN(CASE + WHEN + COALESCE(CU.[HidePasswords], CG.[HidePasswords], 0) = 0 + THEN 0 + ELSE 1 + END) AS [HidePasswords], + MAX(CASE + WHEN + COALESCE(CU.[Manage], CG.[Manage], 0) = 0 + THEN 0 + ELSE 1 + END) AS [Manage], + MAX(CASE + WHEN + CU.[CollectionId] IS NULL AND CG.[CollectionId] IS NULL + THEN 0 + ELSE 1 + END) AS [Assigned], + CASE + WHEN + -- No user or group has manage rights + NOT EXISTS( + SELECT 1 + FROM [dbo].[CollectionUser] CU2 + JOIN [dbo].[OrganizationUser] OU2 ON CU2.[OrganizationUserId] = OU2.[Id] + WHERE + CU2.[CollectionId] = C.[Id] AND + CU2.[Manage] = 1 + ) + AND NOT EXISTS ( + SELECT 1 + FROM [dbo].[CollectionGroup] CG2 + WHERE + CG2.[CollectionId] = C.[Id] AND + CG2.[Manage] = 1 + ) + THEN 1 + ELSE 0 + END AS [Unmanaged] + FROM + [dbo].[CollectionView] C + LEFT JOIN + [dbo].[OrganizationUser] OU ON C.[OrganizationId] = OU.[OrganizationId] AND OU.[UserId] = @UserId + LEFT JOIN + [dbo].[CollectionUser] CU ON CU.[CollectionId] = C.[Id] AND CU.[OrganizationUserId] = [OU].[Id] + LEFT JOIN + [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id] + LEFT JOIN + [dbo].[Group] G ON G.[Id] = GU.[GroupId] + LEFT JOIN + [dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId] + WHERE + C.[OrganizationId] = @OrganizationId AND + C.[Type] = 0 -- Only SharedCollection + GROUP BY + C.[Id], + C.[OrganizationId], + C.[Name], + C.[CreationDate], + C.[RevisionDate], + C.[ExternalId], + C.[DefaultUserCollectionEmail], + C.[Type], + C.[AccessRuleId] + + IF (@IncludeAccessRelationships = 1) + BEGIN + EXEC [dbo].[CollectionGroup_ReadByOrganizationId] @OrganizationId + EXEC [dbo].[CollectionUser_ReadByOrganizationId] @OrganizationId + END +END +GO diff --git a/util/Migrator/DbScripts/2026-06-01_00_AddAccessRuleCollectionAssociations.sql b/util/Migrator/DbScripts/2026-06-01_00_AddAccessRuleCollectionAssociations.sql new file mode 100644 index 000000000000..b0f3716da293 --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-01_00_AddAccessRuleCollectionAssociations.sql @@ -0,0 +1,117 @@ +-- Read an access rule along with the IDs of the collections it governs (second result set) +CREATE OR ALTER PROCEDURE [dbo].[AccessRule_ReadDetailsById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT * + FROM [dbo].[AccessRule] + WHERE [Id] = @Id + + SELECT [Id] + FROM [dbo].[Collection] + WHERE [AccessRuleId] = @Id +END +GO + +-- Read all access rules in an organization (result set 1) along with their governed collections (result set 2) +CREATE OR ALTER PROCEDURE [dbo].[AccessRule_ReadDetailsByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT * + FROM [dbo].[AccessRule] + WHERE [OrganizationId] = @OrganizationId + + SELECT + [AccessRuleId], + [Id] AS [CollectionId] + FROM [dbo].[Collection] + WHERE [OrganizationId] = @OrganizationId + AND [AccessRuleId] IS NOT NULL +END +GO + +-- Replace an access rule's collection associations: clear the rule from @ToClear and point @ToAssign at it. +-- Both sets are scoped to @OrganizationId. +CREATE OR ALTER PROCEDURE [dbo].[Collection_SetAccessRuleAssociations] + @AccessRuleId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @ToAssign AS [dbo].[GuidIdArray] READONLY, + @ToClear AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + DECLARE @RevisionDate DATETIME2(7) = SYSUTCDATETIME() + + BEGIN TRANSACTION + + UPDATE + C + SET + C.[AccessRuleId] = NULL, + C.[RevisionDate] = @RevisionDate + FROM + [dbo].[Collection] C + INNER JOIN + @ToClear T ON T.[Id] = C.[Id] + WHERE + C.[OrganizationId] = @OrganizationId + AND C.[AccessRuleId] = @AccessRuleId + + UPDATE + C + SET + C.[AccessRuleId] = @AccessRuleId, + C.[RevisionDate] = @RevisionDate + FROM + [dbo].[Collection] C + INNER JOIN + @ToAssign T ON T.[Id] = C.[Id] + WHERE + C.[OrganizationId] = @OrganizationId + + COMMIT TRANSACTION + + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId +END +GO + +-- Deleting an access rule first clears it from any collections it governs (FK is NO ACTION), then deletes the rule. +CREATE OR ALTER PROCEDURE [dbo].[AccessRule_DeleteById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DECLARE @RevisionDate DATETIME2(7) = SYSUTCDATETIME() + DECLARE @OrganizationId UNIQUEIDENTIFIER + + SELECT @OrganizationId = [OrganizationId] + FROM [dbo].[AccessRule] + WHERE [Id] = @Id + + BEGIN TRANSACTION + + UPDATE + [dbo].[Collection] + SET + [AccessRuleId] = NULL, + [RevisionDate] = @RevisionDate + WHERE + [AccessRuleId] = @Id + + DELETE FROM [dbo].[AccessRule] WHERE [Id] = @Id + + COMMIT TRANSACTION + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId + END +END +GO diff --git a/util/Migrator/DbScripts/2026-06-04_00_AddAccessLeaseTables.sql b/util/Migrator/DbScripts/2026-06-04_00_AddAccessLeaseTables.sql new file mode 100644 index 000000000000..54f3d3e25963 --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-04_00_AddAccessLeaseTables.sql @@ -0,0 +1,244 @@ +-- PAM Credential Leasing: AccessRequest / AccessLease / AccessDecision tables + procedures. + +-- AccessRequest (created first; the FK to AccessLease is added later, once AccessLease exists). +IF OBJECT_ID('[dbo].[AccessRequest]') IS NULL +BEGIN + CREATE TABLE [dbo].[AccessRequest] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [ExtensionOfLeaseId] UNIQUEIDENTIFIER NULL, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [CollectionId] UNIQUEIDENTIFIER NOT NULL, + [CipherId] UNIQUEIDENTIFIER NOT NULL, + [RequesterId] UNIQUEIDENTIFIER NOT NULL, + [NotBefore] DATETIME2 (7) NOT NULL, + [NotAfter] DATETIME2 (7) NOT NULL, + [Reason] NVARCHAR(MAX) NULL, + [Status] TINYINT NOT NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + [ResolvedDate] DATETIME2 (7) NULL, + CONSTRAINT [PK_AccessRequest] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_AccessRequest_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE + ); +END +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE [Name] = 'IX_AccessRequest_RequesterId_CipherId_Status' AND object_id = OBJECT_ID('[dbo].[AccessRequest]')) +BEGIN + CREATE NONCLUSTERED INDEX [IX_AccessRequest_RequesterId_CipherId_Status] + ON [dbo].[AccessRequest] ([RequesterId] ASC, [CipherId] ASC, [Status] ASC); +END +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE [Name] = 'IX_AccessRequest_OrganizationId_Status' AND object_id = OBJECT_ID('[dbo].[AccessRequest]')) +BEGIN + CREATE NONCLUSTERED INDEX [IX_AccessRequest_OrganizationId_Status] + ON [dbo].[AccessRequest] ([OrganizationId] ASC, [Status] ASC); +END +GO + +-- AccessLease +IF OBJECT_ID('[dbo].[AccessLease]') IS NULL +BEGIN + CREATE TABLE [dbo].[AccessLease] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [AccessRequestId] UNIQUEIDENTIFIER NOT NULL, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [CollectionId] UNIQUEIDENTIFIER NOT NULL, + [CipherId] UNIQUEIDENTIFIER NOT NULL, + [RequesterId] UNIQUEIDENTIFIER NOT NULL, + [Status] TINYINT NOT NULL, + [NotBefore] DATETIME2 (7) NOT NULL, + [NotAfter] DATETIME2 (7) NOT NULL, + [RevokedDate] DATETIME2 (7) NULL, + [RevokedBy] UNIQUEIDENTIFIER NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + CONSTRAINT [PK_AccessLease] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_AccessLease_AccessRequest] FOREIGN KEY ([AccessRequestId]) REFERENCES [dbo].[AccessRequest] ([Id]), + CONSTRAINT [FK_AccessLease_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE + ); +END +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE [Name] = 'IX_AccessLease_RequesterId_CipherId_Status' AND object_id = OBJECT_ID('[dbo].[AccessLease]')) +BEGIN + CREATE NONCLUSTERED INDEX [IX_AccessLease_RequesterId_CipherId_Status] + ON [dbo].[AccessLease] ([RequesterId] ASC, [CipherId] ASC, [Status] ASC); +END +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE [Name] = 'IX_AccessLease_NotAfter_Status' AND object_id = OBJECT_ID('[dbo].[AccessLease]')) +BEGIN + CREATE NONCLUSTERED INDEX [IX_AccessLease_NotAfter_Status] + ON [dbo].[AccessLease] ([NotAfter] ASC, [Status] ASC); +END +GO + +-- Now that AccessLease exists, add the reciprocal FK from AccessRequest.ExtensionOfLeaseId (used by future extension requests). +IF OBJECT_ID('[dbo].[FK_AccessRequest_AccessLease]', 'F') IS NULL +BEGIN + ALTER TABLE [dbo].[AccessRequest] + ADD CONSTRAINT [FK_AccessRequest_AccessLease] FOREIGN KEY ([ExtensionOfLeaseId]) REFERENCES [dbo].[AccessLease] ([Id]); +END +GO + +-- AccessDecision +IF OBJECT_ID('[dbo].[AccessDecision]') IS NULL +BEGIN + CREATE TABLE [dbo].[AccessDecision] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [AccessRequestId] UNIQUEIDENTIFIER NOT NULL, + [DeciderKind] TINYINT NOT NULL, + [ApproverId] UNIQUEIDENTIFIER NULL, + [ConditionKind] NVARCHAR(50) NULL, + [Verdict] TINYINT NOT NULL, + [Comment] NVARCHAR(MAX) NULL, + [EvaluationContext] NVARCHAR(MAX) NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + CONSTRAINT [PK_AccessDecision] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_AccessDecision_AccessRequest] FOREIGN KEY ([AccessRequestId]) REFERENCES [dbo].[AccessRequest] ([Id]) ON DELETE CASCADE + ); +END +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE [Name] = 'IX_AccessDecision_AccessRequestId' AND object_id = OBJECT_ID('[dbo].[AccessDecision]')) +BEGIN + CREATE NONCLUSTERED INDEX [IX_AccessDecision_AccessRequestId] + ON [dbo].[AccessDecision] ([AccessRequestId] ASC); +END +GO + +-- Stored procedures +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @ExtensionOfLeaseId UNIQUEIDENTIFIER = NULL, + @OrganizationId UNIQUEIDENTIFIER, + @CollectionId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @RequesterId UNIQUEIDENTIFIER, + @NotBefore DATETIME2(7), + @NotAfter DATETIME2(7), + @Reason NVARCHAR(MAX) = NULL, + @Status TINYINT, + @CreationDate DATETIME2(7), + @ResolvedDate DATETIME2(7) = NULL +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[AccessRequest] + ( + [Id], [ExtensionOfLeaseId], [OrganizationId], [CollectionId], [CipherId], [RequesterId], + [NotBefore], [NotAfter], [Reason], [Status], [CreationDate], [ResolvedDate] + ) + VALUES + ( + @Id, @ExtensionOfLeaseId, @OrganizationId, @CollectionId, @CipherId, @RequesterId, + @NotBefore, @NotAfter, @Reason, @Status, @CreationDate, @ResolvedDate + ) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_ReadById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + SELECT * FROM [dbo].[AccessRequest] WHERE [Id] = @Id +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_ReadActivePendingByRequesterIdCipherId] + @RequesterId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + SELECT TOP 1 * + FROM [dbo].[AccessRequest] + WHERE [RequesterId] = @RequesterId AND [CipherId] = @CipherId AND [Status] = 0 -- Pending + ORDER BY [CreationDate] DESC +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessLease_ReadById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + SELECT * FROM [dbo].[AccessLease] WHERE [Id] = @Id +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessLease_ReadActiveByRequesterIdCipherId] + @RequesterId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + SELECT TOP 1 * + FROM [dbo].[AccessLease] + WHERE [RequesterId] = @RequesterId + AND [CipherId] = @CipherId + AND [Status] = 0 -- Active + AND [NotBefore] <= @Now + AND [NotAfter] > @Now + ORDER BY [NotAfter] DESC +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessLease_CreateAutoApproved] + @AccessRequestId UNIQUEIDENTIFIER, + @AccessLeaseId UNIQUEIDENTIFIER, + @AccessDecisionId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @CollectionId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @RequesterId UNIQUEIDENTIFIER, + @NotBefore DATETIME2(7), + @NotAfter DATETIME2(7), + @Reason NVARCHAR(MAX) = NULL, + @ConditionKind NVARCHAR(50) = NULL, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + BEGIN TRANSACTION AccessLease_CreateAutoApproved + + INSERT INTO [dbo].[AccessRequest] + ( + [Id], [ExtensionOfLeaseId], [OrganizationId], [CollectionId], [CipherId], [RequesterId], + [NotBefore], [NotAfter], [Reason], [Status], [CreationDate], [ResolvedDate] + ) + VALUES + ( + @AccessRequestId, NULL, @OrganizationId, @CollectionId, @CipherId, @RequesterId, + @NotBefore, @NotAfter, @Reason, 1 /* Approved */, @Now, @Now + ) + + INSERT INTO [dbo].[AccessDecision] + ( + [Id], [AccessRequestId], [DeciderKind], [ApproverId], [ConditionKind], + [Verdict], [Comment], [EvaluationContext], [CreationDate] + ) + VALUES + ( + @AccessDecisionId, @AccessRequestId, 0 /* Automatic */, NULL, @ConditionKind, + 0 /* Approve */, NULL, NULL, @Now + ) + + INSERT INTO [dbo].[AccessLease] + ( + [Id], [AccessRequestId], [OrganizationId], [CollectionId], [CipherId], [RequesterId], + [Status], [NotBefore], [NotAfter], [RevokedDate], [RevokedBy], [CreationDate] + ) + VALUES + ( + @AccessLeaseId, @AccessRequestId, @OrganizationId, @CollectionId, @CipherId, @RequesterId, + 0 /* Active */, @NotBefore, @NotAfter, NULL, NULL, @Now + ) + + COMMIT TRANSACTION AccessLease_CreateAutoApproved +END +GO diff --git a/util/Migrator/DbScripts/2026-06-05_00_AddApproverInboxSprocs.sql b/util/Migrator/DbScripts/2026-06-05_00_AddApproverInboxSprocs.sql new file mode 100644 index 000000000000..7e9236d26f6b --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-05_00_AddApproverInboxSprocs.sql @@ -0,0 +1,216 @@ + +-- PAM Approver Inbox: read procedures for the pending/history queues, the resolve-with-decision and lease-revoke +-- mutations, and Collection_ReadManagingUserIds (the collection managers resolved for the RefreshApproverInbox push). + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_ReadInboxPendingByCollectionIds] + @CollectionIds [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + SELECT + LR.[Id], + LR.[ExtensionOfLeaseId], + LR.[OrganizationId], + LR.[CollectionId], + LR.[CipherId], + LR.[RequesterId], + LR.[NotBefore], + LR.[NotAfter], + LR.[Reason], + LR.[Status], + LR.[CreationDate], + LR.[ResolvedDate], + PL.[Id] AS [ProducedLeaseId], + RES.[ApproverId] AS [ApproverId], + RES.[Comment] AS [ApproverComment], + JSON_VALUE(C.[Data], '$.Name') AS [CipherName], + COL.[Name] AS [CollectionName], + U.[Name] AS [RequesterName], + U.[Email] AS [RequesterEmail] + FROM [dbo].[AccessRequest] LR + INNER JOIN @CollectionIds CI ON CI.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[Cipher] C ON C.[Id] = LR.[CipherId] + LEFT JOIN [dbo].[Collection] COL ON COL.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[User] U ON U.[Id] = LR.[RequesterId] + OUTER APPLY ( + SELECT TOP 1 L.[Id] + FROM [dbo].[AccessLease] L + WHERE L.[AccessRequestId] = LR.[Id] + ORDER BY L.[CreationDate] DESC + ) PL + OUTER APPLY ( + SELECT TOP 1 LD.[ApproverId], LD.[Comment] + FROM [dbo].[AccessDecision] LD + WHERE LD.[AccessRequestId] = LR.[Id] AND LD.[DeciderKind] = 1 -- Human + ORDER BY LD.[CreationDate] ASC + ) RES + WHERE LR.[Status] = 0 -- Pending +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_ReadInboxHistoryByCollectionIds] + @CollectionIds [dbo].[GuidIdArray] READONLY, + @Since DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + SELECT + LR.[Id], + LR.[ExtensionOfLeaseId], + LR.[OrganizationId], + LR.[CollectionId], + LR.[CipherId], + LR.[RequesterId], + LR.[NotBefore], + LR.[NotAfter], + LR.[Reason], + LR.[Status], + LR.[CreationDate], + LR.[ResolvedDate], + PL.[Id] AS [ProducedLeaseId], + RES.[ApproverId] AS [ApproverId], + RES.[Comment] AS [ApproverComment], + JSON_VALUE(C.[Data], '$.Name') AS [CipherName], + COL.[Name] AS [CollectionName], + U.[Name] AS [RequesterName], + U.[Email] AS [RequesterEmail] + FROM [dbo].[AccessRequest] LR + INNER JOIN @CollectionIds CI ON CI.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[Cipher] C ON C.[Id] = LR.[CipherId] + LEFT JOIN [dbo].[Collection] COL ON COL.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[User] U ON U.[Id] = LR.[RequesterId] + OUTER APPLY ( + SELECT TOP 1 L.[Id] + FROM [dbo].[AccessLease] L + WHERE L.[AccessRequestId] = LR.[Id] + ORDER BY L.[CreationDate] DESC + ) PL + OUTER APPLY ( + SELECT TOP 1 LD.[ApproverId], LD.[Comment] + FROM [dbo].[AccessDecision] LD + WHERE LD.[AccessRequestId] = LR.[Id] AND LD.[DeciderKind] = 1 -- Human + ORDER BY LD.[CreationDate] ASC + ) RES + WHERE LR.[Status] <> 0 -- not Pending + AND LR.[CreationDate] >= @Since +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_ResolveWithDecision] + @AccessRequestId UNIQUEIDENTIFIER, + @Status TINYINT, + @AccessDecisionId UNIQUEIDENTIFIER, + @ApproverId UNIQUEIDENTIFIER, + @Verdict TINYINT, + @Comment NVARCHAR(MAX) = NULL, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + BEGIN TRANSACTION AccessRequest_Resolve + + UPDATE [dbo].[AccessRequest] + SET [Status] = @Status, + [ResolvedDate] = @Now + WHERE [Id] = @AccessRequestId AND [Status] = 0 -- Pending + + INSERT INTO [dbo].[AccessDecision] + ( + [Id], [AccessRequestId], [DeciderKind], [ApproverId], [ConditionKind], + [Verdict], [Comment], [EvaluationContext], [CreationDate] + ) + VALUES + ( + @AccessDecisionId, @AccessRequestId, 1 /* Human */, @ApproverId, NULL, + @Verdict, @Comment, NULL, @Now + ) + + COMMIT TRANSACTION AccessRequest_Resolve +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessLease_Revoke] + @AccessLeaseId UNIQUEIDENTIFIER, + @AccessRequestId UNIQUEIDENTIFIER, + @RevokedBy UNIQUEIDENTIFIER, + @AccessDecisionId UNIQUEIDENTIFIER, + @Reason NVARCHAR(MAX) = NULL, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + BEGIN TRANSACTION AccessLease_Revoke + + UPDATE [dbo].[AccessLease] + SET [Status] = 2 /* Revoked */, + [RevokedDate] = @Now, + [RevokedBy] = @RevokedBy + WHERE [Id] = @AccessLeaseId AND [Status] = 0 -- Active + + INSERT INTO [dbo].[AccessDecision] + ( + [Id], [AccessRequestId], [DeciderKind], [ApproverId], [ConditionKind], + [Verdict], [Comment], [EvaluationContext], [CreationDate] + ) + VALUES + ( + @AccessDecisionId, @AccessRequestId, 1 /* Human */, @RevokedBy, NULL, + 1 /* Deny */, @Reason, NULL, @Now + ) + + COMMIT TRANSACTION AccessLease_Revoke +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadManagingUserIds] + @CollectionId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DECLARE @OrganizationId UNIQUEIDENTIFIER + SELECT @OrganizationId = [OrganizationId] FROM [dbo].[Collection] WHERE [Id] = @CollectionId + + SELECT DISTINCT [UserId] + FROM + ( + SELECT OU.[UserId] + FROM [dbo].[CollectionUser] CU + INNER JOIN [dbo].[OrganizationUser] OU ON OU.[Id] = CU.[OrganizationUserId] + WHERE CU.[CollectionId] = @CollectionId + AND CU.[Manage] = 1 + AND OU.[Status] = 2 -- Confirmed + AND OU.[UserId] IS NOT NULL + + UNION + + SELECT OU.[UserId] + FROM [dbo].[CollectionGroup] CG + INNER JOIN [dbo].[GroupUser] GU ON GU.[GroupId] = CG.[GroupId] + INNER JOIN [dbo].[OrganizationUser] OU ON OU.[Id] = GU.[OrganizationUserId] + WHERE CG.[CollectionId] = @CollectionId + AND CG.[Manage] = 1 + AND OU.[Status] = 2 -- Confirmed + AND OU.[UserId] IS NOT NULL + + UNION + + SELECT OU.[UserId] + FROM [dbo].[OrganizationUser] OU + INNER JOIN [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] + WHERE OU.[OrganizationId] = @OrganizationId + AND OU.[Status] = 2 -- Confirmed + AND OU.[UserId] IS NOT NULL + AND ( + (O.[AllowAdminAccessToAllCollectionItems] = 1 AND OU.[Type] IN (0, 1)) -- Owner, Admin + OR (OU.[Type] = 4 -- Custom + AND ISJSON(OU.[Permissions]) = 1 + AND JSON_VALUE(OU.[Permissions], '$.editAnyCollection') = 'true') + ) + ) AS ManagingUsers +END +GO diff --git a/util/Migrator/DbScripts/2026-06-05_01_CreateAccessLeaseOnApproval.sql b/util/Migrator/DbScripts/2026-06-05_01_CreateAccessLeaseOnApproval.sql new file mode 100644 index 000000000000..9ed76367f7fe --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-05_01_CreateAccessLeaseOnApproval.sql @@ -0,0 +1,60 @@ +-- PAM Credential Leasing: when an approver approves a human request, mint the active lease that authorizes access. +-- Previously [AccessRequest_ResolveWithDecision] only flipped the request to Approved and recorded the decision, so the +-- human path never produced a [AccessLease] and the approved requester could not read the credential. This adds an optional +-- @AccessLeaseId; when supplied (approvals only), the lease is created in the same transaction with the request's approved +-- window, mirroring [AccessLease_CreateAutoApproved] on the automatic path. + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_ResolveWithDecision] + @AccessRequestId UNIQUEIDENTIFIER, + @Status TINYINT, + @AccessDecisionId UNIQUEIDENTIFIER, + @ApproverId UNIQUEIDENTIFIER, + @Verdict TINYINT, + @Comment NVARCHAR(MAX) = NULL, + @AccessLeaseId UNIQUEIDENTIFIER = NULL, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- Atomically resolve a pending request and record the human approver's decision. The caller has already verified + -- (and the application enforces) that the request is still Pending; the WHERE guard keeps the write idempotent + -- under a race so a second approver can't move an already-resolved request. + BEGIN TRANSACTION AccessRequest_Resolve + + UPDATE [dbo].[AccessRequest] + SET [Status] = @Status, + [ResolvedDate] = @Now + WHERE [Id] = @AccessRequestId AND [Status] = 0 -- Pending + + INSERT INTO [dbo].[AccessDecision] + ( + [Id], [AccessRequestId], [DeciderKind], [ApproverId], [ConditionKind], + [Verdict], [Comment], [EvaluationContext], [CreationDate] + ) + VALUES + ( + @AccessDecisionId, @AccessRequestId, 1 /* Human */, @ApproverId, NULL, + @Verdict, @Comment, NULL, @Now + ) + + -- An approval mints the active lease that authorizes access, mirroring [AccessLease_CreateAutoApproved] on the automatic + -- path. @AccessLeaseId is supplied only when approving; the lease window is the request's approved window, so the lease + -- is found by [AccessLease_ReadActiveByRequesterIdCipherId] once @Now falls inside it. + IF @AccessLeaseId IS NOT NULL + BEGIN + INSERT INTO [dbo].[AccessLease] + ( + [Id], [AccessRequestId], [OrganizationId], [CollectionId], [CipherId], [RequesterId], + [Status], [NotBefore], [NotAfter], [RevokedDate], [RevokedBy], [CreationDate] + ) + SELECT + @AccessLeaseId, [Id], [OrganizationId], [CollectionId], [CipherId], [RequesterId], + 0 /* Active */, [NotBefore], [NotAfter], NULL, NULL, @Now + FROM [dbo].[AccessRequest] + WHERE [Id] = @AccessRequestId + END + + COMMIT TRANSACTION AccessRequest_Resolve +END +GO diff --git a/util/Migrator/DbScripts/2026-06-05_02_AddRequesterReadSprocs.sql b/util/Migrator/DbScripts/2026-06-05_02_AddRequesterReadSprocs.sql new file mode 100644 index 000000000000..c6310cb95ba2 --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-05_02_AddRequesterReadSprocs.sql @@ -0,0 +1,67 @@ +-- PAM member read endpoints: caller-scoped reads for the cipher-lease banner, vault-row badge, and the +-- "My access requests" page. Lease_ReadManyActiveByRequesterId backs "my active leases"; +-- AccessRequest_ReadManyByRequesterId backs "my requests" (all statuses, names omitted — caller-scoped self-read). + +CREATE OR ALTER PROCEDURE [dbo].[AccessLease_ReadManyActiveByRequesterId] + @RequesterId UNIQUEIDENTIFIER, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[AccessLease] + WHERE + [RequesterId] = @RequesterId + AND [Status] = 0 -- Active + AND [NotBefore] <= @Now + AND [NotAfter] > @Now + ORDER BY + [NotAfter] ASC +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_ReadManyByRequesterId] + @RequesterId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + -- The caller's own requests across every org, all statuses. Unlike the approver-inbox reads this is a + -- caller-scoped self-read, so the cipher/collection/requester display-name joins are intentionally omitted + -- (those name fields stay null). Capped at the 250 most recent; the client renders far fewer. + SELECT TOP (250) + LR.[Id], + LR.[ExtensionOfLeaseId], + LR.[OrganizationId], + LR.[CollectionId], + LR.[CipherId], + LR.[RequesterId], + LR.[NotBefore], + LR.[NotAfter], + LR.[Reason], + LR.[Status], + LR.[CreationDate], + LR.[ResolvedDate], + PL.[Id] AS [ProducedLeaseId], + RES.[ApproverId] AS [ApproverId], + RES.[Comment] AS [ApproverComment] + FROM [dbo].[AccessRequest] LR + OUTER APPLY ( + SELECT TOP 1 L.[Id] + FROM [dbo].[AccessLease] L + WHERE L.[AccessRequestId] = LR.[Id] + ORDER BY L.[CreationDate] DESC + ) PL + OUTER APPLY ( + SELECT TOP 1 LD.[ApproverId], LD.[Comment] + FROM [dbo].[AccessDecision] LD + WHERE LD.[AccessRequestId] = LR.[Id] AND LD.[DeciderKind] = 1 -- Human + ORDER BY LD.[CreationDate] ASC + ) RES + WHERE LR.[RequesterId] = @RequesterId + ORDER BY LR.[CreationDate] DESC +END +GO diff --git a/util/Migrator/DbScripts/2026-06-10_00_DeferAccessLeaseMintToActivation.sql b/util/Migrator/DbScripts/2026-06-10_00_DeferAccessLeaseMintToActivation.sql new file mode 100644 index 000000000000..259fc97c6150 --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-10_00_DeferAccessLeaseMintToActivation.sql @@ -0,0 +1,158 @@ +-- PAM Credential Leasing: approval no longer mints the lease. A human approval now only records the verdict and +-- flips the request to Approved; the requester activates the approved request when they actually access the item +-- ([AccessLease_CreateFromApprovedRequest]), which is when the active lease is minted. The automatic path is +-- unchanged and still mints instantly via [AccessLease_CreateAutoApproved] — there the requester is online and +-- asking for access now. +-- +-- [AccessRequest_ResolveWithDecision] loses its @AccessLeaseId parameter outright instead of keeping an ignored +-- default: during a mixed-binary window an old server passing @AccessLeaseId would error. Acceptable here — the +-- feature is an unshipped POC behind the pm-37044-pam-v-0 flag and server + migration deploy together. + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_ResolveWithDecision] + @AccessRequestId UNIQUEIDENTIFIER, + @Status TINYINT, + @AccessDecisionId UNIQUEIDENTIFIER, + @ApproverId UNIQUEIDENTIFIER, + @Verdict TINYINT, + @Comment NVARCHAR(MAX) = NULL, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- Atomically resolve a pending request and record the human approver's decision. The caller has already verified + -- (and the application enforces) that the request is still Pending; the WHERE guard keeps the write idempotent + -- under a race so a second approver can't move an already-resolved request. + -- + -- Approval does not mint the lease: the requester activates the approved request later via + -- [AccessLease_CreateFromApprovedRequest]. The automatic path still mints instantly via + -- [AccessLease_CreateAutoApproved], where the requester is online and asking for access now. + BEGIN TRANSACTION AccessRequest_Resolve + + UPDATE [dbo].[AccessRequest] + SET [Status] = @Status, + [ResolvedDate] = @Now + WHERE [Id] = @AccessRequestId AND [Status] = 0 -- Pending + + INSERT INTO [dbo].[AccessDecision] + ( + [Id], [AccessRequestId], [DeciderKind], [ApproverId], [ConditionKind], + [Verdict], [Comment], [EvaluationContext], [CreationDate] + ) + VALUES + ( + @AccessDecisionId, @AccessRequestId, 1 /* Human */, @ApproverId, NULL, + @Verdict, @Comment, NULL, @Now + ) + + COMMIT TRANSACTION AccessRequest_Resolve +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_ReadActiveApprovedByRequesterIdCipherId] + @RequesterId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- The caller's approved-but-not-yet-activated request whose window can still produce access. Future windows are + -- included (the client shows the upcoming window); lapsed windows are excluded so the client never offers an + -- activation that the server would reject. A request that has produced a lease is activated, not approved. + SELECT TOP 1 + AR.* + FROM + [dbo].[AccessRequest] AR + WHERE + AR.[RequesterId] = @RequesterId + AND AR.[CipherId] = @CipherId + AND AR.[Status] = 1 -- Approved + AND AR.[NotAfter] > @Now + AND NOT EXISTS (SELECT 1 FROM [dbo].[AccessLease] AL WHERE AL.[AccessRequestId] = AR.[Id]) + ORDER BY + AR.[CreationDate] DESC +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessLease_ReadByAccessRequestId] + @AccessRequestId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + -- A request produces at most one lease ([IX_AccessLease_AccessRequestId] is unique); TOP 1 is belt and braces. + SELECT TOP 1 + * + FROM + [dbo].[AccessLease] + WHERE + [AccessRequestId] = @AccessRequestId + ORDER BY + [CreationDate] DESC +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessLease_CreateFromApprovedRequest] + @AccessLeaseId UNIQUEIDENTIFIER, + @AccessRequestId UNIQUEIDENTIFIER, + @RequesterId UNIQUEIDENTIFIER, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- Activation of an approved request: mints the active lease that authorizes access, spanning the request's + -- approved window. Every application-level precondition is re-checked inside the INSERT so a concurrent + -- activation cannot double-mint; zero rows inserted means a precondition no longer held and the caller decides + -- how to surface that. [IX_AccessLease_AccessRequestId] (unique) is the backstop when two calls pass the + -- NOT EXISTS check simultaneously. + INSERT INTO [dbo].[AccessLease] + ( + [Id], [AccessRequestId], [OrganizationId], [CollectionId], [CipherId], [RequesterId], + [Status], [NotBefore], [NotAfter], [RevokedDate], [RevokedBy], [CreationDate] + ) + SELECT + @AccessLeaseId, AR.[Id], AR.[OrganizationId], AR.[CollectionId], AR.[CipherId], AR.[RequesterId], + 0 /* Active */, AR.[NotBefore], AR.[NotAfter], NULL, NULL, @Now + FROM [dbo].[AccessRequest] AR + WHERE + AR.[Id] = @AccessRequestId + AND AR.[RequesterId] = @RequesterId + AND AR.[Status] = 1 -- Approved + AND AR.[NotBefore] <= @Now + AND AR.[NotAfter] > @Now + AND NOT EXISTS (SELECT 1 FROM [dbo].[AccessLease] AL WHERE AL.[AccessRequestId] = AR.[Id]) + + SELECT @@ROWCOUNT +END +GO + +-- One lease per request, ever. Dev databases may hold duplicates from the earlier approval-mint race +-- ([AccessRequest_ResolveWithDecision] inserted the decision and lease even when the pending-guard UPDATE matched +-- zero rows); keep the earliest lease per request. No production data exists, and the server never writes +-- [AccessRequest].[ExtensionOfLeaseId] yet, so its FK cannot block these deletes. +DELETE AL +FROM [dbo].[AccessLease] AL +WHERE EXISTS ( + SELECT 1 + FROM [dbo].[AccessLease] AL2 + WHERE AL2.[AccessRequestId] = AL.[AccessRequestId] + AND (AL2.[CreationDate] < AL.[CreationDate] + OR (AL2.[CreationDate] = AL.[CreationDate] AND AL2.[Id] < AL.[Id])) +) +GO + +IF NOT EXISTS ( + SELECT 1 + FROM sys.indexes + WHERE [name] = 'IX_AccessLease_AccessRequestId' + AND [object_id] = OBJECT_ID('[dbo].[AccessLease]') +) +BEGIN + -- A request produces at most one lease, ever: activating an approved request and the automatic path each insert + -- exactly one. Unique to backstop racing activations that pass the application-level checks simultaneously. + CREATE UNIQUE NONCLUSTERED INDEX [IX_AccessLease_AccessRequestId] + ON [dbo].[AccessLease] ([AccessRequestId] ASC); +END +GO diff --git a/util/Migrator/DbScripts/2026-06-11_00_AddProducedLeaseStatusToInbox.sql b/util/Migrator/DbScripts/2026-06-11_00_AddProducedLeaseStatusToInbox.sql new file mode 100644 index 000000000000..ffc3c266871c --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-11_00_AddProducedLeaseStatusToInbox.sql @@ -0,0 +1,103 @@ +-- PAM Approver Inbox: surface the produced lease's status alongside its id. The inbox previously knew only that a +-- request had produced a lease (ProducedLeaseId), not whether that lease was still Active. A lease ended elsewhere +-- (the requester's "End lease", natural expiry, or another approver) still looked active in the inbox, leaving a +-- clickable Revoke that the server then rejects with 409. Selecting [Status] lets the client keep an ended lease out +-- of the "active" group so Revoke is no longer offered for it. + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_ReadInboxPendingByCollectionIds] + @CollectionIds [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + SELECT + LR.[Id], + LR.[ExtensionOfLeaseId], + LR.[OrganizationId], + LR.[CollectionId], + LR.[CipherId], + LR.[RequesterId], + LR.[NotBefore], + LR.[NotAfter], + LR.[Reason], + LR.[Status], + LR.[CreationDate], + LR.[ResolvedDate], + PL.[Id] AS [ProducedLeaseId], + PL.[Status] AS [ProducedLeaseStatus], + RES.[ApproverId] AS [ApproverId], + RES.[Comment] AS [ApproverComment], + JSON_VALUE(C.[Data], '$.Name') AS [CipherName], + COL.[Name] AS [CollectionName], + U.[Name] AS [RequesterName], + U.[Email] AS [RequesterEmail] + FROM [dbo].[AccessRequest] LR + INNER JOIN @CollectionIds CI ON CI.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[Cipher] C ON C.[Id] = LR.[CipherId] + LEFT JOIN [dbo].[Collection] COL ON COL.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[User] U ON U.[Id] = LR.[RequesterId] + OUTER APPLY ( + SELECT TOP 1 L.[Id], L.[Status] + FROM [dbo].[AccessLease] L + WHERE L.[AccessRequestId] = LR.[Id] + ORDER BY L.[CreationDate] DESC + ) PL + OUTER APPLY ( + SELECT TOP 1 LD.[ApproverId], LD.[Comment] + FROM [dbo].[AccessDecision] LD + WHERE LD.[AccessRequestId] = LR.[Id] AND LD.[DeciderKind] = 1 -- Human + ORDER BY LD.[CreationDate] ASC + ) RES + WHERE LR.[Status] = 0 -- Pending +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_ReadInboxHistoryByCollectionIds] + @CollectionIds [dbo].[GuidIdArray] READONLY, + @Since DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + SELECT + LR.[Id], + LR.[ExtensionOfLeaseId], + LR.[OrganizationId], + LR.[CollectionId], + LR.[CipherId], + LR.[RequesterId], + LR.[NotBefore], + LR.[NotAfter], + LR.[Reason], + LR.[Status], + LR.[CreationDate], + LR.[ResolvedDate], + PL.[Id] AS [ProducedLeaseId], + PL.[Status] AS [ProducedLeaseStatus], + RES.[ApproverId] AS [ApproverId], + RES.[Comment] AS [ApproverComment], + JSON_VALUE(C.[Data], '$.Name') AS [CipherName], + COL.[Name] AS [CollectionName], + U.[Name] AS [RequesterName], + U.[Email] AS [RequesterEmail] + FROM [dbo].[AccessRequest] LR + INNER JOIN @CollectionIds CI ON CI.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[Cipher] C ON C.[Id] = LR.[CipherId] + LEFT JOIN [dbo].[Collection] COL ON COL.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[User] U ON U.[Id] = LR.[RequesterId] + OUTER APPLY ( + SELECT TOP 1 L.[Id], L.[Status] + FROM [dbo].[AccessLease] L + WHERE L.[AccessRequestId] = LR.[Id] + ORDER BY L.[CreationDate] DESC + ) PL + OUTER APPLY ( + SELECT TOP 1 LD.[ApproverId], LD.[Comment] + FROM [dbo].[AccessDecision] LD + WHERE LD.[AccessRequestId] = LR.[Id] AND LD.[DeciderKind] = 1 -- Human + ORDER BY LD.[CreationDate] ASC + ) RES + WHERE LR.[Status] <> 0 -- not Pending + AND LR.[CreationDate] >= @Since +END +GO diff --git a/util/Migrator/DbScripts/2026-06-11_01_AddSingleActiveLease.sql b/util/Migrator/DbScripts/2026-06-11_01_AddSingleActiveLease.sql new file mode 100644 index 000000000000..adf5dda3c6a7 --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-11_01_AddSingleActiveLease.sql @@ -0,0 +1,235 @@ +-- PAM Credential Leasing: per-cipher single-active-lease. +-- +-- AccessRule gains a [SingleActiveLease] flag. When the governing rule(s) ask for it, at most one active in-window +-- lease may exist for a given cipher across all users. The flag binds for a member only when EVERY collection through +-- which they reach the cipher is governed by a singleton rule (the union/OR gating logic lives in C#); any ungated or +-- non-singleton path is an escape that leaves them unconstrained. The mint procs receive @EnforceSingleActiveLease and +-- serialize concurrent activations of the same cipher with a UPDLOCK, HOLDLOCK range lock, returning a distinct +-- outcome code: 1 = minted, 0 = precondition fail, -1 = single-active conflict. +-- +-- PAM is an unshipped POC behind the pm-37044-pam-v-0 flag with no production data; server + migration deploy +-- together, so the affected procs are altered in place rather than versioned. + +IF COL_LENGTH('[dbo].[AccessRule]', 'SingleActiveLease') IS NULL +BEGIN + ALTER TABLE [dbo].[AccessRule] + ADD [SingleActiveLease] BIT NOT NULL + CONSTRAINT [DF_AccessRule_SingleActiveLease] DEFAULT (0) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRule_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @Name NVARCHAR(256), + @Description NVARCHAR(MAX) = NULL, + @Conditions NVARCHAR(MAX), + @SingleActiveLease BIT = 0, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[AccessRule] + ( + [Id], + [OrganizationId], + [Name], + [Description], + [Conditions], + [SingleActiveLease], + [CreationDate], + [RevisionDate] + ) + VALUES + ( + @Id, + @OrganizationId, + @Name, + @Description, + @Conditions, + @SingleActiveLease, + @CreationDate, + @RevisionDate + ) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRule_Update] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Name NVARCHAR(256), + @Description NVARCHAR(MAX) = NULL, + @Conditions NVARCHAR(MAX), + @SingleActiveLease BIT = 0, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[AccessRule] + SET + [OrganizationId] = @OrganizationId, + [Name] = @Name, + [Description] = @Description, + [Conditions] = @Conditions, + [SingleActiveLease] = @SingleActiveLease, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate + WHERE + [Id] = @Id +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessLease_CreateFromApprovedRequest] + @AccessLeaseId UNIQUEIDENTIFIER, + @AccessRequestId UNIQUEIDENTIFIER, + @RequesterId UNIQUEIDENTIFIER, + @Now DATETIME2(7), + @EnforceSingleActiveLease BIT = 0 +AS +BEGIN + SET NOCOUNT ON + -- An explicit transaction is required so the singleton guard's range lock is held until the INSERT commits; + -- XACT_ABORT guarantees the transaction is rolled back (and the pooled connection left clean) if the + -- unique-index backstop [IX_AccessLease_AccessRequestId] trips on a concurrent activation of the same request. + SET XACT_ABORT ON + + BEGIN TRANSACTION + + -- Per-cipher singleton guard. When the governing rule(s) ask for a single active lease, activation is allowed + -- only if no other active in-window lease exists for the same cipher across all users. The UPDLOCK, HOLDLOCK + -- range lock is held for the life of this transaction, so it serializes against the INSERT below: a concurrent + -- same-cipher activation blocks here until this transaction commits, then sees the new lease and is rejected. + -- Outcome -1 is distinct from the precondition-fail outcome (0) so the caller can surface a 409 conflict. + IF @EnforceSingleActiveLease = 1 + AND EXISTS ( + SELECT 1 + FROM [dbo].[AccessLease] WITH (UPDLOCK, HOLDLOCK) + WHERE [CipherId] = (SELECT [CipherId] FROM [dbo].[AccessRequest] WHERE [Id] = @AccessRequestId) + AND [Status] = 0 /* Active */ + AND [NotBefore] <= @Now + AND [NotAfter] > @Now + ) + BEGIN + ROLLBACK TRANSACTION + SELECT -1 + RETURN + END + + -- Activation of an approved request: mints the active lease that authorizes access, spanning the request's + -- approved window. Every application-level precondition is re-checked inside the INSERT so a concurrent + -- activation cannot double-mint; zero rows inserted means a precondition no longer held and the caller decides + -- how to surface that. [IX_AccessLease_AccessRequestId] (unique) is the backstop when two calls pass the + -- NOT EXISTS check simultaneously. + INSERT INTO [dbo].[AccessLease] + ( + [Id], [AccessRequestId], [OrganizationId], [CollectionId], [CipherId], [RequesterId], + [Status], [NotBefore], [NotAfter], [RevokedDate], [RevokedBy], [CreationDate] + ) + SELECT + @AccessLeaseId, AR.[Id], AR.[OrganizationId], AR.[CollectionId], AR.[CipherId], AR.[RequesterId], + 0 /* Active */, AR.[NotBefore], AR.[NotAfter], NULL, NULL, @Now + FROM [dbo].[AccessRequest] AR + WHERE + AR.[Id] = @AccessRequestId + AND AR.[RequesterId] = @RequesterId + AND AR.[Status] = 1 -- Approved + AND AR.[NotBefore] <= @Now + AND AR.[NotAfter] > @Now + AND NOT EXISTS (SELECT 1 FROM [dbo].[AccessLease] AL WHERE AL.[AccessRequestId] = AR.[Id]) + + DECLARE @Rows INT = @@ROWCOUNT + + COMMIT TRANSACTION + + -- 1 = minted, 0 = precondition no longer held (caller re-reads the winner). + SELECT CASE WHEN @Rows = 1 THEN 1 ELSE 0 END +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessLease_CreateAutoApproved] + @AccessRequestId UNIQUEIDENTIFIER, + @AccessLeaseId UNIQUEIDENTIFIER, + @AccessDecisionId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @CollectionId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @RequesterId UNIQUEIDENTIFIER, + @NotBefore DATETIME2(7), + @NotAfter DATETIME2(7), + @Reason NVARCHAR(MAX) = NULL, + @ConditionKind NVARCHAR(50) = NULL, + @Now DATETIME2(7), + @EnforceSingleActiveLease BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + BEGIN TRANSACTION AccessLease_CreateAutoApproved + + -- Per-cipher singleton guard. When the governing rule(s) ask for a single active lease, the auto-approved lease + -- is minted only if no other active in-window lease exists for the same cipher across all users. The check runs + -- inside the transaction before any insert so the UPDLOCK, HOLDLOCK range lock serializes concurrent activations + -- of the same cipher; a conflict rolls back leaving nothing persisted and returns -1. + IF @EnforceSingleActiveLease = 1 + BEGIN + IF EXISTS ( + SELECT 1 + FROM [dbo].[AccessLease] WITH (UPDLOCK, HOLDLOCK) + WHERE [CipherId] = @CipherId + AND [Status] = 0 /* Active */ + AND [NotBefore] <= @Now + AND [NotAfter] > @Now + ) + BEGIN + ROLLBACK TRANSACTION AccessLease_CreateAutoApproved + SELECT -1 + RETURN + END + END + + -- The request is created already resolved (Approved). ExtensionOfLeaseId stays NULL: it is reserved for extension + -- requests; provenance for an original lease flows the other way, via AccessLease.AccessRequestId. + INSERT INTO [dbo].[AccessRequest] + ( + [Id], [ExtensionOfLeaseId], [OrganizationId], [CollectionId], [CipherId], [RequesterId], + [NotBefore], [NotAfter], [Reason], [Status], [CreationDate], [ResolvedDate] + ) + VALUES + ( + @AccessRequestId, NULL, @OrganizationId, @CollectionId, @CipherId, @RequesterId, + @NotBefore, @NotAfter, @Reason, 1 /* Approved */, @Now, @Now + ) + + INSERT INTO [dbo].[AccessDecision] + ( + [Id], [AccessRequestId], [DeciderKind], [ApproverId], [ConditionKind], + [Verdict], [Comment], [EvaluationContext], [CreationDate] + ) + VALUES + ( + @AccessDecisionId, @AccessRequestId, 0 /* Automatic */, NULL, @ConditionKind, + 0 /* Approve */, NULL, NULL, @Now + ) + + INSERT INTO [dbo].[AccessLease] + ( + [Id], [AccessRequestId], [OrganizationId], [CollectionId], [CipherId], [RequesterId], + [Status], [NotBefore], [NotAfter], [RevokedDate], [RevokedBy], [CreationDate] + ) + VALUES + ( + @AccessLeaseId, @AccessRequestId, @OrganizationId, @CollectionId, @CipherId, @RequesterId, + 0 /* Active */, @NotBefore, @NotAfter, NULL, NULL, @Now + ) + + COMMIT TRANSACTION AccessLease_CreateAutoApproved + + -- 1 = minted (request + decision + lease all written). + SELECT 1 +END +GO diff --git a/util/Migrator/DbScripts/2026-06-11_02_AddAccessRuleLeaseDurations.sql b/util/Migrator/DbScripts/2026-06-11_02_AddAccessRuleLeaseDurations.sql new file mode 100644 index 000000000000..a4aa3153444d --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-11_02_AddAccessRuleLeaseDurations.sql @@ -0,0 +1,101 @@ +-- PAM Credential Leasing: per-rule lease durations. +-- +-- AccessRule gains [DefaultLeaseDurationSeconds] and [MaxLeaseDurationSeconds] (both nullable INT seconds). +-- The default is used to pre-fill a request opened under the rule; null means the backend default applies. +-- The max is a hard ceiling on any single lease granted under the rule; null means no per-rule cap. The +-- client already sends and reads both fields, but the server had nowhere to persist them, so they were +-- dropped on every save. AccessRule_Create/_Update gain matching parameters (defaulting to NULL so the +-- procs stay backward compatible). The Read procs use SELECT * and pick the new columns up automatically. +-- +-- PAM is an unshipped POC behind the pm-37044-pam-v-0 flag with no production data; server + migration deploy +-- together, so the affected procs are altered in place rather than versioned. + +IF COL_LENGTH('[dbo].[AccessRule]', 'DefaultLeaseDurationSeconds') IS NULL +BEGIN + ALTER TABLE [dbo].[AccessRule] + ADD [DefaultLeaseDurationSeconds] INT NULL +END +GO + +IF COL_LENGTH('[dbo].[AccessRule]', 'MaxLeaseDurationSeconds') IS NULL +BEGIN + ALTER TABLE [dbo].[AccessRule] + ADD [MaxLeaseDurationSeconds] INT NULL +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRule_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @Name NVARCHAR(256), + @Description NVARCHAR(MAX) = NULL, + @Conditions NVARCHAR(MAX), + @SingleActiveLease BIT = 0, + @DefaultLeaseDurationSeconds INT = NULL, + @MaxLeaseDurationSeconds INT = NULL, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[AccessRule] + ( + [Id], + [OrganizationId], + [Name], + [Description], + [Conditions], + [SingleActiveLease], + [DefaultLeaseDurationSeconds], + [MaxLeaseDurationSeconds], + [CreationDate], + [RevisionDate] + ) + VALUES + ( + @Id, + @OrganizationId, + @Name, + @Description, + @Conditions, + @SingleActiveLease, + @DefaultLeaseDurationSeconds, + @MaxLeaseDurationSeconds, + @CreationDate, + @RevisionDate + ) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRule_Update] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Name NVARCHAR(256), + @Description NVARCHAR(MAX) = NULL, + @Conditions NVARCHAR(MAX), + @SingleActiveLease BIT = 0, + @DefaultLeaseDurationSeconds INT = NULL, + @MaxLeaseDurationSeconds INT = NULL, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[AccessRule] + SET + [OrganizationId] = @OrganizationId, + [Name] = @Name, + [Description] = @Description, + [Conditions] = @Conditions, + [SingleActiveLease] = @SingleActiveLease, + [DefaultLeaseDurationSeconds] = @DefaultLeaseDurationSeconds, + [MaxLeaseDurationSeconds] = @MaxLeaseDurationSeconds, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate + WHERE + [Id] = @Id +END +GO diff --git a/util/Migrator/DbScripts/2026-06-11_02_DeferAutoApprovedLeaseMintToActivation.sql b/util/Migrator/DbScripts/2026-06-11_02_DeferAutoApprovedLeaseMintToActivation.sql new file mode 100644 index 000000000000..09487a0262d1 --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-11_02_DeferAutoApprovedLeaseMintToActivation.sql @@ -0,0 +1,62 @@ +-- PAM Credential Leasing: the automatic (no-approval) path no longer mints a lease at submit. Like the human path, +-- it now only records the request — already Approved, with its automatic verdict — and the requester activates the +-- approved request when they actually want access ([AccessLease_CreateFromApprovedRequest]), which is when the active +-- lease is minted. This makes "start the lease" an explicit caller action on both paths and leaves the per-cipher +-- single-active-lease guard living on the single remaining mint site (activation). +-- +-- [AccessRequest_CreateAutoApproved] replaces [AccessLease_CreateAutoApproved]: it writes the request + automatic +-- decision in one transaction and inserts no lease. The old proc is dropped outright rather than left as a no-longer +-- called shim. Acceptable here — the feature is an unshipped POC behind the pm-37044-pam-v-0 flag and server + +-- migration deploy together. + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_CreateAutoApproved] + @AccessRequestId UNIQUEIDENTIFIER, + @AccessDecisionId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @CollectionId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @RequesterId UNIQUEIDENTIFIER, + @NotBefore DATETIME2(7), + @NotAfter DATETIME2(7), + @Reason NVARCHAR(MAX) = NULL, + @ConditionKind NVARCHAR(50) = NULL, + @CreationDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- Atomically record an auto-approved request and its automatic verdict. No lease is minted here: the requester + -- activates the approved request later via [AccessLease_CreateFromApprovedRequest], exactly like the human path + -- after approval. The per-cipher single-active-lease guard therefore lives entirely on that activation path. + BEGIN TRANSACTION AccessRequest_CreateAutoApproved + + -- The request is created already resolved (Approved). ExtensionOfLeaseId stays NULL: it is reserved for extension + -- requests; provenance for an original lease flows the other way, via AccessLease.AccessRequestId. + INSERT INTO [dbo].[AccessRequest] + ( + [Id], [ExtensionOfLeaseId], [OrganizationId], [CollectionId], [CipherId], [RequesterId], + [NotBefore], [NotAfter], [Reason], [Status], [CreationDate], [ResolvedDate] + ) + VALUES + ( + @AccessRequestId, NULL, @OrganizationId, @CollectionId, @CipherId, @RequesterId, + @NotBefore, @NotAfter, @Reason, 1 /* Approved */, @CreationDate, @CreationDate + ) + + INSERT INTO [dbo].[AccessDecision] + ( + [Id], [AccessRequestId], [DeciderKind], [ApproverId], [ConditionKind], + [Verdict], [Comment], [EvaluationContext], [CreationDate] + ) + VALUES + ( + @AccessDecisionId, @AccessRequestId, 0 /* Automatic */, NULL, @ConditionKind, + 0 /* Approve */, NULL, NULL, @CreationDate + ) + + COMMIT TRANSACTION AccessRequest_CreateAutoApproved +END +GO + +DROP PROCEDURE IF EXISTS [dbo].[AccessLease_CreateAutoApproved] +GO diff --git a/util/Migrator/DbScripts/2026-06-11_03_AddAccessRequestCancel.sql b/util/Migrator/DbScripts/2026-06-11_03_AddAccessRequestCancel.sql new file mode 100644 index 000000000000..3c58e008d989 --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-11_03_AddAccessRequestCancel.sql @@ -0,0 +1,22 @@ +-- PAM Credential Leasing: the requester can withdraw their own still-pending access request. The client already +-- issues DELETE /leasing/requests/{id} for this, but the server had no handler — this adds the missing transition. +-- +-- [AccessRequest_Cancel] flips a Pending request to Cancelled (status 3) and stamps its ResolvedDate. No AccessDecision +-- row is written: a cancellation is the requester acting on their own request, not an approver verdict. The WHERE guard +-- keeps the write idempotent under a race so an already-resolved request is left untouched. +-- +-- Feature is behind the pm-37044-pam-v-0 flag (unshipped POC); server + migration deploy together. + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_Cancel] + @AccessRequestId UNIQUEIDENTIFIER, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE [dbo].[AccessRequest] + SET [Status] = 3, -- Cancelled + [ResolvedDate] = @Now + WHERE [Id] = @AccessRequestId AND [Status] = 0 -- Pending +END +GO diff --git a/util/Migrator/DbScripts/2026-06-12_00_AddAccessRuleEnabled.sql b/util/Migrator/DbScripts/2026-06-12_00_AddAccessRuleEnabled.sql new file mode 100644 index 000000000000..f27c0b564de4 --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-12_00_AddAccessRuleEnabled.sql @@ -0,0 +1,98 @@ +-- PAM Credential Leasing: per-rule enabled flag. +-- +-- AccessRule gains [Enabled] (BIT NOT NULL, default 1). When false the rule is inactive and does not gate +-- access for the collections it governs. The client already sends and reads the flag (and toggles it from the +-- access-rules list), but the server had nowhere to persist it, so it was dropped on every save and every read +-- defaulted it back to enabled. AccessRule_Create/_Update gain a matching @Enabled parameter (defaulting to 1 +-- so the procs stay backward compatible). The Read procs use SELECT * and pick the new column up automatically. +-- +-- PAM is an unshipped POC behind the pm-37044-pam-v-0 flag with no production data; server + migration deploy +-- together, so the affected procs are altered in place rather than versioned. + +IF COL_LENGTH('[dbo].[AccessRule]', 'Enabled') IS NULL +BEGIN + ALTER TABLE [dbo].[AccessRule] + ADD [Enabled] BIT NOT NULL CONSTRAINT [DF_AccessRule_Enabled] DEFAULT (1) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRule_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @Name NVARCHAR(256), + @Description NVARCHAR(MAX) = NULL, + @Conditions NVARCHAR(MAX), + @SingleActiveLease BIT = 0, + @DefaultLeaseDurationSeconds INT = NULL, + @MaxLeaseDurationSeconds INT = NULL, + @Enabled BIT = 1, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[AccessRule] + ( + [Id], + [OrganizationId], + [Name], + [Description], + [Conditions], + [SingleActiveLease], + [DefaultLeaseDurationSeconds], + [MaxLeaseDurationSeconds], + [Enabled], + [CreationDate], + [RevisionDate] + ) + VALUES + ( + @Id, + @OrganizationId, + @Name, + @Description, + @Conditions, + @SingleActiveLease, + @DefaultLeaseDurationSeconds, + @MaxLeaseDurationSeconds, + @Enabled, + @CreationDate, + @RevisionDate + ) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRule_Update] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Name NVARCHAR(256), + @Description NVARCHAR(MAX) = NULL, + @Conditions NVARCHAR(MAX), + @SingleActiveLease BIT = 0, + @DefaultLeaseDurationSeconds INT = NULL, + @MaxLeaseDurationSeconds INT = NULL, + @Enabled BIT = 1, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[AccessRule] + SET + [OrganizationId] = @OrganizationId, + [Name] = @Name, + [Description] = @Description, + [Conditions] = @Conditions, + [SingleActiveLease] = @SingleActiveLease, + [DefaultLeaseDurationSeconds] = @DefaultLeaseDurationSeconds, + [MaxLeaseDurationSeconds] = @MaxLeaseDurationSeconds, + [Enabled] = @Enabled, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate + WHERE + [Id] = @Id +END +GO diff --git a/util/Migrator/DbScripts/2026-06-12_01_AddAccessRuleExtensionSettings.sql b/util/Migrator/DbScripts/2026-06-12_01_AddAccessRuleExtensionSettings.sql new file mode 100644 index 000000000000..b1343aff9f18 --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-12_01_AddAccessRuleExtensionSettings.sql @@ -0,0 +1,114 @@ +-- PAM Credential Leasing: per-rule extension settings. +-- +-- AccessRule gains [AllowsExtensions] (BIT NOT NULL, default 0) and [MaxExtensions] (INT NULL). When +-- AllowsExtensions is true, a member holding an active lease under the rule may extend it (always auto-approved), +-- up to MaxExtensions times. AccessRule_Create/_Update gain matching parameters (defaulting so the procs stay +-- backward compatible). The Read procs use SELECT * and pick the new columns up automatically. +-- +-- PAM is an unshipped POC behind the pm-37044-pam-v-0 flag with no production data; server + migration deploy +-- together, so the affected procs are altered in place rather than versioned. + +IF COL_LENGTH('[dbo].[AccessRule]', 'AllowsExtensions') IS NULL +BEGIN + ALTER TABLE [dbo].[AccessRule] + ADD [AllowsExtensions] BIT NOT NULL CONSTRAINT [DF_AccessRule_AllowsExtensions] DEFAULT (0) +END +GO + +IF COL_LENGTH('[dbo].[AccessRule]', 'MaxExtensions') IS NULL +BEGIN + ALTER TABLE [dbo].[AccessRule] + ADD [MaxExtensions] INT NULL +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRule_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @Name NVARCHAR(256), + @Description NVARCHAR(MAX) = NULL, + @Conditions NVARCHAR(MAX), + @SingleActiveLease BIT = 0, + @DefaultLeaseDurationSeconds INT = NULL, + @MaxLeaseDurationSeconds INT = NULL, + @Enabled BIT = 1, + @AllowsExtensions BIT = 0, + @MaxExtensions INT = NULL, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[AccessRule] + ( + [Id], + [OrganizationId], + [Name], + [Description], + [Conditions], + [SingleActiveLease], + [DefaultLeaseDurationSeconds], + [MaxLeaseDurationSeconds], + [Enabled], + [AllowsExtensions], + [MaxExtensions], + [CreationDate], + [RevisionDate] + ) + VALUES + ( + @Id, + @OrganizationId, + @Name, + @Description, + @Conditions, + @SingleActiveLease, + @DefaultLeaseDurationSeconds, + @MaxLeaseDurationSeconds, + @Enabled, + @AllowsExtensions, + @MaxExtensions, + @CreationDate, + @RevisionDate + ) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRule_Update] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Name NVARCHAR(256), + @Description NVARCHAR(MAX) = NULL, + @Conditions NVARCHAR(MAX), + @SingleActiveLease BIT = 0, + @DefaultLeaseDurationSeconds INT = NULL, + @MaxLeaseDurationSeconds INT = NULL, + @Enabled BIT = 1, + @AllowsExtensions BIT = 0, + @MaxExtensions INT = NULL, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[AccessRule] + SET + [OrganizationId] = @OrganizationId, + [Name] = @Name, + [Description] = @Description, + [Conditions] = @Conditions, + [SingleActiveLease] = @SingleActiveLease, + [DefaultLeaseDurationSeconds] = @DefaultLeaseDurationSeconds, + [MaxLeaseDurationSeconds] = @MaxLeaseDurationSeconds, + [Enabled] = @Enabled, + [AllowsExtensions] = @AllowsExtensions, + [MaxExtensions] = @MaxExtensions, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate + WHERE + [Id] = @Id +END +GO diff --git a/util/Migrator/DbScripts/2026-06-12_02_AddLeaseExtension.sql b/util/Migrator/DbScripts/2026-06-12_02_AddLeaseExtension.sql new file mode 100644 index 000000000000..d120ee1e5210 --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-12_02_AddLeaseExtension.sql @@ -0,0 +1,125 @@ +-- PAM Credential Leasing: lease extension. +-- +-- A member holding an active lease on an extension-enabled item (see AccessRule.AllowsExtensions / MaxExtensions) +-- may extend it. Extensions are always auto-approved: an already-Approved AccessRequest with a non-NULL +-- ExtensionOfLeaseId is recorded together with its automatic AccessDecision, and the parent lease's NotAfter is +-- pushed out in place — no new lease is minted, so the single-active-lease invariant is preserved. +-- +-- * AccessRequest_CreateApprovedExtension - atomic, per-lease-locked: guards the lease is active and the +-- per-rule max has not been reached, then writes the request + +-- decision and extends the lease. +-- * AccessRequest_CountExtensionsByLeaseId - extension count for a lease (cap pre-check + UI "remaining"). +-- * AccessRequest_ReadActiveApprovedByRequesterIdCipherId - now excludes extension requests, which extend the +-- parent lease in place and must never surface as activatable. +-- +-- PAM is an unshipped POC behind the pm-37044-pam-v-0 flag with no production data; server + migration deploy +-- together, so the affected proc is altered in place rather than versioned. + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_CreateApprovedExtension] + @AccessRequestId UNIQUEIDENTIFIER, + @AccessDecisionId UNIQUEIDENTIFIER, + @ExtensionOfLeaseId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @CollectionId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @RequesterId UNIQUEIDENTIFIER, + @NotBefore DATETIME2(7), + @NotAfter DATETIME2(7), + @Reason NVARCHAR(MAX) = NULL, + @MaxExtensions INT, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + SET XACT_ABORT ON + + BEGIN TRANSACTION + + IF NOT EXISTS ( + SELECT 1 + FROM [dbo].[AccessLease] WITH (UPDLOCK, HOLDLOCK) + WHERE [Id] = @ExtensionOfLeaseId + AND [RequesterId] = @RequesterId + AND [Status] = 0 /* Active */ + AND [NotAfter] > @Now + ) + BEGIN + ROLLBACK TRANSACTION + SELECT 0 -- LeaseNotActive + RETURN + END + + IF (SELECT COUNT(*) FROM [dbo].[AccessRequest] WHERE [ExtensionOfLeaseId] = @ExtensionOfLeaseId) >= @MaxExtensions + BEGIN + ROLLBACK TRANSACTION + SELECT -1 -- MaxExtensionsReached + RETURN + END + + INSERT INTO [dbo].[AccessRequest] + ( + [Id], [ExtensionOfLeaseId], [OrganizationId], [CollectionId], [CipherId], [RequesterId], + [NotBefore], [NotAfter], [Reason], [Status], [CreationDate], [ResolvedDate] + ) + VALUES + ( + @AccessRequestId, @ExtensionOfLeaseId, @OrganizationId, @CollectionId, @CipherId, @RequesterId, + @NotBefore, @NotAfter, @Reason, 1 /* Approved */, @Now, @Now + ) + + INSERT INTO [dbo].[AccessDecision] + ( + [Id], [AccessRequestId], [DeciderKind], [ApproverId], [ConditionKind], + [Verdict], [Comment], [EvaluationContext], [CreationDate] + ) + VALUES + ( + @AccessDecisionId, @AccessRequestId, 0 /* Automatic */, NULL, NULL, + 0 /* Approve */, NULL, NULL, @Now + ) + + UPDATE [dbo].[AccessLease] + SET [NotAfter] = @NotAfter + WHERE [Id] = @ExtensionOfLeaseId + + COMMIT TRANSACTION + + SELECT 1 -- Extended +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_CountExtensionsByLeaseId] + @LeaseId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT COUNT(*) + FROM [dbo].[AccessRequest] + WHERE [ExtensionOfLeaseId] = @LeaseId +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_ReadActiveApprovedByRequesterIdCipherId] + @RequesterId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + SELECT TOP 1 + AR.* + FROM + [dbo].[AccessRequest] AR + WHERE + AR.[RequesterId] = @RequesterId + AND AR.[CipherId] = @CipherId + AND AR.[Status] = 1 -- Approved + AND AR.[NotAfter] > @Now + AND AR.[ExtensionOfLeaseId] IS NULL + AND NOT EXISTS (SELECT 1 FROM [dbo].[AccessLease] AL WHERE AL.[AccessRequestId] = AR.[Id]) + ORDER BY + AR.[CreationDate] DESC +END +GO diff --git a/util/Migrator/DbScripts/2026-06-15_00_PamCancelApprovedRequests.sql b/util/Migrator/DbScripts/2026-06-15_00_PamCancelApprovedRequests.sql new file mode 100644 index 000000000000..68a97c3a2d37 --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-15_00_PamCancelApprovedRequests.sql @@ -0,0 +1,66 @@ +-- PAM Credential Leasing: broaden cancellation so a not-yet-activated access request can be ended by either the +-- requester or a managing approver. A request is cancellable while Pending or Approved-without-a-lease; once it has +-- produced a lease it is governed by that lease (revoke instead). +-- +-- [AccessRequest_Cancel] (requester path): flips a Pending/Approved request with no lease to Cancelled (status 3), +-- stamps ResolvedDate. No AccessDecision is written. +-- [AccessRequest_CancelWithDecision] (approver path): flips it to Denied (status 2) and records the approver's human +-- Deny decision, mirroring [AccessRequest_ResolveWithDecision]. The decision is inserted only when the transition +-- happens (@@ROWCOUNT > 0), so a no-op never orphans a decision. +-- Both procs guard on Status IN (0,1) AND no AccessLease for the request, keeping the write race-safe. +-- +-- Feature is behind the pm-37044-pam-v-0 flag (unshipped POC); server + migration deploy together. + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_Cancel] + @AccessRequestId UNIQUEIDENTIFIER, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE [dbo].[AccessRequest] + SET [Status] = 3, -- Cancelled + [ResolvedDate] = @Now + WHERE [Id] = @AccessRequestId + AND [Status] IN (0, 1) -- Pending or Approved + AND NOT EXISTS (SELECT 1 FROM [dbo].[AccessLease] L WHERE L.[AccessRequestId] = @AccessRequestId) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_CancelWithDecision] + @AccessRequestId UNIQUEIDENTIFIER, + @AccessDecisionId UNIQUEIDENTIFIER, + @ApproverId UNIQUEIDENTIFIER, + @Verdict TINYINT, + @Comment NVARCHAR(MAX) = NULL, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + BEGIN TRANSACTION AccessRequest_CancelWithDecision + + UPDATE [dbo].[AccessRequest] + SET [Status] = 2, -- Denied + [ResolvedDate] = @Now + WHERE [Id] = @AccessRequestId + AND [Status] IN (0, 1) -- Pending or Approved + AND NOT EXISTS (SELECT 1 FROM [dbo].[AccessLease] L WHERE L.[AccessRequestId] = @AccessRequestId) + + IF @@ROWCOUNT > 0 + BEGIN + INSERT INTO [dbo].[AccessDecision] + ( + [Id], [AccessRequestId], [DeciderKind], [ApproverId], [ConditionKind], + [Verdict], [Comment], [EvaluationContext], [CreationDate] + ) + VALUES + ( + @AccessDecisionId, @AccessRequestId, 1 /* Human */, @ApproverId, NULL, + @Verdict, @Comment, NULL, @Now + ) + END + + COMMIT TRANSACTION AccessRequest_CancelWithDecision +END +GO diff --git a/util/Migrator/DbScripts/2026-06-15_00_RenameMaxExtensionsToMaxExtensionDuration.sql b/util/Migrator/DbScripts/2026-06-15_00_RenameMaxExtensionsToMaxExtensionDuration.sql new file mode 100644 index 000000000000..dd5a23e72aa9 --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-15_00_RenameMaxExtensionsToMaxExtensionDuration.sql @@ -0,0 +1,187 @@ +-- PAM Credential Leasing: rework extension settings. +-- +-- Product direction changed: a lease may now be extended exactly once, and the admin instead caps the *length* of +-- that extension. So AccessRule.[MaxExtensions] (a count) is dropped in favour of [MaxExtensionDurationSeconds] (the +-- longest a single extension may run), and AccessRequest_CreateApprovedExtension no longer takes @MaxExtensions — +-- it rejects when any extension request already exists for the lease. +-- +-- Supersedes the column/proc shapes added in 2026-06-12_01/02 (kept intact so already-migrated dev DBs roll forward +-- cleanly). PAM is an unshipped POC behind the pm-37044-pam-v-0 flag with no production data; the affected procs are +-- altered in place. + +IF COL_LENGTH('[dbo].[AccessRule]', 'MaxExtensions') IS NOT NULL +BEGIN + ALTER TABLE [dbo].[AccessRule] DROP COLUMN [MaxExtensions] +END +GO + +IF COL_LENGTH('[dbo].[AccessRule]', 'MaxExtensionDurationSeconds') IS NULL +BEGIN + ALTER TABLE [dbo].[AccessRule] ADD [MaxExtensionDurationSeconds] INT NULL +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRule_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @Name NVARCHAR(256), + @Description NVARCHAR(MAX) = NULL, + @Conditions NVARCHAR(MAX), + @SingleActiveLease BIT = 0, + @DefaultLeaseDurationSeconds INT = NULL, + @MaxLeaseDurationSeconds INT = NULL, + @Enabled BIT = 1, + @AllowsExtensions BIT = 0, + @MaxExtensionDurationSeconds INT = NULL, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[AccessRule] + ( + [Id], + [OrganizationId], + [Name], + [Description], + [Conditions], + [SingleActiveLease], + [DefaultLeaseDurationSeconds], + [MaxLeaseDurationSeconds], + [Enabled], + [AllowsExtensions], + [MaxExtensionDurationSeconds], + [CreationDate], + [RevisionDate] + ) + VALUES + ( + @Id, + @OrganizationId, + @Name, + @Description, + @Conditions, + @SingleActiveLease, + @DefaultLeaseDurationSeconds, + @MaxLeaseDurationSeconds, + @Enabled, + @AllowsExtensions, + @MaxExtensionDurationSeconds, + @CreationDate, + @RevisionDate + ) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRule_Update] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Name NVARCHAR(256), + @Description NVARCHAR(MAX) = NULL, + @Conditions NVARCHAR(MAX), + @SingleActiveLease BIT = 0, + @DefaultLeaseDurationSeconds INT = NULL, + @MaxLeaseDurationSeconds INT = NULL, + @Enabled BIT = 1, + @AllowsExtensions BIT = 0, + @MaxExtensionDurationSeconds INT = NULL, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[AccessRule] + SET + [OrganizationId] = @OrganizationId, + [Name] = @Name, + [Description] = @Description, + [Conditions] = @Conditions, + [SingleActiveLease] = @SingleActiveLease, + [DefaultLeaseDurationSeconds] = @DefaultLeaseDurationSeconds, + [MaxLeaseDurationSeconds] = @MaxLeaseDurationSeconds, + [Enabled] = @Enabled, + [AllowsExtensions] = @AllowsExtensions, + [MaxExtensionDurationSeconds] = @MaxExtensionDurationSeconds, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate + WHERE + [Id] = @Id +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_CreateApprovedExtension] + @AccessRequestId UNIQUEIDENTIFIER, + @AccessDecisionId UNIQUEIDENTIFIER, + @ExtensionOfLeaseId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @CollectionId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @RequesterId UNIQUEIDENTIFIER, + @NotBefore DATETIME2(7), + @NotAfter DATETIME2(7), + @Reason NVARCHAR(MAX) = NULL, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + SET XACT_ABORT ON + + BEGIN TRANSACTION + + IF NOT EXISTS ( + SELECT 1 + FROM [dbo].[AccessLease] WITH (UPDLOCK, HOLDLOCK) + WHERE [Id] = @ExtensionOfLeaseId + AND [RequesterId] = @RequesterId + AND [Status] = 0 /* Active */ + AND [NotAfter] > @Now + ) + BEGIN + ROLLBACK TRANSACTION + SELECT 0 -- LeaseNotActive + RETURN + END + + -- A lease may be extended exactly once. Counted under the lease lock, so it is race-safe against a concurrent + -- extension of the same lease. + IF EXISTS (SELECT 1 FROM [dbo].[AccessRequest] WHERE [ExtensionOfLeaseId] = @ExtensionOfLeaseId) + BEGIN + ROLLBACK TRANSACTION + SELECT -1 -- AlreadyExtended + RETURN + END + + INSERT INTO [dbo].[AccessRequest] + ( + [Id], [ExtensionOfLeaseId], [OrganizationId], [CollectionId], [CipherId], [RequesterId], + [NotBefore], [NotAfter], [Reason], [Status], [CreationDate], [ResolvedDate] + ) + VALUES + ( + @AccessRequestId, @ExtensionOfLeaseId, @OrganizationId, @CollectionId, @CipherId, @RequesterId, + @NotBefore, @NotAfter, @Reason, 1 /* Approved */, @Now, @Now + ) + + INSERT INTO [dbo].[AccessDecision] + ( + [Id], [AccessRequestId], [DeciderKind], [ApproverId], [ConditionKind], + [Verdict], [Comment], [EvaluationContext], [CreationDate] + ) + VALUES + ( + @AccessDecisionId, @AccessRequestId, 0 /* Automatic */, NULL, NULL, + 0 /* Approve */, NULL, NULL, @Now + ) + + UPDATE [dbo].[AccessLease] + SET [NotAfter] = @NotAfter + WHERE [Id] = @ExtensionOfLeaseId + + COMMIT TRANSACTION + + SELECT 1 -- Extended +END +GO diff --git a/util/Migrator/DbScripts/2026-06-15_01_AddPamGovernanceLeaseListSprocs.sql b/util/Migrator/DbScripts/2026-06-15_01_AddPamGovernanceLeaseListSprocs.sql new file mode 100644 index 000000000000..7750e8988fdf --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-15_01_AddPamGovernanceLeaseListSprocs.sql @@ -0,0 +1,56 @@ +-- PAM Credential Leasing: governance lease read models. Two new sprocs back GET /leases/active and +-- GET /leases/history, which list all active / all recently-ended leases on the collections the caller can Manage +-- (resolved the same way as the approver inbox), powering the governance dashboard. Both take the caller's +-- manageable collection ids as a GuidIdArray TVP, mirroring [AccessRequest_ReadInboxPendingByCollectionIds] / +-- [AccessRequest_ReadInboxHistoryByCollectionIds]. +-- +-- Feature is behind the pm-37044-pam-v-0 flag (unshipped POC); server + migration deploy together. + +CREATE OR ALTER PROCEDURE [dbo].[AccessLease_ReadManyActiveByCollectionIds] + @CollectionIds [dbo].[GuidIdArray] READONLY, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- Every currently-active lease (Active, window containing @Now) on the supplied (caller-manageable) collections, + -- across all members -- not just the caller's own. + SELECT + L.* + FROM + [dbo].[AccessLease] L + INNER JOIN @CollectionIds CI ON CI.[Id] = L.[CollectionId] + WHERE + L.[Status] = 0 -- Active + AND L.[NotBefore] <= @Now + AND L.[NotAfter] > @Now + ORDER BY + L.[NotAfter] ASC +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessLease_ReadManyEndedByCollectionIds] + @CollectionIds [dbo].[GuidIdArray] READONLY, + @Since DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- Leases that have ended (Expired or Revoked) on the supplied (caller-manageable) collections, that ended on or + -- after @Since. A revoked lease's end is its RevokedDate; an expired lease's end is its NotAfter. Most recently + -- ended first. + SELECT + L.* + FROM + [dbo].[AccessLease] L + INNER JOIN @CollectionIds CI ON CI.[Id] = L.[CollectionId] + WHERE + L.[Status] IN (1, 2) -- Expired, Revoked + AND ( + (L.[Status] = 2 AND L.[RevokedDate] >= @Since) -- Revoked + OR (L.[Status] = 1 AND L.[NotAfter] >= @Since) -- Expired + ) + ORDER BY + CASE WHEN L.[Status] = 2 THEN L.[RevokedDate] ELSE L.[NotAfter] END DESC +END +GO diff --git a/util/Migrator/DbScripts/2026-06-15_02_AddAccessLeaseCollectionIdIndex.sql b/util/Migrator/DbScripts/2026-06-15_02_AddAccessLeaseCollectionIdIndex.sql new file mode 100644 index 000000000000..dd1516f96943 --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-15_02_AddAccessLeaseCollectionIdIndex.sql @@ -0,0 +1,13 @@ +-- PAM Credential Leasing: supporting index for the governance lease lists. AccessLease_ReadManyActiveByCollectionIds +-- and AccessLease_ReadManyEndedByCollectionIds (GET /leases/active, /leases/history) filter AccessLease by the +-- caller's manageable collection ids; without a CollectionId index those scans fall back to the table. This adds the +-- seekable index. No behaviour change. +-- +-- Feature is behind the pm-37044-pam-v-0 flag (unshipped POC); server + migration deploy together. + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE [Name] = 'IX_AccessLease_CollectionId_Status' AND object_id = OBJECT_ID('[dbo].[AccessLease]')) +BEGIN + CREATE NONCLUSTERED INDEX [IX_AccessLease_CollectionId_Status] + ON [dbo].[AccessLease] ([CollectionId] ASC, [Status] ASC); +END +GO diff --git a/util/Migrator/DbScripts/2026-06-16_00_AddApproverIdentityToAccessRequestReads.sql b/util/Migrator/DbScripts/2026-06-16_00_AddApproverIdentityToAccessRequestReads.sql new file mode 100644 index 000000000000..8141867eca9a --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-16_00_AddApproverIdentityToAccessRequestReads.sql @@ -0,0 +1,158 @@ +-- PAM access-request reads: resolve the human approver's identity (name + email) alongside the existing ApproverId. +-- The reads already surfaced the resolving approver's id, but only as a raw GUID; the caller's own request list +-- ("My Requests") and the approver-inbox audit log had no way to name who decided a request. Each read resolves the +-- approver inside the existing human-decision OUTER APPLY (LEFT JOIN to [User] on the decision's ApproverId), so the +-- new ApproverName/ApproverEmail follow the same earliest-human-decision row as ApproverId/ApproverComment. + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_ReadManyByRequesterId] + @RequesterId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + -- The caller's own requests across every org, all statuses. Unlike the approver-inbox reads this is a + -- caller-scoped self-read, so the cipher/collection/requester display-name joins are intentionally omitted + -- (those name fields stay null) -- cipher/collection names come from the caller's local vault, and the + -- requester is the caller. The approver, however, is resolved here (ApproverName/ApproverEmail) because the + -- requester has no other way to name who decided their request. Capped at the 250 most recent; the client + -- renders far fewer. + SELECT TOP (250) + LR.[Id], + LR.[ExtensionOfLeaseId], + LR.[OrganizationId], + LR.[CollectionId], + LR.[CipherId], + LR.[RequesterId], + LR.[NotBefore], + LR.[NotAfter], + LR.[Reason], + LR.[Status], + LR.[CreationDate], + LR.[ResolvedDate], + PL.[Id] AS [ProducedLeaseId], + RES.[ApproverId] AS [ApproverId], + RES.[ApproverName] AS [ApproverName], + RES.[ApproverEmail] AS [ApproverEmail], + RES.[Comment] AS [ApproverComment] + FROM [dbo].[AccessRequest] LR + OUTER APPLY ( + SELECT TOP 1 L.[Id] + FROM [dbo].[AccessLease] L + WHERE L.[AccessRequestId] = LR.[Id] + ORDER BY L.[CreationDate] DESC + ) PL + OUTER APPLY ( + SELECT TOP 1 LD.[ApproverId], LD.[Comment], AU.[Name] AS [ApproverName], AU.[Email] AS [ApproverEmail] + FROM [dbo].[AccessDecision] LD + LEFT JOIN [dbo].[User] AU ON AU.[Id] = LD.[ApproverId] + WHERE LD.[AccessRequestId] = LR.[Id] AND LD.[DeciderKind] = 1 -- Human + ORDER BY LD.[CreationDate] ASC + ) RES + WHERE LR.[RequesterId] = @RequesterId + ORDER BY LR.[CreationDate] DESC +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_ReadInboxPendingByCollectionIds] + @CollectionIds [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + SELECT + LR.[Id], + LR.[ExtensionOfLeaseId], + LR.[OrganizationId], + LR.[CollectionId], + LR.[CipherId], + LR.[RequesterId], + LR.[NotBefore], + LR.[NotAfter], + LR.[Reason], + LR.[Status], + LR.[CreationDate], + LR.[ResolvedDate], + PL.[Id] AS [ProducedLeaseId], + PL.[Status] AS [ProducedLeaseStatus], + RES.[ApproverId] AS [ApproverId], + RES.[ApproverName] AS [ApproverName], + RES.[ApproverEmail] AS [ApproverEmail], + RES.[Comment] AS [ApproverComment], + JSON_VALUE(C.[Data], '$.Name') AS [CipherName], + COL.[Name] AS [CollectionName], + U.[Name] AS [RequesterName], + U.[Email] AS [RequesterEmail] + FROM [dbo].[AccessRequest] LR + INNER JOIN @CollectionIds CI ON CI.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[Cipher] C ON C.[Id] = LR.[CipherId] + LEFT JOIN [dbo].[Collection] COL ON COL.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[User] U ON U.[Id] = LR.[RequesterId] + OUTER APPLY ( + SELECT TOP 1 L.[Id], L.[Status] + FROM [dbo].[AccessLease] L + WHERE L.[AccessRequestId] = LR.[Id] + ORDER BY L.[CreationDate] DESC + ) PL + OUTER APPLY ( + SELECT TOP 1 LD.[ApproverId], LD.[Comment], AU.[Name] AS [ApproverName], AU.[Email] AS [ApproverEmail] + FROM [dbo].[AccessDecision] LD + LEFT JOIN [dbo].[User] AU ON AU.[Id] = LD.[ApproverId] + WHERE LD.[AccessRequestId] = LR.[Id] AND LD.[DeciderKind] = 1 -- Human + ORDER BY LD.[CreationDate] ASC + ) RES + WHERE LR.[Status] = 0 -- Pending +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_ReadInboxHistoryByCollectionIds] + @CollectionIds [dbo].[GuidIdArray] READONLY, + @Since DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + SELECT + LR.[Id], + LR.[ExtensionOfLeaseId], + LR.[OrganizationId], + LR.[CollectionId], + LR.[CipherId], + LR.[RequesterId], + LR.[NotBefore], + LR.[NotAfter], + LR.[Reason], + LR.[Status], + LR.[CreationDate], + LR.[ResolvedDate], + PL.[Id] AS [ProducedLeaseId], + PL.[Status] AS [ProducedLeaseStatus], + RES.[ApproverId] AS [ApproverId], + RES.[ApproverName] AS [ApproverName], + RES.[ApproverEmail] AS [ApproverEmail], + RES.[Comment] AS [ApproverComment], + JSON_VALUE(C.[Data], '$.Name') AS [CipherName], + COL.[Name] AS [CollectionName], + U.[Name] AS [RequesterName], + U.[Email] AS [RequesterEmail] + FROM [dbo].[AccessRequest] LR + INNER JOIN @CollectionIds CI ON CI.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[Cipher] C ON C.[Id] = LR.[CipherId] + LEFT JOIN [dbo].[Collection] COL ON COL.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[User] U ON U.[Id] = LR.[RequesterId] + OUTER APPLY ( + SELECT TOP 1 L.[Id], L.[Status] + FROM [dbo].[AccessLease] L + WHERE L.[AccessRequestId] = LR.[Id] + ORDER BY L.[CreationDate] DESC + ) PL + OUTER APPLY ( + SELECT TOP 1 LD.[ApproverId], LD.[Comment], AU.[Name] AS [ApproverName], AU.[Email] AS [ApproverEmail] + FROM [dbo].[AccessDecision] LD + LEFT JOIN [dbo].[User] AU ON AU.[Id] = LD.[ApproverId] + WHERE LD.[AccessRequestId] = LR.[Id] AND LD.[DeciderKind] = 1 -- Human + ORDER BY LD.[CreationDate] ASC + ) RES + WHERE LR.[Status] <> 0 -- not Pending + AND LR.[CreationDate] >= @Since +END +GO diff --git a/util/Migrator/DbScripts/2026-06-16_00_PamConditionKindToTinyint.sql b/util/Migrator/DbScripts/2026-06-16_00_PamConditionKindToTinyint.sql new file mode 100644 index 000000000000..1c7925bc7146 --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-16_00_PamConditionKindToTinyint.sql @@ -0,0 +1,63 @@ +-- PAM Credential Leasing: convert [AccessDecision].[ConditionKind] from a magic-string NVARCHAR(50) to a TINYINT, +-- backed by the new AccessConditionKind enum (HumanApproval = 0, IpAllowlist = 1, TimeOfDay = 2). The column records +-- which condition produced an automatic decision; it is internal-only (never returned to clients) and currently always +-- NULL, so no data backfill is required. The conditions JSON keeps its string `kind` discriminator. Acceptable as a +-- straight type change here: the feature is an unshipped POC behind the pm-37044-pam-v-0 flag and the server + +-- migration deploy together. + +IF COL_LENGTH('[dbo].[AccessDecision]', 'ConditionKind') IS NOT NULL +BEGIN + ALTER TABLE [dbo].[AccessDecision] + ALTER COLUMN [ConditionKind] TINYINT NULL; +END +GO + +-- Re-create the only proc that takes @ConditionKind as a parameter so its type matches the column. +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_CreateAutoApproved] + @AccessRequestId UNIQUEIDENTIFIER, + @AccessDecisionId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @CollectionId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @RequesterId UNIQUEIDENTIFIER, + @NotBefore DATETIME2(7), + @NotAfter DATETIME2(7), + @Reason NVARCHAR(MAX) = NULL, + @ConditionKind TINYINT = NULL, + @CreationDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- Atomically record an auto-approved request and its automatic verdict. No lease is minted here: the requester + -- activates the approved request later via [AccessLease_CreateFromApprovedRequest], exactly like the human path + -- after approval. The per-cipher single-active-lease guard therefore lives entirely on that activation path. + BEGIN TRANSACTION AccessRequest_CreateAutoApproved + + -- The request is created already resolved (Approved). ExtensionOfLeaseId stays NULL: it is reserved for extension + -- requests; provenance for an original lease flows the other way, via AccessLease.AccessRequestId. + INSERT INTO [dbo].[AccessRequest] + ( + [Id], [ExtensionOfLeaseId], [OrganizationId], [CollectionId], [CipherId], [RequesterId], + [NotBefore], [NotAfter], [Reason], [Status], [CreationDate], [ResolvedDate] + ) + VALUES + ( + @AccessRequestId, NULL, @OrganizationId, @CollectionId, @CipherId, @RequesterId, + @NotBefore, @NotAfter, @Reason, 1 /* Approved */, @CreationDate, @CreationDate + ) + + INSERT INTO [dbo].[AccessDecision] + ( + [Id], [AccessRequestId], [DeciderKind], [ApproverId], [ConditionKind], + [Verdict], [Comment], [EvaluationContext], [CreationDate] + ) + VALUES + ( + @AccessDecisionId, @AccessRequestId, 0 /* Automatic */, NULL, @ConditionKind, + 0 /* Approve */, NULL, NULL, @CreationDate + ) + + COMMIT TRANSACTION AccessRequest_CreateAutoApproved +END +GO diff --git a/util/Migrator/DbScripts/2026-06-16_01_AddDecisionsToAccessRequestReads.sql b/util/Migrator/DbScripts/2026-06-16_01_AddDecisionsToAccessRequestReads.sql new file mode 100644 index 000000000000..9e172f6705d9 --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-16_01_AddDecisionsToAccessRequestReads.sql @@ -0,0 +1,153 @@ +-- PAM access-request reads: return each request's decisions as a second result set instead of collapsing to a single +-- denormalized resolver. AccessDecision is a 1-to-many with AccessRequest, so the resolved reads now return (1) the +-- request rows and (2) every decision (human or automatic) keyed by AccessRequestId, oldest first -- DeciderKind says +-- which, and a human decision's identity is denormalized from [User]; the caller groups them onto each request's +-- decision list. This drops the previous TOP-1 earliest-decision collapse and the flat +-- ApproverId/ApproverName/ApproverEmail/ApproverComment columns. Pending requests have no decision yet, so that read +-- stays a single result set. + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_ReadManyByRequesterId] + @RequesterId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT TOP (250) + LR.[Id], + LR.[ExtensionOfLeaseId], + LR.[OrganizationId], + LR.[CollectionId], + LR.[CipherId], + LR.[RequesterId], + LR.[NotBefore], + LR.[NotAfter], + LR.[Reason], + LR.[Status], + LR.[CreationDate], + LR.[ResolvedDate], + PL.[Id] AS [ProducedLeaseId] + FROM [dbo].[AccessRequest] LR + OUTER APPLY ( + SELECT TOP 1 L.[Id] + FROM [dbo].[AccessLease] L + WHERE L.[AccessRequestId] = LR.[Id] + ORDER BY L.[CreationDate] DESC + ) PL + WHERE LR.[RequesterId] = @RequesterId + ORDER BY LR.[CreationDate] DESC + + SELECT + AD.[AccessRequestId], + AD.[DeciderKind] AS [DeciderKind], + AD.[ApproverId] AS [Id], + AU.[Name] AS [Name], + AU.[Email] AS [Email], + AD.[Comment] AS [Comment], + AD.[Verdict] AS [Verdict], + AD.[CreationDate] AS [DecidedAt] + FROM [dbo].[AccessDecision] AD + INNER JOIN [dbo].[AccessRequest] LR ON LR.[Id] = AD.[AccessRequestId] + LEFT JOIN [dbo].[User] AU ON AU.[Id] = AD.[ApproverId] + WHERE LR.[RequesterId] = @RequesterId + ORDER BY AD.[AccessRequestId], AD.[CreationDate] ASC +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_ReadInboxPendingByCollectionIds] + @CollectionIds [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + SELECT + LR.[Id], + LR.[ExtensionOfLeaseId], + LR.[OrganizationId], + LR.[CollectionId], + LR.[CipherId], + LR.[RequesterId], + LR.[NotBefore], + LR.[NotAfter], + LR.[Reason], + LR.[Status], + LR.[CreationDate], + LR.[ResolvedDate], + PL.[Id] AS [ProducedLeaseId], + PL.[Status] AS [ProducedLeaseStatus], + JSON_VALUE(C.[Data], '$.Name') AS [CipherName], + COL.[Name] AS [CollectionName], + U.[Name] AS [RequesterName], + U.[Email] AS [RequesterEmail] + FROM [dbo].[AccessRequest] LR + INNER JOIN @CollectionIds CI ON CI.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[Cipher] C ON C.[Id] = LR.[CipherId] + LEFT JOIN [dbo].[Collection] COL ON COL.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[User] U ON U.[Id] = LR.[RequesterId] + OUTER APPLY ( + SELECT TOP 1 L.[Id], L.[Status] + FROM [dbo].[AccessLease] L + WHERE L.[AccessRequestId] = LR.[Id] + ORDER BY L.[CreationDate] DESC + ) PL + WHERE LR.[Status] = 0 -- Pending +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_ReadInboxHistoryByCollectionIds] + @CollectionIds [dbo].[GuidIdArray] READONLY, + @Since DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + SELECT + LR.[Id], + LR.[ExtensionOfLeaseId], + LR.[OrganizationId], + LR.[CollectionId], + LR.[CipherId], + LR.[RequesterId], + LR.[NotBefore], + LR.[NotAfter], + LR.[Reason], + LR.[Status], + LR.[CreationDate], + LR.[ResolvedDate], + PL.[Id] AS [ProducedLeaseId], + PL.[Status] AS [ProducedLeaseStatus], + JSON_VALUE(C.[Data], '$.Name') AS [CipherName], + COL.[Name] AS [CollectionName], + U.[Name] AS [RequesterName], + U.[Email] AS [RequesterEmail] + FROM [dbo].[AccessRequest] LR + INNER JOIN @CollectionIds CI ON CI.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[Cipher] C ON C.[Id] = LR.[CipherId] + LEFT JOIN [dbo].[Collection] COL ON COL.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[User] U ON U.[Id] = LR.[RequesterId] + OUTER APPLY ( + SELECT TOP 1 L.[Id], L.[Status] + FROM [dbo].[AccessLease] L + WHERE L.[AccessRequestId] = LR.[Id] + ORDER BY L.[CreationDate] DESC + ) PL + WHERE LR.[Status] <> 0 -- not Pending + AND LR.[CreationDate] >= @Since + + SELECT + AD.[AccessRequestId], + AD.[DeciderKind] AS [DeciderKind], + AD.[ApproverId] AS [Id], + AU.[Name] AS [Name], + AU.[Email] AS [Email], + AD.[Comment] AS [Comment], + AD.[Verdict] AS [Verdict], + AD.[CreationDate] AS [DecidedAt] + FROM [dbo].[AccessDecision] AD + INNER JOIN [dbo].[AccessRequest] LR ON LR.[Id] = AD.[AccessRequestId] + INNER JOIN @CollectionIds CI ON CI.[Id] = LR.[CollectionId] + LEFT JOIN [dbo].[User] AU ON AU.[Id] = AD.[ApproverId] + WHERE LR.[Status] <> 0 -- not Pending + AND LR.[CreationDate] >= @Since + ORDER BY AD.[AccessRequestId], AD.[CreationDate] ASC +END +GO diff --git a/util/Migrator/DbScripts/2026-06-17_00_FlipAccessDecisionVerdict.sql b/util/Migrator/DbScripts/2026-06-17_00_FlipAccessDecisionVerdict.sql new file mode 100644 index 000000000000..708aa898393e --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-17_00_FlipAccessDecisionVerdict.sql @@ -0,0 +1,181 @@ +-- Flip the AccessDecisionVerdict encoding so the values read naturally falsy/truthy: Deny = 0, Approve = 1 +-- (previously Approve = 0, Deny = 1). The member names are unchanged; only the numeric wire/stored values swap. +-- +-- DATA: any existing AccessDecision rows were written under the old encoding, so this migration MUST flip their +-- stored Verdict in the same step the procedures change -- otherwise every historical verdict inverts. The CASE +-- swap below is a no-op on an empty table and self-inverse, but this migration is NOT safe to run twice. +UPDATE [dbo].[AccessDecision] +SET [Verdict] = CASE [Verdict] WHEN 0 THEN 1 ELSE 0 END +GO + +-- Auto-approved request: the automatic verdict literal moves from 0 to 1 (Approve). +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_CreateAutoApproved] + @AccessRequestId UNIQUEIDENTIFIER, + @AccessDecisionId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @CollectionId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @RequesterId UNIQUEIDENTIFIER, + @NotBefore DATETIME2(7), + @NotAfter DATETIME2(7), + @Reason NVARCHAR(MAX) = NULL, + @ConditionKind TINYINT = NULL, + @CreationDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- Atomically record an auto-approved request and its automatic verdict. No lease is minted here: the requester + -- activates the approved request later via [AccessLease_CreateFromApprovedRequest], exactly like the human path + -- after approval. The per-cipher single-active-lease guard therefore lives entirely on that activation path. + BEGIN TRANSACTION AccessRequest_CreateAutoApproved + + -- The request is created already resolved (Approved). ExtensionOfLeaseId stays NULL: it is reserved for extension + -- requests; provenance for an original lease flows the other way, via AccessLease.AccessRequestId. + INSERT INTO [dbo].[AccessRequest] + ( + [Id], [ExtensionOfLeaseId], [OrganizationId], [CollectionId], [CipherId], [RequesterId], + [NotBefore], [NotAfter], [Reason], [Status], [CreationDate], [ResolvedDate] + ) + VALUES + ( + @AccessRequestId, NULL, @OrganizationId, @CollectionId, @CipherId, @RequesterId, + @NotBefore, @NotAfter, @Reason, 1 /* Approved */, @CreationDate, @CreationDate + ) + + INSERT INTO [dbo].[AccessDecision] + ( + [Id], [AccessRequestId], [DeciderKind], [ApproverId], [ConditionKind], + [Verdict], [Comment], [EvaluationContext], [CreationDate] + ) + VALUES + ( + @AccessDecisionId, @AccessRequestId, 0 /* Automatic */, NULL, @ConditionKind, + 1 /* Approve */, NULL, NULL, @CreationDate + ) + + COMMIT TRANSACTION AccessRequest_CreateAutoApproved +END +GO + +-- Auto-approved extension: the automatic verdict literal moves from 0 to 1 (Approve). +CREATE OR ALTER PROCEDURE [dbo].[AccessRequest_CreateApprovedExtension] + @AccessRequestId UNIQUEIDENTIFIER, + @AccessDecisionId UNIQUEIDENTIFIER, + @ExtensionOfLeaseId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @CollectionId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @RequesterId UNIQUEIDENTIFIER, + @NotBefore DATETIME2(7), + @NotAfter DATETIME2(7), + @Reason NVARCHAR(MAX) = NULL, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + -- An explicit transaction holds the per-lease range lock until the writes commit, so concurrent extensions of + -- the same lease serialize. XACT_ABORT guarantees rollback (and a clean pooled connection) on any error. + SET XACT_ABORT ON + + BEGIN TRANSACTION + + -- Lock the parent lease row for the life of the transaction. A second concurrent extension of the same lease + -- blocks here until this transaction commits, then re-counts below and sees this extension. The lease must + -- still be active and in-window to be extendable; outcome 0 is distinct from the cap conflict (-1). + IF NOT EXISTS ( + SELECT 1 + FROM [dbo].[AccessLease] WITH (UPDLOCK, HOLDLOCK) + WHERE [Id] = @ExtensionOfLeaseId + AND [RequesterId] = @RequesterId + AND [Status] = 0 /* Active */ + AND [NotAfter] > @Now + ) + BEGIN + ROLLBACK TRANSACTION + SELECT 0 -- LeaseNotActive + RETURN + END + + -- A lease may be extended exactly once. Counted under the lease lock, so it is race-safe against a concurrent + -- extension of the same lease. + IF EXISTS (SELECT 1 FROM [dbo].[AccessRequest] WHERE [ExtensionOfLeaseId] = @ExtensionOfLeaseId) + BEGIN + ROLLBACK TRANSACTION + SELECT -1 -- AlreadyExtended + RETURN + END + + -- Record the auto-approved extension request and its automatic verdict, then push the parent lease's end out in + -- place. No new lease is minted — extending reuses the existing lease, preserving the single-active-lease + -- invariant. The request's window spans the extension ([old lease end] .. [new lease end]); its NotAfter is the + -- lease's new end. + INSERT INTO [dbo].[AccessRequest] + ( + [Id], [ExtensionOfLeaseId], [OrganizationId], [CollectionId], [CipherId], [RequesterId], + [NotBefore], [NotAfter], [Reason], [Status], [CreationDate], [ResolvedDate] + ) + VALUES + ( + @AccessRequestId, @ExtensionOfLeaseId, @OrganizationId, @CollectionId, @CipherId, @RequesterId, + @NotBefore, @NotAfter, @Reason, 1 /* Approved */, @Now, @Now + ) + + INSERT INTO [dbo].[AccessDecision] + ( + [Id], [AccessRequestId], [DeciderKind], [ApproverId], [ConditionKind], + [Verdict], [Comment], [EvaluationContext], [CreationDate] + ) + VALUES + ( + @AccessDecisionId, @AccessRequestId, 0 /* Automatic */, NULL, NULL, + 1 /* Approve */, NULL, NULL, @Now + ) + + UPDATE [dbo].[AccessLease] + SET [NotAfter] = @NotAfter + WHERE [Id] = @ExtensionOfLeaseId + + COMMIT TRANSACTION + + SELECT 1 -- Extended +END +GO + +-- Lease revocation: the human Deny verdict literal moves from 1 to 0 (Deny). +CREATE OR ALTER PROCEDURE [dbo].[AccessLease_Revoke] + @AccessLeaseId UNIQUEIDENTIFIER, + @AccessRequestId UNIQUEIDENTIFIER, + @RevokedBy UNIQUEIDENTIFIER, + @AccessDecisionId UNIQUEIDENTIFIER, + @Reason NVARCHAR(MAX) = NULL, + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- Atomically revoke an active lease and capture who/why. The revocation reason has no dedicated column, so it is + -- preserved as a human AccessDecision (Deny) against the lease's originating request, keeping the audit trail + -- without a schema change. The WHERE guard keeps revocation idempotent if two approvers race. + BEGIN TRANSACTION AccessLease_Revoke + + UPDATE [dbo].[AccessLease] + SET [Status] = 2 /* Revoked */, + [RevokedDate] = @Now, + [RevokedBy] = @RevokedBy + WHERE [Id] = @AccessLeaseId AND [Status] = 0 -- Active + + INSERT INTO [dbo].[AccessDecision] + ( + [Id], [AccessRequestId], [DeciderKind], [ApproverId], [ConditionKind], + [Verdict], [Comment], [EvaluationContext], [CreationDate] + ) + VALUES + ( + @AccessDecisionId, @AccessRequestId, 1 /* Human */, @RevokedBy, NULL, + 0 /* Deny */, @Reason, NULL, @Now + ) + + COMMIT TRANSACTION AccessLease_Revoke +END +GO diff --git a/util/Migrator/packages.lock.json b/util/Migrator/packages.lock.json index 7f2638dbd1b1..486544b61d07 100644 --- a/util/Migrator/packages.lock.json +++ b/util/Migrator/packages.lock.json @@ -1177,6 +1177,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1201,6 +1202,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1213,6 +1215,15 @@ "ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis": "[2.0.2, 2.0.2]", "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } + }, + "data": { + "type": "Project" + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } } } } diff --git a/util/MsSqlMigratorUtility/packages.lock.json b/util/MsSqlMigratorUtility/packages.lock.json index de08a4200ff9..d9d5a85261a8 100644 --- a/util/MsSqlMigratorUtility/packages.lock.json +++ b/util/MsSqlMigratorUtility/packages.lock.json @@ -1210,6 +1210,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1234,6 +1235,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1247,13 +1249,22 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "migrator": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "Microsoft.Extensions.Logging": "[10.0.8, 10.0.8]", "dbup-sqlserver": "[7.2.0, 7.2.0]" } + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } } } } diff --git a/util/MySqlMigrations/Migrations/20260526122321_AddAccessRule.Designer.cs b/util/MySqlMigrations/Migrations/20260526122321_AddAccessRule.Designer.cs new file mode 100644 index 000000000000..98519865e331 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20260526122321_AddAccessRule.Designer.cs @@ -0,0 +1,3816 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20260526122321_AddAccessRule")] + partial class AddAccessRule + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CollectionName") + .HasColumnType("longtext"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("GroupName") + .HasColumnType("longtext"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("UserGuid") + .HasColumnType("char(36)"); + + b.Property("UserName") + .HasColumnType("longtext"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessRuleId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("longtext"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AccessRuleId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExemptFromBillingAutomation") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("LimitItemDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("SelfHost") + .HasColumnType("tinyint(1)"); + + b.Property("SmSeats") + .HasColumnType("int"); + + b.Property("SmServiceAccounts") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("SyncSeats") + .HasColumnType("tinyint(1)"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("tinyint(1)"); + + b.Property("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseDisableSmAdsForUsers") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseInviteLinks") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UseMyItems") + .HasColumnType("tinyint(1)"); + + b.Property("UseOrganizationDomains") + .HasColumnType("tinyint(1)"); + + b.Property("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePhishingBlocker") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseRiskInsights") + .HasColumnType("tinyint(1)"); + + b.Property("UseScim") + .HasColumnType("tinyint(1)"); + + b.Property("UseSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("UseSso") + .HasColumnType("tinyint(1)"); + + b.Property("UseTotp") + .HasColumnType("tinyint(1)"); + + b.Property("UsersGetPremium") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp", "UsersGetPremium" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationInviteLink", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowedDomains") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Code") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedInviteKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("EncryptedOrgKey") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInviteLink", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BillingPhone") + .HasColumnType("longtext"); + + b.Property("BusinessAddress1") + .HasColumnType("longtext"); + + b.Property("BusinessAddress2") + .HasColumnType("longtext"); + + b.Property("BusinessAddress3") + .HasColumnType("longtext"); + + b.Property("BusinessCountry") + .HasColumnType("longtext"); + + b.Property("BusinessName") + .HasColumnType("longtext"); + + b.Property("BusinessTaxNumber") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DiscountId") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Settings") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("varchar(25)"); + + b.Property("Approved") + .HasColumnType("tinyint(1)"); + + b.Property("AuthenticationDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MasterPasswordHash") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ResponseDate") + .HasColumnType("datetime(6)"); + + b.Property("ResponseDeviceId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("GranteeId") + .HasColumnType("char(36)"); + + b.Property("GrantorId") + .HasColumnType("char(36)"); + + b.Property("KeyEncrypted") + .HasColumnType("longtext"); + + b.Property("LastNotificationDate") + .HasColumnType("datetime(6)"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("WaitTimeDays") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AaGuid") + .HasColumnType("char(36)"); + + b.Property("Counter") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SupportsPrf") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohort", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ChurnDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("MigrationPathId") + .HasColumnType("tinyint unsigned"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ProactiveDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Name") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohort", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohortAssignment", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ChurnDiscountAppliedDate") + .HasColumnType("datetime(6)"); + + b.Property("CohortId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("MigratedDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ScheduledDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("CohortId", "ScheduledDate", "MigratedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohortAssignment", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AssignedSeats") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Total") + .HasColumnType("decimal(65,30)"); + + b.Property("UsedSeats") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllocatedSeats") + .HasColumnType("int"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("PurchasedSeats") + .HasColumnType("int"); + + b.Property("SeatMinimum") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.SubscriptionDiscount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AmountOff") + .HasColumnType("bigint"); + + b.Property("AudienceType") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Currency") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Duration") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("DurationInMonths") + .HasColumnType("int"); + + b.Property("EndDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PercentOff") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("StartDate") + .HasColumnType("datetime(6)"); + + b.Property("StripeCouponId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("StripeProductIds") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("StripeCouponId") + .IsUnique(); + + b.HasIndex("StartDate", "EndDate") + .HasDatabaseName("IX_SubscriptionDiscount_DateRange") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SubscriptionDiscount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("Filters") + .HasColumnType("longtext"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Template") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApplicationAtRiskCount") + .HasColumnType("int"); + + b.Property("ApplicationCount") + .HasColumnType("int"); + + b.Property("ApplicationData") + .HasColumnType("longtext"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CriticalApplicationAtRiskCount") + .HasColumnType("int"); + + b.Property("CriticalApplicationCount") + .HasColumnType("int"); + + b.Property("CriticalMemberAtRiskCount") + .HasColumnType("int"); + + b.Property("CriticalMemberCount") + .HasColumnType("int"); + + b.Property("CriticalPasswordAtRiskCount") + .HasColumnType("int"); + + b.Property("CriticalPasswordCount") + .HasColumnType("int"); + + b.Property("MemberAtRiskCount") + .HasColumnType("int"); + + b.Property("MemberCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PasswordAtRiskCount") + .HasColumnType("int"); + + b.Property("PasswordCount") + .HasColumnType("int"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ReportFile") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SummaryData") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Uri") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("varchar(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAtTime") + .HasColumnType("datetime(6)"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Active") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("ClientVersion") + .HasMaxLength(43) + .HasColumnType("varchar(43)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("longtext"); + + b.Property("EncryptedPublicKey") + .HasColumnType("longtext"); + + b.Property("EncryptedUserKey") + .HasColumnType("longtext"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ActingUserId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("DeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("DomainName") + .HasColumnType("longtext"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("PolicyId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("ProviderOrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderUserId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SystemUser") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("JobRunCount") + .HasColumnType("int"); + + b.Property("LastCheckedDate") + .HasColumnType("datetime(6)"); + + b.Property("NextRunDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VerifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("tinyint(1)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("RevocationReason") + .HasColumnType("tinyint unsigned"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlayId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("PlayId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PlayItem", null, t => + { + t.HasCheckConstraint("CK_PlayItem_UserOrOrganization", "(\"UserId\" IS NOT NULL AND \"OrganizationId\" IS NULL) OR (\"UserId\" IS NULL AND \"OrganizationId\" IS NOT NULL)"); + }); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("AuthType") + .HasColumnType("tinyint unsigned"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("Emails") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MaxAccessCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property("KdfIterations") + .HasColumnType("int"); + + b.Property("KdfMemory") + .HasColumnType("int"); + + b.Property("KdfParallelism") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastApiKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastEmailChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKdfChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MasterPasswordSalt") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Premium") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("SecurityState") + .HasColumnType("longtext"); + + b.Property("SecurityVersion") + .HasColumnType("int"); + + b.Property("SignedPublicKey") + .HasColumnType("longtext"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("V2UpgradeToken") + .HasColumnType("longtext"); + + b.Property("VerifyDevices") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SignatureAlgorithm") + .HasColumnType("tinyint unsigned"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("varchar(3000)"); + + b.Property("ClientType") + .HasColumnType("tinyint unsigned"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Global") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Priority") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("TaskId") + .HasColumnType("char(36)"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("NotificationId") + .HasColumnType("char(36)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReadDate") + .HasColumnType("datetime(6)"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Conditions") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("AccessRule", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property("Read") + .HasColumnType("tinyint(1)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Write") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("EditorServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VersionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Archives") + .HasColumnType("longtext"); + + b.Property("Attachments") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Favorites") + .HasColumnType("longtext"); + + b.Property("Folders") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("char(36)"); + + b.Property("SecretsId") + .HasColumnType("char(36)"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", null) + .WithMany() + .HasForeignKey("AccessRuleId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationInviteLink", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohortAssignment", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohort", "Cohort") + .WithMany() + .HasForeignKey("CohortId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cohort"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("SecretVersions"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/MySqlMigrations/Migrations/20260526122321_AddAccessRule.cs b/util/MySqlMigrations/Migrations/20260526122321_AddAccessRule.cs new file mode 100644 index 000000000000..4b909a234265 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20260526122321_AddAccessRule.cs @@ -0,0 +1,85 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class AddAccessRule : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AccessRuleId", + table: "Collection", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci"); + + migrationBuilder.CreateTable( + name: "AccessRule", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + OrganizationId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Name = table.Column(type: "varchar(256)", maxLength: 256, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Description = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + Conditions = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreationDate = table.Column(type: "datetime(6)", nullable: false), + RevisionDate = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AccessRule", x => x.Id); + table.ForeignKey( + name: "FK_AccessRule_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_Collection_AccessRuleId", + table: "Collection", + column: "AccessRuleId"); + + migrationBuilder.CreateIndex( + name: "IX_AccessRule_OrganizationId_Name", + table: "AccessRule", + columns: new[] { "OrganizationId", "Name" }, + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Collection_AccessRule_AccessRuleId", + table: "Collection", + column: "AccessRuleId", + principalTable: "AccessRule", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Collection_AccessRule_AccessRuleId", + table: "Collection"); + + migrationBuilder.DropTable( + name: "AccessRule"); + + migrationBuilder.DropIndex( + name: "IX_Collection_AccessRuleId", + table: "Collection"); + + migrationBuilder.DropColumn( + name: "AccessRuleId", + table: "Collection"); + } +} diff --git a/util/MySqlMigrations/Migrations/20260616102519_AddAccessRuleLeaseConfiguration.Designer.cs b/util/MySqlMigrations/Migrations/20260616102519_AddAccessRuleLeaseConfiguration.Designer.cs new file mode 100644 index 000000000000..68e99b901e8a --- /dev/null +++ b/util/MySqlMigrations/Migrations/20260616102519_AddAccessRuleLeaseConfiguration.Designer.cs @@ -0,0 +1,3839 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20260616102519_AddAccessRuleLeaseConfiguration")] + partial class AddAccessRuleLeaseConfiguration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CollectionName") + .HasColumnType("longtext"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("GroupName") + .HasColumnType("longtext"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("UserGuid") + .HasColumnType("char(36)"); + + b.Property("UserName") + .HasColumnType("longtext"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessRuleId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("longtext"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AccessRuleId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExemptFromBillingAutomation") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("LimitItemDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("SelfHost") + .HasColumnType("tinyint(1)"); + + b.Property("SmSeats") + .HasColumnType("int"); + + b.Property("SmServiceAccounts") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("SyncSeats") + .HasColumnType("tinyint(1)"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("tinyint(1)"); + + b.Property("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseDisableSmAdsForUsers") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseInviteLinks") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UseMyItems") + .HasColumnType("tinyint(1)"); + + b.Property("UseOrganizationDomains") + .HasColumnType("tinyint(1)"); + + b.Property("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePhishingBlocker") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseRiskInsights") + .HasColumnType("tinyint(1)"); + + b.Property("UseScim") + .HasColumnType("tinyint(1)"); + + b.Property("UseSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("UseSso") + .HasColumnType("tinyint(1)"); + + b.Property("UseTotp") + .HasColumnType("tinyint(1)"); + + b.Property("UsersGetPremium") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp", "UsersGetPremium" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationInviteLink", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowedDomains") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Code") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedInviteKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("EncryptedOrgKey") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInviteLink", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BillingPhone") + .HasColumnType("longtext"); + + b.Property("BusinessAddress1") + .HasColumnType("longtext"); + + b.Property("BusinessAddress2") + .HasColumnType("longtext"); + + b.Property("BusinessAddress3") + .HasColumnType("longtext"); + + b.Property("BusinessCountry") + .HasColumnType("longtext"); + + b.Property("BusinessName") + .HasColumnType("longtext"); + + b.Property("BusinessTaxNumber") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DiscountId") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Settings") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("varchar(25)"); + + b.Property("Approved") + .HasColumnType("tinyint(1)"); + + b.Property("AuthenticationDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MasterPasswordHash") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ResponseDate") + .HasColumnType("datetime(6)"); + + b.Property("ResponseDeviceId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("GranteeId") + .HasColumnType("char(36)"); + + b.Property("GrantorId") + .HasColumnType("char(36)"); + + b.Property("KeyEncrypted") + .HasColumnType("longtext"); + + b.Property("LastNotificationDate") + .HasColumnType("datetime(6)"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("WaitTimeDays") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AaGuid") + .HasColumnType("char(36)"); + + b.Property("Counter") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SupportsPrf") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohort", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ChurnDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("MigrationPathId") + .HasColumnType("tinyint unsigned"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ProactiveDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Name") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohort", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohortAssignment", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ChurnDiscountAppliedDate") + .HasColumnType("datetime(6)"); + + b.Property("CohortId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("MigratedDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ScheduledDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("CohortId", "ScheduledDate", "MigratedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohortAssignment", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AssignedSeats") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Total") + .HasColumnType("decimal(65,30)"); + + b.Property("UsedSeats") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllocatedSeats") + .HasColumnType("int"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("PurchasedSeats") + .HasColumnType("int"); + + b.Property("SeatMinimum") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.SubscriptionDiscount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AmountOff") + .HasColumnType("bigint"); + + b.Property("AudienceType") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Currency") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Duration") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("DurationInMonths") + .HasColumnType("int"); + + b.Property("EndDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PercentOff") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("StartDate") + .HasColumnType("datetime(6)"); + + b.Property("StripeCouponId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("StripeProductIds") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("StripeCouponId") + .IsUnique(); + + b.HasIndex("StartDate", "EndDate") + .HasDatabaseName("IX_SubscriptionDiscount_DateRange") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SubscriptionDiscount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("Filters") + .HasColumnType("longtext"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Template") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApplicationAtRiskCount") + .HasColumnType("int"); + + b.Property("ApplicationCount") + .HasColumnType("int"); + + b.Property("ApplicationData") + .HasColumnType("longtext"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CriticalApplicationAtRiskCount") + .HasColumnType("int"); + + b.Property("CriticalApplicationCount") + .HasColumnType("int"); + + b.Property("CriticalMemberAtRiskCount") + .HasColumnType("int"); + + b.Property("CriticalMemberCount") + .HasColumnType("int"); + + b.Property("CriticalPasswordAtRiskCount") + .HasColumnType("int"); + + b.Property("CriticalPasswordCount") + .HasColumnType("int"); + + b.Property("MemberAtRiskCount") + .HasColumnType("int"); + + b.Property("MemberCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PasswordAtRiskCount") + .HasColumnType("int"); + + b.Property("PasswordCount") + .HasColumnType("int"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ReportFile") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SummaryData") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Uri") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("varchar(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAtTime") + .HasColumnType("datetime(6)"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Active") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("ClientVersion") + .HasMaxLength(43) + .HasColumnType("varchar(43)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("longtext"); + + b.Property("EncryptedPublicKey") + .HasColumnType("longtext"); + + b.Property("EncryptedUserKey") + .HasColumnType("longtext"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ActingUserId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("DeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("DomainName") + .HasColumnType("longtext"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("PolicyId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("ProviderOrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderUserId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SystemUser") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("JobRunCount") + .HasColumnType("int"); + + b.Property("LastCheckedDate") + .HasColumnType("datetime(6)"); + + b.Property("NextRunDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VerifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("tinyint(1)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("RevocationReason") + .HasColumnType("tinyint unsigned"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("StatusNew") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlayId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("PlayId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PlayItem", null, t => + { + t.HasCheckConstraint("CK_PlayItem_UserOrOrganization", "(\"UserId\" IS NOT NULL AND \"OrganizationId\" IS NULL) OR (\"UserId\" IS NULL AND \"OrganizationId\" IS NOT NULL)"); + }); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("AuthType") + .HasColumnType("tinyint unsigned"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("Emails") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MaxAccessCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property("KdfIterations") + .HasColumnType("int"); + + b.Property("KdfMemory") + .HasColumnType("int"); + + b.Property("KdfParallelism") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastApiKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastEmailChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKdfChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MasterPasswordSalt") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Premium") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("SecurityState") + .HasColumnType("longtext"); + + b.Property("SecurityVersion") + .HasColumnType("int"); + + b.Property("SignedPublicKey") + .HasColumnType("longtext"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("V2UpgradeToken") + .HasColumnType("longtext"); + + b.Property("VerifyDevices") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SignatureAlgorithm") + .HasColumnType("tinyint unsigned"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("varchar(3000)"); + + b.Property("ClientType") + .HasColumnType("tinyint unsigned"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Global") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Priority") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("TaskId") + .HasColumnType("char(36)"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("NotificationId") + .HasColumnType("char(36)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReadDate") + .HasColumnType("datetime(6)"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowsExtensions") + .HasColumnType("tinyint(1)"); + + b.Property("Conditions") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DefaultLeaseDurationSeconds") + .HasColumnType("int"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("MaxExtensionDurationSeconds") + .HasColumnType("int"); + + b.Property("MaxLeaseDurationSeconds") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SingleActiveLease") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("AccessRule", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property("Read") + .HasColumnType("tinyint(1)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Write") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("EditorServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VersionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Archives") + .HasColumnType("longtext"); + + b.Property("Attachments") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Favorites") + .HasColumnType("longtext"); + + b.Property("Folders") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("char(36)"); + + b.Property("SecretsId") + .HasColumnType("char(36)"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", null) + .WithMany() + .HasForeignKey("AccessRuleId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationInviteLink", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohortAssignment", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohort", "Cohort") + .WithMany() + .HasForeignKey("CohortId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cohort"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("SecretVersions"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/MySqlMigrations/Migrations/20260616102519_AddAccessRuleLeaseConfiguration.cs b/util/MySqlMigrations/Migrations/20260616102519_AddAccessRuleLeaseConfiguration.cs new file mode 100644 index 000000000000..e7f8ba13dc04 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20260616102519_AddAccessRuleLeaseConfiguration.cs @@ -0,0 +1,80 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class AddAccessRuleLeaseConfiguration : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AllowsExtensions", + table: "AccessRule", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "DefaultLeaseDurationSeconds", + table: "AccessRule", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "Enabled", + table: "AccessRule", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "MaxExtensionDurationSeconds", + table: "AccessRule", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "MaxLeaseDurationSeconds", + table: "AccessRule", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "SingleActiveLease", + table: "AccessRule", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AllowsExtensions", + table: "AccessRule"); + + migrationBuilder.DropColumn( + name: "DefaultLeaseDurationSeconds", + table: "AccessRule"); + + migrationBuilder.DropColumn( + name: "Enabled", + table: "AccessRule"); + + migrationBuilder.DropColumn( + name: "MaxExtensionDurationSeconds", + table: "AccessRule"); + + migrationBuilder.DropColumn( + name: "MaxLeaseDurationSeconds", + table: "AccessRule"); + + migrationBuilder.DropColumn( + name: "SingleActiveLease", + table: "AccessRule"); + } +} diff --git a/util/MySqlMigrations/Migrations/20260616102606_AddUsePamToOrganization.Designer.cs b/util/MySqlMigrations/Migrations/20260616102606_AddUsePamToOrganization.Designer.cs new file mode 100644 index 000000000000..4e33c65d3d6d --- /dev/null +++ b/util/MySqlMigrations/Migrations/20260616102606_AddUsePamToOrganization.Designer.cs @@ -0,0 +1,3842 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20260616102606_AddUsePamToOrganization")] + partial class AddUsePamToOrganization + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CollectionName") + .HasColumnType("longtext"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("GroupName") + .HasColumnType("longtext"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("UserGuid") + .HasColumnType("char(36)"); + + b.Property("UserName") + .HasColumnType("longtext"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessRuleId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("longtext"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AccessRuleId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExemptFromBillingAutomation") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("LimitItemDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("SelfHost") + .HasColumnType("tinyint(1)"); + + b.Property("SmSeats") + .HasColumnType("int"); + + b.Property("SmServiceAccounts") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("SyncSeats") + .HasColumnType("tinyint(1)"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("tinyint(1)"); + + b.Property("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseDisableSmAdsForUsers") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseInviteLinks") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UseMyItems") + .HasColumnType("tinyint(1)"); + + b.Property("UseOrganizationDomains") + .HasColumnType("tinyint(1)"); + + b.Property("UsePam") + .HasColumnType("tinyint(1)"); + + b.Property("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePhishingBlocker") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseRiskInsights") + .HasColumnType("tinyint(1)"); + + b.Property("UseScim") + .HasColumnType("tinyint(1)"); + + b.Property("UseSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("UseSso") + .HasColumnType("tinyint(1)"); + + b.Property("UseTotp") + .HasColumnType("tinyint(1)"); + + b.Property("UsersGetPremium") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp", "UsersGetPremium" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationInviteLink", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowedDomains") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Code") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedInviteKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("EncryptedOrgKey") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInviteLink", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BillingPhone") + .HasColumnType("longtext"); + + b.Property("BusinessAddress1") + .HasColumnType("longtext"); + + b.Property("BusinessAddress2") + .HasColumnType("longtext"); + + b.Property("BusinessAddress3") + .HasColumnType("longtext"); + + b.Property("BusinessCountry") + .HasColumnType("longtext"); + + b.Property("BusinessName") + .HasColumnType("longtext"); + + b.Property("BusinessTaxNumber") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DiscountId") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Settings") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("varchar(25)"); + + b.Property("Approved") + .HasColumnType("tinyint(1)"); + + b.Property("AuthenticationDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MasterPasswordHash") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ResponseDate") + .HasColumnType("datetime(6)"); + + b.Property("ResponseDeviceId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("GranteeId") + .HasColumnType("char(36)"); + + b.Property("GrantorId") + .HasColumnType("char(36)"); + + b.Property("KeyEncrypted") + .HasColumnType("longtext"); + + b.Property("LastNotificationDate") + .HasColumnType("datetime(6)"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("WaitTimeDays") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AaGuid") + .HasColumnType("char(36)"); + + b.Property("Counter") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SupportsPrf") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohort", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ChurnDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("MigrationPathId") + .HasColumnType("tinyint unsigned"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ProactiveDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Name") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohort", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohortAssignment", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ChurnDiscountAppliedDate") + .HasColumnType("datetime(6)"); + + b.Property("CohortId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("MigratedDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ScheduledDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("CohortId", "ScheduledDate", "MigratedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohortAssignment", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AssignedSeats") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Total") + .HasColumnType("decimal(65,30)"); + + b.Property("UsedSeats") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllocatedSeats") + .HasColumnType("int"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("PurchasedSeats") + .HasColumnType("int"); + + b.Property("SeatMinimum") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.SubscriptionDiscount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AmountOff") + .HasColumnType("bigint"); + + b.Property("AudienceType") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Currency") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Duration") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("DurationInMonths") + .HasColumnType("int"); + + b.Property("EndDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PercentOff") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("StartDate") + .HasColumnType("datetime(6)"); + + b.Property("StripeCouponId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("StripeProductIds") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("StripeCouponId") + .IsUnique(); + + b.HasIndex("StartDate", "EndDate") + .HasDatabaseName("IX_SubscriptionDiscount_DateRange") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SubscriptionDiscount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("Filters") + .HasColumnType("longtext"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Template") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApplicationAtRiskCount") + .HasColumnType("int"); + + b.Property("ApplicationCount") + .HasColumnType("int"); + + b.Property("ApplicationData") + .HasColumnType("longtext"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CriticalApplicationAtRiskCount") + .HasColumnType("int"); + + b.Property("CriticalApplicationCount") + .HasColumnType("int"); + + b.Property("CriticalMemberAtRiskCount") + .HasColumnType("int"); + + b.Property("CriticalMemberCount") + .HasColumnType("int"); + + b.Property("CriticalPasswordAtRiskCount") + .HasColumnType("int"); + + b.Property("CriticalPasswordCount") + .HasColumnType("int"); + + b.Property("MemberAtRiskCount") + .HasColumnType("int"); + + b.Property("MemberCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PasswordAtRiskCount") + .HasColumnType("int"); + + b.Property("PasswordCount") + .HasColumnType("int"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ReportFile") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SummaryData") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Uri") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("varchar(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAtTime") + .HasColumnType("datetime(6)"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Active") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("ClientVersion") + .HasMaxLength(43) + .HasColumnType("varchar(43)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("longtext"); + + b.Property("EncryptedPublicKey") + .HasColumnType("longtext"); + + b.Property("EncryptedUserKey") + .HasColumnType("longtext"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ActingUserId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("DeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("DomainName") + .HasColumnType("longtext"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("PolicyId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("ProviderOrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderUserId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SystemUser") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("JobRunCount") + .HasColumnType("int"); + + b.Property("LastCheckedDate") + .HasColumnType("datetime(6)"); + + b.Property("NextRunDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VerifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("tinyint(1)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("RevocationReason") + .HasColumnType("tinyint unsigned"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("StatusNew") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlayId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("PlayId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PlayItem", null, t => + { + t.HasCheckConstraint("CK_PlayItem_UserOrOrganization", "(\"UserId\" IS NOT NULL AND \"OrganizationId\" IS NULL) OR (\"UserId\" IS NULL AND \"OrganizationId\" IS NOT NULL)"); + }); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("AuthType") + .HasColumnType("tinyint unsigned"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("Emails") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MaxAccessCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property("KdfIterations") + .HasColumnType("int"); + + b.Property("KdfMemory") + .HasColumnType("int"); + + b.Property("KdfParallelism") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastApiKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastEmailChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKdfChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MasterPasswordSalt") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Premium") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("SecurityState") + .HasColumnType("longtext"); + + b.Property("SecurityVersion") + .HasColumnType("int"); + + b.Property("SignedPublicKey") + .HasColumnType("longtext"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("V2UpgradeToken") + .HasColumnType("longtext"); + + b.Property("VerifyDevices") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SignatureAlgorithm") + .HasColumnType("tinyint unsigned"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("varchar(3000)"); + + b.Property("ClientType") + .HasColumnType("tinyint unsigned"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Global") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Priority") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("TaskId") + .HasColumnType("char(36)"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("NotificationId") + .HasColumnType("char(36)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReadDate") + .HasColumnType("datetime(6)"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowsExtensions") + .HasColumnType("tinyint(1)"); + + b.Property("Conditions") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DefaultLeaseDurationSeconds") + .HasColumnType("int"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("MaxExtensionDurationSeconds") + .HasColumnType("int"); + + b.Property("MaxLeaseDurationSeconds") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SingleActiveLease") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("AccessRule", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property("Read") + .HasColumnType("tinyint(1)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Write") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("EditorServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VersionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Archives") + .HasColumnType("longtext"); + + b.Property("Attachments") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Favorites") + .HasColumnType("longtext"); + + b.Property("Folders") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("char(36)"); + + b.Property("SecretsId") + .HasColumnType("char(36)"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", null) + .WithMany() + .HasForeignKey("AccessRuleId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationInviteLink", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohortAssignment", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohort", "Cohort") + .WithMany() + .HasForeignKey("CohortId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cohort"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("SecretVersions"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/MySqlMigrations/Migrations/20260616102606_AddUsePamToOrganization.cs b/util/MySqlMigrations/Migrations/20260616102606_AddUsePamToOrganization.cs new file mode 100644 index 000000000000..73919eea00e1 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20260616102606_AddUsePamToOrganization.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class AddUsePamToOrganization : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UsePam", + table: "Organization", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UsePam", + table: "Organization"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 3ab6827f29fb..edbef12d6303 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -74,6 +74,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .HasColumnType("char(36)"); + b.Property("AccessRuleId") + .HasColumnType("char(36)"); + b.Property("CreationDate") .HasColumnType("datetime(6)"); @@ -99,6 +102,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("AccessRuleId"); + b.HasIndex("OrganizationId"); b.ToTable("Collection", (string)null); @@ -2343,6 +2348,58 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("NotificationStatus", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowsExtensions") + .HasColumnType("tinyint(1)"); + + b.Property("Conditions") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DefaultLeaseDurationSeconds") + .HasColumnType("int"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("MaxExtensionDurationSeconds") + .HasColumnType("int"); + + b.Property("MaxLeaseDurationSeconds") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SingleActiveLease") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("AccessRule", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => { b.Property("Id") @@ -2882,6 +2939,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", null) + .WithMany() + .HasForeignKey("AccessRuleId") + .OnDelete(DeleteBehavior.Restrict); + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") .WithMany("Collections") .HasForeignKey("OrganizationId") @@ -3411,6 +3473,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => { b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") diff --git a/util/MySqlMigrations/packages.lock.json b/util/MySqlMigrations/packages.lock.json index d51baca04d6c..f88bbe9833de 100644 --- a/util/MySqlMigrations/packages.lock.json +++ b/util/MySqlMigrations/packages.lock.json @@ -1436,6 +1436,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1460,6 +1461,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1473,19 +1475,29 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } } } } diff --git a/util/PostgresMigrations/Migrations/20260526122317_AddAccessRule.Designer.cs b/util/PostgresMigrations/Migrations/20260526122317_AddAccessRule.Designer.cs new file mode 100644 index 000000000000..e9632299e314 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20260526122317_AddAccessRule.Designer.cs @@ -0,0 +1,3822 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20260526122317_AddAccessRule")] + partial class AddAccessRule + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CollectionName") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("GroupName") + .HasColumnType("text"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("UserGuid") + .HasColumnType("uuid"); + + b.Property("UserName") + .HasColumnType("text"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessRuleId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("text"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccessRuleId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExemptFromBillingAutomation") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("boolean"); + + b.Property("LimitItemDeletion") + .HasColumnType("boolean"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp with time zone"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("SmSeats") + .HasColumnType("integer"); + + b.Property("SmServiceAccounts") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("SyncSeats") + .HasColumnType("boolean"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("boolean"); + + b.Property("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseDisableSmAdsForUsers") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseInviteLinks") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UseMyItems") + .HasColumnType("boolean"); + + b.Property("UseOrganizationDomains") + .HasColumnType("boolean"); + + b.Property("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePhishingBlocker") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseRiskInsights") + .HasColumnType("boolean"); + + b.Property("UseScim") + .HasColumnType("boolean"); + + b.Property("UseSecretsManager") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Id", "Enabled"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Id", "Enabled"), new[] { "UseTotp", "UsersGetPremium" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationInviteLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowedDomains") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedInviteKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("EncryptedOrgKey") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInviteLink", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BillingPhone") + .HasColumnType("text"); + + b.Property("BusinessAddress1") + .HasColumnType("text"); + + b.Property("BusinessAddress2") + .HasColumnType("text"); + + b.Property("BusinessAddress3") + .HasColumnType("text"); + + b.Property("BusinessCountry") + .HasColumnType("text"); + + b.Property("BusinessName") + .HasColumnType("text"); + + b.Property("BusinessTaxNumber") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountId") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property("Approved") + .HasColumnType("boolean"); + + b.Property("AuthenticationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MasterPasswordHash") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("smallint"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResponseDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ResponseDeviceId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GranteeId") + .HasColumnType("uuid"); + + b.Property("GrantorId") + .HasColumnType("uuid"); + + b.Property("KeyEncrypted") + .HasColumnType("text"); + + b.Property("LastNotificationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("WaitTimeDays") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("OrganizationId", "ExternalId"), new[] { "UserId" }); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AaGuid") + .HasColumnType("uuid"); + + b.Property("Counter") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SupportsPrf") + .HasColumnType("boolean"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohort", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ChurnDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MigrationPathId") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProactiveDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Name") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohort", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohortAssignment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ChurnDiscountAppliedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CohortId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MigratedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ScheduledDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("CohortId", "ScheduledDate", "MigratedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohortAssignment", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssignedSeats") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UsedSeats") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllocatedSeats") + .HasColumnType("integer"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("PurchasedSeats") + .HasColumnType("integer"); + + b.Property("SeatMinimum") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.SubscriptionDiscount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AmountOff") + .HasColumnType("bigint"); + + b.Property("AudienceType") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Duration") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("DurationInMonths") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PercentOff") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StripeCouponId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("StripeProductIds") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("StripeCouponId") + .IsUnique(); + + b.HasIndex("StartDate", "EndDate") + .HasDatabaseName("IX_SubscriptionDiscount_DateRange") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SubscriptionDiscount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("Filters") + .HasColumnType("text"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Template") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationAtRiskCount") + .HasColumnType("integer"); + + b.Property("ApplicationCount") + .HasColumnType("integer"); + + b.Property("ApplicationData") + .HasColumnType("text"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CriticalApplicationAtRiskCount") + .HasColumnType("integer"); + + b.Property("CriticalApplicationCount") + .HasColumnType("integer"); + + b.Property("CriticalMemberAtRiskCount") + .HasColumnType("integer"); + + b.Property("CriticalMemberCount") + .HasColumnType("integer"); + + b.Property("CriticalPasswordAtRiskCount") + .HasColumnType("integer"); + + b.Property("CriticalPasswordCount") + .HasColumnType("integer"); + + b.Property("MemberAtRiskCount") + .HasColumnType("integer"); + + b.Property("MemberCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PasswordAtRiskCount") + .HasColumnType("integer"); + + b.Property("PasswordCount") + .HasColumnType("integer"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportFile") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SummaryData") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Uri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("character varying(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ClientVersion") + .HasMaxLength(43) + .HasColumnType("character varying(43)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("text"); + + b.Property("EncryptedPublicKey") + .HasColumnType("text"); + + b.Property("EncryptedUserKey") + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActingUserId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("DomainName") + .HasColumnType("text"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("PolicyId") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("ProviderOrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderUserId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SystemUser") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("JobRunCount") + .HasColumnType("integer"); + + b.Property("LastCheckedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextRunDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("boolean"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessSecretsManager") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevocationReason") + .HasColumnType("smallint"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlayId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("PlayId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PlayItem", null, t => + { + t.HasCheckConstraint("CK_PlayItem_UserOrOrganization", "(\"UserId\" IS NOT NULL AND \"OrganizationId\" IS NULL) OR (\"UserId\" IS NULL AND \"OrganizationId\" IS NOT NULL)"); + }); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("AuthType") + .HasColumnType("smallint"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("Emails") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MaxAccessCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Refunded") + .HasColumnType("boolean"); + + b.Property("RefundedAmount") + .HasColumnType("numeric"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("EquivalentDomains") + .HasColumnType("text"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property("FailedLoginCount") + .HasColumnType("integer"); + + b.Property("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Kdf") + .HasColumnType("smallint"); + + b.Property("KdfIterations") + .HasColumnType("integer"); + + b.Property("KdfMemory") + .HasColumnType("integer"); + + b.Property("KdfParallelism") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastApiKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastEmailChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKdfChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MasterPasswordSalt") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Premium") + .HasColumnType("boolean"); + + b.Property("PremiumExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("SecurityState") + .HasColumnType("text"); + + b.Property("SecurityVersion") + .HasColumnType("integer"); + + b.Property("SignedPublicKey") + .HasColumnType("text"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.Property("V2UpgradeToken") + .HasColumnType("text"); + + b.Property("VerifyDevices") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SignatureAlgorithm") + .HasColumnType("smallint"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("character varying(3000)"); + + b.Property("ClientType") + .HasColumnType("smallint"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Global") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("NotificationId") + .HasColumnType("uuid"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Conditions") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("AccessRule", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("Read") + .HasColumnType("boolean"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Write") + .HasColumnType("boolean"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("EditorServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.Property("VersionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Archives") + .HasColumnType("text"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("uuid"); + + b.Property("SecretsId") + .HasColumnType("uuid"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", null) + .WithMany() + .HasForeignKey("AccessRuleId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationInviteLink", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohortAssignment", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohort", "Cohort") + .WithMany() + .HasForeignKey("CohortId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cohort"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("SecretVersions"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/PostgresMigrations/Migrations/20260526122317_AddAccessRule.cs b/util/PostgresMigrations/Migrations/20260526122317_AddAccessRule.cs new file mode 100644 index 000000000000..5ecdeacbb743 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20260526122317_AddAccessRule.cs @@ -0,0 +1,80 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class AddAccessRule : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AccessRuleId", + table: "Collection", + type: "uuid", + nullable: true); + + migrationBuilder.CreateTable( + name: "AccessRule", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrganizationId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Description = table.Column(type: "text", nullable: true), + Conditions = table.Column(type: "text", nullable: false), + CreationDate = table.Column(type: "timestamp with time zone", nullable: false), + RevisionDate = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AccessRule", x => x.Id); + table.ForeignKey( + name: "FK_AccessRule_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Collection_AccessRuleId", + table: "Collection", + column: "AccessRuleId"); + + migrationBuilder.CreateIndex( + name: "IX_AccessRule_OrganizationId_Name", + table: "AccessRule", + columns: new[] { "OrganizationId", "Name" }, + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Collection_AccessRule_AccessRuleId", + table: "Collection", + column: "AccessRuleId", + principalTable: "AccessRule", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Collection_AccessRule_AccessRuleId", + table: "Collection"); + + migrationBuilder.DropTable( + name: "AccessRule"); + + migrationBuilder.DropIndex( + name: "IX_Collection_AccessRuleId", + table: "Collection"); + + migrationBuilder.DropColumn( + name: "AccessRuleId", + table: "Collection"); + } +} diff --git a/util/PostgresMigrations/Migrations/20260616102515_AddAccessRuleLeaseConfiguration.Designer.cs b/util/PostgresMigrations/Migrations/20260616102515_AddAccessRuleLeaseConfiguration.Designer.cs new file mode 100644 index 000000000000..5d9601633d14 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20260616102515_AddAccessRuleLeaseConfiguration.Designer.cs @@ -0,0 +1,3845 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20260616102515_AddAccessRuleLeaseConfiguration")] + partial class AddAccessRuleLeaseConfiguration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CollectionName") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("GroupName") + .HasColumnType("text"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("UserGuid") + .HasColumnType("uuid"); + + b.Property("UserName") + .HasColumnType("text"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessRuleId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("text"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccessRuleId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExemptFromBillingAutomation") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("boolean"); + + b.Property("LimitItemDeletion") + .HasColumnType("boolean"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp with time zone"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("SmSeats") + .HasColumnType("integer"); + + b.Property("SmServiceAccounts") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("SyncSeats") + .HasColumnType("boolean"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("boolean"); + + b.Property("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseDisableSmAdsForUsers") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseInviteLinks") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UseMyItems") + .HasColumnType("boolean"); + + b.Property("UseOrganizationDomains") + .HasColumnType("boolean"); + + b.Property("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePhishingBlocker") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseRiskInsights") + .HasColumnType("boolean"); + + b.Property("UseScim") + .HasColumnType("boolean"); + + b.Property("UseSecretsManager") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Id", "Enabled"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Id", "Enabled"), new[] { "UseTotp", "UsersGetPremium" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationInviteLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowedDomains") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedInviteKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("EncryptedOrgKey") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInviteLink", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BillingPhone") + .HasColumnType("text"); + + b.Property("BusinessAddress1") + .HasColumnType("text"); + + b.Property("BusinessAddress2") + .HasColumnType("text"); + + b.Property("BusinessAddress3") + .HasColumnType("text"); + + b.Property("BusinessCountry") + .HasColumnType("text"); + + b.Property("BusinessName") + .HasColumnType("text"); + + b.Property("BusinessTaxNumber") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountId") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property("Approved") + .HasColumnType("boolean"); + + b.Property("AuthenticationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MasterPasswordHash") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("smallint"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResponseDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ResponseDeviceId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GranteeId") + .HasColumnType("uuid"); + + b.Property("GrantorId") + .HasColumnType("uuid"); + + b.Property("KeyEncrypted") + .HasColumnType("text"); + + b.Property("LastNotificationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("WaitTimeDays") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("OrganizationId", "ExternalId"), new[] { "UserId" }); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AaGuid") + .HasColumnType("uuid"); + + b.Property("Counter") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SupportsPrf") + .HasColumnType("boolean"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohort", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ChurnDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MigrationPathId") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProactiveDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Name") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohort", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohortAssignment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ChurnDiscountAppliedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CohortId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MigratedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ScheduledDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("CohortId", "ScheduledDate", "MigratedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohortAssignment", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssignedSeats") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UsedSeats") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllocatedSeats") + .HasColumnType("integer"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("PurchasedSeats") + .HasColumnType("integer"); + + b.Property("SeatMinimum") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.SubscriptionDiscount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AmountOff") + .HasColumnType("bigint"); + + b.Property("AudienceType") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Duration") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("DurationInMonths") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PercentOff") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StripeCouponId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("StripeProductIds") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("StripeCouponId") + .IsUnique(); + + b.HasIndex("StartDate", "EndDate") + .HasDatabaseName("IX_SubscriptionDiscount_DateRange") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SubscriptionDiscount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("Filters") + .HasColumnType("text"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Template") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationAtRiskCount") + .HasColumnType("integer"); + + b.Property("ApplicationCount") + .HasColumnType("integer"); + + b.Property("ApplicationData") + .HasColumnType("text"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CriticalApplicationAtRiskCount") + .HasColumnType("integer"); + + b.Property("CriticalApplicationCount") + .HasColumnType("integer"); + + b.Property("CriticalMemberAtRiskCount") + .HasColumnType("integer"); + + b.Property("CriticalMemberCount") + .HasColumnType("integer"); + + b.Property("CriticalPasswordAtRiskCount") + .HasColumnType("integer"); + + b.Property("CriticalPasswordCount") + .HasColumnType("integer"); + + b.Property("MemberAtRiskCount") + .HasColumnType("integer"); + + b.Property("MemberCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PasswordAtRiskCount") + .HasColumnType("integer"); + + b.Property("PasswordCount") + .HasColumnType("integer"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportFile") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SummaryData") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Uri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("character varying(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ClientVersion") + .HasMaxLength(43) + .HasColumnType("character varying(43)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("text"); + + b.Property("EncryptedPublicKey") + .HasColumnType("text"); + + b.Property("EncryptedUserKey") + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActingUserId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("DomainName") + .HasColumnType("text"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("PolicyId") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("ProviderOrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderUserId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SystemUser") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("JobRunCount") + .HasColumnType("integer"); + + b.Property("LastCheckedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextRunDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("boolean"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessSecretsManager") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevocationReason") + .HasColumnType("smallint"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("StatusNew") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlayId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("PlayId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PlayItem", null, t => + { + t.HasCheckConstraint("CK_PlayItem_UserOrOrganization", "(\"UserId\" IS NOT NULL AND \"OrganizationId\" IS NULL) OR (\"UserId\" IS NULL AND \"OrganizationId\" IS NOT NULL)"); + }); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("AuthType") + .HasColumnType("smallint"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("Emails") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("MaxAccessCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Refunded") + .HasColumnType("boolean"); + + b.Property("RefundedAmount") + .HasColumnType("numeric"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("EquivalentDomains") + .HasColumnType("text"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property("FailedLoginCount") + .HasColumnType("integer"); + + b.Property("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Kdf") + .HasColumnType("smallint"); + + b.Property("KdfIterations") + .HasColumnType("integer"); + + b.Property("KdfMemory") + .HasColumnType("integer"); + + b.Property("KdfParallelism") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastApiKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastEmailChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKdfChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MasterPasswordSalt") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Premium") + .HasColumnType("boolean"); + + b.Property("PremiumExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("SecurityState") + .HasColumnType("text"); + + b.Property("SecurityVersion") + .HasColumnType("integer"); + + b.Property("SignedPublicKey") + .HasColumnType("text"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.Property("V2UpgradeToken") + .HasColumnType("text"); + + b.Property("VerifyDevices") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SignatureAlgorithm") + .HasColumnType("smallint"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("character varying(3000)"); + + b.Property("ClientType") + .HasColumnType("smallint"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Global") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("NotificationId") + .HasColumnType("uuid"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowsExtensions") + .HasColumnType("boolean"); + + b.Property("Conditions") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultLeaseDurationSeconds") + .HasColumnType("integer"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("MaxExtensionDurationSeconds") + .HasColumnType("integer"); + + b.Property("MaxLeaseDurationSeconds") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SingleActiveLease") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("AccessRule", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("Read") + .HasColumnType("boolean"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Write") + .HasColumnType("boolean"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("EditorServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.Property("VersionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Archives") + .HasColumnType("text"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("uuid"); + + b.Property("SecretsId") + .HasColumnType("uuid"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", null) + .WithMany() + .HasForeignKey("AccessRuleId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationInviteLink", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohortAssignment", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohort", "Cohort") + .WithMany() + .HasForeignKey("CohortId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cohort"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("SecretVersions"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/PostgresMigrations/Migrations/20260616102515_AddAccessRuleLeaseConfiguration.cs b/util/PostgresMigrations/Migrations/20260616102515_AddAccessRuleLeaseConfiguration.cs new file mode 100644 index 000000000000..8402293a9934 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20260616102515_AddAccessRuleLeaseConfiguration.cs @@ -0,0 +1,80 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class AddAccessRuleLeaseConfiguration : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AllowsExtensions", + table: "AccessRule", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "DefaultLeaseDurationSeconds", + table: "AccessRule", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "Enabled", + table: "AccessRule", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "MaxExtensionDurationSeconds", + table: "AccessRule", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "MaxLeaseDurationSeconds", + table: "AccessRule", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "SingleActiveLease", + table: "AccessRule", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AllowsExtensions", + table: "AccessRule"); + + migrationBuilder.DropColumn( + name: "DefaultLeaseDurationSeconds", + table: "AccessRule"); + + migrationBuilder.DropColumn( + name: "Enabled", + table: "AccessRule"); + + migrationBuilder.DropColumn( + name: "MaxExtensionDurationSeconds", + table: "AccessRule"); + + migrationBuilder.DropColumn( + name: "MaxLeaseDurationSeconds", + table: "AccessRule"); + + migrationBuilder.DropColumn( + name: "SingleActiveLease", + table: "AccessRule"); + } +} diff --git a/util/PostgresMigrations/Migrations/20260616102601_AddUsePamToOrganization.Designer.cs b/util/PostgresMigrations/Migrations/20260616102601_AddUsePamToOrganization.Designer.cs new file mode 100644 index 000000000000..87c3b7d179cc --- /dev/null +++ b/util/PostgresMigrations/Migrations/20260616102601_AddUsePamToOrganization.Designer.cs @@ -0,0 +1,3848 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20260616102601_AddUsePamToOrganization")] + partial class AddUsePamToOrganization + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CollectionName") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("GroupName") + .HasColumnType("text"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("UserGuid") + .HasColumnType("uuid"); + + b.Property("UserName") + .HasColumnType("text"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessRuleId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("text"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccessRuleId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExemptFromBillingAutomation") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("boolean"); + + b.Property("LimitItemDeletion") + .HasColumnType("boolean"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp with time zone"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("SmSeats") + .HasColumnType("integer"); + + b.Property("SmServiceAccounts") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("SyncSeats") + .HasColumnType("boolean"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("boolean"); + + b.Property("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseDisableSmAdsForUsers") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseInviteLinks") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UseMyItems") + .HasColumnType("boolean"); + + b.Property("UseOrganizationDomains") + .HasColumnType("boolean"); + + b.Property("UsePam") + .HasColumnType("boolean"); + + b.Property("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePhishingBlocker") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseRiskInsights") + .HasColumnType("boolean"); + + b.Property("UseScim") + .HasColumnType("boolean"); + + b.Property("UseSecretsManager") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Id", "Enabled"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Id", "Enabled"), new[] { "UseTotp", "UsersGetPremium" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationInviteLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowedDomains") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedInviteKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("EncryptedOrgKey") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInviteLink", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BillingPhone") + .HasColumnType("text"); + + b.Property("BusinessAddress1") + .HasColumnType("text"); + + b.Property("BusinessAddress2") + .HasColumnType("text"); + + b.Property("BusinessAddress3") + .HasColumnType("text"); + + b.Property("BusinessCountry") + .HasColumnType("text"); + + b.Property("BusinessName") + .HasColumnType("text"); + + b.Property("BusinessTaxNumber") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountId") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property("Approved") + .HasColumnType("boolean"); + + b.Property("AuthenticationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MasterPasswordHash") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("smallint"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResponseDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ResponseDeviceId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GranteeId") + .HasColumnType("uuid"); + + b.Property("GrantorId") + .HasColumnType("uuid"); + + b.Property("KeyEncrypted") + .HasColumnType("text"); + + b.Property("LastNotificationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("WaitTimeDays") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("OrganizationId", "ExternalId"), new[] { "UserId" }); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AaGuid") + .HasColumnType("uuid"); + + b.Property("Counter") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SupportsPrf") + .HasColumnType("boolean"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohort", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ChurnDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MigrationPathId") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProactiveDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Name") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohort", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohortAssignment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ChurnDiscountAppliedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CohortId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MigratedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ScheduledDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("CohortId", "ScheduledDate", "MigratedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohortAssignment", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssignedSeats") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UsedSeats") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllocatedSeats") + .HasColumnType("integer"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("PurchasedSeats") + .HasColumnType("integer"); + + b.Property("SeatMinimum") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.SubscriptionDiscount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AmountOff") + .HasColumnType("bigint"); + + b.Property("AudienceType") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Duration") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("DurationInMonths") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PercentOff") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StripeCouponId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("StripeProductIds") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("StripeCouponId") + .IsUnique(); + + b.HasIndex("StartDate", "EndDate") + .HasDatabaseName("IX_SubscriptionDiscount_DateRange") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SubscriptionDiscount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("Filters") + .HasColumnType("text"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Template") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationAtRiskCount") + .HasColumnType("integer"); + + b.Property("ApplicationCount") + .HasColumnType("integer"); + + b.Property("ApplicationData") + .HasColumnType("text"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CriticalApplicationAtRiskCount") + .HasColumnType("integer"); + + b.Property("CriticalApplicationCount") + .HasColumnType("integer"); + + b.Property("CriticalMemberAtRiskCount") + .HasColumnType("integer"); + + b.Property("CriticalMemberCount") + .HasColumnType("integer"); + + b.Property("CriticalPasswordAtRiskCount") + .HasColumnType("integer"); + + b.Property("CriticalPasswordCount") + .HasColumnType("integer"); + + b.Property("MemberAtRiskCount") + .HasColumnType("integer"); + + b.Property("MemberCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PasswordAtRiskCount") + .HasColumnType("integer"); + + b.Property("PasswordCount") + .HasColumnType("integer"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportFile") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SummaryData") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Uri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("character varying(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ClientVersion") + .HasMaxLength(43) + .HasColumnType("character varying(43)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("text"); + + b.Property("EncryptedPublicKey") + .HasColumnType("text"); + + b.Property("EncryptedUserKey") + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActingUserId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("DomainName") + .HasColumnType("text"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("PolicyId") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("ProviderOrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderUserId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SystemUser") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("JobRunCount") + .HasColumnType("integer"); + + b.Property("LastCheckedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextRunDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("boolean"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessSecretsManager") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevocationReason") + .HasColumnType("smallint"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("StatusNew") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlayId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("PlayId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PlayItem", null, t => + { + t.HasCheckConstraint("CK_PlayItem_UserOrOrganization", "(\"UserId\" IS NOT NULL AND \"OrganizationId\" IS NULL) OR (\"UserId\" IS NULL AND \"OrganizationId\" IS NOT NULL)"); + }); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("AuthType") + .HasColumnType("smallint"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("Emails") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("MaxAccessCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Refunded") + .HasColumnType("boolean"); + + b.Property("RefundedAmount") + .HasColumnType("numeric"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("EquivalentDomains") + .HasColumnType("text"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property("FailedLoginCount") + .HasColumnType("integer"); + + b.Property("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Kdf") + .HasColumnType("smallint"); + + b.Property("KdfIterations") + .HasColumnType("integer"); + + b.Property("KdfMemory") + .HasColumnType("integer"); + + b.Property("KdfParallelism") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastApiKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastEmailChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKdfChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MasterPasswordSalt") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Premium") + .HasColumnType("boolean"); + + b.Property("PremiumExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("SecurityState") + .HasColumnType("text"); + + b.Property("SecurityVersion") + .HasColumnType("integer"); + + b.Property("SignedPublicKey") + .HasColumnType("text"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.Property("V2UpgradeToken") + .HasColumnType("text"); + + b.Property("VerifyDevices") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SignatureAlgorithm") + .HasColumnType("smallint"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("character varying(3000)"); + + b.Property("ClientType") + .HasColumnType("smallint"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Global") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("NotificationId") + .HasColumnType("uuid"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowsExtensions") + .HasColumnType("boolean"); + + b.Property("Conditions") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultLeaseDurationSeconds") + .HasColumnType("integer"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("MaxExtensionDurationSeconds") + .HasColumnType("integer"); + + b.Property("MaxLeaseDurationSeconds") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SingleActiveLease") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("AccessRule", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("Read") + .HasColumnType("boolean"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Write") + .HasColumnType("boolean"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("EditorServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.Property("VersionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Archives") + .HasColumnType("text"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("uuid"); + + b.Property("SecretsId") + .HasColumnType("uuid"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", null) + .WithMany() + .HasForeignKey("AccessRuleId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationInviteLink", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohortAssignment", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohort", "Cohort") + .WithMany() + .HasForeignKey("CohortId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cohort"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("SecretVersions"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/PostgresMigrations/Migrations/20260616102601_AddUsePamToOrganization.cs b/util/PostgresMigrations/Migrations/20260616102601_AddUsePamToOrganization.cs new file mode 100644 index 000000000000..a7baa8cc3ab2 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20260616102601_AddUsePamToOrganization.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class AddUsePamToOrganization : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UsePam", + table: "Organization", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UsePam", + table: "Organization"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 2ed871d6546e..70df22136258 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -75,6 +75,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .HasColumnType("uuid"); + b.Property("AccessRuleId") + .HasColumnType("uuid"); + b.Property("CreationDate") .HasColumnType("timestamp with time zone"); @@ -100,6 +103,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("AccessRuleId"); + b.HasIndex("OrganizationId"); b.ToTable("Collection", (string)null); @@ -2349,6 +2354,58 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("NotificationStatus", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowsExtensions") + .HasColumnType("boolean"); + + b.Property("Conditions") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultLeaseDurationSeconds") + .HasColumnType("integer"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("MaxExtensionDurationSeconds") + .HasColumnType("integer"); + + b.Property("MaxLeaseDurationSeconds") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SingleActiveLease") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("AccessRule", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => { b.Property("Id") @@ -2888,6 +2945,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", null) + .WithMany() + .HasForeignKey("AccessRuleId") + .OnDelete(DeleteBehavior.Restrict); + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") .WithMany("Collections") .HasForeignKey("OrganizationId") @@ -3417,6 +3479,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => { b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") diff --git a/util/PostgresMigrations/packages.lock.json b/util/PostgresMigrations/packages.lock.json index d51baca04d6c..f88bbe9833de 100644 --- a/util/PostgresMigrations/packages.lock.json +++ b/util/PostgresMigrations/packages.lock.json @@ -1436,6 +1436,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1460,6 +1461,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1473,19 +1475,29 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } } } } diff --git a/util/Seeder/packages.lock.json b/util/Seeder/packages.lock.json index af591b5ad20b..9a77b252997c 100644 --- a/util/Seeder/packages.lock.json +++ b/util/Seeder/packages.lock.json @@ -1346,6 +1346,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1370,6 +1371,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1383,36 +1385,47 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "rustsdk": { "type": "Project" }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/util/SeederApi/packages.lock.json b/util/SeederApi/packages.lock.json index b70461e382fd..10de2477f18a 100644 --- a/util/SeederApi/packages.lock.json +++ b/util/SeederApi/packages.lock.json @@ -1018,6 +1018,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1042,6 +1043,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1055,27 +1057,38 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "rustsdk": { "type": "Project" }, @@ -1083,18 +1096,18 @@ "type": "Project", "dependencies": { "Bogus": "[35.6.5, 35.6.5]", - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", - "RustSdk": "[2026.6.0, )", - "SharedWeb": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", + "RustSdk": "[2026.6.1, )", + "SharedWeb": "[2026.6.1, )" } }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/util/SeederUtility/packages.lock.json b/util/SeederUtility/packages.lock.json index 50835e0fecd7..eaebd73aff09 100644 --- a/util/SeederUtility/packages.lock.json +++ b/util/SeederUtility/packages.lock.json @@ -1365,6 +1365,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1389,6 +1390,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1402,27 +1404,38 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "rustsdk": { "type": "Project" }, @@ -1430,18 +1443,18 @@ "type": "Project", "dependencies": { "Bogus": "[35.6.5, 35.6.5]", - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", - "RustSdk": "[2026.6.0, )", - "SharedWeb": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", + "RustSdk": "[2026.6.1, )", + "SharedWeb": "[2026.6.1, )" } }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/util/Setup/packages.lock.json b/util/Setup/packages.lock.json index 20b77d21fe3f..03a1d4170693 100644 --- a/util/Setup/packages.lock.json +++ b/util/Setup/packages.lock.json @@ -1183,6 +1183,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1207,6 +1208,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1220,13 +1222,22 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "migrator": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "Microsoft.Extensions.Logging": "[10.0.8, 10.0.8]", "dbup-sqlserver": "[7.2.0, 7.2.0]" } + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } } } } diff --git a/util/SqlServerEFScaffold/packages.lock.json b/util/SqlServerEFScaffold/packages.lock.json index 30bc59c24af2..9cb00b8d4c51 100644 --- a/util/SqlServerEFScaffold/packages.lock.json +++ b/util/SqlServerEFScaffold/packages.lock.json @@ -1626,9 +1626,10 @@ "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]", - "Commercial.Core": "[2026.6.0, )", - "Commercial.Infrastructure.EntityFramework": "[2026.6.0, )", - "Core": "[2026.6.0, )", + "Commercial.Core": "[2026.6.1, )", + "Commercial.Infrastructure.EntityFramework": "[2026.6.1, )", + "Commercial.Pam": "[2026.6.1, )", + "Core": "[2026.6.1, )", "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )", "OpenTelemetry.Extensions.Hosting": "[1.15.3, )", "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )", @@ -1636,14 +1637,15 @@ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )", "OpenTelemetry.Instrumentation.SqlClient": "[1.12.0-beta.3, )", - "SharedWeb": "[2026.6.0, )", + "Pam.Domain": "[2026.6.1, )", + "SharedWeb": "[2026.6.1, )", "Swashbuckle.AspNetCore": "[10.1.7, 10.1.7]" } }, "commercial.core": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "CsvHelper": "[33.1.0, 33.1.0]" } }, @@ -1651,8 +1653,16 @@ "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" + } + }, + "commercial.pam": { + "type": "Project", + "dependencies": { + "Core": "[2026.6.1, )", + "Pam.Domain": "[2026.6.1, )", + "SharedWeb": "[2026.6.1, )" } }, "core": { @@ -1671,6 +1681,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1695,6 +1706,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1708,33 +1720,44 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Dapper": "[2.1.66, 2.1.66]" + "Core": "[2026.6.1, )", + "Dapper": "[2.1.66, 2.1.66]", + "Pam.Domain": "[2026.6.1, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } + }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.Dapper": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )", + "Core": "[2026.6.1, )", + "Infrastructure.Dapper": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )", "Microsoft.Bot.Builder.Integration.AspNet.Core": "[4.23.0, 4.23.0]", "Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, 10.1.7]" } diff --git a/util/SqliteMigrations/Migrations/20260526122325_AddAccessRule.Designer.cs b/util/SqliteMigrations/Migrations/20260526122325_AddAccessRule.Designer.cs new file mode 100644 index 000000000000..192e8e16a839 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20260526122325_AddAccessRule.Designer.cs @@ -0,0 +1,3805 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20260526122325_AddAccessRule")] + partial class AddAccessRule + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CollectionName") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("GroupName") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("UserGuid") + .HasColumnType("TEXT"); + + b.Property("UserName") + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessRuleId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccessRuleId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("ExemptFromBillingAutomation") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LimitCollectionCreation") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("INTEGER"); + + b.Property("LimitItemDeletion") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("MaxCollections") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("TEXT"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("SelfHost") + .HasColumnType("INTEGER"); + + b.Property("SmSeats") + .HasColumnType("INTEGER"); + + b.Property("SmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("SyncSeats") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .HasColumnType("INTEGER"); + + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("INTEGER"); + + b.Property("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property("UseDisableSmAdsForUsers") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.Property("UseGroups") + .HasColumnType("INTEGER"); + + b.Property("UseInviteLinks") + .HasColumnType("INTEGER"); + + b.Property("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("UseMyItems") + .HasColumnType("INTEGER"); + + b.Property("UseOrganizationDomains") + .HasColumnType("INTEGER"); + + b.Property("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePhishingBlocker") + .HasColumnType("INTEGER"); + + b.Property("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property("UseResetPassword") + .HasColumnType("INTEGER"); + + b.Property("UseRiskInsights") + .HasColumnType("INTEGER"); + + b.Property("UseScim") + .HasColumnType("INTEGER"); + + b.Property("UseSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("UseSso") + .HasColumnType("INTEGER"); + + b.Property("UseTotp") + .HasColumnType("INTEGER"); + + b.Property("UsersGetPremium") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp", "UsersGetPremium" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationInviteLink", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowedDomains") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Code") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedInviteKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EncryptedOrgKey") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInviteLink", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BillingEmail") + .HasColumnType("TEXT"); + + b.Property("BillingPhone") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DiscountId") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.Property("Approved") + .HasColumnType("INTEGER"); + + b.Property("AuthenticationDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHash") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceType") + .HasColumnType("INTEGER"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ResponseDate") + .HasColumnType("TEXT"); + + b.Property("ResponseDeviceId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("GranteeId") + .HasColumnType("TEXT"); + + b.Property("GrantorId") + .HasColumnType("TEXT"); + + b.Property("KeyEncrypted") + .HasColumnType("TEXT"); + + b.Property("LastNotificationDate") + .HasColumnType("TEXT"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WaitTimeDays") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ConsumedDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AaGuid") + .HasColumnType("TEXT"); + + b.Property("Counter") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SupportsPrf") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohort", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ChurnDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("MigrationPathId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("ProactiveDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Name") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohort", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohortAssignment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ChurnDiscountAppliedDate") + .HasColumnType("TEXT"); + + b.Property("CohortId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("MigratedDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("ScheduledDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("CohortId", "ScheduledDate", "MigratedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohortAssignment", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AssignedSeats") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Total") + .HasColumnType("TEXT"); + + b.Property("UsedSeats") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllocatedSeats") + .HasColumnType("INTEGER"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("PurchasedSeats") + .HasColumnType("INTEGER"); + + b.Property("SeatMinimum") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.SubscriptionDiscount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AmountOff") + .HasColumnType("INTEGER"); + + b.Property("AudienceType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Currency") + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Duration") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("DurationInMonths") + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PercentOff") + .HasPrecision(5, 2) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("StripeCouponId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("StripeProductIds") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("StripeCouponId") + .IsUnique(); + + b.HasIndex("StartDate", "EndDate") + .HasDatabaseName("IX_SubscriptionDiscount_DateRange") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SubscriptionDiscount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("Filters") + .HasColumnType("TEXT"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Template") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("ApplicationCount") + .HasColumnType("INTEGER"); + + b.Property("ApplicationData") + .HasColumnType("TEXT"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CriticalApplicationAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalApplicationCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalMemberAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalMemberCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalPasswordAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalPasswordCount") + .HasColumnType("INTEGER"); + + b.Property("MemberAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("MemberCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PasswordAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("PasswordCount") + .HasColumnType("INTEGER"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ReportFile") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SummaryData") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("TEXT"); + + b.Property("AbsoluteExpiration") + .HasColumnType("TEXT"); + + b.Property("ExpiresAtTime") + .HasColumnType("TEXT"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("BLOB"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("ClientVersion") + .HasMaxLength(43) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasColumnType("TEXT"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActingUserId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasColumnType("INTEGER"); + + b.Property("DomainName") + .HasColumnType("TEXT"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("PolicyId") + .HasColumnType("TEXT"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderUserId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SystemUser") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Config") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("JobRunCount") + .HasColumnType("INTEGER"); + + b.Property("LastCheckedDate") + .HasColumnType("TEXT"); + + b.Property("NextRunDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VerifiedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsAdminInitiated") + .HasColumnType("INTEGER"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlanSponsorshipType") + .HasColumnType("INTEGER"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("ToDelete") + .HasColumnType("INTEGER"); + + b.Property("ValidUntil") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("RevocationReason") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlayId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("PlayId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PlayItem", null, t => + { + t.HasCheckConstraint("CK_PlayItem_UserOrOrganization", "(\"UserId\" IS NOT NULL AND \"OrganizationId\" IS NULL) OR (\"UserId\" IS NULL AND \"OrganizationId\" IS NOT NULL)"); + }); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("AuthType") + .HasColumnType("INTEGER"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletionDate") + .HasColumnType("TEXT"); + + b.Property("Disabled") + .HasColumnType("INTEGER"); + + b.Property("Emails") + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("HideEmail") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MaxAccessCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethodType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Refunded") + .HasColumnType("INTEGER"); + + b.Property("RefundedAmount") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountRevisionDate") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailVerified") + .HasColumnType("INTEGER"); + + b.Property("EquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("FailedLoginCount") + .HasColumnType("INTEGER"); + + b.Property("ForcePasswordReset") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Kdf") + .HasColumnType("INTEGER"); + + b.Property("KdfIterations") + .HasColumnType("INTEGER"); + + b.Property("KdfMemory") + .HasColumnType("INTEGER"); + + b.Property("KdfParallelism") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastApiKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastEmailChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastFailedLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastKdfChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordSalt") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Premium") + .HasColumnType("INTEGER"); + + b.Property("PremiumExpirationDate") + .HasColumnType("TEXT"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SecurityState") + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("SignedPublicKey") + .HasColumnType("TEXT"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("V2UpgradeToken") + .HasColumnType("TEXT"); + + b.Property("VerifyDevices") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SignatureAlgorithm") + .HasColumnType("INTEGER"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Global") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("TaskId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("NotificationId") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("ReadDate") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Conditions") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("AccessRule", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("Read") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Write") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpireAt") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("EditorServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VersionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Archives") + .HasColumnType("TEXT"); + + b.Property("Attachments") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Favorites") + .HasColumnType("TEXT"); + + b.Property("Folders") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Reprompt") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("TEXT"); + + b.Property("SecretsId") + .HasColumnType("TEXT"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", null) + .WithMany() + .HasForeignKey("AccessRuleId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationInviteLink", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohortAssignment", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohort", "Cohort") + .WithMany() + .HasForeignKey("CohortId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cohort"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("SecretVersions"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/SqliteMigrations/Migrations/20260526122325_AddAccessRule.cs b/util/SqliteMigrations/Migrations/20260526122325_AddAccessRule.cs new file mode 100644 index 000000000000..dc621ea9e190 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20260526122325_AddAccessRule.cs @@ -0,0 +1,80 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class AddAccessRule : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AccessRuleId", + table: "Collection", + type: "TEXT", + nullable: true); + + migrationBuilder.CreateTable( + name: "AccessRule", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 256, nullable: false), + Description = table.Column(type: "TEXT", nullable: true), + Conditions = table.Column(type: "TEXT", nullable: false), + CreationDate = table.Column(type: "TEXT", nullable: false), + RevisionDate = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AccessRule", x => x.Id); + table.ForeignKey( + name: "FK_AccessRule_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Collection_AccessRuleId", + table: "Collection", + column: "AccessRuleId"); + + migrationBuilder.CreateIndex( + name: "IX_AccessRule_OrganizationId_Name", + table: "AccessRule", + columns: new[] { "OrganizationId", "Name" }, + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Collection_AccessRule_AccessRuleId", + table: "Collection", + column: "AccessRuleId", + principalTable: "AccessRule", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Collection_AccessRule_AccessRuleId", + table: "Collection"); + + migrationBuilder.DropTable( + name: "AccessRule"); + + migrationBuilder.DropIndex( + name: "IX_Collection_AccessRuleId", + table: "Collection"); + + migrationBuilder.DropColumn( + name: "AccessRuleId", + table: "Collection"); + } +} diff --git a/util/SqliteMigrations/Migrations/20260616102508_AddAccessRuleLeaseConfiguration.Designer.cs b/util/SqliteMigrations/Migrations/20260616102508_AddAccessRuleLeaseConfiguration.Designer.cs new file mode 100644 index 000000000000..48471cfdb20c --- /dev/null +++ b/util/SqliteMigrations/Migrations/20260616102508_AddAccessRuleLeaseConfiguration.Designer.cs @@ -0,0 +1,3828 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20260616102508_AddAccessRuleLeaseConfiguration")] + partial class AddAccessRuleLeaseConfiguration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CollectionName") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("GroupName") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("UserGuid") + .HasColumnType("TEXT"); + + b.Property("UserName") + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessRuleId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccessRuleId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("ExemptFromBillingAutomation") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LimitCollectionCreation") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("INTEGER"); + + b.Property("LimitItemDeletion") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("MaxCollections") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("TEXT"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("SelfHost") + .HasColumnType("INTEGER"); + + b.Property("SmSeats") + .HasColumnType("INTEGER"); + + b.Property("SmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("SyncSeats") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .HasColumnType("INTEGER"); + + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("INTEGER"); + + b.Property("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property("UseDisableSmAdsForUsers") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.Property("UseGroups") + .HasColumnType("INTEGER"); + + b.Property("UseInviteLinks") + .HasColumnType("INTEGER"); + + b.Property("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("UseMyItems") + .HasColumnType("INTEGER"); + + b.Property("UseOrganizationDomains") + .HasColumnType("INTEGER"); + + b.Property("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePhishingBlocker") + .HasColumnType("INTEGER"); + + b.Property("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property("UseResetPassword") + .HasColumnType("INTEGER"); + + b.Property("UseRiskInsights") + .HasColumnType("INTEGER"); + + b.Property("UseScim") + .HasColumnType("INTEGER"); + + b.Property("UseSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("UseSso") + .HasColumnType("INTEGER"); + + b.Property("UseTotp") + .HasColumnType("INTEGER"); + + b.Property("UsersGetPremium") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp", "UsersGetPremium" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationInviteLink", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowedDomains") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Code") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedInviteKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EncryptedOrgKey") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInviteLink", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BillingEmail") + .HasColumnType("TEXT"); + + b.Property("BillingPhone") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DiscountId") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.Property("Approved") + .HasColumnType("INTEGER"); + + b.Property("AuthenticationDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHash") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceType") + .HasColumnType("INTEGER"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ResponseDate") + .HasColumnType("TEXT"); + + b.Property("ResponseDeviceId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("GranteeId") + .HasColumnType("TEXT"); + + b.Property("GrantorId") + .HasColumnType("TEXT"); + + b.Property("KeyEncrypted") + .HasColumnType("TEXT"); + + b.Property("LastNotificationDate") + .HasColumnType("TEXT"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WaitTimeDays") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ConsumedDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AaGuid") + .HasColumnType("TEXT"); + + b.Property("Counter") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SupportsPrf") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohort", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ChurnDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("MigrationPathId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("ProactiveDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Name") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohort", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohortAssignment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ChurnDiscountAppliedDate") + .HasColumnType("TEXT"); + + b.Property("CohortId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("MigratedDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("ScheduledDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("CohortId", "ScheduledDate", "MigratedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohortAssignment", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AssignedSeats") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Total") + .HasColumnType("TEXT"); + + b.Property("UsedSeats") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllocatedSeats") + .HasColumnType("INTEGER"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("PurchasedSeats") + .HasColumnType("INTEGER"); + + b.Property("SeatMinimum") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.SubscriptionDiscount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AmountOff") + .HasColumnType("INTEGER"); + + b.Property("AudienceType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Currency") + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Duration") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("DurationInMonths") + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PercentOff") + .HasPrecision(5, 2) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("StripeCouponId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("StripeProductIds") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("StripeCouponId") + .IsUnique(); + + b.HasIndex("StartDate", "EndDate") + .HasDatabaseName("IX_SubscriptionDiscount_DateRange") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SubscriptionDiscount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("Filters") + .HasColumnType("TEXT"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Template") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("ApplicationCount") + .HasColumnType("INTEGER"); + + b.Property("ApplicationData") + .HasColumnType("TEXT"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CriticalApplicationAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalApplicationCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalMemberAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalMemberCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalPasswordAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalPasswordCount") + .HasColumnType("INTEGER"); + + b.Property("MemberAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("MemberCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PasswordAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("PasswordCount") + .HasColumnType("INTEGER"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ReportFile") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SummaryData") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("TEXT"); + + b.Property("AbsoluteExpiration") + .HasColumnType("TEXT"); + + b.Property("ExpiresAtTime") + .HasColumnType("TEXT"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("BLOB"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("ClientVersion") + .HasMaxLength(43) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasColumnType("TEXT"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActingUserId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasColumnType("INTEGER"); + + b.Property("DomainName") + .HasColumnType("TEXT"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("PolicyId") + .HasColumnType("TEXT"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderUserId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SystemUser") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Config") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("JobRunCount") + .HasColumnType("INTEGER"); + + b.Property("LastCheckedDate") + .HasColumnType("TEXT"); + + b.Property("NextRunDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VerifiedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsAdminInitiated") + .HasColumnType("INTEGER"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlanSponsorshipType") + .HasColumnType("INTEGER"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("ToDelete") + .HasColumnType("INTEGER"); + + b.Property("ValidUntil") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("RevocationReason") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("StatusNew") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlayId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("PlayId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PlayItem", null, t => + { + t.HasCheckConstraint("CK_PlayItem_UserOrOrganization", "(\"UserId\" IS NOT NULL AND \"OrganizationId\" IS NULL) OR (\"UserId\" IS NULL AND \"OrganizationId\" IS NOT NULL)"); + }); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("AuthType") + .HasColumnType("INTEGER"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeletionDate") + .HasColumnType("TEXT"); + + b.Property("Disabled") + .HasColumnType("INTEGER"); + + b.Property("Emails") + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("HideEmail") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MaxAccessCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethodType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Refunded") + .HasColumnType("INTEGER"); + + b.Property("RefundedAmount") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountRevisionDate") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailVerified") + .HasColumnType("INTEGER"); + + b.Property("EquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("FailedLoginCount") + .HasColumnType("INTEGER"); + + b.Property("ForcePasswordReset") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Kdf") + .HasColumnType("INTEGER"); + + b.Property("KdfIterations") + .HasColumnType("INTEGER"); + + b.Property("KdfMemory") + .HasColumnType("INTEGER"); + + b.Property("KdfParallelism") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastApiKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastEmailChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastFailedLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastKdfChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordSalt") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Premium") + .HasColumnType("INTEGER"); + + b.Property("PremiumExpirationDate") + .HasColumnType("TEXT"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SecurityState") + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("SignedPublicKey") + .HasColumnType("TEXT"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("V2UpgradeToken") + .HasColumnType("TEXT"); + + b.Property("VerifyDevices") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SignatureAlgorithm") + .HasColumnType("INTEGER"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Global") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("TaskId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("NotificationId") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("ReadDate") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowsExtensions") + .HasColumnType("INTEGER"); + + b.Property("Conditions") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DefaultLeaseDurationSeconds") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("MaxExtensionDurationSeconds") + .HasColumnType("INTEGER"); + + b.Property("MaxLeaseDurationSeconds") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SingleActiveLease") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("AccessRule", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("Read") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Write") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpireAt") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("EditorServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VersionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Archives") + .HasColumnType("TEXT"); + + b.Property("Attachments") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Favorites") + .HasColumnType("TEXT"); + + b.Property("Folders") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Reprompt") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("TEXT"); + + b.Property("SecretsId") + .HasColumnType("TEXT"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", null) + .WithMany() + .HasForeignKey("AccessRuleId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationInviteLink", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohortAssignment", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohort", "Cohort") + .WithMany() + .HasForeignKey("CohortId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cohort"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("SecretVersions"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/SqliteMigrations/Migrations/20260616102508_AddAccessRuleLeaseConfiguration.cs b/util/SqliteMigrations/Migrations/20260616102508_AddAccessRuleLeaseConfiguration.cs new file mode 100644 index 000000000000..97132fa50a66 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20260616102508_AddAccessRuleLeaseConfiguration.cs @@ -0,0 +1,80 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class AddAccessRuleLeaseConfiguration : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AllowsExtensions", + table: "AccessRule", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "DefaultLeaseDurationSeconds", + table: "AccessRule", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "Enabled", + table: "AccessRule", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "MaxExtensionDurationSeconds", + table: "AccessRule", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "MaxLeaseDurationSeconds", + table: "AccessRule", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "SingleActiveLease", + table: "AccessRule", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AllowsExtensions", + table: "AccessRule"); + + migrationBuilder.DropColumn( + name: "DefaultLeaseDurationSeconds", + table: "AccessRule"); + + migrationBuilder.DropColumn( + name: "Enabled", + table: "AccessRule"); + + migrationBuilder.DropColumn( + name: "MaxExtensionDurationSeconds", + table: "AccessRule"); + + migrationBuilder.DropColumn( + name: "MaxLeaseDurationSeconds", + table: "AccessRule"); + + migrationBuilder.DropColumn( + name: "SingleActiveLease", + table: "AccessRule"); + } +} diff --git a/util/SqliteMigrations/Migrations/20260616102557_AddUsePamToOrganization.Designer.cs b/util/SqliteMigrations/Migrations/20260616102557_AddUsePamToOrganization.Designer.cs new file mode 100644 index 000000000000..0735e524948e --- /dev/null +++ b/util/SqliteMigrations/Migrations/20260616102557_AddUsePamToOrganization.Designer.cs @@ -0,0 +1,3831 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20260616102557_AddUsePamToOrganization")] + partial class AddUsePamToOrganization + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CollectionName") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("GroupName") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("UserGuid") + .HasColumnType("TEXT"); + + b.Property("UserName") + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessRuleId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccessRuleId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("ExemptFromBillingAutomation") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LimitCollectionCreation") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("INTEGER"); + + b.Property("LimitItemDeletion") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("MaxCollections") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("TEXT"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("SelfHost") + .HasColumnType("INTEGER"); + + b.Property("SmSeats") + .HasColumnType("INTEGER"); + + b.Property("SmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("SyncSeats") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .HasColumnType("INTEGER"); + + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("INTEGER"); + + b.Property("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property("UseDisableSmAdsForUsers") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.Property("UseGroups") + .HasColumnType("INTEGER"); + + b.Property("UseInviteLinks") + .HasColumnType("INTEGER"); + + b.Property("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("UseMyItems") + .HasColumnType("INTEGER"); + + b.Property("UseOrganizationDomains") + .HasColumnType("INTEGER"); + + b.Property("UsePam") + .HasColumnType("INTEGER"); + + b.Property("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePhishingBlocker") + .HasColumnType("INTEGER"); + + b.Property("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property("UseResetPassword") + .HasColumnType("INTEGER"); + + b.Property("UseRiskInsights") + .HasColumnType("INTEGER"); + + b.Property("UseScim") + .HasColumnType("INTEGER"); + + b.Property("UseSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("UseSso") + .HasColumnType("INTEGER"); + + b.Property("UseTotp") + .HasColumnType("INTEGER"); + + b.Property("UsersGetPremium") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp", "UsersGetPremium" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationInviteLink", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowedDomains") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Code") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedInviteKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EncryptedOrgKey") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInviteLink", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BillingEmail") + .HasColumnType("TEXT"); + + b.Property("BillingPhone") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DiscountId") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.Property("Approved") + .HasColumnType("INTEGER"); + + b.Property("AuthenticationDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHash") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceType") + .HasColumnType("INTEGER"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ResponseDate") + .HasColumnType("TEXT"); + + b.Property("ResponseDeviceId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("GranteeId") + .HasColumnType("TEXT"); + + b.Property("GrantorId") + .HasColumnType("TEXT"); + + b.Property("KeyEncrypted") + .HasColumnType("TEXT"); + + b.Property("LastNotificationDate") + .HasColumnType("TEXT"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WaitTimeDays") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ConsumedDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AaGuid") + .HasColumnType("TEXT"); + + b.Property("Counter") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SupportsPrf") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohort", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ChurnDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("MigrationPathId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("ProactiveDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Name") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohort", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohortAssignment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ChurnDiscountAppliedDate") + .HasColumnType("TEXT"); + + b.Property("CohortId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("MigratedDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("ScheduledDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("CohortId", "ScheduledDate", "MigratedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohortAssignment", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AssignedSeats") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Total") + .HasColumnType("TEXT"); + + b.Property("UsedSeats") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllocatedSeats") + .HasColumnType("INTEGER"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("PurchasedSeats") + .HasColumnType("INTEGER"); + + b.Property("SeatMinimum") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.SubscriptionDiscount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AmountOff") + .HasColumnType("INTEGER"); + + b.Property("AudienceType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Currency") + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Duration") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("DurationInMonths") + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PercentOff") + .HasPrecision(5, 2) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("StripeCouponId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("StripeProductIds") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("StripeCouponId") + .IsUnique(); + + b.HasIndex("StartDate", "EndDate") + .HasDatabaseName("IX_SubscriptionDiscount_DateRange") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SubscriptionDiscount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("Filters") + .HasColumnType("TEXT"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Template") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("ApplicationCount") + .HasColumnType("INTEGER"); + + b.Property("ApplicationData") + .HasColumnType("TEXT"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CriticalApplicationAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalApplicationCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalMemberAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalMemberCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalPasswordAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalPasswordCount") + .HasColumnType("INTEGER"); + + b.Property("MemberAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("MemberCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PasswordAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("PasswordCount") + .HasColumnType("INTEGER"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ReportFile") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SummaryData") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("TEXT"); + + b.Property("AbsoluteExpiration") + .HasColumnType("TEXT"); + + b.Property("ExpiresAtTime") + .HasColumnType("TEXT"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("BLOB"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("ClientVersion") + .HasMaxLength(43) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasColumnType("TEXT"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActingUserId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasColumnType("INTEGER"); + + b.Property("DomainName") + .HasColumnType("TEXT"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("PolicyId") + .HasColumnType("TEXT"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderUserId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SystemUser") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Config") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("JobRunCount") + .HasColumnType("INTEGER"); + + b.Property("LastCheckedDate") + .HasColumnType("TEXT"); + + b.Property("NextRunDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VerifiedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsAdminInitiated") + .HasColumnType("INTEGER"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlanSponsorshipType") + .HasColumnType("INTEGER"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("ToDelete") + .HasColumnType("INTEGER"); + + b.Property("ValidUntil") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("RevocationReason") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("StatusNew") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlayId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("PlayId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PlayItem", null, t => + { + t.HasCheckConstraint("CK_PlayItem_UserOrOrganization", "(\"UserId\" IS NOT NULL AND \"OrganizationId\" IS NULL) OR (\"UserId\" IS NULL AND \"OrganizationId\" IS NOT NULL)"); + }); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("AuthType") + .HasColumnType("INTEGER"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeletionDate") + .HasColumnType("TEXT"); + + b.Property("Disabled") + .HasColumnType("INTEGER"); + + b.Property("Emails") + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("HideEmail") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MaxAccessCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethodType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Refunded") + .HasColumnType("INTEGER"); + + b.Property("RefundedAmount") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountRevisionDate") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailVerified") + .HasColumnType("INTEGER"); + + b.Property("EquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("FailedLoginCount") + .HasColumnType("INTEGER"); + + b.Property("ForcePasswordReset") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Kdf") + .HasColumnType("INTEGER"); + + b.Property("KdfIterations") + .HasColumnType("INTEGER"); + + b.Property("KdfMemory") + .HasColumnType("INTEGER"); + + b.Property("KdfParallelism") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastApiKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastEmailChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastFailedLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastKdfChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordSalt") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Premium") + .HasColumnType("INTEGER"); + + b.Property("PremiumExpirationDate") + .HasColumnType("TEXT"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SecurityState") + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("SignedPublicKey") + .HasColumnType("TEXT"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("V2UpgradeToken") + .HasColumnType("TEXT"); + + b.Property("VerifyDevices") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SignatureAlgorithm") + .HasColumnType("INTEGER"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Global") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("TaskId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("NotificationId") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("ReadDate") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowsExtensions") + .HasColumnType("INTEGER"); + + b.Property("Conditions") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DefaultLeaseDurationSeconds") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("MaxExtensionDurationSeconds") + .HasColumnType("INTEGER"); + + b.Property("MaxLeaseDurationSeconds") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SingleActiveLease") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("AccessRule", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("Read") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Write") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpireAt") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("EditorServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VersionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Archives") + .HasColumnType("TEXT"); + + b.Property("Attachments") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Favorites") + .HasColumnType("TEXT"); + + b.Property("Folders") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Reprompt") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("TEXT"); + + b.Property("SecretsId") + .HasColumnType("TEXT"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", null) + .WithMany() + .HasForeignKey("AccessRuleId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationInviteLink", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohortAssignment", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohort", "Cohort") + .WithMany() + .HasForeignKey("CohortId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cohort"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("SecretVersions"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/SqliteMigrations/Migrations/20260616102557_AddUsePamToOrganization.cs b/util/SqliteMigrations/Migrations/20260616102557_AddUsePamToOrganization.cs new file mode 100644 index 000000000000..01de7c1e3411 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20260616102557_AddUsePamToOrganization.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class AddUsePamToOrganization : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UsePam", + table: "Organization", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UsePam", + table: "Organization"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index 1a303be50670..6f6890b68508 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -69,6 +69,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .HasColumnType("TEXT"); + b.Property("AccessRuleId") + .HasColumnType("TEXT"); + b.Property("CreationDate") .HasColumnType("TEXT"); @@ -94,6 +97,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("AccessRuleId"); + b.HasIndex("OrganizationId"); b.ToTable("Collection", (string)null); @@ -2332,6 +2337,58 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("NotificationStatus", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowsExtensions") + .HasColumnType("INTEGER"); + + b.Property("Conditions") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DefaultLeaseDurationSeconds") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("MaxExtensionDurationSeconds") + .HasColumnType("INTEGER"); + + b.Property("MaxLeaseDurationSeconds") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SingleActiveLease") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("AccessRule", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => { b.Property("Id") @@ -2871,6 +2928,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", null) + .WithMany() + .HasForeignKey("AccessRuleId") + .OnDelete(DeleteBehavior.Restrict); + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") .WithMany("Collections") .HasForeignKey("OrganizationId") @@ -3400,6 +3462,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => { b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") diff --git a/util/SqliteMigrations/packages.lock.json b/util/SqliteMigrations/packages.lock.json index d51baca04d6c..f88bbe9833de 100644 --- a/util/SqliteMigrations/packages.lock.json +++ b/util/SqliteMigrations/packages.lock.json @@ -1436,6 +1436,7 @@ "BitPay.Light": "[1.0.1907, 1.0.1907]", "Braintree": "[5.36.0, 5.36.0]", "CsvHelper": "[33.1.0, 33.1.0]", + "Data": "[2026.6.1, )", "DnsClient": "[1.8.0, 1.8.0]", "Duende.IdentityServer": "[7.4.6, 7.4.6]", "DuoUniversal": "[1.3.1, 1.3.1]", @@ -1460,6 +1461,7 @@ "Newtonsoft.Json": "[13.0.3, 13.0.3]", "OneOf": "[3.0.271, 3.0.271]", "Otp.NET": "[1.4.0, 1.4.0]", + "Pam.Domain": "[2026.6.1, )", "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]", @@ -1473,19 +1475,29 @@ "ZiggyCreatures.FusionCache.Serialization.SystemTextJson": "[2.0.2, 2.0.2]" } }, + "data": { + "type": "Project" + }, "infrastructure.entityframework": { "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "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.6.1, )", "Pomelo.EntityFrameworkCore.MySql": "[8.0.2, 8.0.2]", "linq2db": "[5.4.1, 5.4.1]", "linq2db.EntityFrameworkCore": "[8.1.0, 8.1.0]" } + }, + "pam.domain": { + "type": "Project", + "dependencies": { + "Data": "[2026.6.1, )" + } } } }