1
0
mirror of https://github.com/bitwarden/server.git synced 2025-02-13 01:21:29 +01:00

Add support for Key Connector OTP and account migration (#1663)

Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
This commit is contained in:
Oscar Hinton 2021-11-09 16:37:32 +01:00 committed by GitHub
parent f6bc35b2d0
commit fd37cb5a12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 3799 additions and 306 deletions

View File

@ -604,14 +604,15 @@ namespace Bit.Sso.Controllers
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_ResetSsoLink);
}
}
private async Task CreateSsoUserRecord(string providerUserId, Guid userId, Guid orgId)
{
var ssoUser = new SsoUser
{
ExternalId = providerUserId,
UserId = userId,
OrganizationId = orgId
};
OrganizationId = orgId,
};
await _ssoUserRepository.CreateAsync(ssoUser);
}

View File

@ -16,7 +16,6 @@ using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Bit.Core.Enums.Provider;
@ -46,7 +45,6 @@ namespace Bit.Api.Controllers
IOrganizationUserRepository organizationUserRepository,
IProviderUserRepository providerUserRepository,
IPaymentService paymentService,
ISsoUserRepository ssoUserRepository,
IUserRepository userRepository,
IUserService userService,
ISendRepository sendRepository,
@ -118,6 +116,11 @@ namespace Bit.Api.Controllers
throw new UnauthorizedAccessException();
}
if (user.UsesKeyConnector)
{
throw new BadRequestException("You cannot change your email when using Key Connector.");
}
if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
{
await Task.Delay(2000);
@ -136,6 +139,11 @@ namespace Bit.Api.Controllers
throw new UnauthorizedAccessException();
}
if (user.UsesKeyConnector)
{
throw new BadRequestException("You cannot change your email when using Key Connector.");
}
var result = await _userService.ChangeEmailAsync(user, model.MasterPasswordHash, model.NewEmail,
model.NewMasterPasswordHash, model.Token, model.Key);
if (result.Succeeded)
@ -238,7 +246,7 @@ namespace Bit.Api.Controllers
}
[HttpPost("verify-password")]
public async Task PostVerifyPassword([FromBody]VerifyPasswordRequestModel model)
public async Task PostVerifyPassword([FromBody]SecretVerificationRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
@ -256,8 +264,8 @@ namespace Bit.Api.Controllers
throw new BadRequestException(ModelState);
}
[HttpPost("set-crypto-agent-key")]
public async Task PostSetCryptoAgentKeyAsync([FromBody]SetCryptoAgentKeyRequestModel model)
[HttpPost("set-key-connector-key")]
public async Task PostSetKeyConnectorKeyAsync([FromBody]SetKeyConnectorKeyRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
@ -265,7 +273,30 @@ namespace Bit.Api.Controllers
throw new UnauthorizedAccessException();
}
var result = await _userService.SetCryptoAgentKeyAsync(model.ToUser(user), model.Key, model.OrgIdentifier);
var result = await _userService.SetKeyConnectorKeyAsync(model.ToUser(user), model.Key, model.OrgIdentifier);
if (result.Succeeded)
{
return;
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
throw new BadRequestException(ModelState);
}
[HttpPost("convert-to-key-connector")]
public async Task PostConvertToKeyConnector()
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var result = await _userService.ConvertToKeyConnectorAsync(user);
if (result.Succeeded)
{
return;
@ -361,7 +392,7 @@ namespace Bit.Api.Controllers
}
[HttpPost("security-stamp")]
public async Task PostSecurityStamp([FromBody]SecurityStampRequestModel model)
public async Task PostSecurityStamp([FromBody]SecretVerificationRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
@ -369,7 +400,7 @@ namespace Bit.Api.Controllers
throw new UnauthorizedAccessException();
}
var result = await _userService.RefreshSecurityStampAsync(user, model.MasterPasswordHash);
var result = await _userService.RefreshSecurityStampAsync(user, model.Secret);
if (result.Succeeded)
{
return;
@ -471,7 +502,7 @@ namespace Bit.Api.Controllers
[HttpDelete]
[HttpPost("delete")]
public async Task Delete([FromBody]DeleteAccountRequestModel model)
public async Task Delete([FromBody]SecretVerificationRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
@ -479,9 +510,9 @@ namespace Bit.Api.Controllers
throw new UnauthorizedAccessException();
}
if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
if (!await _userService.VerifySecretAsync(user, model.Secret))
{
ModelState.AddModelError("MasterPasswordHash", "Invalid password.");
ModelState.AddModelError(string.Empty, "User verification failed.");
await Task.Delay(2000);
}
else
@ -761,7 +792,7 @@ namespace Bit.Api.Controllers
}
[HttpPost("api-key")]
public async Task<ApiKeyResponseModel> ApiKey([FromBody]ApiKeyRequestModel model)
public async Task<ApiKeyResponseModel> ApiKey([FromBody]SecretVerificationRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
@ -769,20 +800,17 @@ namespace Bit.Api.Controllers
throw new UnauthorizedAccessException();
}
if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
if (!await _userService.VerifySecretAsync(user, model.Secret))
{
await Task.Delay(2000);
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
}
else
{
var response = new ApiKeyResponseModel(user);
return response;
throw new BadRequestException(string.Empty, "User verification failed.");
}
return new ApiKeyResponseModel(user);
}
[HttpPost("rotate-api-key")]
public async Task<ApiKeyResponseModel> RotateApiKey([FromBody]ApiKeyRequestModel model)
public async Task<ApiKeyResponseModel> RotateApiKey([FromBody]SecretVerificationRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
@ -790,17 +818,15 @@ namespace Bit.Api.Controllers
throw new UnauthorizedAccessException();
}
if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
if (!await _userService.VerifySecretAsync(user, model.Secret))
{
await Task.Delay(2000);
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
}
else
{
await _userService.RotateApiKeyAsync(user);
var response = new ApiKeyResponseModel(user);
return response;
throw new BadRequestException(string.Empty, "User verification failed.");
}
await _userService.RotateApiKeyAsync(user);
var response = new ApiKeyResponseModel(user);
return response;
}
[HttpPut("update-temp-password")]
@ -825,5 +851,33 @@ namespace Bit.Api.Controllers
throw new BadRequestException(ModelState);
}
[HttpPost("request-otp")]
public async Task PostRequestOTP()
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user is not { UsesKeyConnector: true })
{
throw new UnauthorizedAccessException();
}
await _userService.SendOTPAsync(user);
}
[HttpPost("verify-otp")]
public async Task VerifyOTP([FromBody]VerifyOTPRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user is not { UsesKeyConnector: true })
{
throw new UnauthorizedAccessException();
}
if (!await _userService.VerifyOTPAsync(user, model.OTP))
{
await Task.Delay(2000);
throw new BadRequestException("Token", "Invalid token");
}
}
}
}

View File

@ -550,7 +550,7 @@ namespace Bit.Api.Controllers
}
[HttpPost("purge")]
public async Task PostPurge([FromBody]CipherPurgeRequestModel model, string organizationId = null)
public async Task PostPurge([FromBody]SecretVerificationRequestModel model, string organizationId = null)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
@ -558,9 +558,9 @@ namespace Bit.Api.Controllers
throw new UnauthorizedAccessException();
}
if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
if (!await _userService.VerifySecretAsync(user, model.Secret))
{
ModelState.AddModelError("MasterPasswordHash", "Invalid password.");
ModelState.AddModelError(string.Empty, "User verification failed.");
await Task.Delay(2000);
throw new BadRequestException(ModelState);
}

View File

@ -390,7 +390,7 @@ namespace Bit.Api.Controllers
[HttpDelete("{id}")]
[HttpPost("{id}/delete")]
public async Task Delete(string id, [FromBody]OrganizationDeleteRequestModel model)
public async Task Delete(string id, [FromBody]SecretVerificationRequestModel model)
{
var orgIdGuid = new Guid(id);
if (!await _currentContext.OrganizationOwner(orgIdGuid))
@ -410,10 +410,10 @@ namespace Bit.Api.Controllers
throw new UnauthorizedAccessException();
}
if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
if (!await _userService.VerifySecretAsync(user, model.Secret))
{
await Task.Delay(2000);
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
throw new BadRequestException(string.Empty, "User verification failed.");
}
else
{
@ -466,7 +466,7 @@ namespace Bit.Api.Controllers
}
[HttpPost("{id}/api-key")]
public async Task<ApiKeyResponseModel> ApiKey(string id, [FromBody]ApiKeyRequestModel model)
public async Task<ApiKeyResponseModel> ApiKey(string id, [FromBody]SecretVerificationRequestModel model)
{
var orgIdGuid = new Guid(id);
if (!await _currentContext.OrganizationOwner(orgIdGuid))
@ -486,7 +486,7 @@ namespace Bit.Api.Controllers
throw new UnauthorizedAccessException();
}
if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
if (!await _userService.VerifySecretAsync(user, model.Secret))
{
await Task.Delay(2000);
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
@ -499,7 +499,7 @@ namespace Bit.Api.Controllers
}
[HttpPost("{id}/rotate-api-key")]
public async Task<ApiKeyResponseModel> RotateApiKey(string id, [FromBody]ApiKeyRequestModel model)
public async Task<ApiKeyResponseModel> RotateApiKey(string id, [FromBody]SecretVerificationRequestModel model)
{
var orgIdGuid = new Guid(id);
if (!await _currentContext.OrganizationOwner(orgIdGuid))
@ -519,7 +519,7 @@ namespace Bit.Api.Controllers
throw new UnauthorizedAccessException();
}
if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
if (!await _userService.VerifySecretAsync(user, model.Secret))
{
await Task.Delay(2000);
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
@ -640,14 +640,7 @@ namespace Bit.Api.Controllers
}
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(id);
if (ssoConfig == null)
{
ssoConfig = model.ToSsoConfig(id);
}
else
{
ssoConfig = model.ToSsoConfig(ssoConfig);
}
ssoConfig = ssoConfig == null ? model.ToSsoConfig(id) : model.ToSsoConfig(ssoConfig);
await _ssoConfigService.SaveAsync(ssoConfig);

