mirror of
https://github.com/bitwarden/server.git
synced 2025-02-15 01:41:40 +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);
|
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_ResetSsoLink);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CreateSsoUserRecord(string providerUserId, Guid userId, Guid orgId)
|
private async Task CreateSsoUserRecord(string providerUserId, Guid userId, Guid orgId)
|
||||||
{
|
{
|
||||||
var ssoUser = new SsoUser
|
var ssoUser = new SsoUser
|
||||||
{
|
{
|
||||||
ExternalId = providerUserId,
|
ExternalId = providerUserId,
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
OrganizationId = orgId
|
OrganizationId = orgId,
|
||||||
};
|
};
|
||||||
await _ssoUserRepository.CreateAsync(ssoUser);
|
await _ssoUserRepository.CreateAsync(ssoUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Enums.Provider;
|
using Bit.Core.Enums.Provider;
|
||||||
|
|
||||||
@ -46,7 +45,6 @@ namespace Bit.Api.Controllers
|
|||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IProviderUserRepository providerUserRepository,
|
IProviderUserRepository providerUserRepository,
|
||||||
IPaymentService paymentService,
|
IPaymentService paymentService,
|
||||||
ISsoUserRepository ssoUserRepository,
|
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
ISendRepository sendRepository,
|
ISendRepository sendRepository,
|
||||||
@ -118,6 +116,11 @@ namespace Bit.Api.Controllers
|
|||||||
throw new UnauthorizedAccessException();
|
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))
|
if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
|
||||||
{
|
{
|
||||||
await Task.Delay(2000);
|
await Task.Delay(2000);
|
||||||
@ -136,6 +139,11 @@ namespace Bit.Api.Controllers
|
|||||||
throw new UnauthorizedAccessException();
|
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,
|
var result = await _userService.ChangeEmailAsync(user, model.MasterPasswordHash, model.NewEmail,
|
||||||
model.NewMasterPasswordHash, model.Token, model.Key);
|
model.NewMasterPasswordHash, model.Token, model.Key);
|
||||||
if (result.Succeeded)
|
if (result.Succeeded)
|
||||||
@ -238,7 +246,7 @@ namespace Bit.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("verify-password")]
|
[HttpPost("verify-password")]
|
||||||
public async Task PostVerifyPassword([FromBody]VerifyPasswordRequestModel model)
|
public async Task PostVerifyPassword([FromBody]SecretVerificationRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
@ -256,8 +264,8 @@ namespace Bit.Api.Controllers
|
|||||||
throw new BadRequestException(ModelState);
|
throw new BadRequestException(ModelState);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("set-crypto-agent-key")]
|
[HttpPost("set-key-connector-key")]
|
||||||
public async Task PostSetCryptoAgentKeyAsync([FromBody]SetCryptoAgentKeyRequestModel model)
|
public async Task PostSetKeyConnectorKeyAsync([FromBody]SetKeyConnectorKeyRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
@ -265,7 +273,30 @@ namespace Bit.Api.Controllers
|
|||||||
throw new UnauthorizedAccessException();
|
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)
|
if (result.Succeeded)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@ -361,7 +392,7 @@ namespace Bit.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("security-stamp")]
|
[HttpPost("security-stamp")]
|
||||||
public async Task PostSecurityStamp([FromBody]SecurityStampRequestModel model)
|
public async Task PostSecurityStamp([FromBody]SecretVerificationRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
@ -369,7 +400,7 @@ namespace Bit.Api.Controllers
|
|||||||
throw new UnauthorizedAccessException();
|
throw new UnauthorizedAccessException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await _userService.RefreshSecurityStampAsync(user, model.MasterPasswordHash);
|
var result = await _userService.RefreshSecurityStampAsync(user, model.Secret);
|
||||||
if (result.Succeeded)
|
if (result.Succeeded)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@ -471,7 +502,7 @@ namespace Bit.Api.Controllers
|
|||||||
|
|
||||||
[HttpDelete]
|
[HttpDelete]
|
||||||
[HttpPost("delete")]
|
[HttpPost("delete")]
|
||||||
public async Task Delete([FromBody]DeleteAccountRequestModel model)
|
public async Task Delete([FromBody]SecretVerificationRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
@ -479,9 +510,9 @@ namespace Bit.Api.Controllers
|
|||||||
throw new UnauthorizedAccessException();
|
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);
|
await Task.Delay(2000);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -761,7 +792,7 @@ namespace Bit.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("api-key")]
|
[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);
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
@ -769,20 +800,17 @@ namespace Bit.Api.Controllers
|
|||||||
throw new UnauthorizedAccessException();
|
throw new UnauthorizedAccessException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
|
if (!await _userService.VerifySecretAsync(user, model.Secret))
|
||||||
{
|
{
|
||||||
await Task.Delay(2000);
|
await Task.Delay(2000);
|
||||||
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
|
throw new BadRequestException(string.Empty, "User verification failed.");
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var response = new ApiKeyResponseModel(user);
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new ApiKeyResponseModel(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("rotate-api-key")]
|
[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);
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
@ -790,17 +818,15 @@ namespace Bit.Api.Controllers
|
|||||||
throw new UnauthorizedAccessException();
|
throw new UnauthorizedAccessException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
|
if (!await _userService.VerifySecretAsync(user, model.Secret))
|
||||||
{
|
{
|
||||||
await Task.Delay(2000);
|
await Task.Delay(2000);
|
||||||
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
|
throw new BadRequestException(string.Empty, "User verification failed.");
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _userService.RotateApiKeyAsync(user);
|
|
||||||
var response = new ApiKeyResponseModel(user);
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _userService.RotateApiKeyAsync(user);
|
||||||
|
var response = new ApiKeyResponseModel(user);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("update-temp-password")]
|
[HttpPut("update-temp-password")]
|
||||||
@ -825,5 +851,33 @@ namespace Bit.Api.Controllers
|
|||||||
|
|
||||||
throw new BadRequestException(ModelState);
|
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")]
|
[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);
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
@ -558,9 +558,9 @@ namespace Bit.Api.Controllers
|
|||||||
throw new UnauthorizedAccessException();
|
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);
|
await Task.Delay(2000);
|
||||||
throw new BadRequestException(ModelState);
|
throw new BadRequestException(ModelState);
|
||||||
}
|
}
|
||||||
|
@ -390,7 +390,7 @@ namespace Bit.Api.Controllers
|
|||||||
|
|
||||||
[HttpDelete("{id}")]
|
[HttpDelete("{id}")]
|
||||||
[HttpPost("{id}/delete")]
|
[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);
|
var orgIdGuid = new Guid(id);
|
||||||
if (!await _currentContext.OrganizationOwner(orgIdGuid))
|
if (!await _currentContext.OrganizationOwner(orgIdGuid))
|
||||||
@ -410,10 +410,10 @@ namespace Bit.Api.Controllers
|
|||||||
throw new UnauthorizedAccessException();
|
throw new UnauthorizedAccessException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
|
if (!await _userService.VerifySecretAsync(user, model.Secret))
|
||||||
{
|
{
|
||||||
await Task.Delay(2000);
|
await Task.Delay(2000);
|
||||||
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
|
throw new BadRequestException(string.Empty, "User verification failed.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -466,7 +466,7 @@ namespace Bit.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/api-key")]
|
[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);
|
var orgIdGuid = new Guid(id);
|
||||||
if (!await _currentContext.OrganizationOwner(orgIdGuid))
|
if (!await _currentContext.OrganizationOwner(orgIdGuid))
|
||||||
@ -486,7 +486,7 @@ namespace Bit.Api.Controllers
|
|||||||
throw new UnauthorizedAccessException();
|
throw new UnauthorizedAccessException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
|
if (!await _userService.VerifySecretAsync(user, model.Secret))
|
||||||
{
|
{
|
||||||
await Task.Delay(2000);
|
await Task.Delay(2000);
|
||||||
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
|
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
|
||||||
@ -499,7 +499,7 @@ namespace Bit.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/rotate-api-key")]
|
[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);
|
var orgIdGuid = new Guid(id);
|
||||||
if (!await _currentContext.OrganizationOwner(orgIdGuid))
|
if (!await _currentContext.OrganizationOwner(orgIdGuid))
|
||||||
@ -519,7 +519,7 @@ namespace Bit.Api.Controllers
|
|||||||
throw new UnauthorizedAccessException();
|
throw new UnauthorizedAccessException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
|
if (!await _userService.VerifySecretAsync(user, model.Secret))
|
||||||
{
|
{
|
||||||
await Task.Delay(2000);
|
await Task.Delay(2000);
|
||||||
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
|
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
|
||||||
@ -640,14 +640,7 @@ namespace Bit.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(id);
|
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(id);
|
||||||
if (ssoConfig == null)
|
ssoConfig = ssoConfig == null ? model.ToSsoConfig(id) : model.ToSsoConfig(ssoConfig);
|
||||||
{
|
|
||||||
ssoConfig = model.ToSsoConfig(id);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ssoConfig = model.ToSsoConfig(ssoConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _ssoConfigService.SaveAsync(ssoConfig);
|
await _ssoConfigService.SaveAsync(ssoConfig);
|
||||||
|
|
||||||
|
@ -80,9 +80,9 @@ namespace Bit.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("get-authenticator")]
|
[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);
|
var response = new TwoFactorAuthenticatorResponseModel(user);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@ -92,7 +92,7 @@ namespace Bit.Api.Controllers
|
|||||||
public async Task<TwoFactorAuthenticatorResponseModel> PutAuthenticator(
|
public async Task<TwoFactorAuthenticatorResponseModel> PutAuthenticator(
|
||||||
[FromBody]UpdateTwoFactorAuthenticatorRequestModel model)
|
[FromBody]UpdateTwoFactorAuthenticatorRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await CheckAsync(model.MasterPasswordHash, false);
|
var user = await CheckAsync(model, false);
|
||||||
model.ToUser(user);
|
model.ToUser(user);
|
||||||
|
|
||||||
if (!await _userManager.VerifyTwoFactorTokenAsync(user,
|
if (!await _userManager.VerifyTwoFactorTokenAsync(user,
|
||||||
@ -108,9 +108,9 @@ namespace Bit.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("get-yubikey")]
|
[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);
|
var response = new TwoFactorYubiKeyResponseModel(user);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@ -119,7 +119,7 @@ namespace Bit.Api.Controllers
|
|||||||
[HttpPost("yubikey")]
|
[HttpPost("yubikey")]
|
||||||
public async Task<TwoFactorYubiKeyResponseModel> PutYubiKey([FromBody]UpdateTwoFactorYubicoOtpRequestModel model)
|
public async Task<TwoFactorYubiKeyResponseModel> PutYubiKey([FromBody]UpdateTwoFactorYubicoOtpRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await CheckAsync(model.MasterPasswordHash, true);
|
var user = await CheckAsync(model, true);
|
||||||
model.ToUser(user);
|
model.ToUser(user);
|
||||||
|
|
||||||
await ValidateYubiKeyAsync(user, nameof(model.Key1), model.Key1);
|
await ValidateYubiKeyAsync(user, nameof(model.Key1), model.Key1);
|
||||||
@ -134,9 +134,9 @@ namespace Bit.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("get-duo")]
|
[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);
|
var response = new TwoFactorDuoResponseModel(user);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@ -145,7 +145,7 @@ namespace Bit.Api.Controllers
|
|||||||
[HttpPost("duo")]
|
[HttpPost("duo")]
|
||||||
public async Task<TwoFactorDuoResponseModel> PutDuo([FromBody]UpdateTwoFactorDuoRequestModel model)
|
public async Task<TwoFactorDuoResponseModel> PutDuo([FromBody]UpdateTwoFactorDuoRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await CheckAsync(model.MasterPasswordHash, true);
|
var user = await CheckAsync(model, true);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host);
|
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")]
|
[HttpPost("~/organizations/{id}/two-factor/get-duo")]
|
||||||
public async Task<TwoFactorDuoResponseModel> GetOrganizationDuo(string id,
|
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);
|
var orgIdGuid = new Guid(id);
|
||||||
if (!await _currentContext.ManagePolicies(orgIdGuid))
|
if (!await _currentContext.ManagePolicies(orgIdGuid))
|
||||||
@ -189,7 +189,7 @@ namespace Bit.Api.Controllers
|
|||||||
public async Task<TwoFactorDuoResponseModel> PutOrganizationDuo(string id,
|
public async Task<TwoFactorDuoResponseModel> PutOrganizationDuo(string id,
|
||||||
[FromBody]UpdateTwoFactorDuoRequestModel model)
|
[FromBody]UpdateTwoFactorDuoRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await CheckAsync(model.MasterPasswordHash, false);
|
var user = await CheckAsync(model, false);
|
||||||
|
|
||||||
var orgIdGuid = new Guid(id);
|
var orgIdGuid = new Guid(id);
|
||||||
if (!await _currentContext.ManagePolicies(orgIdGuid))
|
if (!await _currentContext.ManagePolicies(orgIdGuid))
|
||||||
@ -221,17 +221,17 @@ namespace Bit.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("get-webauthn")]
|
[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);
|
var response = new TwoFactorWebAuthnResponseModel(user);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("get-webauthn-challenge")]
|
[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);
|
var reg = await _userService.StartWebAuthnRegistrationAsync(user);
|
||||||
return reg;
|
return reg;
|
||||||
}
|
}
|
||||||
@ -240,7 +240,7 @@ namespace Bit.Api.Controllers
|
|||||||
[HttpPost("webauthn")]
|
[HttpPost("webauthn")]
|
||||||
public async Task<TwoFactorWebAuthnResponseModel> PutWebAuthn([FromBody]TwoFactorWebAuthnRequestModel model)
|
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(
|
var success = await _userService.CompleteWebAuthRegistrationAsync(
|
||||||
user, model.Id.Value, model.Name, model.DeviceResponse);
|
user, model.Id.Value, model.Name, model.DeviceResponse);
|
||||||
@ -255,16 +255,16 @@ namespace Bit.Api.Controllers
|
|||||||
[HttpDelete("webauthn")]
|
[HttpDelete("webauthn")]
|
||||||
public async Task<TwoFactorWebAuthnResponseModel> DeleteWebAuthn([FromBody]TwoFactorWebAuthnDeleteRequestModel model)
|
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);
|
await _userService.DeleteWebAuthnKeyAsync(user, model.Id.Value);
|
||||||
var response = new TwoFactorWebAuthnResponseModel(user);
|
var response = new TwoFactorWebAuthnResponseModel(user);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("get-email")]
|
[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);
|
var response = new TwoFactorEmailResponseModel(user);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@ -272,7 +272,7 @@ namespace Bit.Api.Controllers
|
|||||||
[HttpPost("send-email")]
|
[HttpPost("send-email")]
|
||||||
public async Task SendEmail([FromBody]TwoFactorEmailRequestModel model)
|
public async Task SendEmail([FromBody]TwoFactorEmailRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await CheckAsync(model.MasterPasswordHash, false);
|
var user = await CheckAsync(model, false);
|
||||||
model.ToUser(user);
|
model.ToUser(user);
|
||||||
await _userService.SendTwoFactorEmailAsync(user);
|
await _userService.SendTwoFactorEmailAsync(user);
|
||||||
}
|
}
|
||||||
@ -284,7 +284,7 @@ namespace Bit.Api.Controllers
|
|||||||
var user = await _userManager.FindByEmailAsync(model.Email.ToLowerInvariant());
|
var user = await _userManager.FindByEmailAsync(model.Email.ToLowerInvariant());
|
||||||
if (user != null)
|
if (user != null)
|
||||||
{
|
{
|
||||||
if (await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
|
if (await _userService.VerifySecretAsync(user, model.Secret))
|
||||||
{
|
{
|
||||||
await _userService.SendTwoFactorEmailAsync(user);
|
await _userService.SendTwoFactorEmailAsync(user);
|
||||||
return;
|
return;
|
||||||
@ -299,7 +299,7 @@ namespace Bit.Api.Controllers
|
|||||||
[HttpPost("email")]
|
[HttpPost("email")]
|
||||||
public async Task<TwoFactorEmailResponseModel> PutEmail([FromBody]UpdateTwoFactorEmailRequestModel model)
|
public async Task<TwoFactorEmailResponseModel> PutEmail([FromBody]UpdateTwoFactorEmailRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await CheckAsync(model.MasterPasswordHash, false);
|
var user = await CheckAsync(model, false);
|
||||||
model.ToUser(user);
|
model.ToUser(user);
|
||||||
|
|
||||||
if (!await _userManager.VerifyTwoFactorTokenAsync(user,
|
if (!await _userManager.VerifyTwoFactorTokenAsync(user,
|
||||||
@ -318,7 +318,7 @@ namespace Bit.Api.Controllers
|
|||||||
[HttpPost("disable")]
|
[HttpPost("disable")]
|
||||||
public async Task<TwoFactorProviderResponseModel> PutDisable([FromBody]TwoFactorProviderRequestModel model)
|
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);
|
await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value, _organizationService);
|
||||||
var response = new TwoFactorProviderResponseModel(model.Type.Value, user);
|
var response = new TwoFactorProviderResponseModel(model.Type.Value, user);
|
||||||
return response;
|
return response;
|
||||||
@ -329,7 +329,7 @@ namespace Bit.Api.Controllers
|
|||||||
public async Task<TwoFactorProviderResponseModel> PutOrganizationDisable(string id,
|
public async Task<TwoFactorProviderResponseModel> PutOrganizationDisable(string id,
|
||||||
[FromBody]TwoFactorProviderRequestModel model)
|
[FromBody]TwoFactorProviderRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await CheckAsync(model.MasterPasswordHash, false);
|
var user = await CheckAsync(model, false);
|
||||||
|
|
||||||
var orgIdGuid = new Guid(id);
|
var orgIdGuid = new Guid(id);
|
||||||
if (!await _currentContext.ManagePolicies(orgIdGuid))
|
if (!await _currentContext.ManagePolicies(orgIdGuid))
|
||||||
@ -349,9 +349,9 @@ namespace Bit.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("get-recover")]
|
[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);
|
var response = new TwoFactorRecoverResponseModel(user);
|
||||||
return response;
|
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);
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
@ -376,10 +376,10 @@ namespace Bit.Api.Controllers
|
|||||||
throw new UnauthorizedAccessException();
|
throw new UnauthorizedAccessException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await _userService.CheckPasswordAsync(user, masterPasswordHash))
|
if (!await _userService.VerifySecretAsync(user, model.Secret))
|
||||||
{
|
{
|
||||||
await Task.Delay(2000);
|
await Task.Delay(2000);
|
||||||
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
|
throw new BadRequestException(string.Empty, "User verification failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (premium && !(await _userService.CanAccessPremium(user)))
|
if (premium && !(await _userService.CanAccessPremium(user)))
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
User_FailedLogIn2fa = 1006,
|
User_FailedLogIn2fa = 1006,
|
||||||
User_ClientExportedVault = 1007,
|
User_ClientExportedVault = 1007,
|
||||||
User_UpdatedTempPassword = 1008,
|
User_UpdatedTempPassword = 1008,
|
||||||
|
User_MigratedKeyToKeyConnector = 1009,
|
||||||
|
|
||||||
Cipher_Created = 1100,
|
Cipher_Created = 1100,
|
||||||
Cipher_Updated = 1101,
|
Cipher_Updated = 1101,
|
||||||
@ -54,6 +55,10 @@
|
|||||||
Organization_PurgedVault = 1601,
|
Organization_PurgedVault = 1601,
|
||||||
// Organization_ClientExportedVault = 1602,
|
// Organization_ClientExportedVault = 1602,
|
||||||
Organization_VaultAccessed = 1603,
|
Organization_VaultAccessed = 1603,
|
||||||
|
Organization_EnabledSso = 1604,
|
||||||
|
Organization_DisabledSso = 1605,
|
||||||
|
Organization_EnabledKeyConnector = 1606,
|
||||||
|
Organization_DisabledKeyConnector = 1607,
|
||||||
|
|
||||||
Policy_Updated = 1700,
|
Policy_Updated = 1700,
|
||||||
|
|
||||||
|
@ -176,7 +176,7 @@ namespace Bit.Core.IdentityServer
|
|||||||
customResponse.Add("TwoFactorToken", token);
|
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)
|
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 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);
|
Dictionary<string, object> customResponse);
|
||||||
|
|
||||||
protected abstract void SetErrorResult(T context, 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 UserManager<User> _userManager;
|
||||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
|
||||||
public CustomTokenRequestValidator(
|
public CustomTokenRequestValidator(
|
||||||
UserManager<User> userManager,
|
UserManager<User> userManager,
|
||||||
@ -47,6 +48,7 @@ namespace Bit.Core.IdentityServer
|
|||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_ssoConfigRepository = ssoConfigRepository;
|
_ssoConfigRepository = ssoConfigRepository;
|
||||||
|
_organizationRepository = organizationRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ValidateAsync(CustomTokenRequestValidationContext context)
|
public async Task ValidateAsync(CustomTokenRequestValidationContext context)
|
||||||
@ -58,25 +60,6 @@ namespace Bit.Core.IdentityServer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await ValidateAsync(context, context.Result.ValidatedRequest);
|
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)
|
protected async override Task<(User, bool)> ValidateContextAsync(CustomTokenRequestValidationContext context)
|
||||||
@ -87,7 +70,7 @@ namespace Bit.Core.IdentityServer
|
|||||||
return (user, user != null);
|
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)
|
List<Claim> claims, Dictionary<string, object> customResponse)
|
||||||
{
|
{
|
||||||
context.Result.CustomResponse = customResponse;
|
context.Result.CustomResponse = customResponse;
|
||||||
@ -100,6 +83,40 @@ namespace Bit.Core.IdentityServer
|
|||||||
context.Result.ValidatedRequest.ClientClaims.Add(claim);
|
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,
|
protected override void SetTwoFactorResult(CustomTokenRequestValidationContext context,
|
||||||
|
@ -106,13 +106,14 @@ namespace Bit.Core.IdentityServer
|
|||||||
return (user, true);
|
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)
|
List<Claim> claims, Dictionary<string, object> customResponse)
|
||||||
{
|
{
|
||||||
context.Result = new GrantValidationResult(user.Id.ToString(), "Application",
|
context.Result = new GrantValidationResult(user.Id.ToString(), "Application",
|
||||||
identityProvider: "bitwarden",
|
identityProvider: "bitwarden",
|
||||||
claims: claims.Count > 0 ? claims : null,
|
claims: claims.Count > 0 ? claims : null,
|
||||||
customResponse: customResponse);
|
customResponse: customResponse);
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void SetTwoFactorResult(ResourceOwnerPasswordValidationContext context,
|
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.Utilities;
|
||||||
|
using Bit.Core.Models.Api;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Api
|
namespace Bit.Core.Models.Api
|
||||||
{
|
{
|
||||||
public class EmailRequestModel
|
public class EmailRequestModel : SecretVerificationRequestModel
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
[StrictEmailAddress]
|
[StrictEmailAddress]
|
||||||
@ -12,9 +12,6 @@ namespace Bit.Core.Models.Api
|
|||||||
public string NewEmail { get; set; }
|
public string NewEmail { get; set; }
|
||||||
[Required]
|
[Required]
|
||||||
[StringLength(300)]
|
[StringLength(300)]
|
||||||
public string MasterPasswordHash { get; set; }
|
|
||||||
[Required]
|
|
||||||
[StringLength(300)]
|
|
||||||
public string NewMasterPasswordHash { get; set; }
|
public string NewMasterPasswordHash { get; set; }
|
||||||
[Required]
|
[Required]
|
||||||
public string Token { get; set; }
|
public string Token { get; set; }
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Core.Models.Api;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Api
|
namespace Bit.Core.Models.Api
|
||||||
{
|
{
|
||||||
public class EmailTokenRequestModel
|
public class EmailTokenRequestModel : SecretVerificationRequestModel
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
[StrictEmailAddress]
|
[StrictEmailAddress]
|
||||||
[StringLength(256)]
|
[StringLength(256)]
|
||||||
public string NewEmail { get; set; }
|
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
|
namespace Bit.Core.Models.Api
|
||||||
{
|
{
|
||||||
public class PasswordRequestModel
|
public class PasswordRequestModel : SecretVerificationRequestModel
|
||||||
{
|
{
|
||||||
[Required]
|
|
||||||
[StringLength(300)]
|
|
||||||
public string MasterPasswordHash { get; set; }
|
|
||||||
[Required]
|
[Required]
|
||||||
[StringLength(300)]
|
[StringLength(300)]
|
||||||
public string NewMasterPasswordHash { get; set; }
|
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
|
namespace Bit.Core.Models.Api.Request.Accounts
|
||||||
{
|
{
|
||||||
public class SetCryptoAgentKeyRequestModel
|
public class SetKeyConnectorKeyRequestModel
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
public string Key { get; set; }
|
public string Key { get; set; }
|
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
namespace Bit.Core.Models.Api
|
namespace Bit.Core.Models.Api
|
||||||
{
|
{
|
||||||
public class ApiKeyRequestModel
|
public class VerifyOTPRequestModel
|
||||||
{
|
{
|
||||||
[Required]
|
[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 U2F.Core.Utils;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Bit.Core.Models.Table;
|
using Bit.Core.Models.Table;
|
||||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||||
@ -40,47 +39,11 @@ namespace Bit.Core.Models.Api
|
|||||||
{
|
{
|
||||||
public SsoConfigurationDataRequest() {}
|
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]
|
[Required]
|
||||||
public SsoType ConfigType { get; set; }
|
public SsoType ConfigType { get; set; }
|
||||||
|
|
||||||
// Crypto Agent
|
public bool UseKeyConnector { get; set; }
|
||||||
public bool UseCryptoAgent { get; set; }
|
public string KeyConnectorUrl { get; set; }
|
||||||
public string CryptoAgentUrl { get; set; }
|
|
||||||
|
|
||||||
// OIDC
|
// OIDC
|
||||||
public string Authority { get; set; }
|
public string Authority { get; set; }
|
||||||
@ -215,8 +178,8 @@ namespace Bit.Core.Models.Api
|
|||||||
return new SsoConfigurationData
|
return new SsoConfigurationData
|
||||||
{
|
{
|
||||||
ConfigType = ConfigType,
|
ConfigType = ConfigType,
|
||||||
UseCryptoAgent = UseCryptoAgent,
|
UseKeyConnector = UseKeyConnector,
|
||||||
CryptoAgentUrl = CryptoAgentUrl,
|
KeyConnectorUrl = KeyConnectorUrl,
|
||||||
Authority = Authority,
|
Authority = Authority,
|
||||||
ClientId = ClientId,
|
ClientId = ClientId,
|
||||||
ClientSecret = ClientSecret,
|
ClientSecret = ClientSecret,
|
||||||
|
@ -7,7 +7,7 @@ using System.Linq;
|
|||||||
|
|
||||||
namespace Bit.Core.Models.Api
|
namespace Bit.Core.Models.Api
|
||||||
{
|
{
|
||||||
public class UpdateTwoFactorAuthenticatorRequestModel : TwoFactorRequestModel
|
public class UpdateTwoFactorAuthenticatorRequestModel : SecretVerificationRequestModel
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
[StringLength(50)]
|
[StringLength(50)]
|
||||||
@ -38,7 +38,7 @@ namespace Bit.Core.Models.Api
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UpdateTwoFactorDuoRequestModel : TwoFactorRequestModel, IValidatableObject
|
public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IValidatableObject
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
[StringLength(50)]
|
[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 Key1 { get; set; }
|
||||||
public string Key2 { 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]
|
[Required]
|
||||||
[EmailAddress]
|
[EmailAddress]
|
||||||
@ -231,13 +231,18 @@ namespace Bit.Core.Models.Api
|
|||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TwoFactorWebAuthnDeleteRequestModel : TwoFactorRequestModel, IValidatableObject
|
public class TwoFactorWebAuthnDeleteRequestModel : SecretVerificationRequestModel, IValidatableObject
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
public int? Id { get; set; }
|
public int? Id { get; set; }
|
||||||
|
|
||||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
{
|
{
|
||||||
|
foreach (var validationResult in Validate(validationContext))
|
||||||
|
{
|
||||||
|
yield return validationResult;
|
||||||
|
}
|
||||||
|
|
||||||
if (!Id.HasValue || Id < 0 || Id > 5)
|
if (!Id.HasValue || Id < 0 || Id > 5)
|
||||||
{
|
{
|
||||||
yield return new ValidationResult("Invalid Key Id", new string[] { nameof(Id) });
|
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 string Token { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TwoFactorProviderRequestModel : TwoFactorRequestModel
|
public class TwoFactorProviderRequestModel : SecretVerificationRequestModel
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
public TwoFactorProviderType? Type { get; set; }
|
public TwoFactorProviderType? Type { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TwoFactorRequestModel
|
|
||||||
{
|
|
||||||
[Required]
|
|
||||||
public string MasterPasswordHash { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TwoFactorRecoveryRequestModel : TwoFactorEmailRequestModel
|
public class TwoFactorRecoveryRequestModel : TwoFactorEmailRequestModel
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
|
@ -79,6 +79,8 @@ namespace Bit.Core.Models.Api
|
|||||||
Email = organizationUser.Email;
|
Email = organizationUser.Email;
|
||||||
TwoFactorEnabled = twoFactorEnabled;
|
TwoFactorEnabled = twoFactorEnabled;
|
||||||
SsoBound = !string.IsNullOrWhiteSpace(organizationUser.SsoExternalId);
|
SsoBound = !string.IsNullOrWhiteSpace(organizationUser.SsoExternalId);
|
||||||
|
// Prevent reset password when using key connector.
|
||||||
|
ResetPasswordEnrolled = ResetPasswordEnrolled && !organizationUser.UsesKeyConnector;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
@ -38,6 +38,12 @@ namespace Bit.Core.Models.Api
|
|||||||
UserId = organization.UserId?.ToString();
|
UserId = organization.UserId?.ToString();
|
||||||
ProviderId = organization.ProviderId?.ToString();
|
ProviderId = organization.ProviderId?.ToString();
|
||||||
ProviderName = organization.ProviderName;
|
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; }
|
public string Id { get; set; }
|
||||||
@ -68,5 +74,7 @@ namespace Bit.Core.Models.Api
|
|||||||
public bool HasPublicAndPrivateKeys { get; set; }
|
public bool HasPublicAndPrivateKeys { get; set; }
|
||||||
public string ProviderId { get; set; }
|
public string ProviderId { get; set; }
|
||||||
public string ProviderName { 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;
|
PrivateKey = user.PrivateKey;
|
||||||
SecurityStamp = user.SecurityStamp;
|
SecurityStamp = user.SecurityStamp;
|
||||||
ForcePasswordReset = user.ForcePasswordReset;
|
ForcePasswordReset = user.ForcePasswordReset;
|
||||||
|
UsesKeyConnector = user.UsesKeyConnector;
|
||||||
Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o));
|
Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o));
|
||||||
Providers = providerUserDetails?.Select(p => new ProfileProviderResponseModel(p));
|
Providers = providerUserDetails?.Select(p => new ProfileProviderResponseModel(p));
|
||||||
ProviderOrganizations =
|
ProviderOrganizations =
|
||||||
@ -49,6 +50,7 @@ namespace Bit.Core.Models.Api
|
|||||||
public string PrivateKey { get; set; }
|
public string PrivateKey { get; set; }
|
||||||
public string SecurityStamp { get; set; }
|
public string SecurityStamp { get; set; }
|
||||||
public bool ForcePasswordReset { get; set; }
|
public bool ForcePasswordReset { get; set; }
|
||||||
|
public bool UsesKeyConnector { get; set; }
|
||||||
public IEnumerable<ProfileOrganizationResponseModel> Organizations { get; set; }
|
public IEnumerable<ProfileOrganizationResponseModel> Organizations { get; set; }
|
||||||
public IEnumerable<ProfileProviderResponseModel> Providers { get; set; }
|
public IEnumerable<ProfileProviderResponseModel> Providers { get; set; }
|
||||||
public IEnumerable<ProfileProviderOrganizationResponseModel> ProviderOrganizations { get; set; }
|
public IEnumerable<ProfileProviderOrganizationResponseModel> ProviderOrganizations { get; set; }
|
||||||
|
@ -33,5 +33,6 @@ namespace Bit.Core.Models.Data
|
|||||||
public string PrivateKey { get; set; }
|
public string PrivateKey { get; set; }
|
||||||
public Guid? ProviderId { get; set; }
|
public Guid? ProviderId { get; set; }
|
||||||
public string ProviderName { 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 SsoExternalId { get; set; }
|
||||||
public string Permissions { get; set; }
|
public string Permissions { get; set; }
|
||||||
public string ResetPasswordKey { get; set; }
|
public string ResetPasswordKey { get; set; }
|
||||||
|
public bool UsesKeyConnector { get; set; }
|
||||||
|
|
||||||
public Dictionary<TwoFactorProviderType, TwoFactorProvider> GetTwoFactorProviders()
|
public Dictionary<TwoFactorProviderType, TwoFactorProvider> GetTwoFactorProviders()
|
||||||
{
|
{
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Sso;
|
using Bit.Core.Sso;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Data
|
namespace Bit.Core.Models.Data
|
||||||
@ -13,11 +15,20 @@ namespace Bit.Core.Models.Data
|
|||||||
private const string _oidcSignedOutPath = "/oidc-signedout";
|
private const string _oidcSignedOutPath = "/oidc-signedout";
|
||||||
private const string _saml2ModulePath = "/saml2";
|
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; }
|
public SsoType ConfigType { get; set; }
|
||||||
|
|
||||||
// Crypto Agent
|
public bool UseKeyConnector { get; set; }
|
||||||
public bool UseCryptoAgent { get; set; }
|
public string KeyConnectorUrl { get; set; }
|
||||||
public string CryptoAgentUrl { get; set; }
|
|
||||||
|
|
||||||
// OIDC
|
// OIDC
|
||||||
public string Authority { get; set; }
|
public string Authority { get; set; }
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text.Json;
|
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Table
|
namespace Bit.Core.Models.Table
|
||||||
@ -13,11 +12,6 @@ namespace Bit.Core.Models.Table
|
|||||||
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
|
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
|
||||||
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
|
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
private JsonSerializerOptions _jsonSerializerOptions = new()
|
|
||||||
{
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
};
|
|
||||||
|
|
||||||
public void SetNewId()
|
public void SetNewId()
|
||||||
{
|
{
|
||||||
// int will be auto-populated
|
// int will be auto-populated
|
||||||
@ -26,12 +20,12 @@ namespace Bit.Core.Models.Table
|
|||||||
|
|
||||||
public SsoConfigurationData GetData()
|
public SsoConfigurationData GetData()
|
||||||
{
|
{
|
||||||
return JsonSerializer.Deserialize<SsoConfigurationData>(Data, _jsonSerializerOptions);
|
return SsoConfigurationData.Deserialize(Data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetData(SsoConfigurationData 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 CreationDate { get; internal set; } = DateTime.UtcNow;
|
||||||
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
|
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
|
||||||
public bool ForcePasswordReset { get; set; }
|
public bool ForcePasswordReset { get; set; }
|
||||||
public bool UsesCryptoAgent { get; set; }
|
public bool UsesKeyConnector { get; set; }
|
||||||
|
|
||||||
public void SetNewId()
|
public void SetNewId()
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
|
using Microsoft.Azure.Documents.SystemFunctions;
|
||||||
|
|
||||||
namespace Bit.Core.Repositories.EntityFramework.Queries
|
namespace Bit.Core.Repositories.EntityFramework.Queries
|
||||||
{
|
{
|
||||||
@ -16,8 +17,10 @@ namespace Bit.Core.Repositories.EntityFramework.Queries
|
|||||||
from po in po_g.DefaultIfEmpty()
|
from po in po_g.DefaultIfEmpty()
|
||||||
join p in dbContext.Providers on po.ProviderId equals p.Id into p_g
|
join p in dbContext.Providers on po.ProviderId equals p.Id into p_g
|
||||||
from p in p_g.DefaultIfEmpty()
|
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)
|
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
|
return query.Select(x => new OrganizationUserOrganizationDetails
|
||||||
{
|
{
|
||||||
OrganizationId = x.ou.OrganizationId,
|
OrganizationId = x.ou.OrganizationId,
|
||||||
@ -48,6 +51,7 @@ namespace Bit.Core.Repositories.EntityFramework.Queries
|
|||||||
PrivateKey = x.o.PrivateKey,
|
PrivateKey = x.o.PrivateKey,
|
||||||
ProviderId = x.p.Id,
|
ProviderId = x.p.Id,
|
||||||
ProviderName = x.p.Name,
|
ProviderName = x.p.Name,
|
||||||
|
SsoConfig = x.ss.Data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ namespace Bit.Core.Repositories.EntityFramework.Queries
|
|||||||
SsoExternalId = x.su.ExternalId,
|
SsoExternalId = x.su.ExternalId,
|
||||||
Permissions = x.ou.Permissions,
|
Permissions = x.ou.Permissions,
|
||||||
ResetPasswordKey = x.ou.ResetPasswordKey,
|
ResetPasswordKey = x.ou.ResetPasswordKey,
|
||||||
|
UsesKeyConnector = x.u.UsesKeyConnector,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,5 +49,6 @@ namespace Bit.Core.Services
|
|||||||
Task SendProviderConfirmedEmailAsync(string providerName, string email);
|
Task SendProviderConfirmedEmailAsync(string providerName, string email);
|
||||||
Task SendProviderUserRemoved(string providerName, string email);
|
Task SendProviderUserRemoved(string providerName, string email);
|
||||||
Task SendUpdatedTempPasswordEmailAsync(string email, string userName);
|
Task SendUpdatedTempPasswordEmailAsync(string email, string userName);
|
||||||
|
Task SendOTPEmailAsync(string email, string token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,8 @@ namespace Bit.Core.Services
|
|||||||
string token, string key);
|
string token, string key);
|
||||||
Task<IdentityResult> ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, 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> 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> AdminResetPasswordAsync(OrganizationUserType type, Guid orgId, Guid id, string newMasterPassword, string key);
|
||||||
Task<IdentityResult> UpdateTempPasswordAsync(User user, string newMasterPassword, string key, string hint);
|
Task<IdentityResult> UpdateTempPasswordAsync(User user, string newMasterPassword, string key, string hint);
|
||||||
Task<IdentityResult> ChangeKdfAsync(User user, string masterPassword, string newMasterPassword, string key,
|
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<string> GenerateSignInTokenAsync(User user, string purpose);
|
||||||
Task RotateApiKeyAsync(User user);
|
Task RotateApiKeyAsync(User user);
|
||||||
string GetUserName(ClaimsPrincipal principal);
|
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";
|
message.Category = "UpdatedTempPassword";
|
||||||
await _mailDeliveryService.SendEmailAsync(message);
|
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)
|
public async Task DeleteAsync(Organization organization)
|
||||||
{
|
{
|
||||||
|
await ValidateDeleteOrganizationAsync(organization);
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
if (!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -2135,5 +2137,14 @@ namespace Bit.Core.Services
|
|||||||
throw new BadRequestException("Custom users can not manage Admins or Owners.");
|
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 IOrganizationRepository _organizationRepository;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyRepository _policyRepository;
|
||||||
|
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
|
|
||||||
public PolicyService(
|
public PolicyService(
|
||||||
@ -22,12 +23,14 @@ namespace Bit.Core.Services
|
|||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IPolicyRepository policyRepository,
|
IPolicyRepository policyRepository,
|
||||||
|
ISsoConfigRepository ssoConfigRepository,
|
||||||
IMailService mailService)
|
IMailService mailService)
|
||||||
{
|
{
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_policyRepository = policyRepository;
|
_policyRepository = policyRepository;
|
||||||
|
_ssoConfigRepository = ssoConfigRepository;
|
||||||
_mailService = mailService;
|
_mailService = mailService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,6 +67,12 @@ namespace Bit.Core.Services
|
|||||||
{
|
{
|
||||||
throw new BadRequestException("Maximum Vault Timeout policy is enabled.");
|
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;
|
break;
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Table;
|
using Bit.Core.Models.Table;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
|
||||||
@ -8,11 +10,20 @@ namespace Bit.Core.Services
|
|||||||
public class SsoConfigService : ISsoConfigService
|
public class SsoConfigService : ISsoConfigService
|
||||||
{
|
{
|
||||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||||
|
private readonly IPolicyRepository _policyRepository;
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
private readonly IEventService _eventService;
|
||||||
|
|
||||||
public SsoConfigService(
|
public SsoConfigService(
|
||||||
ISsoConfigRepository ssoConfigRepository)
|
ISsoConfigRepository ssoConfigRepository,
|
||||||
|
IPolicyRepository policyRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IEventService eventService)
|
||||||
{
|
{
|
||||||
_ssoConfigRepository = ssoConfigRepository;
|
_ssoConfigRepository = ssoConfigRepository;
|
||||||
|
_policyRepository = policyRepository;
|
||||||
|
_organizationRepository = organizationRepository;
|
||||||
|
_eventService = eventService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveAsync(SsoConfig config)
|
public async Task SaveAsync(SsoConfig config)
|
||||||
@ -23,7 +34,49 @@ namespace Bit.Core.Services
|
|||||||
{
|
{
|
||||||
config.CreationDate = now;
|
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);
|
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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Threading.Tasks;
|
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.AspNetCore.Identity;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
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 Newtonsoft.Json;
|
||||||
using Microsoft.AspNetCore.DataProtection;
|
using File = System.IO.File;
|
||||||
using Fido2NetLib;
|
using U2fLib = U2F.Core.Crypto.U2F;
|
||||||
using Fido2NetLib.Objects;
|
|
||||||
|
|
||||||
namespace Bit.Core.Services
|
namespace Bit.Core.Services
|
||||||
{
|
{
|
||||||
@ -602,7 +603,7 @@ namespace Bit.Core.Services
|
|||||||
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
|
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)
|
string orgIdentifier = null)
|
||||||
{
|
{
|
||||||
if (user == null)
|
if (user == null)
|
||||||
@ -627,42 +628,63 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
await _userRepository.ReplaceAsync(user);
|
await _userRepository.ReplaceAsync(user);
|
||||||
await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);
|
await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(orgIdentifier))
|
if (!string.IsNullOrWhiteSpace(orgIdentifier))
|
||||||
{
|
{
|
||||||
await _organizationService.AcceptUserAsync(orgIdentifier, user, this);
|
await _organizationService.AcceptUserAsync(orgIdentifier, user, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
return IdentityResult.Success;
|
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)
|
if (user == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(user));
|
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());
|
return IdentityResult.Failed(_identityErrorDescriber.UserAlreadyHasPassword());
|
||||||
}
|
}
|
||||||
|
|
||||||
user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;
|
user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;
|
||||||
user.Key = key;
|
user.Key = key;
|
||||||
user.UsesCryptoAgent = true;
|
user.UsesKeyConnector = true;
|
||||||
|
|
||||||
await _userRepository.ReplaceAsync(user);
|
await _userRepository.ReplaceAsync(user);
|
||||||
// TODO: Use correct event
|
await _eventService.LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);
|
||||||
await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);
|
|
||||||
|
|
||||||
await _organizationService.AcceptUserAsync(orgIdentifier, user, this);
|
await _organizationService.AcceptUserAsync(orgIdentifier, user, this);
|
||||||
|
|
||||||
return IdentityResult.Success;
|
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)
|
public async Task<IdentityResult> AdminResetPasswordAsync(OrganizationUserType callingUserType, Guid orgId, Guid id, string newMasterPassword, string key)
|
||||||
{
|
{
|
||||||
// Org must be able to use reset password
|
// 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.");
|
throw new BadRequestException("Organization does not allow password reset.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enterprise policy must be enabled
|
// Enterprise policy must be enabled
|
||||||
var resetPasswordPolicy =
|
var resetPasswordPolicy =
|
||||||
await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
||||||
if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled)
|
if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Organization does not have the password reset policy enabled.");
|
throw new BadRequestException("Organization does not have the password reset policy enabled.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Org User must be confirmed and have a ResetPasswordKey
|
// Org User must be confirmed and have a ResetPasswordKey
|
||||||
var orgUser = await _organizationUserRepository.GetByIdAsync(id);
|
var orgUser = await _organizationUserRepository.GetByIdAsync(id);
|
||||||
if (orgUser == null || orgUser.Status != OrganizationUserStatusType.Confirmed ||
|
if (orgUser == null || orgUser.Status != OrganizationUserStatusType.Confirmed ||
|
||||||
@ -688,7 +710,7 @@ namespace Bit.Core.Services
|
|||||||
{
|
{
|
||||||
throw new BadRequestException("Organization User not valid");
|
throw new BadRequestException("Organization User not valid");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calling User must be of higher/equal user type to reset user's password
|
// Calling User must be of higher/equal user type to reset user's password
|
||||||
var canAdjustPassword = false;
|
var canAdjustPassword = false;
|
||||||
switch (callingUserType)
|
switch (callingUserType)
|
||||||
@ -715,7 +737,12 @@ namespace Bit.Core.Services
|
|||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.UsesKeyConnector)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cannot reset password of a user with key connector.");
|
||||||
|
}
|
||||||
|
|
||||||
var result = await UpdatePasswordHash(user, newMasterPassword);
|
var result = await UpdatePasswordHash(user, newMasterPassword);
|
||||||
if (!result.Succeeded)
|
if (!result.Succeeded)
|
||||||
{
|
{
|
||||||
@ -733,14 +760,14 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
return IdentityResult.Success;
|
return IdentityResult.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IdentityResult> UpdateTempPasswordAsync(User user, string newMasterPassword, string key, string hint)
|
public async Task<IdentityResult> UpdateTempPasswordAsync(User user, string newMasterPassword, string key, string hint)
|
||||||
{
|
{
|
||||||
if (!user.ForcePasswordReset)
|
if (!user.ForcePasswordReset)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("User does not have a temporary password to update.");
|
throw new BadRequestException("User does not have a temporary password to update.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await UpdatePasswordHash(user, newMasterPassword);
|
var result = await UpdatePasswordHash(user, newMasterPassword);
|
||||||
if (!result.Succeeded)
|
if (!result.Succeeded)
|
||||||
{
|
{
|
||||||
@ -820,14 +847,14 @@ namespace Bit.Core.Services
|
|||||||
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
|
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)
|
if (user == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(user));
|
throw new ArgumentNullException(nameof(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await CheckPasswordAsync(user, masterPassword))
|
if (await VerifySecretAsync(user, secret))
|
||||||
{
|
{
|
||||||
var result = await base.UpdateSecurityStampAsync(user);
|
var result = await base.UpdateSecurityStampAsync(user);
|
||||||
if (!result.Succeeded)
|
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)
|
IOrganizationService organizationService)
|
||||||
{
|
{
|
||||||
var user = await _userRepository.GetByEmailAsync(email);
|
var user = await _userRepository.GetByEmailAsync(email);
|
||||||
@ -888,7 +915,7 @@ namespace Bit.Core.Services
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await CheckPasswordAsync(user, masterPassword))
|
if (!await VerifySecretAsync(user, secret))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1253,7 +1280,7 @@ namespace Bit.Core.Services
|
|||||||
purpose);
|
purpose);
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IdentityResult> UpdatePasswordHash(User user, string newPassword,
|
private async Task<IdentityResult> UpdatePasswordHash(User user, string newPassword,
|
||||||
bool validatePassword = true, bool refreshStamp = true)
|
bool validatePassword = true, bool refreshStamp = true)
|
||||||
{
|
{
|
||||||
@ -1350,5 +1377,35 @@ namespace Bit.Core.Services
|
|||||||
user.RevisionDate = DateTime.UtcNow;
|
user.RevisionDate = DateTime.UtcNow;
|
||||||
await _userRepository.ReplaceAsync(user);
|
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);
|
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);
|
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)
|
public static ICollection<T> AddIfNotExists<T>(this ICollection<T> list, T item)
|
||||||
{
|
{
|
||||||
if (list.Contains(item))
|
if (list.Contains(item))
|
||||||
|
@ -175,7 +175,7 @@ namespace Bit.Core.Utilities
|
|||||||
services.AddScoped<IEmergencyAccessService, EmergencyAccessService>();
|
services.AddScoped<IEmergencyAccessService, EmergencyAccessService>();
|
||||||
services.AddSingleton<IDeviceService, DeviceService>();
|
services.AddSingleton<IDeviceService, DeviceService>();
|
||||||
services.AddSingleton<IAppleIapService, AppleIapService>();
|
services.AddSingleton<IAppleIapService, AppleIapService>();
|
||||||
services.AddSingleton<ISsoConfigService, SsoConfigService>();
|
services.AddScoped<ISsoConfigService, SsoConfigService>();
|
||||||
services.AddScoped<ISendService, SendService>();
|
services.AddScoped<ISendService, SendService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
@RevisionDate DATETIME2(7),
|
@RevisionDate DATETIME2(7),
|
||||||
@ApiKey VARCHAR(30),
|
@ApiKey VARCHAR(30),
|
||||||
@ForcePasswordReset BIT = 0,
|
@ForcePasswordReset BIT = 0,
|
||||||
@UsesCryptoAgent BIT = 0
|
@UsesKeyConnector BIT = 0
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@ -70,7 +70,7 @@ BEGIN
|
|||||||
[RevisionDate],
|
[RevisionDate],
|
||||||
[ApiKey],
|
[ApiKey],
|
||||||
[ForcePasswordReset],
|
[ForcePasswordReset],
|
||||||
[UsesCryptoAgent]
|
[UsesKeyConnector]
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
@ -106,6 +106,6 @@ BEGIN
|
|||||||
@RevisionDate,
|
@RevisionDate,
|
||||||
@ApiKey,
|
@ApiKey,
|
||||||
@ForcePasswordReset,
|
@ForcePasswordReset,
|
||||||
@UsesCryptoAgent
|
@UsesKeyConnector
|
||||||
)
|
)
|
||||||
END
|
END
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
@RevisionDate DATETIME2(7),
|
@RevisionDate DATETIME2(7),
|
||||||
@ApiKey VARCHAR(30),
|
@ApiKey VARCHAR(30),
|
||||||
@ForcePasswordReset BIT = 0,
|
@ForcePasswordReset BIT = 0,
|
||||||
@UsesCryptoAgent BIT = 0
|
@UsesKeyConnector BIT = 0
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@ -70,7 +70,7 @@ BEGIN
|
|||||||
[RevisionDate] = @RevisionDate,
|
[RevisionDate] = @RevisionDate,
|
||||||
[ApiKey] = @ApiKey,
|
[ApiKey] = @ApiKey,
|
||||||
[ForcePasswordReset] = @ForcePasswordReset,
|
[ForcePasswordReset] = @ForcePasswordReset,
|
||||||
[UsesCryptoAgent] = @UsesCryptoAgent
|
[UsesKeyConnector] = @UsesKeyConnector
|
||||||
WHERE
|
WHERE
|
||||||
[Id] = @Id
|
[Id] = @Id
|
||||||
END
|
END
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
[RevisionDate] DATETIME2 (7) NOT NULL,
|
[RevisionDate] DATETIME2 (7) NOT NULL,
|
||||||
[ApiKey] VARCHAR (30) NOT NULL,
|
[ApiKey] VARCHAR (30) NOT NULL,
|
||||||
[ForcePasswordReset] BIT NOT NULL,
|
[ForcePasswordReset] BIT NOT NULL,
|
||||||
[UsesCryptoAgent] BIT NOT NULL,
|
[UsesKeyConnector] BIT NOT NULL,
|
||||||
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC)
|
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -29,7 +29,8 @@ SELECT
|
|||||||
SU.[ExternalId] SsoExternalId,
|
SU.[ExternalId] SsoExternalId,
|
||||||
OU.[Permissions],
|
OU.[Permissions],
|
||||||
PO.[ProviderId],
|
PO.[ProviderId],
|
||||||
P.[Name] ProviderName
|
P.[Name] ProviderName,
|
||||||
|
SS.[Data] SsoConfig
|
||||||
FROM
|
FROM
|
||||||
[dbo].[OrganizationUser] OU
|
[dbo].[OrganizationUser] OU
|
||||||
INNER JOIN
|
INNER JOIN
|
||||||
@ -40,3 +41,5 @@ LEFT JOIN
|
|||||||
[dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id]
|
[dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id]
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
[dbo].[Provider] P ON P.[Id] = PO.[ProviderId]
|
[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],
|
OU.[ExternalId],
|
||||||
SU.[ExternalId] SsoExternalId,
|
SU.[ExternalId] SsoExternalId,
|
||||||
OU.[Permissions],
|
OU.[Permissions],
|
||||||
OU.[ResetPasswordKey]
|
OU.[ResetPasswordKey],
|
||||||
|
U.[UsesKeyConnector]
|
||||||
FROM
|
FROM
|
||||||
[dbo].[OrganizationUser] OU
|
[dbo].[OrganizationUser] OU
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
|
@ -55,7 +55,6 @@ namespace Bit.Api.Test.Controllers
|
|||||||
_organizationUserRepository,
|
_organizationUserRepository,
|
||||||
_providerUserRepository,
|
_providerUserRepository,
|
||||||
_paymentService,
|
_paymentService,
|
||||||
_ssoUserRepository,
|
|
||||||
_userRepository,
|
_userRepository,
|
||||||
_userService,
|
_userService,
|
||||||
_sendRepository,
|
_sendRepository,
|
||||||
@ -320,7 +319,7 @@ namespace Bit.Api.Test.Controllers
|
|||||||
var user = GenerateExampleUser();
|
var user = GenerateExampleUser();
|
||||||
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||||
ConfigureUserServiceToAcceptPasswordFor(user);
|
ConfigureUserServiceToAcceptPasswordFor(user);
|
||||||
await _sut.ApiKey(new ApiKeyRequestModel());
|
await _sut.ApiKey(new SecretVerificationRequestModel());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -329,7 +328,7 @@ namespace Bit.Api.Test.Controllers
|
|||||||
ConfigureUserServiceToReturnNullPrincipal();
|
ConfigureUserServiceToReturnNullPrincipal();
|
||||||
|
|
||||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(
|
await Assert.ThrowsAsync<UnauthorizedAccessException>(
|
||||||
() => _sut.ApiKey(new ApiKeyRequestModel())
|
() => _sut.ApiKey(new SecretVerificationRequestModel())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,7 +339,7 @@ namespace Bit.Api.Test.Controllers
|
|||||||
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||||
ConfigureUserServiceToRejectPasswordFor(user);
|
ConfigureUserServiceToRejectPasswordFor(user);
|
||||||
await Assert.ThrowsAsync<BadRequestException>(
|
await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => _sut.ApiKey(new ApiKeyRequestModel())
|
() => _sut.ApiKey(new SecretVerificationRequestModel())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -350,7 +349,7 @@ namespace Bit.Api.Test.Controllers
|
|||||||
var user = GenerateExampleUser();
|
var user = GenerateExampleUser();
|
||||||
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||||
ConfigureUserServiceToAcceptPasswordFor(user);
|
ConfigureUserServiceToAcceptPasswordFor(user);
|
||||||
await _sut.RotateApiKey(new ApiKeyRequestModel());
|
await _sut.RotateApiKey(new SecretVerificationRequestModel());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -359,7 +358,7 @@ namespace Bit.Api.Test.Controllers
|
|||||||
ConfigureUserServiceToReturnNullPrincipal();
|
ConfigureUserServiceToReturnNullPrincipal();
|
||||||
|
|
||||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(
|
await Assert.ThrowsAsync<UnauthorizedAccessException>(
|
||||||
() => _sut.ApiKey(new ApiKeyRequestModel())
|
() => _sut.ApiKey(new SecretVerificationRequestModel())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,7 +369,7 @@ namespace Bit.Api.Test.Controllers
|
|||||||
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||||
ConfigureUserServiceToRejectPasswordFor(user);
|
ConfigureUserServiceToRejectPasswordFor(user);
|
||||||
await Assert.ThrowsAsync<BadRequestException>(
|
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>())
|
_userService.CheckPasswordAsync(user, Arg.Any<string>())
|
||||||
.Returns(Task.FromResult(true));
|
.Returns(Task.FromResult(true));
|
||||||
|
_userService.VerifySecretAsync(user, Arg.Any<string>())
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ConfigureUserServiceToReturnValidIdFor(User user)
|
private void ConfigureUserServiceToReturnValidIdFor(User user)
|
||||||
|
@ -891,5 +891,38 @@ namespace Bit.Core.Test.Services
|
|||||||
Assert.False(result);
|
Assert.False(result);
|
||||||
Assert.Contains("Cannot autoscale on self-hosted instance", failureMessage);
|
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.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Table;
|
using Bit.Core.Models.Table;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -125,6 +126,40 @@ namespace Bit.Core.Test.Services
|
|||||||
.UpsertAsync(default);
|
.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))]
|
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||||
public async Task SaveAsync_RequireSsoPolicy_NotEnabled_ThrowsBadRequestAsync([PolicyFixtures.Policy(Enums.PolicyType.RequireSso)] Core.Models.Table.Policy policy, SutProvider<PolicyService> sutProvider)
|
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;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Table;
|
using Bit.Core.Models.Table;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -21,7 +22,7 @@ namespace Bit.Core.Test.Services
|
|||||||
var ssoConfig = new SsoConfig
|
var ssoConfig = new SsoConfig
|
||||||
{
|
{
|
||||||
Id = 1,
|
Id = 1,
|
||||||
Data = "TESTDATA",
|
Data = "{}",
|
||||||
Enabled = true,
|
Enabled = true,
|
||||||
OrganizationId = Guid.NewGuid(),
|
OrganizationId = Guid.NewGuid(),
|
||||||
CreationDate = utcNow.AddDays(-10),
|
CreationDate = utcNow.AddDays(-10),
|
||||||
@ -48,7 +49,7 @@ namespace Bit.Core.Test.Services
|
|||||||
var ssoConfig = new SsoConfig
|
var ssoConfig = new SsoConfig
|
||||||
{
|
{
|
||||||
Id = default,
|
Id = default,
|
||||||
Data = "TESTDATA",
|
Data = "{}",
|
||||||
Enabled = true,
|
Enabled = true,
|
||||||
OrganizationId = Guid.NewGuid(),
|
OrganizationId = Guid.NewGuid(),
|
||||||
CreationDate = utcNow.AddDays(-10),
|
CreationDate = utcNow.AddDays(-10),
|
||||||
@ -66,5 +67,67 @@ namespace Bit.Core.Test.Services
|
|||||||
Assert.True(ssoConfig.CreationDate - utcNow < TimeSpan.FromSeconds(1));
|
Assert.True(ssoConfig.CreationDate - utcNow < TimeSpan.FromSeconds(1));
|
||||||
Assert.True(ssoConfig.RevisionDate - 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
|
BEGIN
|
||||||
ALTER TABLE
|
ALTER TABLE
|
||||||
[dbo].[User]
|
[dbo].[User]
|
||||||
ADD
|
ADD
|
||||||
[UsesCryptoAgent] BIT NULL
|
[UsesKeyConnector] BIT NULL
|
||||||
END
|
END
|
||||||
GO
|
GO
|
||||||
|
|
||||||
UPDATE
|
UPDATE
|
||||||
[dbo].[User]
|
[dbo].[User]
|
||||||
SET
|
SET
|
||||||
[UsesCryptoAgent] = 0
|
[UsesKeyConnector] = 0
|
||||||
WHERE
|
WHERE
|
||||||
[UsesCryptoAgent] IS NULL
|
[UsesKeyConnector] IS NULL
|
||||||
GO
|
GO
|
||||||
|
|
||||||
ALTER TABLE
|
ALTER TABLE
|
||||||
[dbo].[User]
|
[dbo].[User]
|
||||||
ALTER COLUMN
|
ALTER COLUMN
|
||||||
[UsesCryptoAgent] BIT NOT NULL
|
[UsesKeyConnector] BIT NOT NULL
|
||||||
GO
|
GO
|
||||||
|
|
||||||
-- View: User
|
-- View: User
|
||||||
@ -75,7 +75,7 @@ CREATE PROCEDURE [dbo].[User_Create]
|
|||||||
@RevisionDate DATETIME2(7),
|
@RevisionDate DATETIME2(7),
|
||||||
@ApiKey VARCHAR(30),
|
@ApiKey VARCHAR(30),
|
||||||
@ForcePasswordReset BIT = 0,
|
@ForcePasswordReset BIT = 0,
|
||||||
@UsesCryptoAgent BIT = 0
|
@UsesKeyConnector BIT = 0
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@ -114,7 +114,7 @@ BEGIN
|
|||||||
[RevisionDate],
|
[RevisionDate],
|
||||||
[ApiKey],
|
[ApiKey],
|
||||||
[ForcePasswordReset],
|
[ForcePasswordReset],
|
||||||
[UsesCryptoAgent]
|
[UsesKeyConnector]
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
@ -150,7 +150,7 @@ BEGIN
|
|||||||
@RevisionDate,
|
@RevisionDate,
|
||||||
@ApiKey,
|
@ApiKey,
|
||||||
@ForcePasswordReset,
|
@ForcePasswordReset,
|
||||||
@UsesCryptoAgent
|
@UsesKeyConnector
|
||||||
)
|
)
|
||||||
END
|
END
|
||||||
GO
|
GO
|
||||||
@ -194,7 +194,7 @@ CREATE PROCEDURE [dbo].[User_Update]
|
|||||||
@RevisionDate DATETIME2(7),
|
@RevisionDate DATETIME2(7),
|
||||||
@ApiKey VARCHAR(30),
|
@ApiKey VARCHAR(30),
|
||||||
@ForcePasswordReset BIT = 0,
|
@ForcePasswordReset BIT = 0,
|
||||||
@UsesCryptoAgent BIT = 0
|
@UsesKeyConnector BIT = 0
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@ -233,7 +233,92 @@ BEGIN
|
|||||||
[RevisionDate] = @RevisionDate,
|
[RevisionDate] = @RevisionDate,
|
||||||
[ApiKey] = @ApiKey,
|
[ApiKey] = @ApiKey,
|
||||||
[ForcePasswordReset] = @ForcePasswordReset,
|
[ForcePasswordReset] = @ForcePasswordReset,
|
||||||
[UsesCryptoAgent] = @UsesCryptoAgent
|
[UsesKeyConnector] = @UsesKeyConnector
|
||||||
WHERE
|
WHERE
|
||||||
[Id] = @Id
|
[Id] = @Id
|
||||||
END
|
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)
|
.HasMaxLength(32)
|
||||||
.HasColumnType("varchar(32)");
|
.HasColumnType("varchar(32)");
|
||||||
|
|
||||||
|
b.Property<bool>("UsesKeyConnector")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("User");
|
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)
|
.HasMaxLength(32)
|
||||||
.HasColumnType("character varying(32)");
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<bool>("UsesKeyConnector")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("User");
|
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