mirror of
https://github.com/bitwarden/server
synced 2025-12-29 14:43:39 +00:00
Merge branch 'master' into flexible-collections/deprecate-custom-collection-perm
# Conflicts: # src/Api/AdminConsole/Controllers/OrganizationUsersController.cs # src/Api/Vault/AuthorizationHandlers/Collections/CollectionAuthorizationHandler.cs # test/Api.Test/Vault/AuthorizationHandlers/CollectionAuthorizationHandlerTests.cs
This commit is contained in:
@@ -82,7 +82,11 @@
|
||||
<label asp-for="PlanType"></label>
|
||||
@{
|
||||
var planTypes = Enum.GetValues<PlanType>()
|
||||
.Where(p => Model.Provider == null || p is >= PlanType.TeamsMonthly and <= PlanType.TeamsStarter)
|
||||
.Where(p =>
|
||||
Model.Provider == null ||
|
||||
(Model.Provider != null
|
||||
&& p is >= PlanType.TeamsMonthly2019 and <= PlanType.EnterpriseAnnually2019 or >= PlanType.TeamsMonthly2020 and <= PlanType.EnterpriseAnnually)
|
||||
)
|
||||
.Select(e => new SelectListItem
|
||||
{
|
||||
Value = ((int)e).ToString(),
|
||||
|
||||
@@ -223,6 +223,24 @@
|
||||
"Microsoft.Win32.Registry": "5.0.0"
|
||||
}
|
||||
},
|
||||
"Duende.IdentityServer": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.4",
|
||||
"contentHash": "4HVjzx1F8v5J+U7oa8RGAQGj2QzmzNSu87r18Sh+dlh10uyZZL8teAaT/FaVLDObnfItGdPFvN8mwpF/HkI3Xw==",
|
||||
"dependencies": {
|
||||
"Duende.IdentityServer.Storage": "6.0.4",
|
||||
"Microsoft.AspNetCore.Authentication.OpenIdConnect": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Duende.IdentityServer.Storage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.4",
|
||||
"contentHash": "s5gAjfbpr2IMgI+fU2Nx+2AZdzstmbt9gpo13iX7GwvqSeSaBVqj9ZskAN0R2KF1OemPdZuGnfaTcevdXMUrrw==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "6.0.0",
|
||||
"Microsoft.AspNetCore.DataProtection.Abstractions": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Fido2": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.1",
|
||||
@@ -259,49 +277,8 @@
|
||||
},
|
||||
"IdentityModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.4.0",
|
||||
"contentHash": "b18wrIx5wnZlMxAX7oVsE+nDtAJ4hajYlH0xPlaRvo4r/fz08K6pPeZvbiqS9nfNbzfIgLFmNX+FL9qR9ZR5PA==",
|
||||
"dependencies": {
|
||||
"Newtonsoft.Json": "11.0.2",
|
||||
"System.Text.Encodings.Web": "4.7.0"
|
||||
}
|
||||
},
|
||||
"IdentityModel.AspNetCore.OAuth2Introspection": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.0.1",
|
||||
"contentHash": "ZNdMZMaj9fqR3j50vYsu+1U3QGd6n8+fqwf+a8mCTcmXGor+HgFDfdq0mM34bsmD6uEgAQup7sv2ZW5kR36dbA==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "4.0.0"
|
||||
}
|
||||
},
|
||||
"IdentityServer4": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.1.2",
|
||||
"contentHash": "blaxxGuOA7v/w1q+fxn97wZ+x2ecG1ZD4mc/N/ZOXMNeFZZhqv+4LF26Gecyik3nWrJPmbMEtQbLmRsKG8k61w==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "4.4.0",
|
||||
"IdentityServer4.Storage": "4.1.2",
|
||||
"Microsoft.AspNetCore.Authentication.OpenIdConnect": "3.1.0",
|
||||
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "5.6.0",
|
||||
"Newtonsoft.Json": "12.0.2"
|
||||
}
|
||||
},
|
||||
"IdentityServer4.AccessTokenValidation": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.1",
|
||||
"contentHash": "qu/M6UyN4o9NVep7q545Ms7hYAnsQqSdLbN1Fjjrn4m35lyBfeQPSSNzDryAKHbodyWOQfHaOqKEyMEJQ5Rpgw==",
|
||||
"dependencies": {
|
||||
"IdentityModel.AspNetCore.OAuth2Introspection": "4.0.1",
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer": "3.0.0"
|
||||
}
|
||||
},
|
||||
"IdentityServer4.Storage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.1.2",
|
||||
"contentHash": "KoSffyZyyeCNTIyJiZnCuPakJ1QbCHlpty6gbWUj/7yl+w0PXIchgmmJnJSvddzBb8iZ2xew/vGlxWUIP17P2g==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "4.4.0"
|
||||
}
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "eVHCR7a6m/dm5RFcBzE3qs/Jg5j9R5Rjpu8aTOv9e4AFvaQtBXb5ah7kmwU+YwA0ufRwz4wf1hnIvsD2hSnI4g=="
|
||||
},
|
||||
"LaunchDarkly.Cache": {
|
||||
"type": "Transitive",
|
||||
@@ -393,10 +370,10 @@
|
||||
},
|
||||
"Microsoft.AspNetCore.Authentication.OpenIdConnect": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.0",
|
||||
"contentHash": "O1cAQYUTU8EfRqwc5/rfTns4E4hKlFlg59fuKRrST+PzsxI6H07KqRN/JjdYhAuVYxF8jPnIGbj+zuc5paOWUw==",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "cJxdro36spFzk/K2OFCddM6vZ+yoj6ug8mTFRH3Gdv1Pul/buSuCtfb/FSCp31UmS5S4C1315dU7wX3ErLFuDg==",
|
||||
"dependencies": {
|
||||
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "5.5.0"
|
||||
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.10.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.AspNetCore.Cryptography.Internal": {
|
||||
@@ -429,8 +406,8 @@
|
||||
},
|
||||
"Microsoft.AspNetCore.DataProtection.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.32",
|
||||
"contentHash": "MPL4iVyiaRxnOUY5VATHjvhDWaAEFb77KFiUxVRklv3Z3v+STofUr1UG/aCt1O9cgN7FVTDaC5A7U+zsLub8Xg=="
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "Z/UU4NEBm5UgNufJmw+j5baW26ytCOIZ0G7sZocPaOzsUeBon1bkM3lSMNZQG2GmDjAIVP2XMSODf2jzSGbibw=="
|
||||
},
|
||||
"Microsoft.Azure.Amqp": {
|
||||
"type": "Transitive",
|
||||
@@ -2808,15 +2785,15 @@
|
||||
"commercial.core": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Core": "[2023.10.2, )"
|
||||
"Core": "[2023.10.3, )"
|
||||
}
|
||||
},
|
||||
"commercial.infrastructure.entityframework": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[12.0.1, )",
|
||||
"Core": "[2023.10.2, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.2, )"
|
||||
"Core": "[2023.10.3, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.3, )"
|
||||
}
|
||||
},
|
||||
"core": {
|
||||
@@ -2834,10 +2811,9 @@
|
||||
"BitPay.Light": "[1.0.1907, )",
|
||||
"Braintree": "[5.19.0, )",
|
||||
"DnsClient": "[1.7.0, )",
|
||||
"Duende.IdentityServer": "[6.0.4, )",
|
||||
"Fido2.AspNet": "[3.0.1, )",
|
||||
"Handlebars.Net": "[2.1.2, )",
|
||||
"IdentityServer4": "[4.1.2, )",
|
||||
"IdentityServer4.AccessTokenValidation": "[3.0.1, )",
|
||||
"LaunchDarkly.ServerSdk": "[8.0.0, )",
|
||||
"MailKit": "[4.2.0, )",
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer": "[6.0.4, )",
|
||||
@@ -2865,7 +2841,7 @@
|
||||
"infrastructure.dapper": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Core": "[2023.10.2, )",
|
||||
"Core": "[2023.10.3, )",
|
||||
"Dapper": "[2.0.123, )"
|
||||
}
|
||||
},
|
||||
@@ -2873,7 +2849,7 @@
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[12.0.1, )",
|
||||
"Core": "[2023.10.2, )",
|
||||
"Core": "[2023.10.3, )",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[7.0.5, )",
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": "[7.0.5, )",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite": "[7.0.5, )",
|
||||
@@ -2885,7 +2861,7 @@
|
||||
"migrator": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Core": "[2023.10.2, )",
|
||||
"Core": "[2023.10.3, )",
|
||||
"Microsoft.Extensions.Logging": "[6.0.0, )",
|
||||
"dbup-sqlserver": "[5.0.8, )"
|
||||
}
|
||||
@@ -2893,30 +2869,30 @@
|
||||
"mysqlmigrations": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Core": "[2023.10.2, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.2, )"
|
||||
"Core": "[2023.10.3, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.3, )"
|
||||
}
|
||||
},
|
||||
"postgresmigrations": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Core": "[2023.10.2, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.2, )"
|
||||
"Core": "[2023.10.3, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.3, )"
|
||||
}
|
||||
},
|
||||
"sharedweb": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Core": "[2023.10.2, )",
|
||||
"Infrastructure.Dapper": "[2023.10.2, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.2, )"
|
||||
"Core": "[2023.10.3, )",
|
||||
"Infrastructure.Dapper": "[2023.10.3, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.3, )"
|
||||
}
|
||||
},
|
||||
"sqlitemigrations": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Core": "[2023.10.2, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.2, )"
|
||||
"Core": "[2023.10.3, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.3, )"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ using Bit.Api.Models.Response;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Context;
|
||||
@@ -12,7 +14,6 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
@@ -11,6 +11,8 @@ using Bit.Api.Models.Request.Accounts;
|
||||
using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Enums;
|
||||
@@ -20,7 +22,6 @@ using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Repositories;
|
||||
@@ -794,7 +795,7 @@ public class OrganizationsController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _organizationService.UpdateAsync(model.ToOrganization(organization));
|
||||
await _organizationService.UpdateAsync(model.ToOrganization(organization), eventType: EventType.Organization_CollectionManagement_Updated);
|
||||
return new OrganizationResponseModel(organization);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
using Bit.Api.AdminConsole.Models.Request;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Api.Response;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Api.Response;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Request;
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
using Bit.Api.AdminConsole.Public.Models.Request;
|
||||
using Bit.Api.AdminConsole.Public.Models.Response;
|
||||
using Bit.Api.Models.Public.Response;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Public.Models.Request;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json;
|
||||
using Bit.Api.Models.Public.Response;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Public.Models.Response;
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ 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.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Api.Response;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Api.Response;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
@@ -3,9 +3,10 @@ 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.Enums;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tokens;
|
||||
|
||||
@@ -7,6 +7,7 @@ using Bit.Api.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Auth.Models.Api.Response.Accounts;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
|
||||
@@ -43,12 +43,12 @@ public class DomainsResponseModel : ResponseModel
|
||||
IEnumerable<GlobalEquivalentDomainsType> excludedDomains,
|
||||
bool excluded)
|
||||
{
|
||||
Type = globalDomain;
|
||||
Type = (byte)globalDomain;
|
||||
Domains = domains;
|
||||
Excluded = excluded && (excludedDomains?.Contains(globalDomain) ?? false);
|
||||
}
|
||||
|
||||
public GlobalEquivalentDomainsType Type { get; set; }
|
||||
public byte Type { get; set; }
|
||||
public IEnumerable<string> Domains { get; set; }
|
||||
public bool Excluded { get; set; }
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ public class Program
|
||||
return e.Level >= globalSettings.MinLogLevel.ApiSettings.IpRateLimit;
|
||||
}
|
||||
|
||||
if (context.Contains("IdentityServer4.Validation.TokenValidator") ||
|
||||
context.Contains("IdentityServer4.Validation.TokenRequestValidator"))
|
||||
if (context.Contains("Duende.IdentityServer.Validation.TokenValidator") ||
|
||||
context.Contains("Duende.IdentityServer.Validation.TokenRequestValidator"))
|
||||
{
|
||||
return e.Level >= globalSettings.MinLogLevel.ApiSettings.IdentityToken;
|
||||
}
|
||||
|
||||
@@ -36,12 +36,12 @@ public class SMImportRequestModel
|
||||
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(1000)]
|
||||
[EncryptedStringLength(35000)]
|
||||
public string Value { get; set; }
|
||||
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(1000)]
|
||||
[EncryptedStringLength(10000)]
|
||||
public string Note { get; set; }
|
||||
|
||||
[Required]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core;
|
||||
#nullable enable
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -19,6 +20,7 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<Colle
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
private readonly IFeatureService _featureService;
|
||||
private Guid _targetOrganizationId;
|
||||
|
||||
private bool UseFlexibleCollections => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||
|
||||
@@ -33,7 +35,7 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<Colle
|
||||
}
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
CollectionOperationRequirement requirement, ICollection<Collection> resources)
|
||||
CollectionOperationRequirement requirement, ICollection<Collection>? resources)
|
||||
{
|
||||
if (!UseFlexibleCollections)
|
||||
{
|
||||
@@ -41,6 +43,13 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<Colle
|
||||
throw new FeatureUnavailableException("Flexible collections is OFF when it should be ON.");
|
||||
}
|
||||
|
||||
// Establish pattern of authorization handler null checking passed resources
|
||||
if (resources == null || !resources.Any())
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
// Acting user is not authenticated, fail
|
||||
if (!_currentContext.UserId.HasValue)
|
||||
{
|
||||
@@ -48,31 +57,15 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<Colle
|
||||
return;
|
||||
}
|
||||
|
||||
// Establish pattern of authorization handler null checking passed resources
|
||||
if (resources == null || !resources.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var targetOrganizationId = resources.FirstOrDefault()?.OrganizationId ?? default;
|
||||
if (targetOrganizationId == default)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_targetOrganizationId = resources.First().OrganizationId;
|
||||
|
||||
// Ensure all target collections belong to the same organization
|
||||
if (resources.Any(tc => tc.OrganizationId != targetOrganizationId))
|
||||
if (resources.Any(tc => tc.OrganizationId != _targetOrganizationId))
|
||||
{
|
||||
throw new BadRequestException("Requested collections must belong to the same organization.");
|
||||
}
|
||||
|
||||
// Acting user is not a member of the target organization, fail
|
||||
var org = _currentContext.GetOrganization(targetOrganizationId);
|
||||
if (org == null)
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
var org = _currentContext.GetOrganization(_targetOrganizationId);
|
||||
|
||||
switch (requirement)
|
||||
{
|
||||
@@ -97,20 +90,21 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<Colle
|
||||
}
|
||||
|
||||
private async Task CanCreateAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement,
|
||||
CurrentContextOrganization org)
|
||||
CurrentContextOrganization? org)
|
||||
{
|
||||
// If false, all organization members are allowed to create collections
|
||||
if (!org.LimitCollectionCreationDeletion)
|
||||
// If the limit collection management setting is disabled, allow any user to create collections
|
||||
// Otherwise, Owners, Admins, and users with CreateNewCollections permission can always create collections
|
||||
if (org is
|
||||
{ LimitCollectionCreationDeletion: false } or
|
||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
||||
{ Permissions.CreateNewCollections: true })
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
// Owners, Admins, Providers, and users with CreateNewCollections permission can always create collections
|
||||
if (
|
||||
org.Type is OrganizationUserType.Owner or OrganizationUserType.Admin ||
|
||||
org.Permissions is { CreateNewCollections: true } ||
|
||||
await _currentContext.ProviderUserForOrgAsync(org.Id))
|
||||
// Allow provider users to create collections if they are a provider for the target organization
|
||||
if (await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
@@ -135,27 +129,31 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<Colle
|
||||
}
|
||||
|
||||
private async Task CanDeleteAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement,
|
||||
ICollection<Collection> targetCollections, CurrentContextOrganization org)
|
||||
ICollection<Collection> resources, CurrentContextOrganization? org)
|
||||
{
|
||||
// Owners, Admins, Providers, and users with DeleteAnyCollection permission can always delete collections
|
||||
if (
|
||||
org.Type is OrganizationUserType.Owner or OrganizationUserType.Admin ||
|
||||
org.Permissions is { DeleteAnyCollection: true } ||
|
||||
await _currentContext.ProviderUserForOrgAsync(org.Id))
|
||||
// Owners, Admins, and users with DeleteAnyCollection permission can always delete collections
|
||||
if (org is
|
||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
||||
{ Permissions.DeleteAnyCollection: true })
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
// The limit collection management setting is enabled and we are not an Admin (above condition), fail
|
||||
if (org.LimitCollectionCreationDeletion)
|
||||
// The limit collection management setting is disabled,
|
||||
// ensure acting user has manage permissions for all collections being deleted
|
||||
if (org is { LimitCollectionCreationDeletion: false })
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
var canManageCollections = await HasCollectionAccessAsync(resources, org, requireManagePermission: true);
|
||||
if (canManageCollections)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var canManageCollections = await HasCollectionAccessAsync(targetCollections, org, requireManagePermission: true);
|
||||
if (canManageCollections)
|
||||
// Allow providers to delete collections if they are a provider for the target organization
|
||||
if (await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
@@ -165,20 +163,32 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<Colle
|
||||
/// Ensures the acting user is allowed to manage access permissions for the target collections.
|
||||
/// </summary>
|
||||
private async Task CanManageCollectionAccessAsync(AuthorizationHandlerContext context,
|
||||
IAuthorizationRequirement requirement, ICollection<Collection> targetCollections, CurrentContextOrganization org)
|
||||
IAuthorizationRequirement requirement, ICollection<Collection> resources,
|
||||
CurrentContextOrganization? org)
|
||||
{
|
||||
// Owners, Admins, Providers, and users with EditAnyCollection permission can always manage collection access
|
||||
if (
|
||||
org.Permissions is { EditAnyCollection: true } ||
|
||||
org.Type is OrganizationUserType.Owner or OrganizationUserType.Admin ||
|
||||
await _currentContext.ProviderUserForOrgAsync(org.Id))
|
||||
// Owners, Admins, and users with EditAnyCollection permission can always manage collection access
|
||||
if (org is
|
||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
||||
{ Permissions.EditAnyCollection: true })
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
var canManageCollections = await HasCollectionAccessAsync(targetCollections, org, requireManagePermission: true);
|
||||
if (canManageCollections)
|
||||
// The limit collection management setting is disabled,
|
||||
// ensure acting user has manage permissions for all collections being deleted
|
||||
if (org is { LimitCollectionCreationDeletion: false })
|
||||
{
|
||||
var canManageCollections = await HasCollectionAccessAsync(resources, org, requireManagePermission: true);
|
||||
if (canManageCollections)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow providers to manage collections if they are a provider for the target organization
|
||||
if (await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core;
|
||||
#nullable enable
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@@ -15,6 +16,7 @@ public class CollectionAuthorizationHandler : AuthorizationHandler<CollectionOpe
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IFeatureService _featureService;
|
||||
private Guid _targetOrganizationId;
|
||||
|
||||
private bool UseFlexibleCollections => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||
|
||||
@@ -44,6 +46,7 @@ public class CollectionAuthorizationHandler : AuthorizationHandler<CollectionOpe
|
||||
|
||||
if (requirement.OrganizationId == default)
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Api.Vault.Models.Response;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
|
||||
@@ -18,6 +18,7 @@ public class CipherFido2CredentialModel
|
||||
RpId = data.RpId;
|
||||
RpName = data.RpName;
|
||||
UserHandle = data.UserHandle;
|
||||
UserName = data.UserName;
|
||||
UserDisplayName = data.UserDisplayName;
|
||||
Counter = data.Counter;
|
||||
Discoverable = data.Discoverable;
|
||||
@@ -50,6 +51,9 @@ public class CipherFido2CredentialModel
|
||||
public string UserHandle { get; set; }
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(1000)]
|
||||
public string UserName { get; set; }
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(1000)]
|
||||
public string UserDisplayName { get; set; }
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(1000)]
|
||||
@@ -72,6 +76,7 @@ public class CipherFido2CredentialModel
|
||||
RpId = RpId,
|
||||
RpName = RpName,
|
||||
UserHandle = UserHandle,
|
||||
UserName = UserName,
|
||||
UserDisplayName = UserDisplayName,
|
||||
Counter = Counter,
|
||||
Discoverable = Discoverable,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Tools.Models.Response;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Api.Response;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Api.Response;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
@@ -307,6 +307,24 @@
|
||||
"Microsoft.Win32.Registry": "5.0.0"
|
||||
}
|
||||
},
|
||||
"Duende.IdentityServer": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.4",
|
||||
"contentHash": "4HVjzx1F8v5J+U7oa8RGAQGj2QzmzNSu87r18Sh+dlh10uyZZL8teAaT/FaVLDObnfItGdPFvN8mwpF/HkI3Xw==",
|
||||
"dependencies": {
|
||||
"Duende.IdentityServer.Storage": "6.0.4",
|
||||
"Microsoft.AspNetCore.Authentication.OpenIdConnect": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Duende.IdentityServer.Storage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.4",
|
||||
"contentHash": "s5gAjfbpr2IMgI+fU2Nx+2AZdzstmbt9gpo13iX7GwvqSeSaBVqj9ZskAN0R2KF1OemPdZuGnfaTcevdXMUrrw==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "6.0.0",
|
||||
"Microsoft.AspNetCore.DataProtection.Abstractions": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Fido2": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.1",
|
||||
@@ -343,49 +361,8 @@
|
||||
},
|
||||
"IdentityModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.4.0",
|
||||
"contentHash": "b18wrIx5wnZlMxAX7oVsE+nDtAJ4hajYlH0xPlaRvo4r/fz08K6pPeZvbiqS9nfNbzfIgLFmNX+FL9qR9ZR5PA==",
|
||||
"dependencies": {
|
||||
"Newtonsoft.Json": "11.0.2",
|
||||
"System.Text.Encodings.Web": "4.7.0"
|
||||
}
|
||||
},
|
||||
"IdentityModel.AspNetCore.OAuth2Introspection": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.0.1",
|
||||
"contentHash": "ZNdMZMaj9fqR3j50vYsu+1U3QGd6n8+fqwf+a8mCTcmXGor+HgFDfdq0mM34bsmD6uEgAQup7sv2ZW5kR36dbA==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "4.0.0"
|
||||
}
|
||||
},
|
||||
"IdentityServer4": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.1.2",
|
||||
"contentHash": "blaxxGuOA7v/w1q+fxn97wZ+x2ecG1ZD4mc/N/ZOXMNeFZZhqv+4LF26Gecyik3nWrJPmbMEtQbLmRsKG8k61w==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "4.4.0",
|
||||
"IdentityServer4.Storage": "4.1.2",
|
||||
"Microsoft.AspNetCore.Authentication.OpenIdConnect": "3.1.0",
|
||||
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "5.6.0",
|
||||
"Newtonsoft.Json": "12.0.2"
|
||||
}
|
||||
},
|
||||
"IdentityServer4.AccessTokenValidation": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.1",
|
||||
"contentHash": "qu/M6UyN4o9NVep7q545Ms7hYAnsQqSdLbN1Fjjrn4m35lyBfeQPSSNzDryAKHbodyWOQfHaOqKEyMEJQ5Rpgw==",
|
||||
"dependencies": {
|
||||
"IdentityModel.AspNetCore.OAuth2Introspection": "4.0.1",
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer": "3.0.0"
|
||||
}
|
||||
},
|
||||
"IdentityServer4.Storage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.1.2",
|
||||
"contentHash": "KoSffyZyyeCNTIyJiZnCuPakJ1QbCHlpty6gbWUj/7yl+w0PXIchgmmJnJSvddzBb8iZ2xew/vGlxWUIP17P2g==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "4.4.0"
|
||||
}
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "eVHCR7a6m/dm5RFcBzE3qs/Jg5j9R5Rjpu8aTOv9e4AFvaQtBXb5ah7kmwU+YwA0ufRwz4wf1hnIvsD2hSnI4g=="
|
||||
},
|
||||
"LaunchDarkly.Cache": {
|
||||
"type": "Transitive",
|
||||
@@ -477,10 +454,10 @@
|
||||
},
|
||||
"Microsoft.AspNetCore.Authentication.OpenIdConnect": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.0",
|
||||
"contentHash": "O1cAQYUTU8EfRqwc5/rfTns4E4hKlFlg59fuKRrST+PzsxI6H07KqRN/JjdYhAuVYxF8jPnIGbj+zuc5paOWUw==",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "cJxdro36spFzk/K2OFCddM6vZ+yoj6ug8mTFRH3Gdv1Pul/buSuCtfb/FSCp31UmS5S4C1315dU7wX3ErLFuDg==",
|
||||
"dependencies": {
|
||||
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "5.5.0"
|
||||
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.10.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.AspNetCore.Cryptography.Internal": {
|
||||
@@ -513,8 +490,8 @@
|
||||
},
|
||||
"Microsoft.AspNetCore.DataProtection.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.32",
|
||||
"contentHash": "MPL4iVyiaRxnOUY5VATHjvhDWaAEFb77KFiUxVRklv3Z3v+STofUr1UG/aCt1O9cgN7FVTDaC5A7U+zsLub8Xg=="
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "Z/UU4NEBm5UgNufJmw+j5baW26ytCOIZ0G7sZocPaOzsUeBon1bkM3lSMNZQG2GmDjAIVP2XMSODf2jzSGbibw=="
|
||||
},
|
||||
"Microsoft.Azure.Amqp": {
|
||||
"type": "Transitive",
|
||||
@@ -2788,15 +2765,15 @@
|
||||
"commercial.core": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Core": "[2023.10.2, )"
|
||||
"Core": "[2023.10.3, )"
|
||||
}
|
||||
},
|
||||
"commercial.infrastructure.entityframework": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[12.0.1, )",
|
||||
"Core": "[2023.10.2, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.2, )"
|
||||
"Core": "[2023.10.3, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.3, )"
|
||||
}
|
||||
},
|
||||
"core": {
|
||||
@@ -2814,10 +2791,9 @@
|
||||
"BitPay.Light": "[1.0.1907, )",
|
||||
"Braintree": "[5.19.0, )",
|
||||
"DnsClient": "[1.7.0, )",
|
||||
"Duende.IdentityServer": "[6.0.4, )",
|
||||
"Fido2.AspNet": "[3.0.1, )",
|
||||
"Handlebars.Net": "[2.1.2, )",
|
||||
"IdentityServer4": "[4.1.2, )",
|
||||
"IdentityServer4.AccessTokenValidation": "[3.0.1, )",
|
||||
"LaunchDarkly.ServerSdk": "[8.0.0, )",
|
||||
"MailKit": "[4.2.0, )",
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer": "[6.0.4, )",
|
||||
@@ -2845,7 +2821,7 @@
|
||||
"infrastructure.dapper": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Core": "[2023.10.2, )",
|
||||
"Core": "[2023.10.3, )",
|
||||
"Dapper": "[2.0.123, )"
|
||||
}
|
||||
},
|
||||
@@ -2853,7 +2829,7 @@
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[12.0.1, )",
|
||||
"Core": "[2023.10.2, )",
|
||||
"Core": "[2023.10.3, )",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[7.0.5, )",
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": "[7.0.5, )",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite": "[7.0.5, )",
|
||||
@@ -2865,9 +2841,9 @@
|
||||
"sharedweb": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Core": "[2023.10.2, )",
|
||||
"Infrastructure.Dapper": "[2023.10.2, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.2, )"
|
||||
"Core": "[2023.10.3, )",
|
||||
"Infrastructure.Dapper": "[2023.10.3, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.3, )"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,9 +264,16 @@ public class StripeController : Controller
|
||||
|
||||
await SendEmails(new List<string> { organization.BillingEmail });
|
||||
|
||||
var ownerEmails = await _organizationRepository.GetOwnerEmailAddressesById(organization.Id);
|
||||
/*
|
||||
* TODO: https://bitwarden.atlassian.net/browse/PM-4862
|
||||
* Disabling this as part of a hot fix. It needs to check whether the organization
|
||||
* belongs to a Reseller provider and only send an email to the organization owners if it does.
|
||||
* It also requires a new email template as the current one contains too much billing information.
|
||||
*/
|
||||
|
||||
await SendEmails(ownerEmails);
|
||||
// var ownerEmails = await _organizationRepository.GetOwnerEmailAddressesById(organization.Id);
|
||||
|
||||
// await SendEmails(ownerEmails);
|
||||
}
|
||||
else if (userId.HasValue)
|
||||
{
|
||||
|
||||
@@ -184,6 +184,24 @@
|
||||
"Microsoft.Win32.Registry": "5.0.0"
|
||||
}
|
||||
},
|
||||
"Duende.IdentityServer": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.4",
|
||||
"contentHash": "4HVjzx1F8v5J+U7oa8RGAQGj2QzmzNSu87r18Sh+dlh10uyZZL8teAaT/FaVLDObnfItGdPFvN8mwpF/HkI3Xw==",
|
||||
"dependencies": {
|
||||
"Duende.IdentityServer.Storage": "6.0.4",
|
||||
"Microsoft.AspNetCore.Authentication.OpenIdConnect": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Duende.IdentityServer.Storage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.4",
|
||||
"contentHash": "s5gAjfbpr2IMgI+fU2Nx+2AZdzstmbt9gpo13iX7GwvqSeSaBVqj9ZskAN0R2KF1OemPdZuGnfaTcevdXMUrrw==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "6.0.0",
|
||||
"Microsoft.AspNetCore.DataProtection.Abstractions": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Fido2": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.1",
|
||||
@@ -220,49 +238,8 @@
|
||||
},
|
||||
"IdentityModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.4.0",
|
||||
"contentHash": "b18wrIx5wnZlMxAX7oVsE+nDtAJ4hajYlH0xPlaRvo4r/fz08K6pPeZvbiqS9nfNbzfIgLFmNX+FL9qR9ZR5PA==",
|
||||
"dependencies": {
|
||||
"Newtonsoft.Json": "11.0.2",
|
||||
"System.Text.Encodings.Web": "4.7.0"
|
||||
}
|
||||
},
|
||||
"IdentityModel.AspNetCore.OAuth2Introspection": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.0.1",
|
||||
"contentHash": "ZNdMZMaj9fqR3j50vYsu+1U3QGd6n8+fqwf+a8mCTcmXGor+HgFDfdq0mM34bsmD6uEgAQup7sv2ZW5kR36dbA==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "4.0.0"
|
||||
}
|
||||
},
|
||||
"IdentityServer4": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.1.2",
|
||||
"contentHash": "blaxxGuOA7v/w1q+fxn97wZ+x2ecG1ZD4mc/N/ZOXMNeFZZhqv+4LF26Gecyik3nWrJPmbMEtQbLmRsKG8k61w==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "4.4.0",
|
||||
"IdentityServer4.Storage": "4.1.2",
|
||||
"Microsoft.AspNetCore.Authentication.OpenIdConnect": "3.1.0",
|
||||
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "5.6.0",
|
||||
"Newtonsoft.Json": "12.0.2"
|
||||
}
|
||||
},
|
||||
"IdentityServer4.AccessTokenValidation": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.1",
|
||||
"contentHash": "qu/M6UyN4o9NVep7q545Ms7hYAnsQqSdLbN1Fjjrn4m35lyBfeQPSSNzDryAKHbodyWOQfHaOqKEyMEJQ5Rpgw==",
|
||||
"dependencies": {
|
||||
"IdentityModel.AspNetCore.OAuth2Introspection": "4.0.1",
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer": "3.0.0"
|
||||
}
|
||||
},
|
||||
"IdentityServer4.Storage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.1.2",
|
||||
"contentHash": "KoSffyZyyeCNTIyJiZnCuPakJ1QbCHlpty6gbWUj/7yl+w0PXIchgmmJnJSvddzBb8iZ2xew/vGlxWUIP17P2g==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "4.4.0"
|
||||
}
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "eVHCR7a6m/dm5RFcBzE3qs/Jg5j9R5Rjpu8aTOv9e4AFvaQtBXb5ah7kmwU+YwA0ufRwz4wf1hnIvsD2hSnI4g=="
|
||||
},
|
||||
"LaunchDarkly.Cache": {
|
||||
"type": "Transitive",
|
||||
@@ -354,10 +331,10 @@
|
||||
},
|
||||
"Microsoft.AspNetCore.Authentication.OpenIdConnect": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.0",
|
||||
"contentHash": "O1cAQYUTU8EfRqwc5/rfTns4E4hKlFlg59fuKRrST+PzsxI6H07KqRN/JjdYhAuVYxF8jPnIGbj+zuc5paOWUw==",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "cJxdro36spFzk/K2OFCddM6vZ+yoj6ug8mTFRH3Gdv1Pul/buSuCtfb/FSCp31UmS5S4C1315dU7wX3ErLFuDg==",
|
||||
"dependencies": {
|
||||
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "5.5.0"
|
||||
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.10.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.AspNetCore.Cryptography.Internal": {
|
||||
@@ -390,8 +367,8 @@
|
||||
},
|
||||
"Microsoft.AspNetCore.DataProtection.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.32",
|
||||
"contentHash": "MPL4iVyiaRxnOUY5VATHjvhDWaAEFb77KFiUxVRklv3Z3v+STofUr1UG/aCt1O9cgN7FVTDaC5A7U+zsLub8Xg=="
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "Z/UU4NEBm5UgNufJmw+j5baW26ytCOIZ0G7sZocPaOzsUeBon1bkM3lSMNZQG2GmDjAIVP2XMSODf2jzSGbibw=="
|
||||
},
|
||||
"Microsoft.Azure.Amqp": {
|
||||
"type": "Transitive",
|
||||
@@ -2617,10 +2594,9 @@
|
||||
"BitPay.Light": "[1.0.1907, )",
|
||||
"Braintree": "[5.19.0, )",
|
||||
"DnsClient": "[1.7.0, )",
|
||||
"Duende.IdentityServer": "[6.0.4, )",
|
||||
"Fido2.AspNet": "[3.0.1, )",
|
||||
"Handlebars.Net": "[2.1.2, )",
|
||||
"IdentityServer4": "[4.1.2, )",
|
||||
"IdentityServer4.AccessTokenValidation": "[3.0.1, )",
|
||||
"LaunchDarkly.ServerSdk": "[8.0.0, )",
|
||||
"MailKit": "[4.2.0, )",
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer": "[6.0.4, )",
|
||||
@@ -2648,7 +2624,7 @@
|
||||
"infrastructure.dapper": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Core": "[2023.10.2, )",
|
||||
"Core": "[2023.10.3, )",
|
||||
"Dapper": "[2.0.123, )"
|
||||
}
|
||||
},
|
||||
@@ -2656,7 +2632,7 @@
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[12.0.1, )",
|
||||
"Core": "[2023.10.2, )",
|
||||
"Core": "[2023.10.3, )",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[7.0.5, )",
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": "[7.0.5, )",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite": "[7.0.5, )",
|
||||
@@ -2668,9 +2644,9 @@
|
||||
"sharedweb": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Core": "[2023.10.2, )",
|
||||
"Infrastructure.Dapper": "[2023.10.2, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.2, )"
|
||||
"Core": "[2023.10.3, )",
|
||||
"Infrastructure.Dapper": "[2023.10.3, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.3, )"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Entities;
|
||||
namespace Bit.Core.AdminConsole.Entities;
|
||||
|
||||
public class Policy : ITableObject<Guid>
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Bit.Core.Enums;
|
||||
namespace Bit.Core.AdminConsole.Enums;
|
||||
|
||||
public enum PolicyType : byte
|
||||
{
|
||||
@@ -1,8 +1,9 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Core.Models.Api.Response;
|
||||
namespace Bit.Core.AdminConsole.Models.Api.Response;
|
||||
|
||||
public class PolicyResponseModel : ResponseModel
|
||||
{
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
|
||||
public interface IPolicyDataModel
|
||||
{
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Bit.Core.Models.Data.Organizations.Policies;
|
||||
namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
|
||||
public class MasterPasswordPolicyData : IPolicyDataModel
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Core.Models.Data.Organizations.Policies;
|
||||
namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
|
||||
public class ResetPasswordDataModel : IPolicyDataModel
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Core.Models.Data.Organizations.Policies;
|
||||
namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
|
||||
public class SendOptionsPolicyData : IPolicyDataModel
|
||||
{
|
||||
@@ -1,7 +1,8 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Core.Repositories;
|
||||
namespace Bit.Core.AdminConsole.Repositories;
|
||||
|
||||
public interface IPolicyRepository : IRepository<Policy, Guid>
|
||||
{
|
||||
@@ -1,9 +1,12 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
namespace Bit.Core.AdminConsole.Services;
|
||||
|
||||
public interface IPolicyService
|
||||
{
|
||||
@@ -1,14 +1,18 @@
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
namespace Bit.Core.AdminConsole.Services.Implementations;
|
||||
|
||||
public class PolicyService : IPolicyService
|
||||
{
|
||||
@@ -114,12 +118,12 @@ public class PolicyService : IPolicyService
|
||||
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(
|
||||
policy.OrganizationId);
|
||||
var removableOrgUsers = orgUsers.Where(ou =>
|
||||
ou.Status != Enums.OrganizationUserStatusType.Invited && ou.Status != Enums.OrganizationUserStatusType.Revoked &&
|
||||
ou.Type != Enums.OrganizationUserType.Owner && ou.Type != Enums.OrganizationUserType.Admin &&
|
||||
ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked &&
|
||||
ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin &&
|
||||
ou.UserId != savingUserId);
|
||||
switch (policy.Type)
|
||||
{
|
||||
case Enums.PolicyType.TwoFactorAuthentication:
|
||||
case PolicyType.TwoFactorAuthentication:
|
||||
foreach (var orgUser in removableOrgUsers)
|
||||
{
|
||||
if (!await userService.TwoFactorIsEnabledAsync(orgUser))
|
||||
@@ -131,7 +135,7 @@ public class PolicyService : IPolicyService
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Enums.PolicyType.SingleOrg:
|
||||
case PolicyType.SingleOrg:
|
||||
var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(
|
||||
removableOrgUsers.Select(ou => ou.UserId.Value));
|
||||
foreach (var orgUser in removableOrgUsers)
|
||||
@@ -154,7 +158,7 @@ public class PolicyService : IPolicyService
|
||||
}
|
||||
policy.RevisionDate = now;
|
||||
await _policyRepository.UpsertAsync(policy);
|
||||
await _eventService.LogPolicyEventAsync(policy, Enums.EventType.Policy_Updated);
|
||||
await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated);
|
||||
}
|
||||
|
||||
public async Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user)
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Auth.Enums;
|
||||
|
||||
public enum WebAuthnLoginAssertionOptionsScope
|
||||
{
|
||||
Authentication = 0,
|
||||
PrfRegistration = 1
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
|
||||
using Bit.Core.Models.Api;
|
||||
using Fido2NetLib;
|
||||
|
||||
namespace Bit.Core.Auth.Models.Api.Response.Accounts;
|
||||
|
||||
public class WebAuthnLoginAssertionOptionsResponseModel : ResponseModel
|
||||
{
|
||||
private const string ResponseObj = "webAuthnLoginAssertionOptions";
|
||||
|
||||
public WebAuthnLoginAssertionOptionsResponseModel() : base(ResponseObj)
|
||||
{
|
||||
}
|
||||
|
||||
public AssertionOptions Options { get; set; }
|
||||
public string Token { get; set; }
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@ public class UserDecryptionOptions : ResponseModel
|
||||
/// </summary>
|
||||
public bool HasMasterPassword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WebAuthn PRF decryption keys.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public WebAuthnPrfDecryptionOption? WebAuthnPrfOption { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets information regarding this users trusted device decryption setup.
|
||||
/// </summary>
|
||||
@@ -29,6 +35,20 @@ public class UserDecryptionOptions : ResponseModel
|
||||
public KeyConnectorUserDecryptionOption? KeyConnectorOption { get; set; }
|
||||
}
|
||||
|
||||
public class WebAuthnPrfDecryptionOption
|
||||
{
|
||||
public string EncryptedPrivateKey { get; }
|
||||
public string EncryptedUserKey { get; }
|
||||
|
||||
public WebAuthnPrfDecryptionOption(
|
||||
string encryptedPrivateKey,
|
||||
string encryptedUserKey)
|
||||
{
|
||||
EncryptedPrivateKey = encryptedPrivateKey;
|
||||
EncryptedUserKey = encryptedUserKey;
|
||||
}
|
||||
}
|
||||
|
||||
public class TrustedDeviceUserDecryptionOption
|
||||
{
|
||||
public bool HasAdminApproval { get; }
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Tokens;
|
||||
using Fido2NetLib;
|
||||
|
||||
namespace Bit.Core.Auth.Models.Business.Tokenables;
|
||||
|
||||
public class WebAuthnLoginAssertionOptionsTokenable : ExpiringTokenable
|
||||
{
|
||||
// Lifetime 17 minutes =
|
||||
// - 6 Minutes for Attestation (max webauthn timeout)
|
||||
// - 6 Minutes for PRF Assertion (max webauthn timeout)
|
||||
// - 5 minutes for user to complete the process (name their passkey, etc)
|
||||
private static readonly TimeSpan _tokenLifetime = TimeSpan.FromMinutes(17);
|
||||
public const string ClearTextPrefix = "BWWebAuthnLoginAssertionOptions_";
|
||||
public const string DataProtectorPurpose = "WebAuthnLoginAssertionOptionsDataProtector";
|
||||
public const string TokenIdentifier = "WebAuthnLoginAssertionOptionsToken";
|
||||
|
||||
public string Identifier { get; set; } = TokenIdentifier;
|
||||
public AssertionOptions Options { get; set; }
|
||||
public WebAuthnLoginAssertionOptionsScope Scope { get; set; }
|
||||
|
||||
[JsonConstructor]
|
||||
public WebAuthnLoginAssertionOptionsTokenable()
|
||||
{
|
||||
ExpirationDate = DateTime.UtcNow.Add(_tokenLifetime);
|
||||
}
|
||||
|
||||
public WebAuthnLoginAssertionOptionsTokenable(WebAuthnLoginAssertionOptionsScope scope, AssertionOptions options) : this()
|
||||
{
|
||||
Scope = scope;
|
||||
Options = options;
|
||||
}
|
||||
|
||||
public bool TokenIsValid(WebAuthnLoginAssertionOptionsScope scope)
|
||||
{
|
||||
if (!Valid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Scope == scope;
|
||||
}
|
||||
|
||||
protected override bool TokenIsValid() => Identifier == TokenIdentifier && Options != null;
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Tokens;
|
||||
|
||||
namespace Bit.Core.Auth.Models.Business.Tokenables;
|
||||
|
||||
public class WebAuthnLoginTokenable : ExpiringTokenable
|
||||
{
|
||||
private const double _tokenLifetimeInHours = (double)1 / 60; // 1 minute
|
||||
public const string ClearTextPrefix = "BWWebAuthnLogin_";
|
||||
public const string DataProtectorPurpose = "WebAuthnLoginDataProtector";
|
||||
public const string TokenIdentifier = "WebAuthnLoginToken";
|
||||
|
||||
public string Identifier { get; set; } = TokenIdentifier;
|
||||
public Guid Id { get; set; }
|
||||
public string Email { get; set; }
|
||||
|
||||
[JsonConstructor]
|
||||
public WebAuthnLoginTokenable()
|
||||
{
|
||||
ExpirationDate = DateTime.UtcNow.AddHours(_tokenLifetimeInHours);
|
||||
}
|
||||
|
||||
public WebAuthnLoginTokenable(User user) : this()
|
||||
{
|
||||
Id = user?.Id ?? default;
|
||||
Email = user?.Email;
|
||||
}
|
||||
|
||||
public bool TokenIsValid(User user)
|
||||
{
|
||||
if (Id == default || Email == default || user == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Id == user.Id &&
|
||||
Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
// Validates deserialized
|
||||
protected override bool TokenIsValid() => Identifier == TokenIdentifier && Id != default && !string.IsNullOrWhiteSpace(Email);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Entities;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
|
||||
19
src/Core/Auth/Utilities/GuidUtilities.cs
Normal file
19
src/Core/Auth/Utilities/GuidUtilities.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace Bit.Core.Auth.Utilities;
|
||||
|
||||
public static class GuidUtilities
|
||||
{
|
||||
public static bool TryParseBytes(ReadOnlySpan<byte> bytes, out Guid guid)
|
||||
{
|
||||
try
|
||||
{
|
||||
guid = new Guid(bytes);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
guid = Guid.Empty;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,18 +26,9 @@ public static class Constants
|
||||
public const string CipherKeyEncryptionMinimumVersion = "2023.9.2";
|
||||
|
||||
/// <summary>
|
||||
/// When you set the ProrationBehavior to create_prorations,
|
||||
/// Stripe will automatically create prorations for any changes made to the subscription,
|
||||
/// such as changing the plan, adding or removing quantities, or applying discounts.
|
||||
/// Used by IdentityServer to identify our own provider.
|
||||
/// </summary>
|
||||
public const string CreateProrations = "create_prorations";
|
||||
|
||||
/// <summary>
|
||||
/// When you set the ProrationBehavior to always_invoice,
|
||||
/// Stripe will always generate an invoice when a subscription update occurs,
|
||||
/// regardless of whether there is a proration or not.
|
||||
/// </summary>
|
||||
public const string AlwaysInvoice = "always_invoice";
|
||||
public const string IdentityProvider = "bitwarden";
|
||||
}
|
||||
|
||||
public static class TokenPurposes
|
||||
@@ -57,6 +48,7 @@ public static class FeatureFlagKeys
|
||||
public const string PasswordlessLogin = "passwordless-login";
|
||||
public const string TrustedDeviceEncryption = "trusted-device-encryption";
|
||||
public const string Fido2VaultCredentials = "fido2-vault-credentials";
|
||||
public const string VaultOnboarding = "vault-onboarding";
|
||||
public const string AutofillV2 = "autofill-v2";
|
||||
public const string BrowserFilelessImport = "browser-fileless-import";
|
||||
public const string FlexibleCollections = "flexible-collections";
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
<PackageReference Include="DnsClient" Version="1.7.0" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="3.0.1" />
|
||||
<PackageReference Include="Handlebars.Net" Version="2.1.2" />
|
||||
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
|
||||
<PackageReference Include="MailKit" Version="4.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.Azure.Cosmos.Table" Version="1.0.8" />
|
||||
@@ -48,7 +47,7 @@
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging.File" Version="2.0.0" />
|
||||
<PackageReference Include="Sentry.Serilog" Version="3.16.0" />
|
||||
<PackageReference Include="IdentityServer4" Version="4.1.2" />
|
||||
<PackageReference Include="Duende.IdentityServer" Version="6.0.4" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.AzureCosmosDB" Version="2.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="2.0.6" />
|
||||
|
||||
@@ -67,6 +67,7 @@ public enum EventType : int
|
||||
Organization_EnabledKeyConnector = 1606,
|
||||
Organization_DisabledKeyConnector = 1607,
|
||||
Organization_SponsorshipsSynced = 1608,
|
||||
Organization_CollectionManagement_Updated = 1609,
|
||||
|
||||
Policy_Updated = 1700,
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using IdentityServer4.Models;
|
||||
using Duende.IdentityServer.Models;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Bit.Core.Settings;
|
||||
using IdentityServer4.Configuration;
|
||||
using Duende.IdentityServer.Configuration;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Caching.StackExchangeRedis;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Bit.Core.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
|
||||
namespace Bit.Core.Models.Api.Response;
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Bit.Core.Models.Business;
|
||||
|
||||
public class InvoicePreviewResult
|
||||
{
|
||||
public bool IsInvoicedNow { get; set; }
|
||||
public string PaymentIntentClientSecret { get; set; }
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Models.Business;
|
||||
|
||||
public class PendingInoviceItems
|
||||
{
|
||||
public IEnumerable<InvoiceItem> PendingInvoiceItems { get; set; }
|
||||
public IDictionary<string, InvoiceItem> PendingInvoiceItemsDict { get; set; }
|
||||
}
|
||||
@@ -44,7 +44,7 @@ public class SecretsManagerSubscribeUpdate : SubscriptionUpdate
|
||||
{
|
||||
updatedItems.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Plan = _plan.SecretsManager.StripeSeatPlanId,
|
||||
Price = _plan.SecretsManager.StripeSeatPlanId,
|
||||
Quantity = _additionalSeats
|
||||
});
|
||||
}
|
||||
@@ -53,7 +53,7 @@ public class SecretsManagerSubscribeUpdate : SubscriptionUpdate
|
||||
{
|
||||
updatedItems.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Plan = _plan.SecretsManager.StripeServiceAccountPlanId,
|
||||
Price = _plan.SecretsManager.StripeServiceAccountPlanId,
|
||||
Quantity = _additionalServiceAccounts
|
||||
});
|
||||
}
|
||||
@@ -63,14 +63,14 @@ public class SecretsManagerSubscribeUpdate : SubscriptionUpdate
|
||||
{
|
||||
updatedItems.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Plan = _plan.SecretsManager.StripeSeatPlanId,
|
||||
Price = _plan.SecretsManager.StripeSeatPlanId,
|
||||
Quantity = _previousSeats,
|
||||
Deleted = _previousSeats == 0 ? true : (bool?)null,
|
||||
});
|
||||
|
||||
updatedItems.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Plan = _plan.SecretsManager.StripeServiceAccountPlanId,
|
||||
Price = _plan.SecretsManager.StripeServiceAccountPlanId,
|
||||
Quantity = _previousServiceAccounts,
|
||||
Deleted = _previousServiceAccounts == 0 ? true : (bool?)null,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace Bit.Core.Models.Data.Organizations.Policies;
|
||||
|
||||
public interface IPolicyDataModel
|
||||
{
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Entities;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
@@ -27,7 +27,7 @@ public interface IOrganizationService
|
||||
Task DisableAsync(Guid organizationId, DateTime? expirationDate);
|
||||
Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate);
|
||||
Task EnableAsync(Guid organizationId);
|
||||
Task UpdateAsync(Organization organization, bool updateBilling = false);
|
||||
Task UpdateAsync(Organization organization, bool updateBilling = false, EventType eventType = EventType.Organization_Updated);
|
||||
Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type);
|
||||
Task DisableTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type);
|
||||
Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, Guid? invitingUserId,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bit.Core.Models.BitStripe;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
@@ -15,11 +14,8 @@ public interface IStripeAdapter
|
||||
Task<Stripe.Subscription> SubscriptionUpdateAsync(string id, Stripe.SubscriptionUpdateOptions options = null);
|
||||
Task<Stripe.Subscription> SubscriptionCancelAsync(string Id, Stripe.SubscriptionCancelOptions options = null);
|
||||
Task<Stripe.Invoice> InvoiceUpcomingAsync(Stripe.UpcomingInvoiceOptions options);
|
||||
Task<Stripe.Invoice> InvoiceCreateAsync(Stripe.InvoiceCreateOptions options);
|
||||
Task<Stripe.InvoiceItem> InvoiceItemCreateAsync(Stripe.InvoiceItemCreateOptions options);
|
||||
Task<Stripe.Invoice> InvoiceGetAsync(string id, Stripe.InvoiceGetOptions options);
|
||||
Task<List<Stripe.Invoice>> InvoiceListAsync(StripeInvoiceListOptions options);
|
||||
IEnumerable<InvoiceItem> InvoiceItemListAsync(InvoiceItemListOptions options);
|
||||
Task<Stripe.Invoice> InvoiceUpdateAsync(string id, Stripe.InvoiceUpdateOptions options);
|
||||
Task<Stripe.Invoice> InvoiceFinalizeInvoiceAsync(string id, Stripe.InvoiceFinalizeOptions options);
|
||||
Task<Stripe.Invoice> InvoiceSendInvoiceAsync(string id, Stripe.InvoiceSendOptions options);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Entities;
|
||||
@@ -29,8 +30,8 @@ public interface IUserService
|
||||
Task<bool> CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse);
|
||||
Task<CredentialCreateOptions> StartWebAuthnLoginRegistrationAsync(User user);
|
||||
Task<bool> CompleteWebAuthLoginRegistrationAsync(User user, string name, CredentialCreateOptions options, AuthenticatorAttestationRawResponse attestationResponse, bool supportsPrf, string encryptedUserKey = null, string encryptedPublicKey = null, string encryptedPrivateKey = null);
|
||||
Task<AssertionOptions> StartWebAuthnLoginAssertionAsync(User user);
|
||||
Task<string> CompleteWebAuthLoginAssertionAsync(AuthenticatorAssertionRawResponse assertionResponse, User user);
|
||||
AssertionOptions StartWebAuthnLoginAssertion();
|
||||
Task<(User, WebAuthnCredential)> CompleteWebAuthLoginAssertionAsync(AssertionOptions options, AuthenticatorAssertionRawResponse assertionResponse);
|
||||
Task SendEmailVerificationAsync(User user);
|
||||
Task<IdentityResult> ConfirmEmailAsync(User user, string token);
|
||||
Task InitiateEmailChangeAsync(User user, string newEmail);
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Business;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
@@ -15,7 +18,6 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
@@ -57,6 +59,7 @@ public class OrganizationService : IOrganizationService
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery;
|
||||
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory;
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||
private readonly IFeatureService _featureService;
|
||||
@@ -90,6 +93,7 @@ public class OrganizationService : IOrganizationService
|
||||
IOrgUserInviteTokenableFactory orgUserInviteTokenableFactory,
|
||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
|
||||
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
||||
IProviderRepository providerRepository,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
@@ -118,6 +122,7 @@ public class OrganizationService : IOrganizationService
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery;
|
||||
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
||||
_providerRepository = providerRepository;
|
||||
_orgUserInviteTokenableFactory = orgUserInviteTokenableFactory;
|
||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||
_featureService = featureService;
|
||||
@@ -736,7 +741,7 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(Organization organization, bool updateBilling = false)
|
||||
public async Task UpdateAsync(Organization organization, bool updateBilling = false, EventType eventType = EventType.Organization_Updated)
|
||||
{
|
||||
if (organization.Id == default(Guid))
|
||||
{
|
||||
@@ -752,7 +757,7 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
}
|
||||
|
||||
await ReplaceAndUpdateCacheAsync(organization, EventType.Organization_Updated);
|
||||
await ReplaceAndUpdateCacheAsync(organization, eventType);
|
||||
|
||||
if (updateBilling && !string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
|
||||
{
|
||||
@@ -862,7 +867,7 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
if (newSeatsRequired > 0)
|
||||
{
|
||||
var (canScale, failureReason) = CanScale(organization, newSeatsRequired);
|
||||
var (canScale, failureReason) = await CanScaleAsync(organization, newSeatsRequired);
|
||||
if (!canScale)
|
||||
{
|
||||
throw new BadRequestException(failureReason);
|
||||
@@ -1182,7 +1187,8 @@ public class OrganizationService : IOrganizationService
|
||||
return result;
|
||||
}
|
||||
|
||||
internal (bool canScale, string failureReason) CanScale(Organization organization,
|
||||
internal async Task<(bool canScale, string failureReason)> CanScaleAsync(
|
||||
Organization organization,
|
||||
int seatsToAdd)
|
||||
{
|
||||
var failureReason = "";
|
||||
@@ -1197,6 +1203,13 @@ public class OrganizationService : IOrganizationService
|
||||
return (true, failureReason);
|
||||
}
|
||||
|
||||
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||
|
||||
if (provider is { Enabled: true, Type: ProviderType.Reseller })
|
||||
{
|
||||
return (false, "Seat limit has been reached. Contact your provider to purchase additional seats.");
|
||||
}
|
||||
|
||||
if (organization.Seats.HasValue &&
|
||||
organization.MaxAutoscaleSeats.HasValue &&
|
||||
organization.MaxAutoscaleSeats.Value < organization.Seats.Value + seatsToAdd)
|
||||
@@ -1214,7 +1227,7 @@ public class OrganizationService : IOrganizationService
|
||||
return;
|
||||
}
|
||||
|
||||
var (canScale, failureMessage) = CanScale(organization, seatsToAdd);
|
||||
var (canScale, failureMessage) = await CanScaleAsync(organization, seatsToAdd);
|
||||
if (!canScale)
|
||||
{
|
||||
throw new BadRequestException(failureMessage);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bit.Core.Models.BitStripe;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
@@ -17,7 +16,6 @@ public class StripeAdapter : IStripeAdapter
|
||||
private readonly Stripe.BankAccountService _bankAccountService;
|
||||
private readonly Stripe.PriceService _priceService;
|
||||
private readonly Stripe.TestHelpers.TestClockService _testClockService;
|
||||
private readonly Stripe.InvoiceItemService _invoiceItemService;
|
||||
|
||||
public StripeAdapter()
|
||||
{
|
||||
@@ -33,7 +31,6 @@ public class StripeAdapter : IStripeAdapter
|
||||
_bankAccountService = new Stripe.BankAccountService();
|
||||
_priceService = new Stripe.PriceService();
|
||||
_testClockService = new Stripe.TestHelpers.TestClockService();
|
||||
_invoiceItemService = new Stripe.InvoiceItemService();
|
||||
}
|
||||
|
||||
public Task<Stripe.Customer> CustomerCreateAsync(Stripe.CustomerCreateOptions options)
|
||||
@@ -82,16 +79,6 @@ public class StripeAdapter : IStripeAdapter
|
||||
return _invoiceService.UpcomingAsync(options);
|
||||
}
|
||||
|
||||
public Task<Stripe.Invoice> InvoiceCreateAsync(Stripe.InvoiceCreateOptions options)
|
||||
{
|
||||
return _invoiceService.CreateAsync(options);
|
||||
}
|
||||
|
||||
public Task<Stripe.InvoiceItem> InvoiceItemCreateAsync(Stripe.InvoiceItemCreateOptions options)
|
||||
{
|
||||
return _invoiceItemService.CreateAsync(options);
|
||||
}
|
||||
|
||||
public Task<Stripe.Invoice> InvoiceGetAsync(string id, Stripe.InvoiceGetOptions options)
|
||||
{
|
||||
return _invoiceService.GetAsync(id, options);
|
||||
@@ -116,11 +103,6 @@ public class StripeAdapter : IStripeAdapter
|
||||
return invoices;
|
||||
}
|
||||
|
||||
public IEnumerable<InvoiceItem> InvoiceItemListAsync(InvoiceItemListOptions options)
|
||||
{
|
||||
return _invoiceItemService.ListAutoPaging(options);
|
||||
}
|
||||
|
||||
public Task<Stripe.Invoice> InvoiceUpdateAsync(string id, Stripe.InvoiceUpdateOptions options)
|
||||
{
|
||||
return _invoiceService.UpdateAsync(id, options);
|
||||
|
||||
@@ -7,7 +7,6 @@ using Bit.Core.Models.Business;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Stripe;
|
||||
using StaticStore = Bit.Core.Models.StaticStore;
|
||||
using TaxRate = Bit.Core.Entities.TaxRate;
|
||||
|
||||
@@ -751,14 +750,16 @@ public class StripePaymentService : IPaymentService
|
||||
prorationDate ??= DateTime.UtcNow;
|
||||
var collectionMethod = sub.CollectionMethod;
|
||||
var daysUntilDue = sub.DaysUntilDue;
|
||||
var chargeNow = collectionMethod == "charge_automatically";
|
||||
var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub);
|
||||
|
||||
var subUpdateOptions = new Stripe.SubscriptionUpdateOptions
|
||||
{
|
||||
Items = updatedItemOptions,
|
||||
ProrationBehavior = Constants.CreateProrations,
|
||||
ProrationBehavior = "always_invoice",
|
||||
DaysUntilDue = daysUntilDue ?? 1,
|
||||
CollectionMethod = "send_invoice"
|
||||
CollectionMethod = "send_invoice",
|
||||
ProrationDate = prorationDate,
|
||||
};
|
||||
|
||||
if (!subscriptionUpdate.UpdateNeeded(sub))
|
||||
@@ -792,50 +793,66 @@ public class StripePaymentService : IPaymentService
|
||||
string paymentIntentClientSecret = null;
|
||||
try
|
||||
{
|
||||
var subItemOptions = updatedItemOptions.Select(itemOption =>
|
||||
new Stripe.InvoiceSubscriptionItemOptions
|
||||
{
|
||||
Id = itemOption.Id,
|
||||
Plan = itemOption.Plan,
|
||||
Quantity = itemOption.Quantity,
|
||||
}).ToList();
|
||||
|
||||
var reviewInvoiceResponse = await PreviewUpcomingInvoiceAndPayAsync(storableSubscriber, subItemOptions);
|
||||
paymentIntentClientSecret = reviewInvoiceResponse.PaymentIntentClientSecret;
|
||||
|
||||
var subResponse = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, subUpdateOptions);
|
||||
var invoice =
|
||||
await _stripeAdapter.InvoiceGetAsync(subResponse?.LatestInvoiceId, new Stripe.InvoiceGetOptions());
|
||||
|
||||
var invoice = await _stripeAdapter.InvoiceGetAsync(subResponse?.LatestInvoiceId, new Stripe.InvoiceGetOptions());
|
||||
if (invoice == null)
|
||||
{
|
||||
throw new BadRequestException("Unable to locate draft invoice for subscription update.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Need to revert the subscription
|
||||
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
|
||||
|
||||
if (invoice.AmountDue > 0 && updatedItemOptions.Any(i => i.Quantity > 0))
|
||||
{
|
||||
Items = subscriptionUpdate.RevertItemsOptions(sub),
|
||||
// This proration behavior prevents a false "credit" from
|
||||
// being applied forward to the next month's invoice
|
||||
ProrationBehavior = "none",
|
||||
CollectionMethod = collectionMethod,
|
||||
DaysUntilDue = daysUntilDue,
|
||||
});
|
||||
throw;
|
||||
try
|
||||
{
|
||||
if (chargeNow)
|
||||
{
|
||||
paymentIntentClientSecret = await PayInvoiceAfterSubscriptionChangeAsync(
|
||||
storableSubscriber, invoice);
|
||||
}
|
||||
else
|
||||
{
|
||||
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(subResponse.LatestInvoiceId, new Stripe.InvoiceFinalizeOptions
|
||||
{
|
||||
AutoAdvance = false,
|
||||
});
|
||||
await _stripeAdapter.InvoiceSendInvoiceAsync(invoice.Id, new Stripe.InvoiceSendOptions());
|
||||
paymentIntentClientSecret = null;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Need to revert the subscription
|
||||
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
|
||||
{
|
||||
Items = subscriptionUpdate.RevertItemsOptions(sub),
|
||||
// This proration behavior prevents a false "credit" from
|
||||
// being applied forward to the next month's invoice
|
||||
ProrationBehavior = "none",
|
||||
CollectionMethod = collectionMethod,
|
||||
DaysUntilDue = daysUntilDue,
|
||||
});
|
||||
throw;
|
||||
}
|
||||
}
|
||||
else if (!invoice.Paid)
|
||||
{
|
||||
// Pay invoice with no charge to customer this completes the invoice immediately without waiting the scheduled 1h
|
||||
invoice = await _stripeAdapter.InvoicePayAsync(subResponse.LatestInvoiceId);
|
||||
paymentIntentClientSecret = null;
|
||||
}
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Change back the subscription collection method and/or days until due
|
||||
if (collectionMethod != "send_invoice" || daysUntilDue == null)
|
||||
{
|
||||
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id,
|
||||
new Stripe.SubscriptionUpdateOptions
|
||||
{
|
||||
CollectionMethod = collectionMethod,
|
||||
DaysUntilDue = daysUntilDue,
|
||||
});
|
||||
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
|
||||
{
|
||||
CollectionMethod = collectionMethod,
|
||||
DaysUntilDue = daysUntilDue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -918,7 +935,6 @@ public class StripePaymentService : IPaymentService
|
||||
await _stripeAdapter.CustomerDeleteAsync(subscriber.GatewayCustomerId);
|
||||
}
|
||||
|
||||
//This method is no-longer is use because we return the dollar threshold feature on invoice will be generated. but we dont want to lose this implementation.
|
||||
public async Task<string> PayInvoiceAfterSubscriptionChangeAsync(ISubscriber subscriber, Stripe.Invoice invoice)
|
||||
{
|
||||
var customerOptions = new Stripe.CustomerGetOptions();
|
||||
@@ -1088,310 +1104,6 @@ public class StripePaymentService : IPaymentService
|
||||
return paymentIntentClientSecret;
|
||||
}
|
||||
|
||||
internal async Task<InvoicePreviewResult> PreviewUpcomingInvoiceAndPayAsync(ISubscriber subscriber,
|
||||
List<Stripe.InvoiceSubscriptionItemOptions> subItemOptions, int prorateThreshold = 50000)
|
||||
{
|
||||
var customer = await CheckInAppPurchaseMethod(subscriber);
|
||||
|
||||
string paymentIntentClientSecret = null;
|
||||
|
||||
var pendingInvoiceItems = GetPendingInvoiceItems(subscriber);
|
||||
|
||||
var upcomingPreview = await GetUpcomingInvoiceAsync(subscriber, subItemOptions);
|
||||
|
||||
var itemsForInvoice = GetItemsForInvoice(subItemOptions, upcomingPreview, pendingInvoiceItems);
|
||||
var invoiceAmount = itemsForInvoice?.Sum(i => i.Amount) ?? 0;
|
||||
var invoiceNow = invoiceAmount >= prorateThreshold;
|
||||
if (invoiceNow)
|
||||
{
|
||||
await ProcessImmediateInvoiceAsync(subscriber, upcomingPreview, invoiceAmount, customer, itemsForInvoice, pendingInvoiceItems, paymentIntentClientSecret);
|
||||
}
|
||||
|
||||
return new InvoicePreviewResult { IsInvoicedNow = invoiceNow, PaymentIntentClientSecret = paymentIntentClientSecret };
|
||||
}
|
||||
|
||||
private async Task<InvoicePreviewResult> ProcessImmediateInvoiceAsync(ISubscriber subscriber, Invoice upcomingPreview, long invoiceAmount,
|
||||
Customer customer, IEnumerable<InvoiceLineItem> itemsForInvoice, PendingInoviceItems pendingInvoiceItems,
|
||||
string paymentIntentClientSecret)
|
||||
{
|
||||
// Owes more than prorateThreshold on the next invoice.
|
||||
// Invoice them and pay now instead of waiting until the next billing cycle.
|
||||
|
||||
string cardPaymentMethodId = null;
|
||||
var invoiceAmountDue = upcomingPreview.StartingBalance + invoiceAmount;
|
||||
cardPaymentMethodId = GetCardPaymentMethodId(invoiceAmountDue, customer, cardPaymentMethodId);
|
||||
|
||||
Stripe.Invoice invoice = null;
|
||||
var createdInvoiceItems = new List<Stripe.InvoiceItem>();
|
||||
Braintree.Transaction braintreeTransaction = null;
|
||||
|
||||
try
|
||||
{
|
||||
await CreateInvoiceItemsAsync(subscriber, itemsForInvoice, pendingInvoiceItems, createdInvoiceItems);
|
||||
|
||||
invoice = await CreateInvoiceAsync(subscriber, cardPaymentMethodId);
|
||||
|
||||
var invoicePayOptions = new Stripe.InvoicePayOptions();
|
||||
await CreateBrainTreeTransactionRequestAsync(subscriber, invoice, customer, invoicePayOptions,
|
||||
cardPaymentMethodId, braintreeTransaction);
|
||||
|
||||
await InvoicePayAsync(invoicePayOptions, invoice, paymentIntentClientSecret);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (braintreeTransaction != null)
|
||||
{
|
||||
await _btGateway.Transaction.RefundAsync(braintreeTransaction.Id);
|
||||
}
|
||||
|
||||
if (invoice != null)
|
||||
{
|
||||
if (invoice.Status == "paid")
|
||||
{
|
||||
// It's apparently paid, so we return without throwing an exception
|
||||
return new InvoicePreviewResult
|
||||
{
|
||||
IsInvoicedNow = false,
|
||||
PaymentIntentClientSecret = paymentIntentClientSecret
|
||||
};
|
||||
}
|
||||
|
||||
await RestoreInvoiceItemsAsync(invoice, customer, pendingInvoiceItems.PendingInvoiceItems);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var ii in createdInvoiceItems)
|
||||
{
|
||||
await _stripeAdapter.InvoiceDeleteAsync(ii.Id);
|
||||
}
|
||||
}
|
||||
|
||||
if (e is Stripe.StripeException strEx &&
|
||||
(strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false))
|
||||
{
|
||||
throw new GatewayException("Bank account is not yet verified.");
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
return new InvoicePreviewResult
|
||||
{
|
||||
IsInvoicedNow = false,
|
||||
PaymentIntentClientSecret = paymentIntentClientSecret
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<InvoiceLineItem> GetItemsForInvoice(List<InvoiceSubscriptionItemOptions> subItemOptions, Invoice upcomingPreview,
|
||||
PendingInoviceItems pendingInvoiceItems)
|
||||
{
|
||||
var itemsForInvoice = upcomingPreview.Lines?.Data?
|
||||
.Where(i => pendingInvoiceItems.PendingInvoiceItemsDict.ContainsKey(i.Id) ||
|
||||
(i.Plan.Id == subItemOptions[0]?.Plan && i.Proration));
|
||||
return itemsForInvoice;
|
||||
}
|
||||
|
||||
private PendingInoviceItems GetPendingInvoiceItems(ISubscriber subscriber)
|
||||
{
|
||||
var pendingInvoiceItems = new PendingInoviceItems();
|
||||
var invoiceItems = _stripeAdapter.InvoiceItemListAsync(new Stripe.InvoiceItemListOptions
|
||||
{
|
||||
Customer = subscriber.GatewayCustomerId
|
||||
}).ToList().Where(i => i.InvoiceId == null);
|
||||
pendingInvoiceItems.PendingInvoiceItemsDict = invoiceItems.ToDictionary(pii => pii.Id);
|
||||
return pendingInvoiceItems;
|
||||
}
|
||||
|
||||
private async Task<Customer> CheckInAppPurchaseMethod(ISubscriber subscriber)
|
||||
{
|
||||
var customerOptions = GetCustomerPaymentOptions();
|
||||
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, customerOptions);
|
||||
var usingInAppPaymentMethod = customer.Metadata.ContainsKey("appleReceipt");
|
||||
if (usingInAppPaymentMethod)
|
||||
{
|
||||
throw new BadRequestException("Cannot perform this action with in-app purchase payment method. " +
|
||||
"Contact support.");
|
||||
}
|
||||
|
||||
return customer;
|
||||
}
|
||||
|
||||
private string GetCardPaymentMethodId(long invoiceAmountDue, Customer customer, string cardPaymentMethodId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (invoiceAmountDue <= 0 || customer.Metadata.ContainsKey("btCustomerId")) return cardPaymentMethodId;
|
||||
var hasDefaultCardPaymentMethod = customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card";
|
||||
var hasDefaultValidSource = customer.DefaultSource != null &&
|
||||
(customer.DefaultSource is Stripe.Card ||
|
||||
customer.DefaultSource is Stripe.BankAccount);
|
||||
if (hasDefaultCardPaymentMethod || hasDefaultValidSource) return cardPaymentMethodId;
|
||||
cardPaymentMethodId = GetLatestCardPaymentMethod(customer.Id)?.Id;
|
||||
if (cardPaymentMethodId == null)
|
||||
{
|
||||
throw new BadRequestException("No payment method is available.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new BadRequestException("No payment method is available.");
|
||||
}
|
||||
|
||||
|
||||
return cardPaymentMethodId;
|
||||
}
|
||||
|
||||
private async Task<Invoice> GetUpcomingInvoiceAsync(ISubscriber subscriber, List<InvoiceSubscriptionItemOptions> subItemOptions)
|
||||
{
|
||||
var upcomingPreview = await _stripeAdapter.InvoiceUpcomingAsync(new Stripe.UpcomingInvoiceOptions
|
||||
{
|
||||
Customer = subscriber.GatewayCustomerId,
|
||||
Subscription = subscriber.GatewaySubscriptionId,
|
||||
SubscriptionItems = subItemOptions
|
||||
});
|
||||
return upcomingPreview;
|
||||
}
|
||||
|
||||
private async Task RestoreInvoiceItemsAsync(Invoice invoice, Customer customer, IEnumerable<InvoiceItem> pendingInvoiceItems)
|
||||
{
|
||||
invoice = await _stripeAdapter.InvoiceVoidInvoiceAsync(invoice.Id, new Stripe.InvoiceVoidOptions());
|
||||
if (invoice.StartingBalance != 0)
|
||||
{
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id,
|
||||
new Stripe.CustomerUpdateOptions { Balance = customer.Balance });
|
||||
}
|
||||
|
||||
// Restore invoice items that were brought in
|
||||
foreach (var item in pendingInvoiceItems)
|
||||
{
|
||||
var i = new Stripe.InvoiceItemCreateOptions
|
||||
{
|
||||
Currency = item.Currency,
|
||||
Description = item.Description,
|
||||
Customer = item.CustomerId,
|
||||
Subscription = item.SubscriptionId,
|
||||
Discountable = item.Discountable,
|
||||
Metadata = item.Metadata,
|
||||
Quantity = item.Proration ? 1 : item.Quantity,
|
||||
UnitAmount = item.UnitAmount
|
||||
};
|
||||
await _stripeAdapter.InvoiceItemCreateAsync(i);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InvoicePayAsync(InvoicePayOptions invoicePayOptions, Invoice invoice, string paymentIntentClientSecret)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _stripeAdapter.InvoicePayAsync(invoice.Id, invoicePayOptions);
|
||||
}
|
||||
catch (Stripe.StripeException e)
|
||||
{
|
||||
if (e.HttpStatusCode == System.Net.HttpStatusCode.PaymentRequired &&
|
||||
e.StripeError?.Code == "invoice_payment_intent_requires_action")
|
||||
{
|
||||
// SCA required, get intent client secret
|
||||
var invoiceGetOptions = new Stripe.InvoiceGetOptions();
|
||||
invoiceGetOptions.AddExpand("payment_intent");
|
||||
invoice = await _stripeAdapter.InvoiceGetAsync(invoice.Id, invoiceGetOptions);
|
||||
paymentIntentClientSecret = invoice?.PaymentIntent?.ClientSecret;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new GatewayException("Unable to pay invoice.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateBrainTreeTransactionRequestAsync(ISubscriber subscriber, Invoice invoice, Customer customer,
|
||||
InvoicePayOptions invoicePayOptions, string cardPaymentMethodId, Braintree.Transaction braintreeTransaction)
|
||||
{
|
||||
if (invoice.AmountDue > 0)
|
||||
{
|
||||
if (customer?.Metadata?.ContainsKey("btCustomerId") ?? false)
|
||||
{
|
||||
invoicePayOptions.PaidOutOfBand = true;
|
||||
var btInvoiceAmount = (invoice.AmountDue / 100M);
|
||||
var transactionResult = await _btGateway.Transaction.SaleAsync(
|
||||
new Braintree.TransactionRequest
|
||||
{
|
||||
Amount = btInvoiceAmount,
|
||||
CustomerId = customer.Metadata["btCustomerId"],
|
||||
Options = new Braintree.TransactionOptionsRequest
|
||||
{
|
||||
SubmitForSettlement = true,
|
||||
PayPal = new Braintree.TransactionOptionsPayPalRequest
|
||||
{
|
||||
CustomField = $"{subscriber.BraintreeIdField()}:{subscriber.Id}"
|
||||
}
|
||||
},
|
||||
CustomFields = new Dictionary<string, string>
|
||||
{
|
||||
[subscriber.BraintreeIdField()] = subscriber.Id.ToString()
|
||||
}
|
||||
});
|
||||
|
||||
if (!transactionResult.IsSuccess())
|
||||
{
|
||||
throw new GatewayException("Failed to charge PayPal customer.");
|
||||
}
|
||||
|
||||
braintreeTransaction = transactionResult.Target;
|
||||
await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new Stripe.InvoiceUpdateOptions
|
||||
{
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["btTransactionId"] = braintreeTransaction.Id,
|
||||
["btPayPalTransactionId"] =
|
||||
braintreeTransaction.PayPalDetails.AuthorizationId
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
invoicePayOptions.OffSession = true;
|
||||
invoicePayOptions.PaymentMethod = cardPaymentMethodId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Invoice> CreateInvoiceAsync(ISubscriber subscriber, string cardPaymentMethodId)
|
||||
{
|
||||
Invoice invoice;
|
||||
invoice = await _stripeAdapter.InvoiceCreateAsync(new Stripe.InvoiceCreateOptions
|
||||
{
|
||||
CollectionMethod = "send_invoice",
|
||||
DaysUntilDue = 1,
|
||||
Customer = subscriber.GatewayCustomerId,
|
||||
Subscription = subscriber.GatewaySubscriptionId,
|
||||
DefaultPaymentMethod = cardPaymentMethodId
|
||||
});
|
||||
return invoice;
|
||||
}
|
||||
|
||||
private async Task CreateInvoiceItemsAsync(ISubscriber subscriber, IEnumerable<InvoiceLineItem> itemsForInvoice,
|
||||
PendingInoviceItems pendingInvoiceItems, List<InvoiceItem> createdInvoiceItems)
|
||||
{
|
||||
foreach (var invoiceLineItem in itemsForInvoice)
|
||||
{
|
||||
if (pendingInvoiceItems.PendingInvoiceItemsDict.ContainsKey(invoiceLineItem.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var invoiceItem = await _stripeAdapter.InvoiceItemCreateAsync(new Stripe.InvoiceItemCreateOptions
|
||||
{
|
||||
Currency = invoiceLineItem.Currency,
|
||||
Description = invoiceLineItem.Description,
|
||||
Customer = subscriber.GatewayCustomerId,
|
||||
Subscription = invoiceLineItem.Subscription,
|
||||
Discountable = invoiceLineItem.Discountable,
|
||||
Amount = invoiceLineItem.Amount
|
||||
});
|
||||
createdInvoiceItems.Add(invoiceItem);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false,
|
||||
bool skipInAppPurchaseCheck = false)
|
||||
{
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.Utilities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -61,9 +64,8 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IStripeSyncService _stripeSyncService;
|
||||
private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository;
|
||||
private readonly IDataProtectorTokenFactory<WebAuthnLoginTokenable> _webAuthnLoginTokenizer;
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||
private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository;
|
||||
|
||||
public UserService(
|
||||
IUserRepository userRepository,
|
||||
@@ -96,8 +98,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IStripeSyncService stripeSyncService,
|
||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
|
||||
IWebAuthnCredentialRepository webAuthnRepository,
|
||||
IDataProtectorTokenFactory<WebAuthnLoginTokenable> webAuthnLoginTokenizer)
|
||||
IWebAuthnCredentialRepository webAuthnRepository)
|
||||
: base(
|
||||
store,
|
||||
optionsAccessor,
|
||||
@@ -136,7 +137,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
_stripeSyncService = stripeSyncService;
|
||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||
_webAuthnCredentialRepository = webAuthnRepository;
|
||||
_webAuthnLoginTokenizer = webAuthnLoginTokenizer;
|
||||
}
|
||||
|
||||
public Guid? GetProperUserId(ClaimsPrincipal principal)
|
||||
@@ -586,45 +586,33 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<AssertionOptions> StartWebAuthnLoginAssertionAsync(User user)
|
||||
public AssertionOptions StartWebAuthnLoginAssertion()
|
||||
{
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
|
||||
var existingKeys = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id);
|
||||
var existingCredentials = existingKeys
|
||||
.Select(k => new PublicKeyCredentialDescriptor(CoreHelpers.Base64UrlDecode(k.CredentialId)))
|
||||
.ToList();
|
||||
|
||||
if (existingCredentials.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: PRF?
|
||||
var exts = new AuthenticationExtensionsClientInputs
|
||||
{
|
||||
UserVerificationMethod = true
|
||||
};
|
||||
var options = _fido2.GetAssertionOptions(existingCredentials, UserVerificationRequirement.Required, exts);
|
||||
|
||||
// TODO: temp save options to user record somehow
|
||||
|
||||
return options;
|
||||
return _fido2.GetAssertionOptions(Enumerable.Empty<PublicKeyCredentialDescriptor>(), UserVerificationRequirement.Required);
|
||||
}
|
||||
|
||||
public async Task<string> CompleteWebAuthLoginAssertionAsync(AuthenticatorAssertionRawResponse assertionResponse, User user)
|
||||
public async Task<(User, WebAuthnCredential)> CompleteWebAuthLoginAssertionAsync(AssertionOptions options, AuthenticatorAssertionRawResponse assertionResponse)
|
||||
{
|
||||
// TODO: Get options from user record somehow, then clear them
|
||||
var options = AssertionOptions.FromJson("");
|
||||
|
||||
var userCredentials = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id);
|
||||
var assertionId = CoreHelpers.Base64UrlEncode(assertionResponse.Id);
|
||||
var credential = userCredentials.FirstOrDefault(c => c.CredentialId == assertionId);
|
||||
if (credential == null)
|
||||
if (!GuidUtilities.TryParseBytes(assertionResponse.Response.UserHandle, out var userId))
|
||||
{
|
||||
return null;
|
||||
throw new BadRequestException("Invalid credential.");
|
||||
}
|
||||
|
||||
// TODO: Callback to ensure credential ID is unique. Do we care? I don't think so.
|
||||
var user = await _userRepository.GetByIdAsync(userId);
|
||||
if (user == null)
|
||||
{
|
||||
throw new BadRequestException("Invalid credential.");
|
||||
}
|
||||
|
||||
var userCredentials = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id);
|
||||
var assertedCredentialId = CoreHelpers.Base64UrlEncode(assertionResponse.Id);
|
||||
var credential = userCredentials.FirstOrDefault(c => c.CredentialId == assertedCredentialId);
|
||||
if (credential == null)
|
||||
{
|
||||
throw new BadRequestException("Invalid credential.");
|
||||
}
|
||||
|
||||
// Always return true, since we've already filtered the credentials after user id
|
||||
IsUserHandleOwnerOfCredentialIdAsync callback = (args, cancellationToken) => Task.FromResult(true);
|
||||
var credentialPublicKey = CoreHelpers.Base64UrlDecode(credential.PublicKey);
|
||||
var assertionVerificationResult = await _fido2.MakeAssertionAsync(
|
||||
@@ -634,15 +622,12 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
credential.Counter = (int)assertionVerificationResult.Counter;
|
||||
await _webAuthnCredentialRepository.ReplaceAsync(credential);
|
||||
|
||||
if (assertionVerificationResult.Status == "ok")
|
||||
if (assertionVerificationResult.Status != "ok")
|
||||
{
|
||||
var token = _webAuthnLoginTokenizer.Protect(new WebAuthnLoginTokenable(user));
|
||||
return token;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
throw new BadRequestException("Invalid credential.");
|
||||
}
|
||||
|
||||
return (user, credential);
|
||||
}
|
||||
|
||||
public async Task SendEmailVerificationAsync(User user)
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
namespace Bit.Core.Tools.Entities;
|
||||
#nullable enable
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
|
||||
namespace Bit.Core.Tools.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// An entity that can be referenced by a <see cref="ReferenceEvent"/>.
|
||||
/// </summary>
|
||||
public interface IReferenceable
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies the entity that generated the event.
|
||||
/// </summary>
|
||||
Guid Id { get; set; }
|
||||
string ReferenceData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Contextual information included in the event.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Do not store secrets in this field.
|
||||
/// </remarks>
|
||||
string? ReferenceData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true" /> when the entity is a user.
|
||||
/// Otherwise returns <see langword="false" />.
|
||||
/// </summary>
|
||||
bool IsUser();
|
||||
}
|
||||
|
||||
@@ -1,29 +1,125 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
#nullable enable
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Tools.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// An end-to-end encrypted secret accessible to arbitrary
|
||||
/// entities through a fixed URI.
|
||||
/// </summary>
|
||||
public class Send : ITableObject<Guid>
|
||||
{
|
||||
/// <summary>
|
||||
/// Uniquely identifies this send.
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the user that created this send.
|
||||
/// </summary>
|
||||
public Guid? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the organization that created this send.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Not presently in-use by client applications.
|
||||
/// </remarks>
|
||||
public Guid? OrganizationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Describes the data being sent. This field determines how
|
||||
/// the <see cref="Data"/> field is interpreted.
|
||||
/// </summary>
|
||||
public SendType Type { get; set; }
|
||||
public string Data { get; set; }
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores data containing or pointing to the transmitted secret. JSON.
|
||||
/// </summary>
|
||||
/// <note>
|
||||
/// Must be nullable due to several database column configuration.
|
||||
/// The application and all other databases assume this is not nullable.
|
||||
/// Tech debt ticket: PM-4128
|
||||
/// </note>
|
||||
public string? Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores the data's encryption key. Encrypted.
|
||||
/// </summary>
|
||||
/// <note>
|
||||
/// Must be nullable due to MySql database column configuration.
|
||||
/// The application and all other databases assume this is not nullable.
|
||||
/// Tech debt ticket: PM-4128
|
||||
/// </note>
|
||||
public string? Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Password provided by the user. Protected with pbkdf2.
|
||||
/// </summary>
|
||||
[MaxLength(300)]
|
||||
public string Password { get; set; }
|
||||
public string? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The send becomes unavailable to API callers when
|
||||
/// <see cref="AccessCount"/> >= <see cref="MaxAccessCount"/>.
|
||||
/// </summary>
|
||||
public int? MaxAccessCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of times the content was accessed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This value is owned by the server. Clients cannot alter it.
|
||||
/// </remarks>
|
||||
public int AccessCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date this send was created.
|
||||
/// </summary>
|
||||
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// The date this send was last modified.
|
||||
/// </summary>
|
||||
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// The date this send becomes unavailable to API callers.
|
||||
/// </summary>
|
||||
public DateTime? ExpirationDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date this send will be unconditionally deleted.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is set by server-side when the user doesn't specify a deletion date.
|
||||
/// </remarks>
|
||||
public DateTime DeletionDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When this is true the send is not available to API callers,
|
||||
/// unless they're the creator.
|
||||
/// </summary>
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the creator's email address should be shown to the recipient.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="false"/> indicates the email may be shown.
|
||||
/// <see langword="true"/> indicates the email should be hidden.
|
||||
/// <see langword="null"/> indicates the client doesn't set the field and
|
||||
/// the email should be hidden.
|
||||
/// </value>
|
||||
public bool? HideEmail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Generates the send's <see cref="Id" />
|
||||
/// </summary>
|
||||
public void SetNewId()
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Text.Json.Serialization;
|
||||
#nullable enable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Tools.Entities;
|
||||
@@ -6,10 +8,23 @@ using Bit.Core.Tools.Enums;
|
||||
|
||||
namespace Bit.Core.Tools.Models.Business;
|
||||
|
||||
/// <summary>
|
||||
/// Product support monitoring.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Do not store secrets in this type.
|
||||
/// </remarks>
|
||||
public class ReferenceEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Instantiates a <see cref="ReferenceEvent"/>.
|
||||
/// </summary>
|
||||
public ReferenceEvent() { }
|
||||
|
||||
/// <inheritdoc cref="ReferenceEvent()" />
|
||||
/// <param name="type">Monitored event type.</param>
|
||||
/// <param name="source">Entity that created the event.</param>
|
||||
/// <param name="currentContext">The conditions in which the event occurred.</param>
|
||||
public ReferenceEvent(ReferenceEventType type, IReferenceable source, ICurrentContext currentContext)
|
||||
{
|
||||
Type = type;
|
||||
@@ -26,48 +41,197 @@ public class ReferenceEvent
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Monitored event type.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public ReferenceEventType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The kind of entity that created the event.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public ReferenceEventSource Source { get; set; }
|
||||
|
||||
/// <inheritdoc cref="IReferenceable.Id"/>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public string ReferenceData { get; set; }
|
||||
/// <inheritdoc cref="IReferenceable.ReferenceData"/>
|
||||
public string? ReferenceData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Moment the event occurred.
|
||||
/// </summary>
|
||||
public DateTime EventDate { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Number of users sent invitations by an organization.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Should contain a value only on <see cref="ReferenceEventType.InvitedUsers"/> events.
|
||||
/// Otherwise the value should be <see langword="null"/>.
|
||||
/// </value>
|
||||
public int? Users { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not a subscription was canceled immediately or at the end of the billing period.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true"/> when a cancellation occurs immediately.
|
||||
/// <see langword="false"/> when a cancellation occurs at the end of a customer's billing period.
|
||||
/// Should contain a value only on <see cref="ReferenceEventType.CancelSubscription"/> events.
|
||||
/// Otherwise the value should be <see langword="null"/>.
|
||||
/// </value>
|
||||
public bool? EndOfPeriod { get; set; }
|
||||
|
||||
public string PlanName { get; set; }
|
||||
/// <summary>
|
||||
/// Branded name of the subscription.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Should contain a value only for subscription management events.
|
||||
/// Otherwise the value should be <see langword="null"/>.
|
||||
/// </value>
|
||||
public string? PlanName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Identifies a subscription.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Should contain a value only for subscription management events.
|
||||
/// Otherwise the value should be <see langword="null"/>.
|
||||
/// </value>
|
||||
public PlanType? PlanType { get; set; }
|
||||
|
||||
public string OldPlanName { get; set; }
|
||||
/// <summary>
|
||||
/// The branded name of the prior plan.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Should contain a value only on <see cref="ReferenceEventType.UpgradePlan"/> events
|
||||
/// initiated by organizations.
|
||||
/// Otherwise the value should be <see langword="null"/>.
|
||||
/// </value>
|
||||
public string? OldPlanName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the prior plan
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Should contain a value only on <see cref="ReferenceEventType.UpgradePlan"/> events
|
||||
/// initiated by organizations.
|
||||
/// Otherwise the value should be <see langword="null"/>.
|
||||
/// </value>
|
||||
public PlanType? OldPlanType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Seat count when a billable action occurs. When adjusting seats, contains
|
||||
/// the new seat count.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Should contain a value only on <see cref="ReferenceEventType.Rebilled"/>,
|
||||
/// <see cref="ReferenceEventType.AdjustSeats"/>, <see cref="ReferenceEventType.UpgradePlan"/>,
|
||||
/// and <see cref="ReferenceEventType.Signup"/> events initiated by organizations.
|
||||
/// Otherwise the value should be <see langword="null"/>.
|
||||
/// </value>
|
||||
public int? Seats { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Seat count when a seat adjustment occurs.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Should contain a value only on <see cref="ReferenceEventType.AdjustSeats"/>
|
||||
/// events initiated by organizations.
|
||||
/// Otherwise the value should be <see langword="null"/>.
|
||||
/// </value>
|
||||
public int? PreviousSeats { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Qty in GB of storage. When adjusting storage, contains the adjusted
|
||||
/// storage qty. Otherwise contains the total storage quantity.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Should contain a value only on <see cref="ReferenceEventType.Rebilled"/>,
|
||||
/// <see cref="ReferenceEventType.AdjustStorage"/>, <see cref="ReferenceEventType.UpgradePlan"/>,
|
||||
/// and <see cref="ReferenceEventType.Signup"/> events.
|
||||
/// Otherwise the value should be <see langword="null"/>.
|
||||
/// </value>
|
||||
public short? Storage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of send created or accessed.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Should contain a value only on <see cref="ReferenceEventType.SendAccessed"/>
|
||||
/// and <see cref="ReferenceEventType.SendCreated"/> events.
|
||||
/// Otherwise the value should be <see langword="null"/>.
|
||||
/// </value>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public SendType? SendType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the send has private notes.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true"/> when the send has private notes, otherwise <see langword="false"/>.
|
||||
/// Should contain a value only on <see cref="ReferenceEventType.SendAccessed"/>
|
||||
/// and <see cref="ReferenceEventType.SendCreated"/> events.
|
||||
/// Otherwise the value should be <see langword="null"/>.
|
||||
/// </value>
|
||||
public bool? SendHasNotes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The send expires after its access count exceeds this value.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// This field only contains a value when the send has a max access count
|
||||
/// and <see cref="Type"/> is <see cref="ReferenceEventType.SendAccessed"/>
|
||||
/// or <see cref="ReferenceEventType.SendCreated"/> events.
|
||||
/// Otherwise, the value should be <see langword="null"/>.
|
||||
/// </value>
|
||||
public int? MaxAccessCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the created send has a password.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Should contain a value only on <see cref="ReferenceEventType.SendAccessed"/>
|
||||
/// and <see cref="ReferenceEventType.SendCreated"/> events.
|
||||
/// Otherwise the value should be <see langword="null"/>.
|
||||
/// </value>
|
||||
public bool? HasPassword { get; set; }
|
||||
|
||||
public string EventRaisedByUser { get; set; }
|
||||
/// <summary>
|
||||
/// The administrator that performed the action.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Should contain a value only on <see cref="ReferenceEventType.OrganizationCreatedByAdmin"/>
|
||||
/// and <see cref="ReferenceEventType.OrganizationEditedByAdmin"/> events.
|
||||
/// Otherwise the value should be <see langword="null"/>.
|
||||
/// </value>
|
||||
public string? EventRaisedByUser { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not an organization's trial period was started by a sales person.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Should contain a value only on <see cref="ReferenceEventType.OrganizationCreatedByAdmin"/>
|
||||
/// and <see cref="ReferenceEventType.OrganizationEditedByAdmin"/> events.
|
||||
/// Otherwise the value should be <see langword="null"/>.
|
||||
/// </value>
|
||||
public bool? SalesAssistedTrialStarted { get; set; }
|
||||
|
||||
public string ClientId { get; set; }
|
||||
public Version ClientVersion { get; set; }
|
||||
/// <summary>
|
||||
/// The installation id of the application that originated the event.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="null"/> when the event was not originated by an application.
|
||||
/// </value>
|
||||
public string? ClientId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The version of the client application that originated the event.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="null"/> when the event was not originated by an application.
|
||||
/// </value>
|
||||
public Version? ClientVersion { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,15 +1,33 @@
|
||||
namespace Bit.Core.Tools.Models.Data;
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Tools.Models.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Shared data for a send
|
||||
/// </summary>
|
||||
public abstract class SendData
|
||||
{
|
||||
/// <summary>
|
||||
/// Instantiates a <see cref="SendData"/>.
|
||||
/// </summary>
|
||||
public SendData() { }
|
||||
|
||||
public SendData(string name, string notes)
|
||||
/// <inheritdoc cref="SendData()" />
|
||||
/// <param name="name">User-provided name of the send.</param>
|
||||
/// <param name="notes">User-provided private notes of the send.</param>
|
||||
public SendData(string name, string? notes)
|
||||
{
|
||||
Name = name;
|
||||
Notes = notes;
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
public string Notes { get; set; }
|
||||
/// <summary>
|
||||
/// User-provided name of the send.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User-provided private notes of the send.
|
||||
/// </summary>
|
||||
public string? Notes { get; set; } = null;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,64 @@
|
||||
using System.Text.Json.Serialization;
|
||||
#nullable enable
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
using static System.Text.Json.Serialization.JsonNumberHandling;
|
||||
|
||||
namespace Bit.Core.Tools.Models.Data;
|
||||
|
||||
/// <summary>
|
||||
/// A file secret being sent.
|
||||
/// </summary>
|
||||
public class SendFileData : SendData
|
||||
{
|
||||
/// <summary>
|
||||
/// Instantiates a <see cref="SendFileData"/>.
|
||||
/// </summary>
|
||||
public SendFileData() { }
|
||||
|
||||
public SendFileData(string name, string notes, string fileName)
|
||||
/// <inheritdoc cref="SendFileData()"/>
|
||||
/// <param name="name">Attached file name.</param>
|
||||
/// <param name="notes">User-provided private notes of the send.</param>
|
||||
/// <param name="fileName">Attached file name.</param>
|
||||
public SendFileData(string name, string? notes, string fileName)
|
||||
: base(name, notes)
|
||||
{
|
||||
FileName = fileName;
|
||||
}
|
||||
|
||||
// We serialize Size as a string since JSON (or Javascript) doesn't support full precision for long numbers
|
||||
[JsonNumberHandling(JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString)]
|
||||
/// <summary>
|
||||
/// Size of the attached file in bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Serialized as a string since JSON (or Javascript) doesn't support
|
||||
/// full precision for long numbers
|
||||
/// </remarks>
|
||||
[JsonNumberHandling(WriteAsString | AllowReadingFromString)]
|
||||
public long Size { get; set; }
|
||||
|
||||
public string Id { get; set; }
|
||||
public string FileName { get; set; }
|
||||
/// <summary>
|
||||
/// Uniquely identifies an uploaded file.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Should contain <see langword="null" /> only when a file
|
||||
/// upload is pending. Should never contain null once the
|
||||
/// file upload completes.
|
||||
/// </value>
|
||||
[DisallowNull]
|
||||
public string? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Attached file name.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Should contain a non-empty string once the file upload completes.
|
||||
/// </value>
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// When true the uploaded file's length was confirmed within
|
||||
/// the expected tolerance and below the maximum supported
|
||||
/// file size.
|
||||
/// </summary>
|
||||
public bool Validated { get; set; } = true;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,42 @@
|
||||
namespace Bit.Core.Tools.Models.Data;
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Tools.Models.Data;
|
||||
|
||||
/// <summary>
|
||||
/// A text secret being sent.
|
||||
/// </summary>
|
||||
public class SendTextData : SendData
|
||||
{
|
||||
/// <summary>
|
||||
/// Instantiates a <see cref="SendTextData"/>.
|
||||
/// </summary>
|
||||
public SendTextData() { }
|
||||
|
||||
public SendTextData(string name, string notes, string text, bool hidden)
|
||||
/// <inheritdoc cref="SendTextData()"/>
|
||||
/// <param name="name">Attached file name.</param>
|
||||
/// <param name="notes">User-provided private notes of the send.</param>
|
||||
/// <param name="text">The secret being sent.</param>
|
||||
/// <param name="hidden">
|
||||
/// Indicates whether the secret should be concealed when opening the send.
|
||||
/// </param>
|
||||
public SendTextData(string name, string? notes, string? text, bool hidden)
|
||||
: base(name, notes)
|
||||
{
|
||||
Text = text;
|
||||
Hidden = hidden;
|
||||
}
|
||||
|
||||
public string Text { get; set; }
|
||||
/// <summary>
|
||||
/// The secret being sent.
|
||||
/// </summary>
|
||||
public string? Text { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the secret should be concealed when opening the send.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true" /> when the secret should be concealed.
|
||||
/// Otherwise <see langword="false" />.
|
||||
/// </value>
|
||||
public bool Hidden { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,10 +1,36 @@
|
||||
using Bit.Core.Repositories;
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Tools.Entities;
|
||||
|
||||
namespace Bit.Core.Tools.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Service for saving and loading <see cref="Send"/>s in persistent storage.
|
||||
/// </summary>
|
||||
public interface ISendRepository : IRepository<Send, Guid>
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads all <see cref="Send"/>s created by a user.
|
||||
/// </summary>
|
||||
/// <param name="userId">
|
||||
/// Identifies the user.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// A task that completes once the <see cref="Send"/>s have been loaded.
|
||||
/// The task's result contains the loaded <see cref="Send"/>s.
|
||||
/// </returns>
|
||||
Task<ICollection<Send>> GetManyByUserIdAsync(Guid userId);
|
||||
|
||||
/// <summary>
|
||||
/// Loads <see cref="Send"/>s scheduled for deletion.
|
||||
/// </summary>
|
||||
/// <param name="deletionDateBefore">
|
||||
/// Load sends whose <see cref="Send.DeletionDate" /> is < this date.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// A task that completes once the <see cref="Send"/>s have been loaded.
|
||||
/// The task's result contains the loaded <see cref="Send"/>s.
|
||||
/// </returns>
|
||||
Task<ICollection<Send>> GetManyByDeletionDateAsync(DateTime deletionDateBefore);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
@@ -12,6 +12,7 @@ public class CipherLoginFido2CredentialData
|
||||
public string RpId { get; set; }
|
||||
public string RpName { get; set; }
|
||||
public string UserHandle { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public string UserDisplayName { get; set; }
|
||||
public string Counter { get; set; }
|
||||
public string Discoverable { get; set; }
|
||||
|
||||
@@ -32,7 +32,7 @@ public interface ICipherRepository : IRepository<Cipher, Guid>
|
||||
Task UpdateCiphersAsync(Guid userId, IEnumerable<Cipher> ciphers);
|
||||
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders);
|
||||
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
|
||||
IEnumerable<CollectionCipher> collectionCiphers);
|
||||
IEnumerable<CollectionCipher> collectionCiphers, IEnumerable<CollectionUser> collectionUsers);
|
||||
Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId);
|
||||
Task SoftDeleteByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId);
|
||||
Task<DateTime> RestoreAsync(IEnumerable<Guid> ids, Guid userId);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -25,6 +27,7 @@ public class CipherService : ICipherService
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly ICollectionCipherRepository _collectionCipherRepository;
|
||||
private readonly IPushNotificationService _pushService;
|
||||
private readonly IAttachmentStorageService _attachmentStorageService;
|
||||
@@ -32,7 +35,7 @@ public class CipherService : ICipherService
|
||||
private readonly IUserService _userService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private const long _fileSizeLeeway = 1024L * 1024L; // 1MB
|
||||
private const long _fileSizeLeeway = 1024L * 1024L; // 1MB
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
|
||||
@@ -42,6 +45,7 @@ public class CipherService : ICipherService
|
||||
ICollectionRepository collectionRepository,
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ICollectionCipherRepository collectionCipherRepository,
|
||||
IPushNotificationService pushService,
|
||||
IAttachmentStorageService attachmentStorageService,
|
||||
@@ -57,6 +61,7 @@ public class CipherService : ICipherService
|
||||
_collectionRepository = collectionRepository;
|
||||
_userRepository = userRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_collectionCipherRepository = collectionCipherRepository;
|
||||
_pushService = pushService;
|
||||
_attachmentStorageService = attachmentStorageService;
|
||||
@@ -650,7 +655,7 @@ public class CipherService : ICipherService
|
||||
|
||||
cipher.RevisionDate = DateTime.UtcNow;
|
||||
|
||||
// The sprocs will validate that all collections belong to this org/user and that they have
|
||||
// The sprocs will validate that all collections belong to this org/user and that they have
|
||||
// proper write permissions.
|
||||
if (orgAdmin)
|
||||
{
|
||||
@@ -745,6 +750,7 @@ public class CipherService : ICipherService
|
||||
var org = collections.Count > 0 ?
|
||||
await _organizationRepository.GetByIdAsync(collections[0].OrganizationId) :
|
||||
await _organizationRepository.GetByIdAsync(ciphers.FirstOrDefault(c => c.OrganizationId.HasValue).OrganizationId.Value);
|
||||
var importingOrgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, importingUserId);
|
||||
|
||||
if (collections.Count > 0 && org != null && org.MaxCollections.HasValue)
|
||||
{
|
||||
@@ -762,18 +768,25 @@ public class CipherService : ICipherService
|
||||
cipher.SetNewId();
|
||||
}
|
||||
|
||||
var userCollectionsIds = (await _collectionRepository.GetManyByOrganizationIdAsync(org.Id)).Select(c => c.Id).ToList();
|
||||
var organizationCollectionsIds = (await _collectionRepository.GetManyByOrganizationIdAsync(org.Id)).Select(c => c.Id).ToList();
|
||||
|
||||
//Assign id to the ones that don't exist in DB
|
||||
//Need to keep the list order to create the relationships
|
||||
List<Collection> newCollections = new List<Collection>();
|
||||
var newCollections = new List<Collection>();
|
||||
var newCollectionUsers = new List<CollectionUser>();
|
||||
|
||||
foreach (var collection in collections)
|
||||
{
|
||||
if (!userCollectionsIds.Contains(collection.Id))
|
||||
if (!organizationCollectionsIds.Contains(collection.Id))
|
||||
{
|
||||
collection.SetNewId();
|
||||
newCollections.Add(collection);
|
||||
newCollectionUsers.Add(new CollectionUser
|
||||
{
|
||||
CollectionId = collection.Id,
|
||||
OrganizationUserId = importingOrgUser.Id,
|
||||
Manage = true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,7 +810,7 @@ public class CipherService : ICipherService
|
||||
}
|
||||
|
||||
// Create it all
|
||||
await _cipherRepository.CreateAsync(ciphers, newCollections, collectionCiphers);
|
||||
await _cipherRepository.CreateAsync(ciphers, newCollections, collectionCiphers, newCollectionUsers);
|
||||
|
||||
// push
|
||||
await _pushService.PushSyncVaultAsync(importingUserId);
|
||||
|
||||
@@ -131,6 +131,16 @@
|
||||
"Microsoft.Win32.Registry": "5.0.0"
|
||||
}
|
||||
},
|
||||
"Duende.IdentityServer": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.0.4, )",
|
||||
"resolved": "6.0.4",
|
||||
"contentHash": "4HVjzx1F8v5J+U7oa8RGAQGj2QzmzNSu87r18Sh+dlh10uyZZL8teAaT/FaVLDObnfItGdPFvN8mwpF/HkI3Xw==",
|
||||
"dependencies": {
|
||||
"Duende.IdentityServer.Storage": "6.0.4",
|
||||
"Microsoft.AspNetCore.Authentication.OpenIdConnect": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Fido2.AspNet": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.0.1, )",
|
||||
@@ -150,29 +160,6 @@
|
||||
"Microsoft.CSharp": "4.7.0"
|
||||
}
|
||||
},
|
||||
"IdentityServer4": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.1.2, )",
|
||||
"resolved": "4.1.2",
|
||||
"contentHash": "blaxxGuOA7v/w1q+fxn97wZ+x2ecG1ZD4mc/N/ZOXMNeFZZhqv+4LF26Gecyik3nWrJPmbMEtQbLmRsKG8k61w==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "4.4.0",
|
||||
"IdentityServer4.Storage": "4.1.2",
|
||||
"Microsoft.AspNetCore.Authentication.OpenIdConnect": "3.1.0",
|
||||
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "5.6.0",
|
||||
"Newtonsoft.Json": "12.0.2"
|
||||
}
|
||||
},
|
||||
"IdentityServer4.AccessTokenValidation": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.0.1, )",
|
||||
"resolved": "3.0.1",
|
||||
"contentHash": "qu/M6UyN4o9NVep7q545Ms7hYAnsQqSdLbN1Fjjrn4m35lyBfeQPSSNzDryAKHbodyWOQfHaOqKEyMEJQ5Rpgw==",
|
||||
"dependencies": {
|
||||
"IdentityModel.AspNetCore.OAuth2Introspection": "4.0.1",
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer": "3.0.0"
|
||||
}
|
||||
},
|
||||
"LaunchDarkly.ServerSdk": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
@@ -465,6 +452,15 @@
|
||||
"resolved": "2.2.1",
|
||||
"contentHash": "A6Zr52zVqJKt18ZBsTnX0qhG0kwIQftVAjLmszmkiR/trSp8H+xj1gUOzk7XHwaKgyREMSV1v9XaKrBUeIOdvQ=="
|
||||
},
|
||||
"Duende.IdentityServer.Storage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.4",
|
||||
"contentHash": "s5gAjfbpr2IMgI+fU2Nx+2AZdzstmbt9gpo13iX7GwvqSeSaBVqj9ZskAN0R2KF1OemPdZuGnfaTcevdXMUrrw==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "6.0.0",
|
||||
"Microsoft.AspNetCore.DataProtection.Abstractions": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Fido2": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.1",
|
||||
@@ -484,28 +480,8 @@
|
||||
},
|
||||
"IdentityModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.4.0",
|
||||
"contentHash": "b18wrIx5wnZlMxAX7oVsE+nDtAJ4hajYlH0xPlaRvo4r/fz08K6pPeZvbiqS9nfNbzfIgLFmNX+FL9qR9ZR5PA==",
|
||||
"dependencies": {
|
||||
"Newtonsoft.Json": "11.0.2",
|
||||
"System.Text.Encodings.Web": "4.7.0"
|
||||
}
|
||||
},
|
||||
"IdentityModel.AspNetCore.OAuth2Introspection": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.0.1",
|
||||
"contentHash": "ZNdMZMaj9fqR3j50vYsu+1U3QGd6n8+fqwf+a8mCTcmXGor+HgFDfdq0mM34bsmD6uEgAQup7sv2ZW5kR36dbA==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "4.0.0"
|
||||
}
|
||||
},
|
||||
"IdentityServer4.Storage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.1.2",
|
||||
"contentHash": "KoSffyZyyeCNTIyJiZnCuPakJ1QbCHlpty6gbWUj/7yl+w0PXIchgmmJnJSvddzBb8iZ2xew/vGlxWUIP17P2g==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "4.4.0"
|
||||
}
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "eVHCR7a6m/dm5RFcBzE3qs/Jg5j9R5Rjpu8aTOv9e4AFvaQtBXb5ah7kmwU+YwA0ufRwz4wf1hnIvsD2hSnI4g=="
|
||||
},
|
||||
"LaunchDarkly.Cache": {
|
||||
"type": "Transitive",
|
||||
@@ -554,10 +530,10 @@
|
||||
},
|
||||
"Microsoft.AspNetCore.Authentication.OpenIdConnect": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.0",
|
||||
"contentHash": "O1cAQYUTU8EfRqwc5/rfTns4E4hKlFlg59fuKRrST+PzsxI6H07KqRN/JjdYhAuVYxF8jPnIGbj+zuc5paOWUw==",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "cJxdro36spFzk/K2OFCddM6vZ+yoj6ug8mTFRH3Gdv1Pul/buSuCtfb/FSCp31UmS5S4C1315dU7wX3ErLFuDg==",
|
||||
"dependencies": {
|
||||
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "5.5.0"
|
||||
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.10.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.AspNetCore.Cryptography.Internal": {
|
||||
@@ -590,8 +566,8 @@
|
||||
},
|
||||
"Microsoft.AspNetCore.DataProtection.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.32",
|
||||
"contentHash": "MPL4iVyiaRxnOUY5VATHjvhDWaAEFb77KFiUxVRklv3Z3v+STofUr1UG/aCt1O9cgN7FVTDaC5A7U+zsLub8Xg=="
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "Z/UU4NEBm5UgNufJmw+j5baW26ytCOIZ0G7sZocPaOzsUeBon1bkM3lSMNZQG2GmDjAIVP2XMSODf2jzSGbibw=="
|
||||
},
|
||||
"Microsoft.Azure.Amqp": {
|
||||
"type": "Transitive",
|
||||
|
||||
@@ -16,8 +16,8 @@ public class Program
|
||||
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
||||
{
|
||||
var context = e.Properties["SourceContext"].ToString();
|
||||
if (context.Contains("IdentityServer4.Validation.TokenValidator") ||
|
||||
context.Contains("IdentityServer4.Validation.TokenRequestValidator"))
|
||||
if (context.Contains("Duende.IdentityServer.Validation.TokenValidator") ||
|
||||
context.Contains("Duende.IdentityServer.Validation.TokenRequestValidator"))
|
||||
{
|
||||
return e.Level >= globalSettings.MinLogLevel.EventsSettings.IdentityToken;
|
||||
}
|
||||
|
||||
@@ -184,6 +184,24 @@
|
||||
"Microsoft.Win32.Registry": "5.0.0"
|
||||
}
|
||||
},
|
||||
"Duende.IdentityServer": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.4",
|
||||
"contentHash": "4HVjzx1F8v5J+U7oa8RGAQGj2QzmzNSu87r18Sh+dlh10uyZZL8teAaT/FaVLDObnfItGdPFvN8mwpF/HkI3Xw==",
|
||||
"dependencies": {
|
||||
"Duende.IdentityServer.Storage": "6.0.4",
|
||||
"Microsoft.AspNetCore.Authentication.OpenIdConnect": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Duende.IdentityServer.Storage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.4",
|
||||
"contentHash": "s5gAjfbpr2IMgI+fU2Nx+2AZdzstmbt9gpo13iX7GwvqSeSaBVqj9ZskAN0R2KF1OemPdZuGnfaTcevdXMUrrw==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "6.0.0",
|
||||
"Microsoft.AspNetCore.DataProtection.Abstractions": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Fido2": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.1",
|
||||
@@ -220,49 +238,8 @@
|
||||
},
|
||||
"IdentityModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.4.0",
|
||||
"contentHash": "b18wrIx5wnZlMxAX7oVsE+nDtAJ4hajYlH0xPlaRvo4r/fz08K6pPeZvbiqS9nfNbzfIgLFmNX+FL9qR9ZR5PA==",
|
||||
"dependencies": {
|
||||
"Newtonsoft.Json": "11.0.2",
|
||||
"System.Text.Encodings.Web": "4.7.0"
|
||||
}
|
||||
},
|
||||
"IdentityModel.AspNetCore.OAuth2Introspection": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.0.1",
|
||||
"contentHash": "ZNdMZMaj9fqR3j50vYsu+1U3QGd6n8+fqwf+a8mCTcmXGor+HgFDfdq0mM34bsmD6uEgAQup7sv2ZW5kR36dbA==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "4.0.0"
|
||||
}
|
||||
},
|
||||
"IdentityServer4": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.1.2",
|
||||
"contentHash": "blaxxGuOA7v/w1q+fxn97wZ+x2ecG1ZD4mc/N/ZOXMNeFZZhqv+4LF26Gecyik3nWrJPmbMEtQbLmRsKG8k61w==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "4.4.0",
|
||||
"IdentityServer4.Storage": "4.1.2",
|
||||
"Microsoft.AspNetCore.Authentication.OpenIdConnect": "3.1.0",
|
||||
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "5.6.0",
|
||||
"Newtonsoft.Json": "12.0.2"
|
||||
}
|
||||
},
|
||||
"IdentityServer4.AccessTokenValidation": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.1",
|
||||
"contentHash": "qu/M6UyN4o9NVep7q545Ms7hYAnsQqSdLbN1Fjjrn4m35lyBfeQPSSNzDryAKHbodyWOQfHaOqKEyMEJQ5Rpgw==",
|
||||
"dependencies": {
|
||||
"IdentityModel.AspNetCore.OAuth2Introspection": "4.0.1",
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer": "3.0.0"
|
||||
}
|
||||
},
|
||||
"IdentityServer4.Storage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.1.2",
|
||||
"contentHash": "KoSffyZyyeCNTIyJiZnCuPakJ1QbCHlpty6gbWUj/7yl+w0PXIchgmmJnJSvddzBb8iZ2xew/vGlxWUIP17P2g==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "4.4.0"
|
||||
}
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "eVHCR7a6m/dm5RFcBzE3qs/Jg5j9R5Rjpu8aTOv9e4AFvaQtBXb5ah7kmwU+YwA0ufRwz4wf1hnIvsD2hSnI4g=="
|
||||
},
|
||||
"LaunchDarkly.Cache": {
|
||||
"type": "Transitive",
|
||||
@@ -354,10 +331,10 @@
|
||||
},
|
||||
"Microsoft.AspNetCore.Authentication.OpenIdConnect": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.0",
|
||||
"contentHash": "O1cAQYUTU8EfRqwc5/rfTns4E4hKlFlg59fuKRrST+PzsxI6H07KqRN/JjdYhAuVYxF8jPnIGbj+zuc5paOWUw==",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "cJxdro36spFzk/K2OFCddM6vZ+yoj6ug8mTFRH3Gdv1Pul/buSuCtfb/FSCp31UmS5S4C1315dU7wX3ErLFuDg==",
|
||||
"dependencies": {
|
||||
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "5.5.0"
|
||||
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.10.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.AspNetCore.Cryptography.Internal": {
|
||||
@@ -390,8 +367,8 @@
|
||||
},
|
||||
"Microsoft.AspNetCore.DataProtection.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.32",
|
||||
"contentHash": "MPL4iVyiaRxnOUY5VATHjvhDWaAEFb77KFiUxVRklv3Z3v+STofUr1UG/aCt1O9cgN7FVTDaC5A7U+zsLub8Xg=="
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "Z/UU4NEBm5UgNufJmw+j5baW26ytCOIZ0G7sZocPaOzsUeBon1bkM3lSMNZQG2GmDjAIVP2XMSODf2jzSGbibw=="
|
||||
},
|
||||
"Microsoft.Azure.Amqp": {
|
||||
"type": "Transitive",
|
||||
@@ -2617,10 +2594,9 @@
|
||||
"BitPay.Light": "[1.0.1907, )",
|
||||
"Braintree": "[5.19.0, )",
|
||||
"DnsClient": "[1.7.0, )",
|
||||
"Duende.IdentityServer": "[6.0.4, )",
|
||||
"Fido2.AspNet": "[3.0.1, )",
|
||||
"Handlebars.Net": "[2.1.2, )",
|
||||
"IdentityServer4": "[4.1.2, )",
|
||||
"IdentityServer4.AccessTokenValidation": "[3.0.1, )",
|
||||
"LaunchDarkly.ServerSdk": "[8.0.0, )",
|
||||
"MailKit": "[4.2.0, )",
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer": "[6.0.4, )",
|
||||
@@ -2648,7 +2624,7 @@
|
||||
"infrastructure.dapper": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Core": "[2023.10.2, )",
|
||||
"Core": "[2023.10.3, )",
|
||||
"Dapper": "[2.0.123, )"
|
||||
}
|
||||
},
|
||||
@@ -2656,7 +2632,7 @@
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[12.0.1, )",
|
||||
"Core": "[2023.10.2, )",
|
||||
"Core": "[2023.10.3, )",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[7.0.5, )",
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": "[7.0.5, )",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite": "[7.0.5, )",
|
||||
@@ -2668,9 +2644,9 @@
|
||||
"sharedweb": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Core": "[2023.10.2, )",
|
||||
"Infrastructure.Dapper": "[2023.10.2, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.2, )"
|
||||
"Core": "[2023.10.3, )",
|
||||
"Infrastructure.Dapper": "[2023.10.3, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.3, )"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +184,24 @@
|
||||
"Microsoft.Win32.Registry": "5.0.0"
|
||||
}
|
||||
},
|
||||
"Duende.IdentityServer": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.4",
|
||||
"contentHash": "4HVjzx1F8v5J+U7oa8RGAQGj2QzmzNSu87r18Sh+dlh10uyZZL8teAaT/FaVLDObnfItGdPFvN8mwpF/HkI3Xw==",
|
||||
"dependencies": {
|
||||
"Duende.IdentityServer.Storage": "6.0.4",
|
||||
"Microsoft.AspNetCore.Authentication.OpenIdConnect": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Duende.IdentityServer.Storage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.4",
|
||||
"contentHash": "s5gAjfbpr2IMgI+fU2Nx+2AZdzstmbt9gpo13iX7GwvqSeSaBVqj9ZskAN0R2KF1OemPdZuGnfaTcevdXMUrrw==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "6.0.0",
|
||||
"Microsoft.AspNetCore.DataProtection.Abstractions": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Fido2": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.1",
|
||||
@@ -220,49 +238,8 @@
|
||||
},
|
||||
"IdentityModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.4.0",
|
||||
"contentHash": "b18wrIx5wnZlMxAX7oVsE+nDtAJ4hajYlH0xPlaRvo4r/fz08K6pPeZvbiqS9nfNbzfIgLFmNX+FL9qR9ZR5PA==",
|
||||
"dependencies": {
|
||||
"Newtonsoft.Json": "11.0.2",
|
||||
"System.Text.Encodings.Web": "4.7.0"
|
||||
}
|
||||
},
|
||||
"IdentityModel.AspNetCore.OAuth2Introspection": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.0.1",
|
||||
"contentHash": "ZNdMZMaj9fqR3j50vYsu+1U3QGd6n8+fqwf+a8mCTcmXGor+HgFDfdq0mM34bsmD6uEgAQup7sv2ZW5kR36dbA==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "4.0.0"
|
||||
}
|
||||
},
|
||||
"IdentityServer4": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.1.2",
|
||||
"contentHash": "blaxxGuOA7v/w1q+fxn97wZ+x2ecG1ZD4mc/N/ZOXMNeFZZhqv+4LF26Gecyik3nWrJPmbMEtQbLmRsKG8k61w==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "4.4.0",
|
||||
"IdentityServer4.Storage": "4.1.2",
|
||||
"Microsoft.AspNetCore.Authentication.OpenIdConnect": "3.1.0",
|
||||
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "5.6.0",
|
||||
"Newtonsoft.Json": "12.0.2"
|
||||
}
|
||||
},
|
||||
"IdentityServer4.AccessTokenValidation": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.1",
|
||||
"contentHash": "qu/M6UyN4o9NVep7q545Ms7hYAnsQqSdLbN1Fjjrn4m35lyBfeQPSSNzDryAKHbodyWOQfHaOqKEyMEJQ5Rpgw==",
|
||||
"dependencies": {
|
||||
"IdentityModel.AspNetCore.OAuth2Introspection": "4.0.1",
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer": "3.0.0"
|
||||
}
|
||||
},
|
||||
"IdentityServer4.Storage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.1.2",
|
||||
"contentHash": "KoSffyZyyeCNTIyJiZnCuPakJ1QbCHlpty6gbWUj/7yl+w0PXIchgmmJnJSvddzBb8iZ2xew/vGlxWUIP17P2g==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "4.4.0"
|
||||
}
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "eVHCR7a6m/dm5RFcBzE3qs/Jg5j9R5Rjpu8aTOv9e4AFvaQtBXb5ah7kmwU+YwA0ufRwz4wf1hnIvsD2hSnI4g=="
|
||||
},
|
||||
"LaunchDarkly.Cache": {
|
||||
"type": "Transitive",
|
||||
@@ -354,10 +331,10 @@
|
||||
},
|
||||
"Microsoft.AspNetCore.Authentication.OpenIdConnect": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.0",
|
||||
"contentHash": "O1cAQYUTU8EfRqwc5/rfTns4E4hKlFlg59fuKRrST+PzsxI6H07KqRN/JjdYhAuVYxF8jPnIGbj+zuc5paOWUw==",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "cJxdro36spFzk/K2OFCddM6vZ+yoj6ug8mTFRH3Gdv1Pul/buSuCtfb/FSCp31UmS5S4C1315dU7wX3ErLFuDg==",
|
||||
"dependencies": {
|
||||
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "5.5.0"
|
||||
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.10.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.AspNetCore.Cryptography.Internal": {
|
||||
@@ -390,8 +367,8 @@
|
||||
},
|
||||
"Microsoft.AspNetCore.DataProtection.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.32",
|
||||
"contentHash": "MPL4iVyiaRxnOUY5VATHjvhDWaAEFb77KFiUxVRklv3Z3v+STofUr1UG/aCt1O9cgN7FVTDaC5A7U+zsLub8Xg=="
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "Z/UU4NEBm5UgNufJmw+j5baW26ytCOIZ0G7sZocPaOzsUeBon1bkM3lSMNZQG2GmDjAIVP2XMSODf2jzSGbibw=="
|
||||
},
|
||||
"Microsoft.Azure.Amqp": {
|
||||
"type": "Transitive",
|
||||
@@ -2617,10 +2594,9 @@
|
||||
"BitPay.Light": "[1.0.1907, )",
|
||||
"Braintree": "[5.19.0, )",
|
||||
"DnsClient": "[1.7.0, )",
|
||||
"Duende.IdentityServer": "[6.0.4, )",
|
||||
"Fido2.AspNet": "[3.0.1, )",
|
||||
"Handlebars.Net": "[2.1.2, )",
|
||||
"IdentityServer4": "[4.1.2, )",
|
||||
"IdentityServer4.AccessTokenValidation": "[3.0.1, )",
|
||||
"LaunchDarkly.ServerSdk": "[8.0.0, )",
|
||||
"MailKit": "[4.2.0, )",
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer": "[6.0.4, )",
|
||||
@@ -2648,7 +2624,7 @@
|
||||
"infrastructure.dapper": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Core": "[2023.10.2, )",
|
||||
"Core": "[2023.10.3, )",
|
||||
"Dapper": "[2.0.123, )"
|
||||
}
|
||||
},
|
||||
@@ -2656,7 +2632,7 @@
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[12.0.1, )",
|
||||
"Core": "[2023.10.2, )",
|
||||
"Core": "[2023.10.3, )",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[7.0.5, )",
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": "[7.0.5, )",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite": "[7.0.5, )",
|
||||
@@ -2668,9 +2644,9 @@
|
||||
"sharedweb": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Core": "[2023.10.2, )",
|
||||
"Infrastructure.Dapper": "[2023.10.2, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.2, )"
|
||||
"Core": "[2023.10.3, )",
|
||||
"Infrastructure.Dapper": "[2023.10.3, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.3, )"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,6 +193,24 @@
|
||||
"Microsoft.Win32.Registry": "5.0.0"
|
||||
}
|
||||
},
|
||||
"Duende.IdentityServer": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.4",
|
||||
"contentHash": "4HVjzx1F8v5J+U7oa8RGAQGj2QzmzNSu87r18Sh+dlh10uyZZL8teAaT/FaVLDObnfItGdPFvN8mwpF/HkI3Xw==",
|
||||
"dependencies": {
|
||||
"Duende.IdentityServer.Storage": "6.0.4",
|
||||
"Microsoft.AspNetCore.Authentication.OpenIdConnect": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Duende.IdentityServer.Storage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.4",
|
||||
"contentHash": "s5gAjfbpr2IMgI+fU2Nx+2AZdzstmbt9gpo13iX7GwvqSeSaBVqj9ZskAN0R2KF1OemPdZuGnfaTcevdXMUrrw==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "6.0.0",
|
||||
"Microsoft.AspNetCore.DataProtection.Abstractions": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Fido2": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.1",
|
||||
@@ -229,49 +247,8 @@
|
||||
},
|
||||
"IdentityModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.4.0",
|
||||
"contentHash": "b18wrIx5wnZlMxAX7oVsE+nDtAJ4hajYlH0xPlaRvo4r/fz08K6pPeZvbiqS9nfNbzfIgLFmNX+FL9qR9ZR5PA==",
|
||||
"dependencies": {
|
||||
"Newtonsoft.Json": "11.0.2",
|
||||
"System.Text.Encodings.Web": "4.7.0"
|
||||
}
|
||||
},
|
||||
"IdentityModel.AspNetCore.OAuth2Introspection": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.0.1",
|
||||
"contentHash": "ZNdMZMaj9fqR3j50vYsu+1U3QGd6n8+fqwf+a8mCTcmXGor+HgFDfdq0mM34bsmD6uEgAQup7sv2ZW5kR36dbA==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "4.0.0"
|
||||
}
|
||||
},
|
||||
"IdentityServer4": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.1.2",
|
||||
"contentHash": "blaxxGuOA7v/w1q+fxn97wZ+x2ecG1ZD4mc/N/ZOXMNeFZZhqv+4LF26Gecyik3nWrJPmbMEtQbLmRsKG8k61w==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "4.4.0",
|
||||
"IdentityServer4.Storage": "4.1.2",
|
||||
"Microsoft.AspNetCore.Authentication.OpenIdConnect": "3.1.0",
|
||||
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "5.6.0",
|
||||
"Newtonsoft.Json": "12.0.2"
|
||||
}
|
||||
},
|
||||
"IdentityServer4.AccessTokenValidation": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.1",
|
||||
"contentHash": "qu/M6UyN4o9NVep7q545Ms7hYAnsQqSdLbN1Fjjrn4m35lyBfeQPSSNzDryAKHbodyWOQfHaOqKEyMEJQ5Rpgw==",
|
||||
"dependencies": {
|
||||
"IdentityModel.AspNetCore.OAuth2Introspection": "4.0.1",
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer": "3.0.0"
|
||||
}
|
||||
},
|
||||
"IdentityServer4.Storage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.1.2",
|
||||
"contentHash": "KoSffyZyyeCNTIyJiZnCuPakJ1QbCHlpty6gbWUj/7yl+w0PXIchgmmJnJSvddzBb8iZ2xew/vGlxWUIP17P2g==",
|
||||
"dependencies": {
|
||||
"IdentityModel": "4.4.0"
|
||||
}
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "eVHCR7a6m/dm5RFcBzE3qs/Jg5j9R5Rjpu8aTOv9e4AFvaQtBXb5ah7kmwU+YwA0ufRwz4wf1hnIvsD2hSnI4g=="
|
||||
},
|
||||
"LaunchDarkly.Cache": {
|
||||
"type": "Transitive",
|
||||
@@ -363,10 +340,10 @@
|
||||
},
|
||||
"Microsoft.AspNetCore.Authentication.OpenIdConnect": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.0",
|
||||
"contentHash": "O1cAQYUTU8EfRqwc5/rfTns4E4hKlFlg59fuKRrST+PzsxI6H07KqRN/JjdYhAuVYxF8jPnIGbj+zuc5paOWUw==",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "cJxdro36spFzk/K2OFCddM6vZ+yoj6ug8mTFRH3Gdv1Pul/buSuCtfb/FSCp31UmS5S4C1315dU7wX3ErLFuDg==",
|
||||
"dependencies": {
|
||||
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "5.5.0"
|
||||
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.10.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.AspNetCore.Cryptography.Internal": {
|
||||
@@ -399,8 +376,8 @@
|
||||
},
|
||||
"Microsoft.AspNetCore.DataProtection.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.32",
|
||||
"contentHash": "MPL4iVyiaRxnOUY5VATHjvhDWaAEFb77KFiUxVRklv3Z3v+STofUr1UG/aCt1O9cgN7FVTDaC5A7U+zsLub8Xg=="
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "Z/UU4NEBm5UgNufJmw+j5baW26ytCOIZ0G7sZocPaOzsUeBon1bkM3lSMNZQG2GmDjAIVP2XMSODf2jzSGbibw=="
|
||||
},
|
||||
"Microsoft.Azure.Amqp": {
|
||||
"type": "Transitive",
|
||||
@@ -2626,10 +2603,9 @@
|
||||
"BitPay.Light": "[1.0.1907, )",
|
||||
"Braintree": "[5.19.0, )",
|
||||
"DnsClient": "[1.7.0, )",
|
||||
"Duende.IdentityServer": "[6.0.4, )",
|
||||
"Fido2.AspNet": "[3.0.1, )",
|
||||
"Handlebars.Net": "[2.1.2, )",
|
||||
"IdentityServer4": "[4.1.2, )",
|
||||
"IdentityServer4.AccessTokenValidation": "[3.0.1, )",
|
||||
"LaunchDarkly.ServerSdk": "[8.0.0, )",
|
||||
"MailKit": "[4.2.0, )",
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer": "[6.0.4, )",
|
||||
@@ -2657,7 +2633,7 @@
|
||||
"infrastructure.dapper": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Core": "[2023.10.2, )",
|
||||
"Core": "[2023.10.3, )",
|
||||
"Dapper": "[2.0.123, )"
|
||||
}
|
||||
},
|
||||
@@ -2665,7 +2641,7 @@
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[12.0.1, )",
|
||||
"Core": "[2023.10.2, )",
|
||||
"Core": "[2023.10.3, )",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[7.0.5, )",
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": "[7.0.5, )",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite": "[7.0.5, )",
|
||||
@@ -2677,9 +2653,9 @@
|
||||
"sharedweb": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Core": "[2023.10.2, )",
|
||||
"Infrastructure.Dapper": "[2023.10.2, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.2, )"
|
||||
"Core": "[2023.10.3, )",
|
||||
"Infrastructure.Dapper": "[2023.10.3, )",
|
||||
"Infrastructure.EntityFramework": "[2023.10.3, )"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Auth.Models.Api.Response.Accounts;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Auth.Utilities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -8,9 +10,9 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Fido2NetLib;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Identity.Controllers;
|
||||
@@ -23,17 +25,21 @@ public class AccountsController : Controller
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IUserService _userService;
|
||||
private readonly ICaptchaValidationService _captchaValidationService;
|
||||
private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
|
||||
|
||||
|
||||
public AccountsController(
|
||||
ILogger<AccountsController> logger,
|
||||
IUserRepository userRepository,
|
||||
IUserService userService,
|
||||
ICaptchaValidationService captchaValidationService)
|
||||
ICaptchaValidationService captchaValidationService,
|
||||
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector)
|
||||
{
|
||||
_logger = logger;
|
||||
_userRepository = userRepository;
|
||||
_userService = userService;
|
||||
_captchaValidationService = captchaValidationService;
|
||||
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
||||
}
|
||||
|
||||
// Moved from API, If you modify this endpoint, please update API as well. Self hosted installs still use the API endpoints.
|
||||
@@ -75,36 +81,19 @@ public class AccountsController : Controller
|
||||
return new PreloginResponseModel(kdfInformation);
|
||||
}
|
||||
|
||||
[HttpPost("webauthn-assertion-options")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)] // Disable Swagger due to CredentialCreateOptions not converting properly
|
||||
[HttpGet("webauthn/assertion-options")]
|
||||
[RequireFeature(FeatureFlagKeys.PasswordlessLogin)]
|
||||
// TODO: Create proper models for this call
|
||||
public async Task<AssertionOptions> PostWebAuthnAssertionOptions([FromBody] PreloginRequestModel model)
|
||||
public WebAuthnLoginAssertionOptionsResponseModel GetWebAuthnLoginAssertionOptions()
|
||||
{
|
||||
var user = await _userRepository.GetByEmailAsync(model.Email);
|
||||
if (user == null)
|
||||
var options = _userService.StartWebAuthnLoginAssertion();
|
||||
|
||||
var tokenable = new WebAuthnLoginAssertionOptionsTokenable(WebAuthnLoginAssertionOptionsScope.Authentication, options);
|
||||
var token = _assertionOptionsDataProtector.Protect(tokenable);
|
||||
|
||||
return new WebAuthnLoginAssertionOptionsResponseModel
|
||||
{
|
||||
// TODO: return something? possible enumeration attacks with this response
|
||||
return new AssertionOptions();
|
||||
}
|
||||
|
||||
var options = await _userService.StartWebAuthnLoginAssertionAsync(user);
|
||||
return options;
|
||||
}
|
||||
|
||||
[HttpPost("webauthn-assertion")]
|
||||
[RequireFeature(FeatureFlagKeys.PasswordlessLogin)]
|
||||
// TODO: Create proper models for this call
|
||||
public async Task<string> PostWebAuthnAssertion([FromBody] PreloginRequestModel model)
|
||||
{
|
||||
var user = await _userRepository.GetByEmailAsync(model.Email);
|
||||
if (user == null)
|
||||
{
|
||||
// TODO: proper response here?
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
var token = await _userService.CompleteWebAuthLoginAssertionAsync(null, user);
|
||||
return token;
|
||||
Options = options,
|
||||
Token = token
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Identity.Models;
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Services;
|
||||
using IdentityModel;
|
||||
using IdentityServer4;
|
||||
using IdentityServer4.Services;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Localization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -267,7 +267,7 @@ public class SsoController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsNativeClient(IdentityServer4.Models.AuthorizationRequest context)
|
||||
private bool IsNativeClient(Duende.IdentityServer.Models.AuthorizationRequest context)
|
||||
{
|
||||
return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal)
|
||||
&& !context.RedirectUri.StartsWith("http", StringComparison.Ordinal);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Bit.Core.Settings;
|
||||
using IdentityServer4.Models;
|
||||
using Duende.IdentityServer.Models;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
@@ -13,7 +13,7 @@ public class ApiClient : Client
|
||||
string[] scopes = null)
|
||||
{
|
||||
ClientId = id;
|
||||
AllowedGrantTypes = new[] { GrantType.ResourceOwnerPassword, GrantType.AuthorizationCode };
|
||||
AllowedGrantTypes = new[] { GrantType.ResourceOwnerPassword, GrantType.AuthorizationCode, WebAuthnGrantValidator.GrantType };
|
||||
RefreshTokenExpiration = TokenExpiration.Sliding;
|
||||
RefreshTokenUsage = TokenUsage.ReUse;
|
||||
SlidingRefreshTokenLifetime = 86400 * refreshTokenSlidingDays;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Duende.IdentityServer.Models;
|
||||
using IdentityModel;
|
||||
using IdentityServer4.Models;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
using IdentityServer4;
|
||||
using IdentityServer4.Extensions;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Services;
|
||||
using IdentityServer4.Stores;
|
||||
using IdentityServer4.Stores.Serialization;
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Extensions;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using Duende.IdentityServer.Stores.Serialization;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
// ref: https://raw.githubusercontent.com/IdentityServer/IdentityServer4/3.1.3/src/IdentityServer4/src/Stores/Default/DefaultAuthorizationCodeStore.cs
|
||||
public class AuthorizationCodeStore : DefaultGrantStore<AuthorizationCode>, IAuthorizationCodeStore
|
||||
{
|
||||
public AuthorizationCodeStore(
|
||||
|
||||
@@ -3,6 +3,8 @@ using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity;
|
||||
@@ -10,7 +12,6 @@ using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Api.Response;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.Utilities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -23,8 +24,7 @@ using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity.Utilities;
|
||||
using IdentityServer4.Validation;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
|
||||
@@ -35,7 +35,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
private UserManager<User> _userManager;
|
||||
private readonly IDeviceRepository _deviceRepository;
|
||||
private readonly IDeviceService _deviceService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
@@ -53,6 +52,8 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
protected IPolicyService PolicyService { get; }
|
||||
protected IFeatureService FeatureService { get; }
|
||||
protected ISsoConfigRepository SsoConfigRepository { get; }
|
||||
protected IUserService _userService { get; }
|
||||
protected IUserDecryptionOptionsBuilder UserDecryptionOptionsBuilder { get; }
|
||||
|
||||
public BaseRequestValidator(
|
||||
UserManager<User> userManager,
|
||||
@@ -73,7 +74,8 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
|
||||
IFeatureService featureService,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IDistributedCache distributedCache)
|
||||
IDistributedCache distributedCache,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_deviceRepository = deviceRepository;
|
||||
@@ -96,11 +98,12 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
_distributedCache = distributedCache;
|
||||
_cacheEntryOptions = new DistributedCacheEntryOptions
|
||||
{
|
||||
// This sets the time an item is cached to 15 minutes. This value is hard coded
|
||||
// to 15 because to it covers all time-out windows for both Authenticators and
|
||||
// This sets the time an item is cached to 17 minutes. This value is hard coded
|
||||
// to 17 because to it covers all time-out windows for both Authenticators and
|
||||
// Email TOTP.
|
||||
AbsoluteExpirationRelativeToNow = new TimeSpan(0, 15, 0)
|
||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(17)
|
||||
};
|
||||
UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder;
|
||||
}
|
||||
|
||||
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
||||
@@ -333,7 +336,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
protected abstract void SetErrorResult(T context, Dictionary<string, object> customResponse);
|
||||
protected abstract ClaimsPrincipal GetSubject(T context);
|
||||
|
||||
private async Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request)
|
||||
protected virtual async Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request)
|
||||
{
|
||||
if (request.GrantType == "client_credentials")
|
||||
{
|
||||
@@ -612,67 +615,12 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
/// </summary>
|
||||
private async Task<UserDecryptionOptions> CreateUserDecryptionOptionsAsync(User user, Device device, ClaimsPrincipal subject)
|
||||
{
|
||||
var ssoConfiguration = await GetSsoConfigurationDataAsync(subject);
|
||||
|
||||
var userDecryptionOption = new UserDecryptionOptions
|
||||
{
|
||||
HasMasterPassword = !string.IsNullOrEmpty(user.MasterPassword)
|
||||
};
|
||||
|
||||
var ssoConfigurationData = ssoConfiguration?.GetData();
|
||||
|
||||
if (ssoConfigurationData is { MemberDecryptionType: MemberDecryptionType.KeyConnector } && !string.IsNullOrEmpty(ssoConfigurationData.KeyConnectorUrl))
|
||||
{
|
||||
// KeyConnector makes it mutually exclusive
|
||||
userDecryptionOption.KeyConnectorOption = new KeyConnectorUserDecryptionOption(ssoConfigurationData.KeyConnectorUrl);
|
||||
return userDecryptionOption;
|
||||
}
|
||||
|
||||
// Only add the trusted device specific option when the flag is turned on
|
||||
if (FeatureService.IsEnabled(FeatureFlagKeys.TrustedDeviceEncryption, CurrentContext) && ssoConfigurationData is { MemberDecryptionType: MemberDecryptionType.TrustedDeviceEncryption })
|
||||
{
|
||||
string? encryptedPrivateKey = null;
|
||||
string? encryptedUserKey = null;
|
||||
if (device.IsTrusted())
|
||||
{
|
||||
encryptedPrivateKey = device.EncryptedPrivateKey;
|
||||
encryptedUserKey = device.EncryptedUserKey;
|
||||
}
|
||||
|
||||
var allDevices = await _deviceRepository.GetManyByUserIdAsync(user.Id);
|
||||
// Checks if the current user has any devices that are capable of approving login with device requests except for
|
||||
// their current device.
|
||||
// NOTE: this doesn't check for if the users have configured the devices to be capable of approving requests as that is a client side setting.
|
||||
var hasLoginApprovingDevice = allDevices
|
||||
.Where(d => d.Identifier != device.Identifier && LoginApprovingDeviceTypes.Types.Contains(d.Type))
|
||||
.Any();
|
||||
|
||||
// Determine if user has manage reset password permission as post sso logic requires it for forcing users with this permission to set a MP
|
||||
var hasManageResetPasswordPermission = false;
|
||||
|
||||
// when a user is being created via JIT provisioning, they will not have any orgs so we can't assume we will have orgs here
|
||||
if (CurrentContext.Organizations.Any(o => o.Id == ssoConfiguration!.OrganizationId))
|
||||
{
|
||||
// TDE requires single org so grabbing first org & id is fine.
|
||||
hasManageResetPasswordPermission = await CurrentContext.ManageResetPassword(ssoConfiguration!.OrganizationId);
|
||||
}
|
||||
|
||||
// If sso configuration data is not null then I know for sure that ssoConfiguration isn't null
|
||||
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(ssoConfiguration!.OrganizationId, user.Id);
|
||||
|
||||
// They are only able to be approved by an admin if they have enrolled is reset password
|
||||
var hasAdminApproval = !string.IsNullOrEmpty(organizationUser.ResetPasswordKey);
|
||||
|
||||
// TrustedDeviceEncryption only exists for SSO, but if that ever changes this value won't always be true
|
||||
userDecryptionOption.TrustedDeviceOption = new TrustedDeviceUserDecryptionOption(
|
||||
hasAdminApproval,
|
||||
hasLoginApprovingDevice,
|
||||
hasManageResetPasswordPermission,
|
||||
encryptedPrivateKey,
|
||||
encryptedUserKey);
|
||||
}
|
||||
|
||||
return userDecryptionOption;
|
||||
var ssoConfig = await GetSsoConfigurationDataAsync(subject);
|
||||
return await UserDecryptionOptionsBuilder
|
||||
.ForUser(user)
|
||||
.WithDevice(device)
|
||||
.WithSso(ssoConfig)
|
||||
.BuildAsync();
|
||||
}
|
||||
|
||||
private async Task<SsoConfig?> GetSsoConfigurationDataAsync(ClaimsPrincipal subject)
|
||||
|
||||
@@ -11,9 +11,9 @@ using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using IdentityModel;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Stores;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Models.Api.Response;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
@@ -10,9 +11,9 @@ using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Duende.IdentityServer.Extensions;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using IdentityModel;
|
||||
using IdentityServer4.Extensions;
|
||||
using IdentityServer4.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
|
||||
@@ -44,12 +45,13 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
IPolicyService policyService,
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
|
||||
IFeatureService featureService,
|
||||
IDistributedCache distributedCache)
|
||||
IDistributedCache distributedCache,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
||||
: base(userManager, deviceRepository, deviceService, userService, eventService,
|
||||
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
|
||||
applicationCacheService, mailService, logger, currentContext, globalSettings,
|
||||
userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository,
|
||||
distributedCache)
|
||||
distributedCache, userDecryptionOptionsBuilder)
|
||||
{
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
13
src/Identity/IdentityServer/IUserDecryptionOptionsBuilder.cs
Normal file
13
src/Identity/IdentityServer/IUserDecryptionOptionsBuilder.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Api.Response;
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
public interface IUserDecryptionOptionsBuilder
|
||||
{
|
||||
IUserDecryptionOptionsBuilder ForUser(User user);
|
||||
IUserDecryptionOptionsBuilder WithDevice(Device device);
|
||||
IUserDecryptionOptionsBuilder WithSso(SsoConfig ssoConfig);
|
||||
IUserDecryptionOptionsBuilder WithWebAuthnLoginCredential(WebAuthnCredential credential);
|
||||
Task<UserDecryptionOptions> BuildAsync();
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Stores;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using Grant = Bit.Core.Auth.Entities.Grant;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
@@ -5,8 +5,8 @@ using Bit.Core.Identity;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Services;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Services;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
@@ -10,8 +12,8 @@ using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Validation;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
|
||||
@@ -46,11 +48,12 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
|
||||
IFeatureService featureService,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IDistributedCache distributedCache)
|
||||
IDistributedCache distributedCache,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
||||
: base(userManager, deviceRepository, deviceService, userService, eventService,
|
||||
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
|
||||
applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService,
|
||||
tokenDataFactory, featureService, ssoConfigRepository, distributedCache)
|
||||
tokenDataFactory, featureService, ssoConfigRepository, distributedCache, userDecryptionOptionsBuilder)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_userService = userService;
|
||||
@@ -144,7 +147,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
List<Claim> claims, Dictionary<string, object> customResponse)
|
||||
{
|
||||
context.Result = new GrantValidationResult(user.Id.ToString(), "Application",
|
||||
identityProvider: "bitwarden",
|
||||
identityProvider: Constants.IdentityProvider,
|
||||
claims: claims.Count > 0 ? claims : null,
|
||||
customResponse: customResponse);
|
||||
return Task.CompletedTask;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Settings;
|
||||
using IdentityServer4.Models;
|
||||
using Duende.IdentityServer.Models;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
|
||||
155
src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs
Normal file
155
src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Api.Response;
|
||||
using Bit.Core.Auth.Utilities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Identity.Utilities;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
#nullable enable
|
||||
/// <summary>
|
||||
/// Used to create a list of all possible ways the newly authenticated user can decrypt their vault contents
|
||||
///
|
||||
/// Note: Do not use this as an injected service if you intend to build multiple independent UserDecryptionOptions
|
||||
/// </summary>
|
||||
public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IDeviceRepository _deviceRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
|
||||
private UserDecryptionOptions _options = new UserDecryptionOptions();
|
||||
private User? _user;
|
||||
private Core.Auth.Entities.SsoConfig? _ssoConfig;
|
||||
private Device? _device;
|
||||
|
||||
public UserDecryptionOptionsBuilder(
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
IDeviceRepository deviceRepository,
|
||||
IOrganizationUserRepository organizationUserRepository
|
||||
)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_featureService = featureService;
|
||||
_deviceRepository = deviceRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
}
|
||||
|
||||
public IUserDecryptionOptionsBuilder ForUser(User user)
|
||||
{
|
||||
_options.HasMasterPassword = user.HasMasterPassword();
|
||||
_user = user;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IUserDecryptionOptionsBuilder WithSso(Core.Auth.Entities.SsoConfig ssoConfig)
|
||||
{
|
||||
_ssoConfig = ssoConfig;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IUserDecryptionOptionsBuilder WithDevice(Device device)
|
||||
{
|
||||
_device = device;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IUserDecryptionOptionsBuilder WithWebAuthnLoginCredential(WebAuthnCredential credential)
|
||||
{
|
||||
if (credential.GetPrfStatus() == WebAuthnPrfStatus.Enabled)
|
||||
{
|
||||
_options.WebAuthnPrfOption = new WebAuthnPrfDecryptionOption(credential.EncryptedPrivateKey, credential.EncryptedUserKey);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public async Task<UserDecryptionOptions> BuildAsync()
|
||||
{
|
||||
BuildKeyConnectorOptions();
|
||||
await BuildTrustedDeviceOptions();
|
||||
|
||||
return _options;
|
||||
}
|
||||
|
||||
private void BuildKeyConnectorOptions()
|
||||
{
|
||||
if (_ssoConfig == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var ssoConfigurationData = _ssoConfig.GetData();
|
||||
if (ssoConfigurationData is { MemberDecryptionType: MemberDecryptionType.KeyConnector } && !string.IsNullOrEmpty(ssoConfigurationData.KeyConnectorUrl))
|
||||
{
|
||||
_options.KeyConnectorOption = new KeyConnectorUserDecryptionOption(ssoConfigurationData.KeyConnectorUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BuildTrustedDeviceOptions()
|
||||
{
|
||||
// TrustedDeviceEncryption only exists for SSO, if that changes then these guards should change
|
||||
if (_ssoConfig == null || !_featureService.IsEnabled(FeatureFlagKeys.TrustedDeviceEncryption, _currentContext))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var ssoConfigurationData = _ssoConfig.GetData();
|
||||
if (ssoConfigurationData is not { MemberDecryptionType: MemberDecryptionType.TrustedDeviceEncryption })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string? encryptedPrivateKey = null;
|
||||
string? encryptedUserKey = null;
|
||||
if (_device != null && _device.IsTrusted())
|
||||
{
|
||||
encryptedPrivateKey = _device.EncryptedPrivateKey;
|
||||
encryptedUserKey = _device.EncryptedUserKey;
|
||||
}
|
||||
|
||||
var hasLoginApprovingDevice = false;
|
||||
if (_device != null && _user != null)
|
||||
{
|
||||
var allDevices = await _deviceRepository.GetManyByUserIdAsync(_user.Id);
|
||||
// Checks if the current user has any devices that are capable of approving login with device requests except for
|
||||
// their current device.
|
||||
// NOTE: this doesn't check for if the users have configured the devices to be capable of approving requests as that is a client side setting.
|
||||
hasLoginApprovingDevice = allDevices
|
||||
.Where(d => d.Identifier != _device.Identifier && LoginApprovingDeviceTypes.Types.Contains(d.Type))
|
||||
.Any();
|
||||
}
|
||||
|
||||
// Determine if user has manage reset password permission as post sso logic requires it for forcing users with this permission to set a MP
|
||||
var hasManageResetPasswordPermission = false;
|
||||
// when a user is being created via JIT provisioning, they will not have any orgs so we can't assume we will have orgs here
|
||||
if (_currentContext.Organizations != null && _currentContext.Organizations.Any(o => o.Id == _ssoConfig.OrganizationId))
|
||||
{
|
||||
// TDE requires single org so grabbing first org & id is fine.
|
||||
hasManageResetPasswordPermission = await _currentContext.ManageResetPassword(_ssoConfig!.OrganizationId);
|
||||
}
|
||||
|
||||
var hasAdminApproval = false;
|
||||
if (_user != null)
|
||||
{
|
||||
// If sso configuration data is not null then I know for sure that ssoConfiguration isn't null
|
||||
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(_ssoConfig.OrganizationId, _user.Id);
|
||||
|
||||
// They are only able to be approved by an admin if they have enrolled is reset password
|
||||
hasAdminApproval = organizationUser != null && !string.IsNullOrEmpty(organizationUser.ResetPasswordKey);
|
||||
}
|
||||
|
||||
_options.TrustedDeviceOption = new TrustedDeviceUserDecryptionOption(
|
||||
hasAdminApproval,
|
||||
hasLoginApprovingDevice,
|
||||
hasManageResetPasswordPermission,
|
||||
encryptedPrivateKey,
|
||||
encryptedUserKey);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using IdentityServer4.Services;
|
||||
using Duende.IdentityServer.Services;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
|
||||
151
src/Identity/IdentityServer/WebAuthnGrantValidator.cs
Normal file
151
src/Identity/IdentityServer/WebAuthnGrantValidator.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using Fido2NetLib;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidationContext>, IExtensionGrantValidator
|
||||
{
|
||||
public const string GrantType = "webauthn";
|
||||
|
||||
private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
|
||||
|
||||
public WebAuthnGrantValidator(
|
||||
UserManager<User> userManager,
|
||||
IDeviceRepository deviceRepository,
|
||||
IDeviceService deviceService,
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IMailService mailService,
|
||||
ILogger<CustomTokenRequestValidator> logger,
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserRepository userRepository,
|
||||
IPolicyService policyService,
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
|
||||
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
|
||||
IFeatureService featureService,
|
||||
IDistributedCache distributedCache,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder
|
||||
)
|
||||
: base(userManager, deviceRepository, deviceService, userService, eventService,
|
||||
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
|
||||
applicationCacheService, mailService, logger, currentContext, globalSettings,
|
||||
userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, distributedCache, userDecryptionOptionsBuilder)
|
||||
{
|
||||
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
||||
}
|
||||
|
||||
string IExtensionGrantValidator.GrantType => "webauthn";
|
||||
|
||||
public async Task ValidateAsync(ExtensionGrantValidationContext context)
|
||||
{
|
||||
if (!FeatureService.IsEnabled(FeatureFlagKeys.PasswordlessLogin, CurrentContext))
|
||||
{
|
||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
|
||||
return;
|
||||
}
|
||||
|
||||
var rawToken = context.Request.Raw.Get("token");
|
||||
var rawDeviceResponse = context.Request.Raw.Get("deviceResponse");
|
||||
if (string.IsNullOrWhiteSpace(rawToken) || string.IsNullOrWhiteSpace(rawDeviceResponse))
|
||||
{
|
||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
|
||||
return;
|
||||
}
|
||||
|
||||
var verified = _assertionOptionsDataProtector.TryUnprotect(rawToken, out var token) &&
|
||||
token.TokenIsValid(WebAuthnLoginAssertionOptionsScope.Authentication);
|
||||
var deviceResponse = JsonSerializer.Deserialize<AuthenticatorAssertionRawResponse>(rawDeviceResponse);
|
||||
|
||||
if (!verified)
|
||||
{
|
||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
var (user, credential) = await _userService.CompleteWebAuthLoginAssertionAsync(token.Options, deviceResponse);
|
||||
var validatorContext = new CustomValidatorRequestContext
|
||||
{
|
||||
User = user,
|
||||
KnownDevice = await KnownDeviceAsync(user, context.Request)
|
||||
};
|
||||
|
||||
UserDecryptionOptionsBuilder.WithWebAuthnLoginCredential(credential);
|
||||
|
||||
await ValidateAsync(context, context.Request, validatorContext);
|
||||
}
|
||||
|
||||
protected override Task<bool> ValidateContextAsync(ExtensionGrantValidationContext context,
|
||||
CustomValidatorRequestContext validatorContext)
|
||||
{
|
||||
if (validatorContext.User == null)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
protected override Task SetSuccessResult(ExtensionGrantValidationContext context, User user,
|
||||
List<Claim> claims, Dictionary<string, object> customResponse)
|
||||
{
|
||||
context.Result = new GrantValidationResult(user.Id.ToString(), "Application",
|
||||
identityProvider: Constants.IdentityProvider,
|
||||
claims: claims.Count > 0 ? claims : null,
|
||||
customResponse: customResponse);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override ClaimsPrincipal GetSubject(ExtensionGrantValidationContext context)
|
||||
{
|
||||
return context.Result.Subject;
|
||||
}
|
||||
|
||||
protected override Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request)
|
||||
{
|
||||
// We consider Fido2 userVerification a second factor, so we don't require a second factor here.
|
||||
return Task.FromResult(new Tuple<bool, Organization>(false, null));
|
||||
}
|
||||
|
||||
protected override void SetTwoFactorResult(ExtensionGrantValidationContext context,
|
||||
Dictionary<string, object> customResponse)
|
||||
{
|
||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Two factor required.",
|
||||
customResponse);
|
||||
}
|
||||
|
||||
protected override void SetSsoResult(ExtensionGrantValidationContext context,
|
||||
Dictionary<string, object> customResponse)
|
||||
{
|
||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Sso authentication required.",
|
||||
customResponse);
|
||||
}
|
||||
|
||||
protected override void SetErrorResult(ExtensionGrantValidationContext context,
|
||||
Dictionary<string, object> customResponse)
|
||||
{
|
||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, customResponse: customResponse);
|
||||
}
|
||||
}
|
||||
@@ -29,8 +29,8 @@ public class Program
|
||||
return e.Level >= globalSettings.MinLogLevel.IdentitySettings.IpRateLimit;
|
||||
}
|
||||
|
||||
if (context.Contains("IdentityServer4.Validation.TokenValidator") ||
|
||||
context.Contains("IdentityServer4.Validation.TokenRequestValidator"))
|
||||
if (context.Contains("Duende.IdentityServer.Validation.TokenValidator") ||
|
||||
context.Contains("Duende.IdentityServer.Validation.TokenRequestValidator"))
|
||||
{
|
||||
return e.Level >= globalSettings.MinLogLevel.IdentitySettings.IdentityToken;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using IdentityServer4.Extensions;
|
||||
using Duende.IdentityServer.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.IdentityModel.Logging;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using IdentityServer4.Configuration;
|
||||
using IdentityServer4.Services;
|
||||
using IdentityServer4.Stores;
|
||||
using IdentityServer4.Validation;
|
||||
using Duende.IdentityServer.Configuration;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using Duende.IdentityServer.Validation;
|
||||
|
||||
namespace Bit.Identity.Utilities;
|
||||
|
||||
public class DiscoveryResponseGenerator : IdentityServer4.ResponseHandling.DiscoveryResponseGenerator
|
||||
public class DiscoveryResponseGenerator : Duende.IdentityServer.ResponseHandling.DiscoveryResponseGenerator
|
||||
{
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Identity.IdentityServer;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using IdentityServer4.ResponseHandling;
|
||||
using IdentityServer4.Services;
|
||||
using IdentityServer4.Stores;
|
||||
using Duende.IdentityServer.ResponseHandling;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Stores;
|
||||
|
||||
namespace Bit.Identity.Utilities;
|
||||
|
||||
@@ -17,6 +17,7 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
services.AddSingleton<StaticClientStore>();
|
||||
services.AddTransient<IAuthorizationCodeStore, AuthorizationCodeStore>();
|
||||
services.AddTransient<IUserDecryptionOptionsBuilder, UserDecryptionOptionsBuilder>();
|
||||
|
||||
var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity);
|
||||
var identityServerBuilder = services
|
||||
@@ -34,6 +35,7 @@ public static class ServiceCollectionExtensions
|
||||
options.Authentication.CookieSameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode.Unspecified;
|
||||
}
|
||||
options.InputLengthRestrictions.UserName = 256;
|
||||
options.KeyManagement.Enabled = false;
|
||||
})
|
||||
.AddInMemoryCaching()
|
||||
.AddInMemoryApiResources(ApiResources.GetApiResources())
|
||||
@@ -44,7 +46,8 @@ public static class ServiceCollectionExtensions
|
||||
.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
|
||||
.AddPersistedGrantStore<PersistedGrantStore>()
|
||||
.AddClientStore<ClientStore>()
|
||||
.AddIdentityServerCertificate(env, globalSettings);
|
||||
.AddIdentityServerCertificate(env, globalSettings)
|
||||
.AddExtensionGrantValidator<WebAuthnGrantValidator>();
|
||||
|
||||
services.AddTransient<ICorsPolicyService, CustomCorsPolicyService>();
|
||||
return identityServerBuilder;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user