View File

@ -80,9 +80,9 @@ namespace Bit.Api.Controllers
}
[HttpPost("get-authenticator")]
public async Task<TwoFactorAuthenticatorResponseModel> GetAuthenticator([FromBody]TwoFactorRequestModel model)
public async Task<TwoFactorAuthenticatorResponseModel> GetAuthenticator([FromBody]SecretVerificationRequestModel model)
{
var user = await CheckAsync(model.MasterPasswordHash, false);
var user = await CheckAsync(model, false);
var response = new TwoFactorAuthenticatorResponseModel(user);
return response;
}
@ -92,7 +92,7 @@ namespace Bit.Api.Controllers
public async Task<TwoFactorAuthenticatorResponseModel> PutAuthenticator(
[FromBody]UpdateTwoFactorAuthenticatorRequestModel model)
{
var user = await CheckAsync(model.MasterPasswordHash, false);
var user = await CheckAsync(model, false);
model.ToUser(user);
if (!await _userManager.VerifyTwoFactorTokenAsync(user,
@ -108,9 +108,9 @@ namespace Bit.Api.Controllers
}
[HttpPost("get-yubikey")]
public async Task<TwoFactorYubiKeyResponseModel> GetYubiKey([FromBody]TwoFactorRequestModel model)
public async Task<TwoFactorYubiKeyResponseModel> GetYubiKey([FromBody]SecretVerificationRequestModel model)
{
var user = await CheckAsync(model.MasterPasswordHash, true);
var user = await CheckAsync(model, true);
var response = new TwoFactorYubiKeyResponseModel(user);
return response;
}
@ -119,7 +119,7 @@ namespace Bit.Api.Controllers
[HttpPost("yubikey")]
public async Task<TwoFactorYubiKeyResponseModel> PutYubiKey([FromBody]UpdateTwoFactorYubicoOtpRequestModel model)
{
var user = await CheckAsync(model.MasterPasswordHash, true);
var user = await CheckAsync(model, true);
model.ToUser(user);
await ValidateYubiKeyAsync(user, nameof(model.Key1), model.Key1);
@ -134,9 +134,9 @@ namespace Bit.Api.Controllers
}
[HttpPost("get-duo")]
public async Task<TwoFactorDuoResponseModel> GetDuo([FromBody]TwoFactorRequestModel model)
public async Task<TwoFactorDuoResponseModel> GetDuo([FromBody]SecretVerificationRequestModel model)
{
var user = await CheckAsync(model.MasterPasswordHash, true);
var user = await CheckAsync(model, true);
var response = new TwoFactorDuoResponseModel(user);
return response;
}
@ -145,7 +145,7 @@ namespace Bit.Api.Controllers
[HttpPost("duo")]
public async Task<TwoFactorDuoResponseModel> PutDuo([FromBody]UpdateTwoFactorDuoRequestModel model)
{
var user = await CheckAsync(model.MasterPasswordHash, true);
var user = await CheckAsync(model, true);
try
{
var duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host);
@ -164,9 +164,9 @@ namespace Bit.Api.Controllers
[HttpPost("~/organizations/{id}/two-factor/get-duo")]
public async Task<TwoFactorDuoResponseModel> GetOrganizationDuo(string id,
[FromBody]TwoFactorRequestModel model)
[FromBody]SecretVerificationRequestModel model)
{
var user = await CheckAsync(model.MasterPasswordHash, false);
var user = await CheckAsync(model, false);
var orgIdGuid = new Guid(id);
if (!await _currentContext.ManagePolicies(orgIdGuid))
@ -189,7 +189,7 @@ namespace Bit.Api.Controllers
public async Task<TwoFactorDuoResponseModel> PutOrganizationDuo(string id,
[FromBody]UpdateTwoFactorDuoRequestModel model)
{
var user = await CheckAsync(model.MasterPasswordHash, false);
var user = await CheckAsync(model, false);
var orgIdGuid = new Guid(id);
if (!await _currentContext.ManagePolicies(orgIdGuid))
@ -221,17 +221,17 @@ namespace Bit.Api.Controllers
}
[HttpPost("get-webauthn")]
public async Task<TwoFactorWebAuthnResponseModel> GetWebAuthn([FromBody]TwoFactorRequestModel model)
public async Task<TwoFactorWebAuthnResponseModel> GetWebAuthn([FromBody]SecretVerificationRequestModel model)
{
var user = await CheckAsync(model.MasterPasswordHash, true);
var user = await CheckAsync(model, true);
var response = new TwoFactorWebAuthnResponseModel(user);
return response;
}
[HttpPost("get-webauthn-challenge")]
public async Task<CredentialCreateOptions> GetWebAuthnChallenge([FromBody]TwoFactorRequestModel model)
public async Task<CredentialCreateOptions> GetWebAuthnChallenge([FromBody]SecretVerificationRequestModel model)
{
var user = await CheckAsync(model.MasterPasswordHash, true);
var user = await CheckAsync(model, true);
var reg = await _userService.StartWebAuthnRegistrationAsync(user);
return reg;
}
@ -240,7 +240,7 @@ namespace Bit.Api.Controllers
[HttpPost("webauthn")]
public async Task<TwoFactorWebAuthnResponseModel> PutWebAuthn([FromBody]TwoFactorWebAuthnRequestModel model)
{
var user = await CheckAsync(model.MasterPasswordHash, true);
var user = await CheckAsync(model, true);
var success = await _userService.CompleteWebAuthRegistrationAsync(
user, model.Id.Value, model.Name, model.DeviceResponse);
@ -255,16 +255,16 @@ namespace Bit.Api.Controllers
[HttpDelete("webauthn")]
public async Task<TwoFactorWebAuthnResponseModel> DeleteWebAuthn([FromBody]TwoFactorWebAuthnDeleteRequestModel model)
{
var user = await CheckAsync(model.MasterPasswordHash, true);
var user = await CheckAsync(model, true);
await _userService.DeleteWebAuthnKeyAsync(user, model.Id.Value);
var response = new TwoFactorWebAuthnResponseModel(user);
return response;
}
[HttpPost("get-email")]
public async Task<TwoFactorEmailResponseModel> GetEmail([FromBody]TwoFactorRequestModel model)
public async Task<TwoFactorEmailResponseModel> GetEmail([FromBody]SecretVerificationRequestModel model)
{
var user = await CheckAsync(model.MasterPasswordHash, false);
var user = await CheckAsync(model, false);
var response = new TwoFactorEmailResponseModel(user);
return response;
}
@ -272,7 +272,7 @@ namespace Bit.Api.Controllers
[HttpPost("send-email")]
public async Task SendEmail([FromBody]TwoFactorEmailRequestModel model)
{
var user = await CheckAsync(model.MasterPasswordHash, false);
var user = await CheckAsync(model, false);
model.ToUser(user);
await _userService.SendTwoFactorEmailAsync(user);
}
@ -284,7 +284,7 @@ namespace Bit.Api.Controllers
var user = await _userManager.FindByEmailAsync(model.Email.ToLowerInvariant());
if (user != null)
{
if (await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
if (await _userService.VerifySecretAsync(user, model.Secret))
{
await _userService.SendTwoFactorEmailAsync(user);
return;
@ -299,7 +299,7 @@ namespace Bit.Api.Controllers
[HttpPost("email")]
public async Task<TwoFactorEmailResponseModel> PutEmail([FromBody]UpdateTwoFactorEmailRequestModel model)
{
var user = await CheckAsync(model.MasterPasswordHash, false);
var user = await CheckAsync(model, false);
model.ToUser(user);
if (!await _userManager.VerifyTwoFactorTokenAsync(user,
@ -318,7 +318,7 @@ namespace Bit.Api.Controllers
[HttpPost("disable")]
public async Task<TwoFactorProviderResponseModel> PutDisable([FromBody]TwoFactorProviderRequestModel model)
{
var user = await CheckAsync(model.MasterPasswordHash, false);
var user = await CheckAsync(model, false);
await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value, _organizationService);
var response = new TwoFactorProviderResponseModel(model.Type.Value, user);
return response;
@ -329,7 +329,7 @@ namespace Bit.Api.Controllers
public async Task<TwoFactorProviderResponseModel> PutOrganizationDisable(string id,
[FromBody]TwoFactorProviderRequestModel model)
{
var user = await CheckAsync(model.MasterPasswordHash, false);
var user = await CheckAsync(model, false);
var orgIdGuid = new Guid(id);
if (!await _currentContext.ManagePolicies(orgIdGuid))
@ -349,9 +349,9 @@ namespace Bit.Api.Controllers
}
[HttpPost("get-recover")]
public async Task<TwoFactorRecoverResponseModel> GetRecover([FromBody]TwoFactorRequestModel model)
public async Task<TwoFactorRecoverResponseModel> GetRecover([FromBody]SecretVerificationRequestModel model)
{
var user = await CheckAsync(model.MasterPasswordHash, false);
var user = await CheckAsync(model, false);
var response = new TwoFactorRecoverResponseModel(user);
return response;
}
@ -368,7 +368,7 @@ namespace Bit.Api.Controllers
}
}
private async Task<User> CheckAsync(string masterPasswordHash, bool premium)
private async Task<User> CheckAsync(SecretVerificationRequestModel model, bool premium)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
@ -376,10 +376,10 @@ namespace Bit.Api.Controllers
throw new UnauthorizedAccessException();
}
if (!await _userService.CheckPasswordAsync(user, masterPasswordHash))
if (!await _userService.VerifySecretAsync(user, model.Secret))
{
await Task.Delay(2000);
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
throw new BadRequestException(string.Empty, "User verification failed.");
}
if (premium && !(await _userService.CanAccessPremium(user)))

View File

@ -11,6 +11,7 @@
User_FailedLogIn2fa = 1006,
User_ClientExportedVault = 1007,
User_UpdatedTempPassword = 1008,
User_MigratedKeyToKeyConnector = 1009,
Cipher_Created = 1100,
Cipher_Updated = 1101,
@ -54,6 +55,10 @@
Organization_PurgedVault = 1601,
// Organization_ClientExportedVault = 1602,
Organization_VaultAccessed = 1603,
Organization_EnabledSso = 1604,
Organization_DisabledSso = 1605,
Organization_EnabledKeyConnector = 1606,
Organization_DisabledKeyConnector = 1607,
Policy_Updated = 1700,

View File

@ -176,7 +176,7 @@ namespace Bit.Core.IdentityServer
customResponse.Add("TwoFactorToken", token);
}
SetSuccessResult(context, user, claims, customResponse);
await SetSuccessResult(context, user, claims, customResponse);
}
protected async Task BuildTwoFactorResultAsync(User user, Organization organization, T context)
@ -256,7 +256,7 @@ namespace Bit.Core.IdentityServer
protected abstract void SetSsoResult(T context, Dictionary<string, object> customResponse);
protected abstract void SetSuccessResult(T context, User user, List<Claim> claims,
protected abstract Task SetSuccessResult(T context, User user, List<Claim> claims,
Dictionary<string, object> customResponse);
protected abstract void SetErrorResult(T context, Dictionary<string, object> customResponse);

View File

@ -24,6 +24,7 @@ namespace Bit.Core.IdentityServer
{
private UserManager<User> _userManager;
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly IOrganizationRepository _organizationRepository;
public CustomTokenRequestValidator(
UserManager<User> userManager,
@ -47,6 +48,7 @@ namespace Bit.Core.IdentityServer
{
_userManager = userManager;
_ssoConfigRepository = ssoConfigRepository;
_organizationRepository = organizationRepository;
}
public async Task ValidateAsync(CustomTokenRequestValidationContext context)
@ -58,25 +60,6 @@ namespace Bit.Core.IdentityServer
return;
}
await ValidateAsync(context, context.Result.ValidatedRequest);
if (context.Result.CustomResponse != null)
{
var organizationClaim = context.Result.ValidatedRequest.Subject?.FindFirst(c => c.Type == "organizationId");
var organizationId = organizationClaim?.Value ?? "";
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(new Guid(organizationId));
var ssoConfigData = ssoConfig.GetData();
if (ssoConfigData is { UseCryptoAgent: true } && !string.IsNullOrEmpty(ssoConfigData.CryptoAgentUrl))
{
context.Result.CustomResponse["CryptoAgentUrl"] = ssoConfigData.CryptoAgentUrl;
// Prevent clients redirecting to set-password
// TODO: Figure out if we can move this logic to the clients since this might break older clients
// although we will have issues either way with some clients supporting crypto anent and some not
// suggestion: We should roll out the clients before enabling it server wise
context.Result.CustomResponse["ResetMasterPassword"] = false;
}
}
}
protected async override Task<(User, bool)> ValidateContextAsync(CustomTokenRequestValidationContext context)
@ -87,7 +70,7 @@ namespace Bit.Core.IdentityServer
return (user, user != null);
}
protected override void SetSuccessResult(CustomTokenRequestValidationContext context, User user,
protected override async Task SetSuccessResult(CustomTokenRequestValidationContext context, User user,
List<Claim> claims, Dictionary<string, object> customResponse)
{
context.Result.CustomResponse = customResponse;
@ -100,6 +83,40 @@ namespace Bit.Core.IdentityServer
context.Result.ValidatedRequest.ClientClaims.Add(claim);
}
}
if (context.Result.CustomResponse == null || user.MasterPassword != null)
{
return;
}
// KeyConnector responses below
// Apikey login
if (context.Result.ValidatedRequest.GrantType == "client_credentials")
{
if (user.UsesKeyConnector) {
// KeyConnectorUrl is configured in the CLI client, just disable master password reset
context.Result.CustomResponse["ResetMasterPassword"] = false;
}
return;
}
// SSO login
var organizationClaim = context.Result.ValidatedRequest.Subject?.FindFirst(c => c.Type == "organizationId");
if (organizationClaim?.Value != null)
{
var organizationId = new Guid(organizationClaim.Value);
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organizationId);
var ssoConfigData = ssoConfig.GetData();
if (ssoConfigData is { UseKeyConnector: true } && !string.IsNullOrEmpty(ssoConfigData.KeyConnectorUrl))
{
context.Result.CustomResponse["KeyConnectorUrl"] = ssoConfigData.KeyConnectorUrl;
// Prevent clients redirecting to set-password
context.Result.CustomResponse["ResetMasterPassword"] = false;
}
}
}
protected override void SetTwoFactorResult(CustomTokenRequestValidationContext context,

View File

@ -106,13 +106,14 @@ namespace Bit.Core.IdentityServer
return (user, true);
}
protected override void SetSuccessResult(ResourceOwnerPasswordValidationContext context, User user,
protected override Task SetSuccessResult(ResourceOwnerPasswordValidationContext context, User user,
List<Claim> claims, Dictionary<string, object> customResponse)
{
context.Result = new GrantValidationResult(user.Id.ToString(), "Application",
identityProvider: "bitwarden",
claims: claims.Count > 0 ? claims : null,
customResponse: customResponse);
return Task.CompletedTask;
}
protected override void SetTwoFactorResult(ResourceOwnerPasswordValidationContext context,

View File

@ -0,0 +1,14 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; box-sizing: border-box;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
Your email verification code is: <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{Token}}</b>
</td>
</tr>
<tr style="margin: 0; box-sizing: border-box;">
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
Use this code to complete the protected action in Bitwarden.
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@ -0,0 +1,5 @@
{{#>BasicTextLayout}}
Your email verification code is: {{Token}}
Use this code to complete the protected action in Bitwarden.
{{/BasicTextLayout}}

View File

@ -1,10 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Api
{
public class DeleteAccountRequestModel
{
[Required]
public string MasterPasswordHash { get; set; }
}
}

View File

@ -1,10 +1,10 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using Bit.Core.Utilities;
using Bit.Core.Models.Api;
namespace Bit.Core.Models.Api
{
public class EmailRequestModel
public class EmailRequestModel : SecretVerificationRequestModel
{
[Required]
[StrictEmailAddress]
@ -12,9 +12,6 @@ namespace Bit.Core.Models.Api
public string NewEmail { get; set; }
[Required]
[StringLength(300)]
public string MasterPasswordHash { get; set; }
[Required]
[StringLength(300)]
public string NewMasterPasswordHash { get; set; }
[Required]
public string Token { get; set; }

View File

@ -1,16 +1,14 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Utilities;
using Bit.Core.Models.Api;
namespace Bit.Core.Models.Api
{
public class EmailTokenRequestModel
public class EmailTokenRequestModel : SecretVerificationRequestModel
{
[Required]
[StrictEmailAddress]
[StringLength(256)]
public string NewEmail { get; set; }
[Required]
[StringLength(300)]
public string MasterPasswordHash { get; set; }
}
}

View File

@ -1,13 +1,9 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Api
{
public class PasswordRequestModel
public class PasswordRequestModel : SecretVerificationRequestModel
{
[Required]
[StringLength(300)]
public string MasterPasswordHash { get; set; }
[Required]
[StringLength(300)]
public string NewMasterPasswordHash { get; set; }

View File

@ -0,0 +1,21 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Api
{
public class SecretVerificationRequestModel : IValidatableObject
{
[StringLength(300)]
public string MasterPasswordHash { get; set; }
public string OTP { get; set; }
public string Secret => !string.IsNullOrEmpty(MasterPasswordHash) ? MasterPasswordHash : OTP;
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrEmpty(Secret))
{
yield return new ValidationResult("MasterPasswordHash or OTP must be supplied.");
}
}
}
}

View File

@ -1,10 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Api
{
public class SecurityStampRequestModel
{
[Required]
public string MasterPasswordHash { get; set; }
}
}

View File

@ -4,7 +4,7 @@ using Bit.Core.Models.Table;
namespace Bit.Core.Models.Api.Request.Accounts
{
public class SetCryptoAgentKeyRequestModel
public class SetKeyConnectorKeyRequestModel
{
[Required]
public string Key { get; set; }

View File

@ -2,9 +2,9 @@
namespace Bit.Core.Models.Api
{
public class ApiKeyRequestModel
public class VerifyOTPRequestModel
{
[Required]
public string MasterPasswordHash { get; set; }
public string OTP { get; set; }
}
}

View File

@ -1,12 +0,0 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Api
{
public class VerifyPasswordRequestModel
{
[Required]
[StringLength(300)]
public string MasterPasswordHash { get; set; }
}
}

View File

@ -1,10 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Api
{
public class CipherPurgeRequestModel
{
[Required]
public string MasterPasswordHash { get; set; }
}
}

View File

@ -1,10 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Api
{
public class OrganizationDeleteRequestModel
{
[Required]
public string MasterPasswordHash { get; set; }
}
}

View File

@ -8,7 +8,6 @@ using Bit.Core.Sso;
using U2F.Core.Utils;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using System.Text.RegularExpressions;
using Bit.Core.Models.Table;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
@ -40,47 +39,11 @@ namespace Bit.Core.Models.Api
{
public SsoConfigurationDataRequest() {}
public SsoConfigurationDataRequest(SsoConfigurationData configurationData)
{
ConfigType = configurationData.ConfigType;
UseCryptoAgent = configurationData.UseCryptoAgent;
CryptoAgentUrl = configurationData.CryptoAgentUrl;
Authority = configurationData.Authority;
ClientId = configurationData.ClientId;
ClientSecret = configurationData.ClientSecret;
MetadataAddress = configurationData.MetadataAddress;
RedirectBehavior = configurationData.RedirectBehavior;
GetClaimsFromUserInfoEndpoint = configurationData.GetClaimsFromUserInfoEndpoint;
IdpEntityId = configurationData.IdpEntityId;
IdpBindingType = configurationData.IdpBindingType;
IdpSingleSignOnServiceUrl = configurationData.IdpSingleSignOnServiceUrl;
IdpSingleLogoutServiceUrl = configurationData.IdpSingleLogoutServiceUrl;
IdpArtifactResolutionServiceUrl = configurationData.IdpArtifactResolutionServiceUrl;
IdpX509PublicCert = configurationData.IdpX509PublicCert;
IdpOutboundSigningAlgorithm = configurationData.IdpOutboundSigningAlgorithm;
IdpAllowUnsolicitedAuthnResponse = configurationData.IdpAllowUnsolicitedAuthnResponse;
IdpDisableOutboundLogoutRequests = configurationData.IdpDisableOutboundLogoutRequests;
IdpWantAuthnRequestsSigned = configurationData.IdpWantAuthnRequestsSigned;
SpNameIdFormat = configurationData.SpNameIdFormat;
SpOutboundSigningAlgorithm = configurationData.SpOutboundSigningAlgorithm ?? SamlSigningAlgorithms.Sha256;
SpSigningBehavior = configurationData.SpSigningBehavior;
SpWantAssertionsSigned = configurationData.SpWantAssertionsSigned;
SpValidateCertificates = configurationData.SpValidateCertificates;
SpMinIncomingSigningAlgorithm = configurationData.SpMinIncomingSigningAlgorithm ?? SamlSigningAlgorithms.Sha256;
AdditionalScopes = configurationData.AdditionalScopes;
AdditionalUserIdClaimTypes = configurationData.AdditionalUserIdClaimTypes;
AdditionalEmailClaimTypes = configurationData.AdditionalEmailClaimTypes;
AdditionalNameClaimTypes = configurationData.AdditionalNameClaimTypes;
AcrValues = configurationData.AcrValues;
ExpectedReturnAcrValue = configurationData.ExpectedReturnAcrValue;
}
[Required]
public SsoType ConfigType { get; set; }
// Crypto Agent
public bool UseCryptoAgent { get; set; }
public string CryptoAgentUrl { get; set; }
public bool UseKeyConnector { get; set; }
public string KeyConnectorUrl { get; set; }
// OIDC
public string Authority { get; set; }
@ -215,8 +178,8 @@ namespace Bit.Core.Models.Api
return new SsoConfigurationData
{
ConfigType = ConfigType,
UseCryptoAgent = UseCryptoAgent,
CryptoAgentUrl = CryptoAgentUrl,
UseKeyConnector = UseKeyConnector,
KeyConnectorUrl = KeyConnectorUrl,
Authority = Authority,
ClientId = ClientId,
ClientSecret = ClientSecret,

View File

@ -7,7 +7,7 @@ using System.Linq;
namespace Bit.Core.Models.Api
{
public class UpdateTwoFactorAuthenticatorRequestModel : TwoFactorRequestModel
public class UpdateTwoFactorAuthenticatorRequestModel : SecretVerificationRequestModel
{
[Required]
[StringLength(50)]
@ -38,7 +38,7 @@ namespace Bit.Core.Models.Api
}
}
public class UpdateTwoFactorDuoRequestModel : TwoFactorRequestModel, IValidatableObject
public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IValidatableObject
{
[Required]
[StringLength(50)]
@ -111,7 +111,7 @@ namespace Bit.Core.Models.Api
}
}
public class UpdateTwoFactorYubicoOtpRequestModel : TwoFactorRequestModel, IValidatableObject
public class UpdateTwoFactorYubicoOtpRequestModel : SecretVerificationRequestModel, IValidatableObject
{
public string Key1 { get; set; }
public string Key2 { get; set; }
@ -195,7 +195,7 @@ namespace Bit.Core.Models.Api
}
}
public class TwoFactorEmailRequestModel : TwoFactorRequestModel
public class TwoFactorEmailRequestModel : SecretVerificationRequestModel
{
[Required]
[EmailAddress]
@ -231,13 +231,18 @@ namespace Bit.Core.Models.Api
public string Name { get; set; }
}
public class TwoFactorWebAuthnDeleteRequestModel : TwoFactorRequestModel, IValidatableObject
public class TwoFactorWebAuthnDeleteRequestModel : SecretVerificationRequestModel, IValidatableObject
{
[Required]
public int? Id { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
foreach (var validationResult in Validate(validationContext))
{
yield return validationResult;
}
if (!Id.HasValue || Id < 0 || Id > 5)
{
yield return new ValidationResult("Invalid Key Id", new string[] { nameof(Id) });
@ -252,18 +257,12 @@ namespace Bit.Core.Models.Api
public string Token { get; set; }
}
public class TwoFactorProviderRequestModel : TwoFactorRequestModel
public class TwoFactorProviderRequestModel : SecretVerificationRequestModel
{
[Required]
public TwoFactorProviderType? Type { get; set; }
}
public class TwoFactorRequestModel
{
[Required]
public string MasterPasswordHash { get; set; }
}
public class TwoFactorRecoveryRequestModel : TwoFactorEmailRequestModel
{
[Required]

View File

@ -79,6 +79,8 @@ namespace Bit.Core.Models.Api
Email = organizationUser.Email;
TwoFactorEnabled = twoFactorEnabled;
SsoBound = !string.IsNullOrWhiteSpace(organizationUser.SsoExternalId);
// Prevent reset password when using key connector.
ResetPasswordEnrolled = ResetPasswordEnrolled && !organizationUser.UsesKeyConnector;
}
public string Name { get; set; }

View File

@ -38,6 +38,12 @@ namespace Bit.Core.Models.Api
UserId = organization.UserId?.ToString();
ProviderId = organization.ProviderId?.ToString();
ProviderName = organization.ProviderName;
if (organization.SsoConfig != null)
{
var ssoConfigData = SsoConfigurationData.Deserialize(organization.SsoConfig);
UsesKeyConnector = ssoConfigData.UseKeyConnector && !string.IsNullOrEmpty(ssoConfigData.KeyConnectorUrl);
KeyConnectorUrl = ssoConfigData.KeyConnectorUrl;
}
}
public string Id { get; set; }
@ -68,5 +74,7 @@ namespace Bit.Core.Models.Api
public bool HasPublicAndPrivateKeys { get; set; }
public string ProviderId { get; set; }
public string ProviderName { get; set; }
public bool UsesKeyConnector { get; set; }
public string KeyConnectorUrl { get; set; }
}
}

View File

@ -31,6 +31,7 @@ namespace Bit.Core.Models.Api
PrivateKey = user.PrivateKey;
SecurityStamp = user.SecurityStamp;
ForcePasswordReset = user.ForcePasswordReset;
UsesKeyConnector = user.UsesKeyConnector;
Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o));
Providers = providerUserDetails?.Select(p => new ProfileProviderResponseModel(p));
ProviderOrganizations =
@ -49,6 +50,7 @@ namespace Bit.Core.Models.Api
public string PrivateKey { get; set; }
public string SecurityStamp { get; set; }
public bool ForcePasswordReset { get; set; }
public bool UsesKeyConnector { get; set; }
public IEnumerable<ProfileOrganizationResponseModel> Organizations { get; set; }
public IEnumerable<ProfileProviderResponseModel> Providers { get; set; }
public IEnumerable<ProfileProviderOrganizationResponseModel> ProviderOrganizations { get; set; }

View File

@ -33,5 +33,6 @@ namespace Bit.Core.Models.Data
public string PrivateKey { get; set; }
public Guid? ProviderId { get; set; }
public string ProviderName { get; set; }
public string SsoConfig { get; set; }
}
}

View File

@ -23,6 +23,7 @@ namespace Bit.Core.Models.Data
public string SsoExternalId { get; set; }
public string Permissions { get; set; }
public string ResetPasswordKey { get; set; }
public bool UsesKeyConnector { get; set; }
public Dictionary<TwoFactorProviderType, TwoFactorProvider> GetTwoFactorProviders()
{

View File

@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Bit.Core.Enums;
using Bit.Core.Sso;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
namespace Bit.Core.Models.Data
@ -13,11 +15,20 @@ namespace Bit.Core.Models.Data
private const string _oidcSignedOutPath = "/oidc-signedout";
private const string _saml2ModulePath = "/saml2";
public static SsoConfigurationData Deserialize(string data)
{
return CoreHelpers.LoadClassFromJsonData<SsoConfigurationData>(data);
}
public string Serialize()
{
return CoreHelpers.ClassToJsonData(this);
}
public SsoType ConfigType { get; set; }
// Crypto Agent
public bool UseCryptoAgent { get; set; }
public string CryptoAgentUrl { get; set; }
public bool UseKeyConnector { get; set; }
public string KeyConnectorUrl { get; set; }
// OIDC
public string Authority { get; set; }

View File

@ -1,5 +1,4 @@
using System;
using System.Text.Json;
using Bit.Core.Models.Data;
namespace Bit.Core.Models.Table
@ -13,11 +12,6 @@ namespace Bit.Core.Models.Table
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
private JsonSerializerOptions _jsonSerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
public void SetNewId()
{
// int will be auto-populated
@ -26,12 +20,12 @@ namespace Bit.Core.Models.Table
public SsoConfigurationData GetData()
{
return JsonSerializer.Deserialize<SsoConfigurationData>(Data, _jsonSerializerOptions);
return SsoConfigurationData.Deserialize(Data);
}
public void SetData(SsoConfigurationData data)
{
Data = JsonSerializer.Serialize(data, _jsonSerializerOptions);
Data = data.Serialize();
}
}
}

View File

@ -58,7 +58,7 @@ namespace Bit.Core.Models.Table
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
public bool ForcePasswordReset { get; set; }
public bool UsesCryptoAgent { get; set; }
public bool UsesKeyConnector { get; set; }
public void SetNewId()
{

View File

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using Bit.Core.Models.Data;
using Microsoft.Azure.Documents.SystemFunctions;
namespace Bit.Core.Repositories.EntityFramework.Queries
{
@ -16,8 +17,10 @@ namespace Bit.Core.Repositories.EntityFramework.Queries
from po in po_g.DefaultIfEmpty()
join p in dbContext.Providers on po.ProviderId equals p.Id into p_g
from p in p_g.DefaultIfEmpty()
join ss in dbContext.SsoConfigs on ou.OrganizationId equals ss.OrganizationId into ss_g
from ss in ss_g.DefaultIfEmpty()
where ((su == null || !su.OrganizationId.HasValue) || su.OrganizationId == ou.OrganizationId)
select new { ou, o, su, p };
select new { ou, o, su, p, ss };
return query.Select(x => new OrganizationUserOrganizationDetails
{
OrganizationId = x.ou.OrganizationId,
@ -48,6 +51,7 @@ namespace Bit.Core.Repositories.EntityFramework.Queries
PrivateKey = x.o.PrivateKey,
ProviderId = x.p.Id,
ProviderName = x.p.Name,
SsoConfig = x.ss.Data,
});
}
}

View File

@ -29,6 +29,7 @@ namespace Bit.Core.Repositories.EntityFramework.Queries
SsoExternalId = x.su.ExternalId,
Permissions = x.ou.Permissions,
ResetPasswordKey = x.ou.ResetPasswordKey,
UsesKeyConnector = x.u.UsesKeyConnector,
});
}
}

View File

@ -49,5 +49,6 @@ namespace Bit.Core.Services
Task SendProviderConfirmedEmailAsync(string providerName, string email);
Task SendProviderUserRemoved(string providerName, string email);
Task SendUpdatedTempPasswordEmailAsync(string email, string userName);
Task SendOTPEmailAsync(string email, string token);
}
}

