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:
parent
f6bc35b2d0
commit
fd37cb5a12
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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)))
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
14
src/Core/MailTemplates/Handlebars/OTPEmail.html.hbs
Normal file
14
src/Core/MailTemplates/Handlebars/OTPEmail.html.hbs
Normal 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}}
|
5
src/Core/MailTemplates/Handlebars/OTPEmail.text.hbs
Normal file
5
src/Core/MailTemplates/Handlebars/OTPEmail.text.hbs
Normal file
@ -0,0 +1,5 @@
|
||||
{{#>BasicTextLayout}}
|
||||
Your email verification code is: {{Token}}
|
||||
|
||||
Use this code to complete the protected action in Bitwarden.
|
||||
{{/BasicTextLayout}}
|
@ -1,10 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Core.Models.Api
|
||||
{
|
||||
public class DeleteAccountRequestModel
|
||||
{
|
||||
[Required]
|
||||
public string MasterPasswordHash { get; set; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Core.Models.Api
|
||||
{
|
||||
public class SecurityStampRequestModel
|
||||
{
|
||||
[Required]
|
||||
public string MasterPasswordHash { get; set; }
|
||||
}
|
||||
}
|
@ -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; }
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Core.Models.Api
|
||||
{
|
||||
public class CipherPurgeRequestModel
|
||||
{
|
||||
[Required]
|
||||
public string MasterPasswordHash { get; set; }
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Core.Models.Api
|
||||
{
|
||||
public class OrganizationDeleteRequestModel
|
||||
{
|
||||
[Required]
|
||||
public string MasterPasswordHash { get; set; }
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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]
|
||||
|
@ -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; }
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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; }
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -200,5 +200,10 @@ namespace Bit.Core.Services
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendOTPEmailAsync(string email, string token)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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>();
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
);
|
||||
|
||||
|
@ -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]
|
||||
|
@ -14,7 +14,8 @@ SELECT
|
||||
OU.[ExternalId],
|
||||
SU.[ExternalId] SsoExternalId,
|
||||
OU.[Permissions],
|
||||
OU.[ResetPasswordKey]
|
||||
OU.[ResetPasswordKey],
|
||||
U.[UsesKeyConnector]
|
||||
FROM
|
||||
[dbo].[OrganizationUser] OU
|
||||
LEFT JOIN
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
1495
util/MySqlMigrations/Migrations/20211108041911_KeyConnector.Designer.cs
generated
Normal file
1495
util/MySqlMigrations/Migrations/20211108041911_KeyConnector.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
|
@ -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;
|
||||
|
1504
util/PostgresMigrations/Migrations/20211108041547_KeyConnector.Designer.cs
generated
Normal file
1504
util/PostgresMigrations/Migrations/20211108041547_KeyConnector.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user