mirror of
https://github.com/bitwarden/server
synced 2026-02-25 08:53:21 +00:00
feat(auth-validator): [Auth/PM-22975] Client Version Validator (#6588)
* feat(auth-validator): [PM-22975] Client Version Validator - Implementation. * test(auth-validator): [PM-22975] Client Version Validator - Added tests.
This commit is contained in:
committed by
GitHub
parent
b5554c6030
commit
3dbd17f61d
@@ -32,7 +32,7 @@ public interface ICurrentContext
|
||||
Guid? OrganizationId { get; set; }
|
||||
IdentityClientType IdentityClientType { get; set; }
|
||||
string ClientId { get; set; }
|
||||
Version ClientVersion { get; set; }
|
||||
Version? ClientVersion { get; set; }
|
||||
bool ClientVersionIsPrerelease { get; set; }
|
||||
|
||||
Task BuildAsync(HttpContext httpContext, GlobalSettings globalSettings);
|
||||
|
||||
@@ -4,6 +4,7 @@ using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Utilities;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
@@ -216,6 +217,42 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
|
||||
return SecurityVersion ?? 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates user state to determine if they are currently in a v2 encryption state.
|
||||
/// </summary>
|
||||
/// <returns>If the shape of their private key is v2 as well as has the proper security version then true, otherwise false</returns>
|
||||
public bool HasV2Encryption()
|
||||
{
|
||||
return HasV2KeyShape() && IsSecurityVersionTwo();
|
||||
}
|
||||
|
||||
private bool HasV2KeyShape()
|
||||
{
|
||||
if (string.IsNullOrEmpty(PrivateKey))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return EncryptionParsing.GetEncryptionType(PrivateKey) == EncryptionType.XChaCha20Poly1305_B64;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// Invalid encryption string format - treat as not v2
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This technically is correct but all versions after 1 are considered v2 encryption. Leaving for now with
|
||||
/// KM's blessing that when a new version comes along they will handle migration.
|
||||
/// </summary>
|
||||
private bool IsSecurityVersionTwo()
|
||||
{
|
||||
return SecurityVersion == 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the C# object to the User.TwoFactorProviders property in JSON format.
|
||||
/// </summary>
|
||||
|
||||
@@ -11,8 +11,11 @@ public enum EncryptionType : byte
|
||||
XChaCha20Poly1305_B64 = 7,
|
||||
|
||||
// asymmetric
|
||||
[Obsolete("PM-29656 - Should probably be removed as it is not known to exist in the real world")]
|
||||
Rsa2048_OaepSha256_B64 = 3,
|
||||
Rsa2048_OaepSha1_B64 = 4,
|
||||
[Obsolete("PM-29656 - Should probably be removed as it is not known to exist in the real world")]
|
||||
Rsa2048_OaepSha256_HmacSha256_B64 = 5,
|
||||
[Obsolete("PM-29656 - Should probably be removed as it is not known to exist in the real world")]
|
||||
Rsa2048_OaepSha1_HmacSha256_B64 = 6
|
||||
}
|
||||
|
||||
6
src/Core/KeyManagement/Constants.cs
Normal file
6
src/Core/KeyManagement/Constants.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.KeyManagement;
|
||||
|
||||
public static class Constants
|
||||
{
|
||||
public static readonly Version MinimumClientVersionForV2Encryption = new("2025.11.0");
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Repositories;
|
||||
using Bit.Core.KeyManagement.Utilities;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@@ -159,7 +160,7 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GetEncryptionType(model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey) != EncryptionType.AesCbc256_HmacSha256_B64)
|
||||
if (EncryptionParsing.GetEncryptionType(model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey) != EncryptionType.AesCbc256_HmacSha256_B64)
|
||||
{
|
||||
throw new InvalidOperationException("The provided account private key was not wrapped with AES-256-CBC-HMAC");
|
||||
}
|
||||
@@ -231,7 +232,7 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
{
|
||||
// Returns whether the user is a V2 user based on the private key's encryption type.
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
var isPrivateKeyEncryptionV2 = GetEncryptionType(user.PrivateKey) == EncryptionType.XChaCha20Poly1305_B64;
|
||||
var isPrivateKeyEncryptionV2 = EncryptionParsing.GetEncryptionType(user.PrivateKey) == EncryptionType.XChaCha20Poly1305_B64;
|
||||
return isPrivateKeyEncryptionV2;
|
||||
}
|
||||
|
||||
@@ -259,7 +260,7 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
{
|
||||
throw new InvalidOperationException("Signature key pair data is required for V2 encryption.");
|
||||
}
|
||||
if (GetEncryptionType(model.AccountKeys.SignatureKeyPairData.WrappedSigningKey) != EncryptionType.XChaCha20Poly1305_B64)
|
||||
if (EncryptionParsing.GetEncryptionType(model.AccountKeys.SignatureKeyPairData.WrappedSigningKey) != EncryptionType.XChaCha20Poly1305_B64)
|
||||
{
|
||||
throw new InvalidOperationException("The provided signing key data is not wrapped with XChaCha20-Poly1305.");
|
||||
}
|
||||
@@ -268,7 +269,7 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
throw new InvalidOperationException("The provided signature key pair data does not contain a valid verifying key.");
|
||||
}
|
||||
|
||||
if (GetEncryptionType(model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey) != EncryptionType.XChaCha20Poly1305_B64)
|
||||
if (EncryptionParsing.GetEncryptionType(model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey) != EncryptionType.XChaCha20Poly1305_B64)
|
||||
{
|
||||
throw new InvalidOperationException("The provided private key encryption key is not wrapped with XChaCha20-Poly1305.");
|
||||
}
|
||||
@@ -281,24 +282,4 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
throw new InvalidOperationException("No signed security state provider for V2 user");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to convert an encryption type string to an enum value.
|
||||
/// </summary>
|
||||
private static EncryptionType GetEncryptionType(string encString)
|
||||
{
|
||||
var parts = encString.Split('.');
|
||||
if (parts.Length == 1)
|
||||
{
|
||||
throw new ArgumentException("Invalid encryption type string.");
|
||||
}
|
||||
if (byte.TryParse(parts[0], out var encryptionTypeNumber))
|
||||
{
|
||||
if (Enum.IsDefined(typeof(EncryptionType), encryptionTypeNumber))
|
||||
{
|
||||
return (EncryptionType)encryptionTypeNumber;
|
||||
}
|
||||
}
|
||||
throw new ArgumentException("Invalid encryption type string.");
|
||||
}
|
||||
}
|
||||
28
src/Core/KeyManagement/Utilities/EncryptionParsing.cs
Normal file
28
src/Core/KeyManagement/Utilities/EncryptionParsing.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Utilities;
|
||||
|
||||
public static class EncryptionParsing
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper method to convert an encryption type string to an enum value.
|
||||
/// </summary>
|
||||
public static EncryptionType GetEncryptionType(string? encString)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(encString);
|
||||
|
||||
var parts = encString.Split('.');
|
||||
if (parts.Length == 1)
|
||||
{
|
||||
throw new ArgumentException("Invalid encryption type string.");
|
||||
}
|
||||
if (byte.TryParse(parts[0], out var encryptionTypeNumber))
|
||||
{
|
||||
if (Enum.IsDefined(typeof(EncryptionType), encryptionTypeNumber))
|
||||
{
|
||||
return (EncryptionType)encryptionTypeNumber;
|
||||
}
|
||||
}
|
||||
throw new ArgumentException("Invalid encryption type string.");
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IAuthRequestRepository _authRequestRepository;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IClientVersionValidator _clientVersionValidator;
|
||||
|
||||
protected ICurrentContext CurrentContext { get; }
|
||||
protected IPolicyService PolicyService { get; }
|
||||
@@ -69,7 +70,8 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
IMailService mailService,
|
||||
IUserAccountKeysQuery userAccountKeysQuery
|
||||
IUserAccountKeysQuery userAccountKeysQuery,
|
||||
IClientVersionValidator clientVersionValidator
|
||||
)
|
||||
{
|
||||
_userManager = userManager;
|
||||
@@ -91,6 +93,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
_authRequestRepository = authRequestRepository;
|
||||
_mailService = mailService;
|
||||
_accountKeysQuery = userAccountKeysQuery;
|
||||
_clientVersionValidator = clientVersionValidator;
|
||||
}
|
||||
|
||||
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
||||
@@ -135,7 +138,13 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
// validation to perform the recovery as part of scheme validation based on the request.
|
||||
return
|
||||
[
|
||||
() => ValidateMasterPasswordAsync(context, validatorContext),
|
||||
() => ValidateGrantSpecificContext(context, validatorContext),
|
||||
// Now check the version number of the client. Do this after ValidateContextAsync so that
|
||||
// we prevent account enumeration. If we were to do this before ValidateContextAsync, then attackers
|
||||
// could use a known invalid client version and make a request for a user (before we know if they have
|
||||
// demonstrated ownership of the account via correct credentials) and identify if they exist by getting
|
||||
// an error response back from the validator saying the user is not compatible with the client.
|
||||
() => ValidateClientVersionAsync(context, validatorContext),
|
||||
() => ValidateTwoFactorAsync(context, request, validatorContext),
|
||||
() => ValidateSsoAsync(context, request, validatorContext),
|
||||
() => ValidateNewDeviceAsync(context, request, validatorContext),
|
||||
@@ -148,7 +157,13 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
// The typical validation scenario.
|
||||
return
|
||||
[
|
||||
() => ValidateMasterPasswordAsync(context, validatorContext),
|
||||
() => ValidateGrantSpecificContext(context, validatorContext),
|
||||
// Now check the version number of the client. Do this after ValidateContextAsync so that
|
||||
// we prevent account enumeration. If we were to do this before ValidateContextAsync, then attackers
|
||||
// could use a known invalid client version and make a request for a user (before we know if they have
|
||||
// demonstrated ownership of the account via correct credentials) and identify if they exist by getting
|
||||
// an error response back from the validator saying the user is not compatible with the client.
|
||||
() => ValidateClientVersionAsync(context, validatorContext),
|
||||
() => ValidateSsoAsync(context, request, validatorContext),
|
||||
() => ValidateTwoFactorAsync(context, request, validatorContext),
|
||||
() => ValidateNewDeviceAsync(context, request, validatorContext),
|
||||
@@ -201,12 +216,29 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the user's Master Password hash.
|
||||
/// Validates whether the client version is compatible for the user attempting to authenticate.
|
||||
/// </summary>
|
||||
/// <returns>true if the scheme successfully passed validation, otherwise false.</returns>
|
||||
private async Task<bool> ValidateClientVersionAsync(T context, CustomValidatorRequestContext validatorContext)
|
||||
{
|
||||
var ok = _clientVersionValidator.Validate(validatorContext.User, validatorContext);
|
||||
if (ok)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
SetValidationErrorResult(context, validatorContext);
|
||||
await LogFailedLoginEvent(validatorContext.User, EventType.User_FailedLogIn);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the user's master password, webauthen, or custom token request via the appropriate context validator.
|
||||
/// </summary>
|
||||
/// <param name="context">The current request context.</param>
|
||||
/// <param name="validatorContext"><see cref="Bit.Identity.IdentityServer.CustomValidatorRequestContext" /></param>
|
||||
/// <returns>true if the scheme successfully passed validation, otherwise false.</returns>
|
||||
private async Task<bool> ValidateMasterPasswordAsync(T context, CustomValidatorRequestContext validatorContext)
|
||||
private async Task<bool> ValidateGrantSpecificContext(T context, CustomValidatorRequestContext validatorContext)
|
||||
{
|
||||
var valid = await ValidateContextAsync(context, validatorContext);
|
||||
var user = validatorContext.User;
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement;
|
||||
using Bit.Core.Models.Api;
|
||||
using Duende.IdentityServer.Validation;
|
||||
|
||||
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||
|
||||
public interface IClientVersionValidator
|
||||
{
|
||||
bool Validate(User user, CustomValidatorRequestContext requestContext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This validator will use the Client Version on a request, which currently maps
|
||||
/// to the "Bitwarden-Client-Version" header, to determine if a user meets minimum
|
||||
/// required client version for issuing tokens on an old client. This is done to
|
||||
/// incentivize users to get on an updated client when their password encryption
|
||||
/// method has already been updated.
|
||||
///
|
||||
/// If the header is omitted, then the validator returns that this request is invalid.
|
||||
/// </summary>
|
||||
public class ClientVersionValidator(
|
||||
ICurrentContext currentContext)
|
||||
: IClientVersionValidator
|
||||
{
|
||||
private const string _upgradeMessage = "Please update your app to continue using Bitwarden";
|
||||
private const string _noUserMessage = "No user found while trying to validate client version";
|
||||
private const string _versionHeaderMissing = "No client version header found, required to prevent encryption errors. Please confirm your client is supplying the header: \"Bitwarden-Client-Version\"";
|
||||
|
||||
public bool Validate(User? user, CustomValidatorRequestContext requestContext)
|
||||
{
|
||||
// Do this nullish check because the base request validator currently is not
|
||||
// strict null checking. Once that gets fixed then we can see about making
|
||||
// the user not nullish checked. If they are null then the validator should fail.
|
||||
if (user == null)
|
||||
{
|
||||
FillRequestContextWithErrorData(requestContext, "no_user", _noUserMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
Version? clientVersion = currentContext.ClientVersion;
|
||||
|
||||
// Deny access if the client version headers are missing.
|
||||
// We want to establish a strict contract with clients that if they omit this header,
|
||||
// then the server cannot guarantee that a client won't do harm to a user's data
|
||||
// with stale encryption architecture.
|
||||
if (clientVersion == null)
|
||||
{
|
||||
FillRequestContextWithErrorData(requestContext, "version_header_missing", _versionHeaderMissing);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine the minimum version client that a user needs. If no V2 encryption detected then
|
||||
// no validation needs to occur, which is why min version number can be null.
|
||||
Version? minVersion = user.HasV2Encryption() ? Constants.MinimumClientVersionForV2Encryption : null;
|
||||
|
||||
// If min version is null then we know that the user had an encryption
|
||||
// configuration that doesn't require a minimum version. Allowing through.
|
||||
if (minVersion == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (clientVersion < minVersion)
|
||||
{
|
||||
FillRequestContextWithErrorData(requestContext, "invalid_client_version", _upgradeMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void FillRequestContextWithErrorData(
|
||||
CustomValidatorRequestContext requestContext,
|
||||
string errorId,
|
||||
string errorMessage)
|
||||
{
|
||||
requestContext.ValidationErrorResult = new ValidationResult
|
||||
{
|
||||
Error = errorId,
|
||||
ErrorDescription = errorMessage,
|
||||
IsError = true
|
||||
};
|
||||
requestContext.CustomResponse = new Dictionary<string, object>
|
||||
{
|
||||
{ "ErrorModel", new ErrorResponseModel(errorMessage) }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,8 @@ using Bit.Core.Settings;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer.Extensions;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using HandlebarsDotNet;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||
|
||||
public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenRequestValidationContext>,
|
||||
@@ -50,7 +47,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
IMailService mailService,
|
||||
IUserAccountKeysQuery userAccountKeysQuery)
|
||||
IUserAccountKeysQuery userAccountKeysQuery,
|
||||
IClientVersionValidator clientVersionValidator)
|
||||
: base(
|
||||
userManager,
|
||||
userService,
|
||||
@@ -70,7 +68,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
policyRequirementQuery,
|
||||
authRequestRepository,
|
||||
mailService,
|
||||
userAccountKeysQuery)
|
||||
userAccountKeysQuery,
|
||||
clientVersionValidator)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_updateInstallationCommand = updateInstallationCommand;
|
||||
|
||||
@@ -44,7 +44,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IMailService mailService,
|
||||
IUserAccountKeysQuery userAccountKeysQuery)
|
||||
IUserAccountKeysQuery userAccountKeysQuery,
|
||||
IClientVersionValidator clientVersionValidator)
|
||||
: base(
|
||||
userManager,
|
||||
userService,
|
||||
@@ -64,7 +65,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
policyRequirementQuery,
|
||||
authRequestRepository,
|
||||
mailService,
|
||||
userAccountKeysQuery)
|
||||
userAccountKeysQuery,
|
||||
clientVersionValidator)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_currentContext = currentContext;
|
||||
@@ -74,7 +76,6 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
|
||||
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
|
||||
{
|
||||
|
||||
var user = await _userManager.FindByEmailAsync(context.UserName.ToLowerInvariant());
|
||||
// We want to keep this device around incase the device is new for the user
|
||||
var requestDevice = DeviceValidator.GetDeviceFromRequest(context.Request);
|
||||
|
||||
@@ -53,7 +53,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
IMailService mailService,
|
||||
IUserAccountKeysQuery userAccountKeysQuery)
|
||||
IUserAccountKeysQuery userAccountKeysQuery,
|
||||
IClientVersionValidator clientVersionValidator)
|
||||
: base(
|
||||
userManager,
|
||||
userService,
|
||||
@@ -73,7 +74,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
policyRequirementQuery,
|
||||
authRequestRepository,
|
||||
mailService,
|
||||
userAccountKeysQuery)
|
||||
userAccountKeysQuery,
|
||||
clientVersionValidator)
|
||||
{
|
||||
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
||||
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
|
||||
|
||||
@@ -25,6 +25,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddTransient<IAuthorizationCodeStore, AuthorizationCodeStore>();
|
||||
services.AddTransient<IUserDecryptionOptionsBuilder, UserDecryptionOptionsBuilder>();
|
||||
services.AddTransient<IDeviceValidator, DeviceValidator>();
|
||||
services.AddTransient<IClientVersionValidator, ClientVersionValidator>();
|
||||
services.AddTransient<ITwoFactorAuthenticationValidator, TwoFactorAuthenticationValidator>();
|
||||
services.AddTransient<ISsoRequestValidator, SsoRequestValidator>();
|
||||
services.AddTransient<ILoginApprovingClientTypes, LoginApprovingClientTypes>();
|
||||
|
||||
@@ -6,7 +6,6 @@ using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using AspNetCoreRateLimit;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.AbilitiesCache;
|
||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
@@ -83,6 +82,7 @@ using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi;
|
||||
using StackExchange.Redis;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Constants = Bit.Core.Constants;
|
||||
using NoopRepos = Bit.Core.Repositories.Noop;
|
||||
using Role = Bit.Core.Entities.Role;
|
||||
using TableStorageRepos = Bit.Core.Repositories.TableStorage;
|
||||
|
||||
Reference in New Issue
Block a user