View File

@ -34,7 +34,8 @@ namespace Bit.Core.Services
string token, string key);
Task<IdentityResult> ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, string key);
Task<IdentityResult> SetPasswordAsync(User user, string newMasterPassword, string key, string orgIdentifier = null);
Task<IdentityResult> SetCryptoAgentKeyAsync(User user, string key, string orgIdentifier);
Task<IdentityResult> SetKeyConnectorKeyAsync(User user, string key, string orgIdentifier);
Task<IdentityResult> ConvertToKeyConnectorAsync(User user);
Task<IdentityResult> AdminResetPasswordAsync(OrganizationUserType type, Guid orgId, Guid id, string newMasterPassword, string key);
Task<IdentityResult> UpdateTempPasswordAsync(User user, string newMasterPassword, string key, string hint);
Task<IdentityResult> ChangeKdfAsync(User user, string masterPassword, string newMasterPassword, string key,
@ -74,5 +75,8 @@ namespace Bit.Core.Services
Task<string> GenerateSignInTokenAsync(User user, string purpose);
Task RotateApiKeyAsync(User user);
string GetUserName(ClaimsPrincipal principal);
Task SendOTPAsync(User user);
Task<bool> VerifyOTPAsync(User user, string token);
Task<bool> VerifySecretAsync(User user, string secret);
}
}

View File

@ -755,5 +755,20 @@ namespace Bit.Core.Services
message.Category = "UpdatedTempPassword";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendOTPEmailAsync(string email, string token)
{
var message = CreateDefaultMessage("Your Bitwarden Verification Code", email);
var model = new EmailTokenViewModel
{
Token = token,
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
};
await AddMessageContentAsync(message, "OTPEmail", model);
message.MetaData.Add("SendGridBypassListManagement", true);
message.Category = "OTP";
await _mailDeliveryService.SendEmailAsync(message);
}
}
}

View File

@ -908,6 +908,8 @@ namespace Bit.Core.Services
public async Task DeleteAsync(Organization organization)
{
await ValidateDeleteOrganizationAsync(organization);
if (!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{
try
@ -2135,5 +2137,14 @@ namespace Bit.Core.Services
throw new BadRequestException("Custom users can not manage Admins or Owners.");
}
}
private async Task ValidateDeleteOrganizationAsync(Organization organization)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
if (ssoConfig?.GetData()?.UseKeyConnector == true)
{
throw new BadRequestException("You cannot delete an Organization that is using Key Connector.");
}
}
}
}

View File

@ -15,6 +15,7 @@ namespace Bit.Core.Services
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IPolicyRepository _policyRepository;
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly IMailService _mailService;
public PolicyService(
@ -22,12 +23,14 @@ namespace Bit.Core.Services
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IPolicyRepository policyRepository,
ISsoConfigRepository ssoConfigRepository,
IMailService mailService)
{
_eventService = eventService;
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
_policyRepository = policyRepository;
_ssoConfigRepository = ssoConfigRepository;
_mailService = mailService;
}
@ -64,6 +67,12 @@ namespace Bit.Core.Services
{
throw new BadRequestException("Maximum Vault Timeout policy is enabled.");
}
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(org.Id);
if (ssoConfig?.GetData()?.UseKeyConnector == true)
{
throw new BadRequestException("KeyConnector is enabled.");
}
}
break;

View File

@ -1,5 +1,7 @@
using System;
using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
@ -8,11 +10,20 @@ namespace Bit.Core.Services
public class SsoConfigService : ISsoConfigService
{
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly IPolicyRepository _policyRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly IEventService _eventService;
public SsoConfigService(
ISsoConfigRepository ssoConfigRepository)
ISsoConfigRepository ssoConfigRepository,
IPolicyRepository policyRepository,
IOrganizationRepository organizationRepository,
IEventService eventService)
{
_ssoConfigRepository = ssoConfigRepository;
_policyRepository = policyRepository;
_organizationRepository = organizationRepository;
_eventService = eventService;
}
public async Task SaveAsync(SsoConfig config)
@ -23,7 +34,49 @@ namespace Bit.Core.Services
{
config.CreationDate = now;
}
var useKeyConnector = config.GetData().UseKeyConnector;
if (useKeyConnector)
{
await VerifyDependenciesAsync(config);
}
var oldConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(config.OrganizationId);
if (oldConfig?.GetData()?.UseKeyConnector == true && !useKeyConnector)
{
throw new BadRequestException("KeyConnector cannot be disabled at this moment.");
}
await LogEventsAsync(config, oldConfig);
await _ssoConfigRepository.UpsertAsync(config);
}
private async Task VerifyDependenciesAsync(SsoConfig config)
{
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.SingleOrg);
if (policy is not { Enabled: true })
{
throw new BadRequestException("KeyConnector requires Single Organization to be enabled.");
}
}
private async Task LogEventsAsync(SsoConfig config, SsoConfig oldConfig)
{
var organization = await _organizationRepository.GetByIdAsync(config.OrganizationId);
if (oldConfig?.Enabled != config.Enabled)
{
var e = config.Enabled ? EventType.Organization_EnabledSso : EventType.Organization_DisabledSso;
await _eventService.LogOrganizationEventAsync(organization, e);
}
var useKeyConnector = config.GetData().UseKeyConnector;
if (oldConfig?.GetData()?.UseKeyConnector != useKeyConnector)
{
var e = useKeyConnector
? EventType.Organization_EnabledKeyConnector
: EventType.Organization_DisabledKeyConnector;
await _eventService.LogOrganizationEventAsync(organization, e);
}
}
}
}

View File

@ -1,26 +1,27 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models;
using Bit.Core.Models.Business;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Fido2NetLib;
using Fido2NetLib.Objects;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
using System.Linq;
using Bit.Core.Enums;
using System.Security.Claims;
using Bit.Core.Models;
using Bit.Core.Models.Business;
using U2fLib = U2F.Core.Crypto.U2F;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Utilities;
using Bit.Core.Settings;
using System.IO;
using Newtonsoft.Json;
using Microsoft.AspNetCore.DataProtection;
using Fido2NetLib;
using Fido2NetLib.Objects;
using File = System.IO.File;
using U2fLib = U2F.Core.Crypto.U2F;
namespace Bit.Core.Services
{
@ -602,7 +603,7 @@ namespace Bit.Core.Services
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
}
public async Task<IdentityResult> SetPasswordAsync(User user, string masterPassword, string key,
public async Task<IdentityResult> SetPasswordAsync(User user, string masterPassword, string key,
string orgIdentifier = null)
{
if (user == null)
@ -627,42 +628,63 @@ namespace Bit.Core.Services
await _userRepository.ReplaceAsync(user);
await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);
if (!string.IsNullOrWhiteSpace(orgIdentifier))
{
await _organizationService.AcceptUserAsync(orgIdentifier, user, this);
}
return IdentityResult.Success;
}
public async Task<IdentityResult> SetCryptoAgentKeyAsync(User user, string key, string orgIdentifier)
public async Task<IdentityResult> SetKeyConnectorKeyAsync(User user, string key, string orgIdentifier)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (user.UsesCryptoAgent)
if (user.UsesKeyConnector)
{
Logger.LogWarning("Already uses crypto agent.");
Logger.LogWarning("Already uses key connector.");
return IdentityResult.Failed(_identityErrorDescriber.UserAlreadyHasPassword());
}
user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;
user.Key = key;
user.UsesCryptoAgent = true;
user.UsesKeyConnector = true;
await _userRepository.ReplaceAsync(user);
// TODO: Use correct event
await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);
await _eventService.LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);
await _organizationService.AcceptUserAsync(orgIdentifier, user, this);
return IdentityResult.Success;
}
public async Task<IdentityResult> ConvertToKeyConnectorAsync(User user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (user.UsesKeyConnector)
{
Logger.LogWarning("Already uses key connector.");
return IdentityResult.Failed(_identityErrorDescriber.UserAlreadyHasPassword());
}
user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;
user.MasterPassword = null;
user.UsesKeyConnector = true;
await _userRepository.ReplaceAsync(user);
await _eventService.LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);
return IdentityResult.Success;
}
public async Task<IdentityResult> AdminResetPasswordAsync(OrganizationUserType callingUserType, Guid orgId, Guid id, string newMasterPassword, string key)
{
// Org must be able to use reset password
@ -671,15 +693,15 @@ namespace Bit.Core.Services
{
throw new BadRequestException("Organization does not allow password reset.");
}
// Enterprise policy must be enabled
// Enterprise policy must be enabled
var resetPasswordPolicy =
await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled)
{
throw new BadRequestException("Organization does not have the password reset policy enabled.");
}
// Org User must be confirmed and have a ResetPasswordKey
var orgUser = await _organizationUserRepository.GetByIdAsync(id);
if (orgUser == null || orgUser.Status != OrganizationUserStatusType.Confirmed ||
@ -688,7 +710,7 @@ namespace Bit.Core.Services
{
throw new BadRequestException("Organization User not valid");
}
// Calling User must be of higher/equal user type to reset user's password
var canAdjustPassword = false;
switch (callingUserType)
@ -715,7 +737,12 @@ namespace Bit.Core.Services
{
throw new NotFoundException();
}
if (user.UsesKeyConnector)
{
throw new BadRequestException("Cannot reset password of a user with key connector.");
}
var result = await UpdatePasswordHash(user, newMasterPassword);
if (!result.Succeeded)
{
@ -733,14 +760,14 @@ namespace Bit.Core.Services
return IdentityResult.Success;
}
public async Task<IdentityResult> UpdateTempPasswordAsync(User user, string newMasterPassword, string key, string hint)
{
if (!user.ForcePasswordReset)
{
throw new BadRequestException("User does not have a temporary password to update.");
}
var result = await UpdatePasswordHash(user, newMasterPassword);
if (!result.Succeeded)
{
@ -820,14 +847,14 @@ namespace Bit.Core.Services
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
}
public async Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPassword)
public async Task<IdentityResult> RefreshSecurityStampAsync(User user, string secret)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (await CheckPasswordAsync(user, masterPassword))
if (await VerifySecretAsync(user, secret))
{
var result = await base.UpdateSecurityStampAsync(user);
if (!result.Succeeded)
@ -878,7 +905,7 @@ namespace Bit.Core.Services
}
}
public async Task<bool> RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode,
public async Task<bool> RecoverTwoFactorAsync(string email, string secret, string recoveryCode,
IOrganizationService organizationService)
{
var user = await _userRepository.GetByEmailAsync(email);
@ -888,7 +915,7 @@ namespace Bit.Core.Services
return false;
}
if (!await CheckPasswordAsync(user, masterPassword))
if (!await VerifySecretAsync(user, secret))
{
return false;
}
@ -1253,7 +1280,7 @@ namespace Bit.Core.Services
purpose);
return token;
}
private async Task<IdentityResult> UpdatePasswordHash(User user, string newPassword,
bool validatePassword = true, bool refreshStamp = true)
{
@ -1350,5 +1377,35 @@ namespace Bit.Core.Services
user.RevisionDate = DateTime.UtcNow;
await _userRepository.ReplaceAsync(user);
}
public async Task SendOTPAsync(User user)
{
if (user.Email == null)
{
throw new BadRequestException("No user email.");
}
if (!user.UsesKeyConnector)
{
throw new BadRequestException("Not using key connector.");
}
var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultEmailProvider,
"otp:" + user.Email);
await _mailService.SendOTPEmailAsync(user.Email, token);
}
public Task<bool> VerifyOTPAsync(User user, string token)
{
return base.VerifyUserTokenAsync(user, TokenOptions.DefaultEmailProvider,
"otp:" + user.Email, token);
}
public async Task<bool> VerifySecretAsync(User user, string secret)
{
return user.UsesKeyConnector
? await VerifyOTPAsync(user, secret)
: await CheckPasswordAsync(user, secret);
}
}
}

View File

@ -200,5 +200,10 @@ namespace Bit.Core.Services
{
return Task.FromResult(0);
}
public Task SendOTPEmailAsync(string email, string token)
{
return Task.FromResult(0);
}
}
}

View File

@ -895,6 +895,16 @@ namespace Bit.Core.Utilities
return System.Text.Json.JsonSerializer.Deserialize<T>(jsonData, options);
}
public static string ClassToJsonData<T>(T data)
{
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
return System.Text.Json.JsonSerializer.Serialize(data, options);
}
public static ICollection<T> AddIfNotExists<T>(this ICollection<T> list, T item)
{
if (list.Contains(item))

View File

@ -175,7 +175,7 @@ namespace Bit.Core.Utilities
services.AddScoped<IEmergencyAccessService, EmergencyAccessService>();
services.AddSingleton<IDeviceService, DeviceService>();
services.AddSingleton<IAppleIapService, AppleIapService>();
services.AddSingleton<ISsoConfigService, SsoConfigService>();
services.AddScoped<ISsoConfigService, SsoConfigService>();
services.AddScoped<ISendService, SendService>();
}

View File

@ -31,7 +31,7 @@
@RevisionDate DATETIME2(7),
@ApiKey VARCHAR(30),
@ForcePasswordReset BIT = 0,
@UsesCryptoAgent BIT = 0
@UsesKeyConnector BIT = 0
AS
BEGIN
SET NOCOUNT ON
@ -70,7 +70,7 @@ BEGIN
[RevisionDate],
[ApiKey],
[ForcePasswordReset],
[UsesCryptoAgent]
[UsesKeyConnector]
)
VALUES
(
@ -106,6 +106,6 @@ BEGIN
@RevisionDate,
@ApiKey,
@ForcePasswordReset,
@UsesCryptoAgent
@UsesKeyConnector
)
END

View File

@ -31,7 +31,7 @@
@RevisionDate DATETIME2(7),
@ApiKey VARCHAR(30),
@ForcePasswordReset BIT = 0,
@UsesCryptoAgent BIT = 0
@UsesKeyConnector BIT = 0
AS
BEGIN
SET NOCOUNT ON
@ -70,7 +70,7 @@ BEGIN
[RevisionDate] = @RevisionDate,
[ApiKey] = @ApiKey,
[ForcePasswordReset] = @ForcePasswordReset,
[UsesCryptoAgent] = @UsesCryptoAgent
[UsesKeyConnector] = @UsesKeyConnector
WHERE
[Id] = @Id
END

View File

@ -31,7 +31,7 @@
[RevisionDate] DATETIME2 (7) NOT NULL,
[ApiKey] VARCHAR (30) NOT NULL,
[ForcePasswordReset] BIT NOT NULL,
[UsesCryptoAgent] BIT NOT NULL,
[UsesKeyConnector] BIT NOT NULL,
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC)
);

View File

@ -29,7 +29,8 @@ SELECT
SU.[ExternalId] SsoExternalId,
OU.[Permissions],
PO.[ProviderId],
P.[Name] ProviderName
P.[Name] ProviderName,
SS.[Data] SsoConfig
FROM
[dbo].[OrganizationUser] OU
INNER JOIN
@ -40,3 +41,5 @@ LEFT JOIN
[dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id]
LEFT JOIN
[dbo].[Provider] P ON P.[Id] = PO.[ProviderId]
LEFT JOIN
[dbo].[SsoConfig] SS ON SS.[OrganizationId] = OU.[OrganizationId]

View File

@ -14,7 +14,8 @@ SELECT
OU.[ExternalId],
SU.[ExternalId] SsoExternalId,
OU.[Permissions],
OU.[ResetPasswordKey]
OU.[ResetPasswordKey],
U.[UsesKeyConnector]
FROM
[dbo].[OrganizationUser] OU
LEFT JOIN

View File

@ -55,7 +55,6 @@ namespace Bit.Api.Test.Controllers
_organizationUserRepository,
_providerUserRepository,
_paymentService,
_ssoUserRepository,
_userRepository,
_userService,
_sendRepository,
@ -320,7 +319,7 @@ namespace Bit.Api.Test.Controllers
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToAcceptPasswordFor(user);
await _sut.ApiKey(new ApiKeyRequestModel());
await _sut.ApiKey(new SecretVerificationRequestModel());
}
[Fact]
@ -329,7 +328,7 @@ namespace Bit.Api.Test.Controllers
ConfigureUserServiceToReturnNullPrincipal();
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => _sut.ApiKey(new ApiKeyRequestModel())
() => _sut.ApiKey(new SecretVerificationRequestModel())
);
}
@ -340,7 +339,7 @@ namespace Bit.Api.Test.Controllers
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToRejectPasswordFor(user);
await Assert.ThrowsAsync<BadRequestException>(
() => _sut.ApiKey(new ApiKeyRequestModel())
() => _sut.ApiKey(new SecretVerificationRequestModel())
);
}
@ -350,7 +349,7 @@ namespace Bit.Api.Test.Controllers
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToAcceptPasswordFor(user);
await _sut.RotateApiKey(new ApiKeyRequestModel());
await _sut.RotateApiKey(new SecretVerificationRequestModel());
}
[Fact]
@ -359,7 +358,7 @@ namespace Bit.Api.Test.Controllers
ConfigureUserServiceToReturnNullPrincipal();
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => _sut.ApiKey(new ApiKeyRequestModel())
() => _sut.ApiKey(new SecretVerificationRequestModel())
);
}
@ -370,7 +369,7 @@ namespace Bit.Api.Test.Controllers
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToRejectPasswordFor(user);
await Assert.ThrowsAsync<BadRequestException>(
() => _sut.ApiKey(new ApiKeyRequestModel())
() => _sut.ApiKey(new SecretVerificationRequestModel())
);
}
@ -409,6 +408,8 @@ namespace Bit.Api.Test.Controllers
{
_userService.CheckPasswordAsync(user, Arg.Any<string>())
.Returns(Task.FromResult(true));
_userService.VerifySecretAsync(user, Arg.Any<string>())
.Returns(Task.FromResult(true));
}
private void ConfigureUserServiceToReturnValidIdFor(User user)

View File

@ -891,5 +891,38 @@ namespace Bit.Core.Test.Services
Assert.False(result);
Assert.Contains("Cannot autoscale on self-hosted instance", failureMessage);
}
[Theory, PaidOrganizationAutoData]
public async Task Delete_Success(Organization organization, SutProvider<OrganizationService> sutProvider)
{
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
await sutProvider.Sut.DeleteAsync(organization);
await organizationRepository.Received().DeleteAsync(organization);
await applicationCacheService.Received().DeleteOrganizationAbilityAsync(organization.Id);
}
[Theory, PaidOrganizationAutoData]
public async Task Delete_Fails_KeyConnector(Organization organization, SutProvider<OrganizationService> sutProvider,
SsoConfig ssoConfig)
{
ssoConfig.Enabled = true;
ssoConfig.SetData(new SsoConfigurationData { UseKeyConnector = true });
var ssoConfigRepository = sutProvider.GetDependency<ISsoConfigRepository>();
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
ssoConfigRepository.GetByOrganizationIdAsync(organization.Id).Returns(ssoConfig);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.DeleteAsync(organization));
Assert.Contains("You cannot delete an Organization that is using Key Connector.", exception.Message);
await organizationRepository.DidNotReceiveWithAnyArgs().DeleteAsync(default);
await applicationCacheService.DidNotReceiveWithAnyArgs().DeleteOrganizationAbilityAsync(default);
}
}
}

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -125,6 +126,40 @@ namespace Bit.Core.Test.Services
.UpsertAsync(default);
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task SaveAsync_SingleOrg_KeyConnectorEnabled_ThrowsBadRequest(
[PolicyFixtures.Policy(Enums.PolicyType.SingleOrg)] Core.Models.Table.Policy policy,
SutProvider<PolicyService> sutProvider)
{
policy.Enabled = false;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
var ssoConfig = new SsoConfig { Enabled = true };
var data = new SsoConfigurationData { UseKeyConnector = true };
ssoConfig.SetData(data);
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policy.OrganizationId)
.Returns(ssoConfig);
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Substitute.For<IUserService>(),
Substitute.For<IOrganizationService>(),
Guid.NewGuid()));
Assert.Contains("KeyConnector is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task SaveAsync_RequireSsoPolicy_NotEnabled_ThrowsBadRequestAsync([PolicyFixtures.Policy(Enums.PolicyType.RequireSso)] Core.Models.Table.Policy policy, SutProvider<PolicyService> sutProvider)
{

View File

@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using Bit.Core.Exceptions;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -21,7 +22,7 @@ namespace Bit.Core.Test.Services
var ssoConfig = new SsoConfig
{
Id = 1,
Data = "TESTDATA",
Data = "{}",
Enabled = true,
OrganizationId = Guid.NewGuid(),
CreationDate = utcNow.AddDays(-10),
@ -48,7 +49,7 @@ namespace Bit.Core.Test.Services
var ssoConfig = new SsoConfig
{
Id = default,
Data = "TESTDATA",
Data = "{}",
Enabled = true,
OrganizationId = Guid.NewGuid(),
CreationDate = utcNow.AddDays(-10),
@ -66,5 +67,67 @@ namespace Bit.Core.Test.Services
Assert.True(ssoConfig.CreationDate - utcNow < TimeSpan.FromSeconds(1));
Assert.True(ssoConfig.RevisionDate - utcNow < TimeSpan.FromSeconds(1));
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task SaveAsync_PreventDisablingKeyConnector(SutProvider<SsoConfigService> sutProvider, Guid orgId)
{
var utcNow = DateTime.UtcNow;
var oldSsoConfig = new SsoConfig
{
Id = 1,
Data = "{\"useKeyConnector\": true}",
Enabled = true,
OrganizationId = orgId,
CreationDate = utcNow.AddDays(-10),
RevisionDate = utcNow.AddDays(-10),
};
var newSsoConfig = new SsoConfig
{
Id = 1,
Data = "{}",
Enabled = true,
OrganizationId = orgId,
CreationDate = utcNow.AddDays(-10),
RevisionDate = utcNow,
};
var ssoConfigRepository = sutProvider.GetDependency<ISsoConfigRepository>();
ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(oldSsoConfig);
ssoConfigRepository.UpsertAsync(newSsoConfig).Returns(Task.CompletedTask);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(newSsoConfig));
Assert.Contains("KeyConnector cannot be disabled at this moment.", exception.Message);
await sutProvider.GetDependency<ISsoConfigRepository>().DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task SaveAsync_KeyConnector_SingleOrgNotEnabled(SutProvider<SsoConfigService> sutProvider)
{
var utcNow = DateTime.UtcNow;
var ssoConfig = new SsoConfig
{
Id = default,
Data = "{\"useKeyConnector\": true}",
Enabled = true,
OrganizationId = Guid.NewGuid(),
CreationDate = utcNow.AddDays(-10),
RevisionDate = utcNow.AddDays(-10),
};
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(ssoConfig));
Assert.Contains("KeyConnector requires Single Organization to be enabled.", exception.Message);
await sutProvider.GetDependency<ISsoConfigRepository>().DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
}
}
}

View File

@ -1,24 +1,24 @@
IF COL_LENGTH('[dbo].[User]', 'UsesCryptoAgent') IS NULL
IF COL_LENGTH('[dbo].[User]', 'UsesKeyConnector') IS NULL
BEGIN
ALTER TABLE
[dbo].[User]
ADD
[UsesCryptoAgent] BIT NULL
[UsesKeyConnector] BIT NULL
END
GO
UPDATE
[dbo].[User]
SET
[UsesCryptoAgent] = 0
[UsesKeyConnector] = 0
WHERE
[UsesCryptoAgent] IS NULL
[UsesKeyConnector] IS NULL
GO
ALTER TABLE
[dbo].[User]
ALTER COLUMN
[UsesCryptoAgent] BIT NOT NULL
[UsesKeyConnector] BIT NOT NULL
GO
-- View: User
@ -75,7 +75,7 @@ CREATE PROCEDURE [dbo].[User_Create]
@RevisionDate DATETIME2(7),
@ApiKey VARCHAR(30),
@ForcePasswordReset BIT = 0,
@UsesCryptoAgent BIT = 0
@UsesKeyConnector BIT = 0
AS
BEGIN
SET NOCOUNT ON
@ -114,7 +114,7 @@ BEGIN
[RevisionDate],
[ApiKey],
[ForcePasswordReset],
[UsesCryptoAgent]
[UsesKeyConnector]
)
VALUES
(
@ -150,7 +150,7 @@ BEGIN
@RevisionDate,
@ApiKey,
@ForcePasswordReset,
@UsesCryptoAgent
@UsesKeyConnector
)
END
GO
@ -194,7 +194,7 @@ CREATE PROCEDURE [dbo].[User_Update]
@RevisionDate DATETIME2(7),
@ApiKey VARCHAR(30),
@ForcePasswordReset BIT = 0,
@UsesCryptoAgent BIT = 0
@UsesKeyConnector BIT = 0
AS
BEGIN
SET NOCOUNT ON
@ -233,7 +233,92 @@ BEGIN
[RevisionDate] = @RevisionDate,
[ApiKey] = @ApiKey,
[ForcePasswordReset] = @ForcePasswordReset,
[UsesCryptoAgent] = @UsesCryptoAgent
[UsesKeyConnector] = @UsesKeyConnector
WHERE
[Id] = @Id
END
GO
IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'OrganizationUserOrganizationDetailsView')
BEGIN
DROP VIEW [dbo].[OrganizationUserOrganizationDetailsView]
END
GO
CREATE VIEW [dbo].[OrganizationUserOrganizationDetailsView]
AS
SELECT
OU.[UserId],
OU.[OrganizationId],
O.[Name],
O.[Enabled],
O.[UsePolicies],
O.[UseSso],
O.[UseGroups],
O.[UseDirectory],
O.[UseEvents],
O.[UseTotp],
O.[Use2fa],
O.[UseApi],
O.[UseResetPassword],
O.[SelfHost],
O.[UsersGetPremium],
O.[Seats],
O.[MaxCollections],
O.[MaxStorageGb],
O.[Identifier],
OU.[Key],
OU.[ResetPasswordKey],
O.[PublicKey],
O.[PrivateKey],
OU.[Status],
OU.[Type],
SU.[ExternalId] SsoExternalId,
OU.[Permissions],
PO.[ProviderId],
P.[Name] ProviderName,
SS.[Data] SsoConfig
FROM
[dbo].[OrganizationUser] OU
INNER JOIN
[dbo].[Organization] O ON O.[Id] = OU.[OrganizationId]
LEFT JOIN
[dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId]
LEFT JOIN
[dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id]
LEFT JOIN
[dbo].[Provider] P ON P.[Id] = PO.[ProviderId]
LEFT JOIN
[dbo].[SsoConfig] SS ON SS.[OrganizationId] = OU.[OrganizationId]
GO
IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'OrganizationUserUserDetailsView')
BEGIN
DROP VIEW [dbo].[OrganizationUserUserDetailsView]
END
GO
CREATE VIEW [dbo].[OrganizationUserUserDetailsView]
AS
SELECT
OU.[Id],
OU.[UserId],
OU.[OrganizationId],
U.[Name],
ISNULL(U.[Email], OU.[Email]) Email,
U.[TwoFactorProviders],
U.[Premium],
OU.[Status],
OU.[Type],
OU.[AccessAll],
OU.[ExternalId],
SU.[ExternalId] SsoExternalId,
OU.[Permissions],
OU.[ResetPasswordKey],
U.[UsesKeyConnector]
FROM
[dbo].[OrganizationUser] OU
LEFT JOIN
[dbo].[User] U ON U.[Id] = OU.[UserId]
LEFT JOIN
[dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Bit.MySqlMigrations.Migrations
{
public partial class KeyConnector : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "UsesKeyConnector",
table: "User",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "UsesKeyConnector",
table: "User");
}
}
}

View File

@ -1127,6 +1127,9 @@ namespace Bit.MySqlMigrations.Migrations
.HasMaxLength(32)
.HasColumnType("varchar(32)");
b.Property<bool>("UsesKeyConnector")
.HasColumnType("tinyint(1)");
b.HasKey("Id");
b.ToTable("User");

View File

@ -0,0 +1,9 @@
START TRANSACTION;
ALTER TABLE `User` ADD `UsesKeyConnector` tinyint(1) NOT NULL DEFAULT FALSE;
INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`)
VALUES ('20211108041911_KeyConnector', '5.0.9');
COMMIT;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Bit.PostgresMigrations.Migrations
{
public partial class KeyConnector : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "UsesKeyConnector",
table: "User",
type: "boolean",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "UsesKeyConnector",
table: "User");
}
}
}

View File

@ -1136,6 +1136,9 @@ namespace Bit.PostgresMigrations.Migrations
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<bool>("UsesKeyConnector")
.HasColumnType("boolean");
b.HasKey("Id");
b.ToTable("User");

View File

@ -0,0 +1,9 @@
START TRANSACTION;
ALTER TABLE "User" ADD "UsesKeyConnector" boolean NOT NULL DEFAULT FALSE;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20211108041547_KeyConnector', '5.0.9');
COMMIT;