1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-01 08:02:49 -05:00

Merge branch 'master' into feature/families-for-enterprise

This commit is contained in:
Justin Baur
2021-11-18 08:32:28 -05:00
59 changed files with 4250 additions and 129 deletions

View File

@ -248,7 +248,9 @@ jobs:
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
with:
keyvault: "bitwarden-prod-kv"
secrets: "docker-password,
secrets: "aws-ecr-access-key-id,
aws-ecr-secret-access-key,
docker-password,
docker-username,
dct-delegate-2-repo-passphrase,
dct-delegate-2-key"
@ -278,7 +280,6 @@ jobs:
DCT_DELEGATE_KEY: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-key }}
run: |
mkdir -p ~/.docker/trust/private
echo "$DCT_DELEGATE_KEY" > ~/.docker/trust/private/$DCT_DELEGATION_KEY_ID.key
- name: Setup service name
@ -306,34 +307,12 @@ jobs:
run: |
if [ "${{ matrix.service_name }}" = "K8S-Proxy" ]; then
docker build -f ${{ matrix.base_path }}/Nginx/Dockerfile-k8s \
-t ${{ matrix.docker_repo }}/${{ steps.setup.outputs.service_name }} ${{ matrix.base_path }}/Nginx
-t ${{ steps.setup.outputs.service_name }} ${{ matrix.base_path }}/Nginx
else
docker build -t ${{ matrix.docker_repo }}/${{ steps.setup.outputs.service_name }} \
docker build -t ${{ steps.setup.outputs.service_name }} \
${{ matrix.base_path }}/${{ matrix.service_name }}
fi
- name: Tag rc
if: github.ref == 'refs/heads/rc'
run: |
docker tag ${{ matrix.docker_repo }}/${{ steps.setup.outputs.service_name }} \
${{ matrix.docker_repo }}/${{ steps.setup.outputs.service_name }}:rc
- name: Tag hotfix
if: github.ref == 'refs/heads/hotfix'
run: |
docker tag ${{ matrix.docker_repo }}/${{ steps.setup.outputs.service_name }} \
${{ matrix.docker_repo }}/${{ steps.setup.outputs.service_name }}:hotfix
- name: Tag dev
if: github.ref == 'refs/heads/master'
run: |
docker tag ${{ matrix.docker_repo }}/${{ steps.setup.outputs.service_name }} \
${{ matrix.docker_repo }}/${{ steps.setup.outputs.service_name }}:dev
- name: List Docker images
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
run: docker images
- name: Docker Trust setup
if: |
matrix.docker_repo == 'bitwarden'
@ -342,26 +321,75 @@ jobs:
DCT_REPO_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
run: |
echo "DOCKER_CONTENT_TRUST=1" >> $GITHUB_ENV
echo "DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE=$DCT_REPO_PASSPHRASE" >> $GITHUB_ENV
echo "DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE=$DCT_REPO_PASSPHRASE" >> $GITHUB_ENV
- name: Push rc images
- name: Tag and Push RC to Docker Hub
if: github.ref == 'refs/heads/rc'
run: |
docker tag ${{ steps.setup.outputs.service_name }} \
${{ matrix.docker_repo }}/${{ steps.setup.outputs.service_name }}:rc
docker push ${{ matrix.docker_repo }}/${{ steps.setup.outputs.service_name }}:rc
- name: Push hotfix images
- name: Tag and Push Hotfix to Docker Hub
if: github.ref == 'refs/heads/hotfix'
run: |
docker tag ${{ steps.setup.outputs.service_name }} \
${{ matrix.docker_repo }}/${{ steps.setup.outputs.service_name }}:hotfix
docker push ${{ matrix.docker_repo }}/${{ steps.setup.outputs.service_name }}:hotfix
- name: Push dev images
- name: Tag and Push Dev to Docker Hub
if: github.ref == 'refs/heads/master'
run: |
docker tag ${{ steps.setup.outputs.service_name }} \
${{ matrix.docker_repo }}/${{ steps.setup.outputs.service_name }}:dev
docker push ${{ matrix.docker_repo }}/${{ steps.setup.outputs.service_name }}:dev
- name: Log out of Docker
- name: Log out of Docker and disable Docker Notary
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
run: docker logout
run: |
docker logout
echo "DOCKER_CONTENT_TRUST=0" >> $GITHUB_ENV
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@0d9a5be0dceea74e09396820e1e522ba4a110d2f # v1
with:
aws-access-key-id: ${{ steps.retrieve-secrets.outputs.aws-ecr-access-key-id }}
aws-secret-access-key: ${{ steps.retrieve-secrets.outputs.aws-ecr-secret-access-key }}
aws-region: us-east-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@aaf69d68aa3fb14c1d5a6be9ac61fe15b48453a2 # v1
- name: Tag and Push RC to AWS ECR nonprod registry
if: github.ref == 'refs/heads/rc'
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker tag ${{ steps.setup.outputs.service_name }} \
$ECR_REGISTRY/nonprod/${{ steps.setup.outputs.service_name }}:rc-${IMAGE_TAG:(-8)}
docker push $ECR_REGISTRY/nonprod/${{ steps.setup.outputs.service_name }}:rc-${IMAGE_TAG:(-8)}
- name: Tag and Push Hotfix to AWS ECR nonprod registry
if: github.ref == 'refs/heads/hotfix'
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker tag ${{ steps.setup.outputs.service_name }} \
$ECR_REGISTRY/nonprod/${{ steps.setup.outputs.service_name }}:hotfix-${IMAGE_TAG:(-8)}
docker push $ECR_REGISTRY/nonprod/${{ steps.setup.outputs.service_name }}:hotfix-${IMAGE_TAG:(-8)}
- name: Tag and Push Dev to AWS ECR nonprod registry
if: github.ref == 'refs/heads/master'
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker tag ${{ steps.setup.outputs.service_name }} \
$ECR_REGISTRY/nonprod/${{ steps.setup.outputs.service_name }}:dev-${IMAGE_TAG:(-8)}
docker push $ECR_REGISTRY/nonprod/${{ steps.setup.outputs.service_name }}:dev-${IMAGE_TAG:(-8)}
upload:

View File

@ -117,7 +117,8 @@ jobs:
release-docker:
name: Build Docker images
runs-on: ubuntu-20.04
needs: setup
needs:
- setup
env:
_RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
_BRANCH_NAME: ${{ needs.setup.outputs.branch-name }}

View File

@ -112,7 +112,7 @@ For more information, see: [Safe storage of app secrets in development in ASP.NE
We provide a helper scripts which simplifies setting user secrets for all projects in the repository.
Start by copying the `secret.json.example` file to `secret.json` and modify the existing settings and add any other required setting. Afterwards run the following command which will add the settings to each project in the bitwarden repository.
Start by copying the `secret.json.example` file to `secrets.json` and modify the existing settings and add any other required setting. Afterwards run the following command which will add the settings to each project in the bitwarden repository.
```powershell
.\setup_secrets.ps1

View File

@ -32,6 +32,7 @@ namespace Bit.Admin.Models
MaxCollections = org.MaxCollections;
UsePolicies = org.UsePolicies;
UseSso = org.UseSso;
UseKeyConnector = org.UseKeyConnector;
UseGroups = org.UseGroups;
UseDirectory = org.UseDirectory;
UseEvents = org.UseEvents;
@ -78,6 +79,8 @@ namespace Bit.Admin.Models
public bool UsePolicies { get; set; }
[Display(Name = "SSO")]
public bool UseSso { get; set; }
[Display(Name = "Key Connector with Customer Encryption")]
public bool UseKeyConnector { get; set; }
[Display(Name = "Groups")]
public bool UseGroups { get; set; }
[Display(Name = "Directory")]
@ -123,6 +126,7 @@ namespace Bit.Admin.Models
existingOrganization.MaxCollections = MaxCollections;
existingOrganization.UsePolicies = UsePolicies;
existingOrganization.UseSso = UseSso;
existingOrganization.UseKeyConnector = UseKeyConnector;
existingOrganization.UseGroups = UseGroups;
existingOrganization.UseDirectory = UseDirectory;
existingOrganization.UseEvents = UseEvents;

View File

@ -215,6 +215,10 @@
<input type="checkbox" class="form-check-input" asp-for="UseSso">
<label class="form-check-label" asp-for="UseSso"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseKeyConnector">
<label class="form-check-label" asp-for="UseKeyConnector"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseDirectory">
<label class="form-check-label" asp-for="UseDirectory"></label>

View File

@ -384,6 +384,13 @@ namespace Bit.Api.Controllers
throw new NotFoundException();
}
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgGuidId);
if (ssoConfig?.GetData()?.KeyConnectorEnabled == true &&
_currentContext.User.UsesKeyConnector)
{
throw new BadRequestException("You cannot leave this Organization because you are using its Key Connector.");
}
var userId = _userService.GetProperUserId(User);
await _organizationService.DeleteUserAsync(orgGuidId, userId.Value);
}
@ -642,7 +649,7 @@ namespace Bit.Api.Controllers
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(id);
ssoConfig = ssoConfig == null ? model.ToSsoConfig(id) : model.ToSsoConfig(ssoConfig);
await _ssoConfigService.SaveAsync(ssoConfig);
await _ssoConfigService.SaveAsync(ssoConfig, organization);
return new OrganizationSsoResponseModel(organization, _globalSettings, ssoConfig);
}

View File

@ -95,7 +95,8 @@ namespace Bit.Core.IdentityServer
if (context.Result.ValidatedRequest.GrantType == "client_credentials")
{
if (user.UsesKeyConnector) {
// KeyConnectorUrl is configured in the CLI client, just disable master password reset
// KeyConnectorUrl is configured in the CLI client, we just need to tell the client to use it
context.Result.CustomResponse["ApiUseKeyConnector"] = true;
context.Result.CustomResponse["ResetMasterPassword"] = false;
}
return;
@ -110,7 +111,7 @@ namespace Bit.Core.IdentityServer
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organizationId);
var ssoConfigData = ssoConfig.GetData();
if (ssoConfigData is { UseKeyConnector: true } && !string.IsNullOrEmpty(ssoConfigData.KeyConnectorUrl))
if (ssoConfigData is { KeyConnectorEnabled: true } && !string.IsNullOrEmpty(ssoConfigData.KeyConnectorUrl))
{
context.Result.CustomResponse["KeyConnectorUrl"] = ssoConfigData.KeyConnectorUrl;
// Prevent clients redirecting to set-password

View File

@ -42,7 +42,7 @@ namespace Bit.Core.Models.Api
[Required]
public SsoType ConfigType { get; set; }
public bool UseKeyConnector { get; set; }
public bool KeyConnectorEnabled { get; set; }
public string KeyConnectorUrl { get; set; }
// OIDC
@ -178,7 +178,7 @@ namespace Bit.Core.Models.Api
return new SsoConfigurationData
{
ConfigType = ConfigType,
UseKeyConnector = UseKeyConnector,
KeyConnectorEnabled = KeyConnectorEnabled,
KeyConnectorUrl = KeyConnectorUrl,
Authority = Authority,
ClientId = ClientId,

View File

@ -35,6 +35,7 @@ namespace Bit.Core.Models.Api
MaxStorageGb = organization.MaxStorageGb;
UsePolicies = organization.UsePolicies;
UseSso = organization.UseSso;
UseKeyConnector = organization.UseKeyConnector;
UseGroups = organization.UseGroups;
UseDirectory = organization.UseDirectory;
UseEvents = organization.UseEvents;
@ -65,6 +66,7 @@ namespace Bit.Core.Models.Api
public short? MaxStorageGb { get; set; }
public bool UsePolicies { get; set; }
public bool UseSso { get; set; }
public bool UseKeyConnector { get; set; }
public bool UseGroups { get; set; }
public bool UseDirectory { get; set; }
public bool UseEvents { get; set; }

View File

@ -14,6 +14,7 @@ namespace Bit.Core.Models.Api
Name = organization.Name;
UsePolicies = organization.UsePolicies;
UseSso = organization.UseSso;
UseKeyConnector = organization.UseKeyConnector;
UseGroups = organization.UseGroups;
UseDirectory = organization.UseDirectory;
UseEvents = organization.UseEvents;
@ -47,7 +48,7 @@ namespace Bit.Core.Models.Api
if (organization.SsoConfig != null)
{
var ssoConfigData = SsoConfigurationData.Deserialize(organization.SsoConfig);
UsesKeyConnector = ssoConfigData.UseKeyConnector && !string.IsNullOrEmpty(ssoConfigData.KeyConnectorUrl);
KeyConnectorEnabled = ssoConfigData.KeyConnectorEnabled && !string.IsNullOrEmpty(ssoConfigData.KeyConnectorUrl);
KeyConnectorUrl = ssoConfigData.KeyConnectorUrl;
}
}
@ -56,6 +57,7 @@ namespace Bit.Core.Models.Api
public string Name { get; set; }
public bool UsePolicies { get; set; }
public bool UseSso { get; set; }
public bool UseKeyConnector { get; set; }
public bool UseGroups { get; set; }
public bool UseDirectory { get; set; }
public bool UseEvents { get; set; }
@ -84,6 +86,7 @@ namespace Bit.Core.Models.Api
public bool FamilySponsorshipAvailable { get; set; }
public ProductType PlanProductType { get; set; }
public bool UsesKeyConnector { get; set; }
public bool KeyConnectorEnabled { get; set; }
public string KeyConnectorUrl { get; set; }
}
}

View File

@ -12,6 +12,7 @@ namespace Bit.Core.Models.Api
Name = organization.Name;
UsePolicies = organization.UsePolicies;
UseSso = organization.UseSso;
UseKeyConnector = organization.UseKeyConnector;
UseGroups = organization.UseGroups;
UseDirectory = organization.UseDirectory;
UseEvents = organization.UseEvents;

View File

@ -20,7 +20,7 @@ namespace Bit.Core.Models.Business
public OrganizationLicense(Organization org, SubscriptionInfo subscriptionInfo, Guid installationId,
ILicensingService licenseService, int? version = null)
{
Version = version.GetValueOrDefault(7); // TODO: bump to version 8
Version = version.GetValueOrDefault(CURRENT_LICENSE_FILE_VERSION); // TODO: Remember to change the constant
LicenseKey = org.LicenseKey;
InstallationId = installationId;
Id = org.Id;
@ -34,6 +34,7 @@ namespace Bit.Core.Models.Business
MaxCollections = org.MaxCollections;
UsePolicies = org.UsePolicies;
UseSso = org.UseSso;
UseKeyConnector = org.UseKeyConnector;
UseGroups = org.UseGroups;
UseEvents = org.UseEvents;
UseDirectory = org.UseDirectory;
@ -104,6 +105,7 @@ namespace Bit.Core.Models.Business
public short? MaxCollections { get; set; }
public bool UsePolicies { get; set; }
public bool UseSso { get; set; }
public bool UseKeyConnector { get; set; }
public bool UseGroups { get; set; }
public bool UseEvents { get; set; }
public bool UseDirectory { get; set; }
@ -124,10 +126,19 @@ namespace Bit.Core.Models.Business
[JsonIgnore]
public byte[] SignatureBytes => Convert.FromBase64String(Signature);
/// <summary>
/// Represents the current version of the license format. Should be updated whenever new fields are added.
/// </summary>
private const int CURRENT_LICENSE_FILE_VERSION = 8;
private bool ValidLicenseVersion
{
get => Version is >= 1 and <= 9;
}
public byte[] GetDataBytes(bool forHash = false)
{
string data = null;
if (Version >= 1 && Version <= 8)
if (ValidLicenseVersion)
{
var props = typeof(OrganizationLicense)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
@ -148,6 +159,8 @@ namespace Bit.Core.Models.Business
(Version >= 7 || !p.Name.Equals(nameof(UseSso))) &&
// UseResetPassword was added in Version 8
(Version >= 8 || !p.Name.Equals(nameof(UseResetPassword))) &&
// UseKeyConnector was added in Version 9
(Version >= 9 || !p.Name.Equals(nameof(UseKeyConnector))) &&
(
!forHash ||
(
@ -184,7 +197,7 @@ namespace Bit.Core.Models.Business
return false;
}
if (Version >= 1 && Version <= 8)
if (ValidLicenseVersion)
{
return InstallationId == globalSettings.Installation.Id && SelfHost;
}
@ -201,7 +214,7 @@ namespace Bit.Core.Models.Business
return false;
}
if (Version >= 1 && Version <= 8)
if (ValidLicenseVersion)
{
var valid =
globalSettings.Installation.Id == InstallationId &&
@ -245,12 +258,17 @@ namespace Bit.Core.Models.Business
{
valid = organization.UseSso == UseSso;
}
if (valid && Version >= 8)
{
valid = organization.UseResetPassword == UseResetPassword;
}
if (valid && Version >= 9)
{
valid = organization.UseKeyConnector == UseKeyConnector;
}
return valid;
}
else

View File

@ -17,6 +17,7 @@ namespace Bit.Core.Models.Data
UsersGetPremium = organization.UsersGetPremium;
Enabled = organization.Enabled;
UseSso = organization.UseSso;
UseKeyConnector = organization.UseKeyConnector;
UseResetPassword = organization.UseResetPassword;
}
@ -27,6 +28,7 @@ namespace Bit.Core.Models.Data
public bool UsersGetPremium { get; set; }
public bool Enabled { get; set; }
public bool UseSso { get; set; }
public bool UseKeyConnector { get; set; }
public bool UseResetPassword { get; set; }
}
}

View File

@ -9,6 +9,7 @@ namespace Bit.Core.Models.Data
public string Name { get; set; }
public bool UsePolicies { get; set; }
public bool UseSso { get; set; }
public bool UseKeyConnector { get; set; }
public bool UseGroups { get; set; }
public bool UseDirectory { get; set; }
public bool UseEvents { get; set; }

View File

@ -10,6 +10,7 @@ namespace Bit.Core.Models.Data
public string Name { get; set; }
public bool UsePolicies { get; set; }
public bool UseSso { get; set; }
public bool UseKeyConnector { get; set; }
public bool UseGroups { get; set; }
public bool UseDirectory { get; set; }
public bool UseEvents { get; set; }

View File

@ -27,7 +27,7 @@ namespace Bit.Core.Models.Data
public SsoType ConfigType { get; set; }
public bool UseKeyConnector { get; set; }
public bool KeyConnectorEnabled { get; set; }
public string KeyConnectorUrl { get; set; }
// OIDC

View File

@ -33,6 +33,7 @@ namespace Bit.Core.Models.StaticStore
public bool Has2fa { get; set; }
public bool HasApi { get; set; }
public bool HasSso { get; set; }
public bool HasKeyConnector { get; set; }
public bool HasResetPassword { get; set; }
public bool UsersGetPremium { get; set; }

View File

@ -38,6 +38,7 @@ namespace Bit.Core.Models.Table
public short? MaxCollections { get; set; }
public bool UsePolicies { get; set; }
public bool UseSso { get; set; }
public bool UseKeyConnector { get; set; }
public bool UseGroups { get; set; }
public bool UseDirectory { get; set; }
public bool UseEvents { get; set; }

View File

@ -87,6 +87,7 @@ namespace Bit.Core.Repositories.EntityFramework
UsersGetPremium = e.UsersGetPremium,
Using2fa = e.Use2fa && e.TwoFactorProviders != null,
UseSso = e.UseSso,
UseKeyConnector = e.UseKeyConnector,
}).ToListAsync();
}
}

View File

@ -33,6 +33,7 @@ namespace Bit.Core.Repositories.EntityFramework.Queries
PlanType = x.o.PlanType,
UsePolicies = x.o.UsePolicies,
UseSso = x.o.UseSso,
UseKeyConnector = x.o.UseKeyConnector,
UseGroups = x.o.UseGroups,
UseDirectory = x.o.UseDirectory,
UseEvents = x.o.UseEvents,

View File

@ -21,6 +21,7 @@ namespace Bit.Core.Repositories.EntityFramework.Queries
Enabled = x.o.Enabled,
UsePolicies = x.o.UsePolicies,
UseSso = x.o.UseSso,
UseKeyConnector = x.o.UseKeyConnector,
UseGroups = x.o.UseGroups,
UseDirectory = x.o.UseDirectory,
UseEvents = x.o.UseEvents,

View File

@ -5,6 +5,6 @@ namespace Bit.Core.Services
{
public interface ISsoConfigService
{
Task SaveAsync(SsoConfig config);
Task SaveAsync(SsoConfig config, Organization organization);
}
}

View File

@ -62,10 +62,15 @@ namespace Bit.Core.Services
public async Task<EmergencyAccess> InviteAsync(User invitingUser, string email, EmergencyAccessType type, int waitTime)
{
if (! await _userService.CanAccessPremium(invitingUser))
if (!await _userService.CanAccessPremium(invitingUser))
{
throw new BadRequestException("Not a premium user.");
}
if (type == EmergencyAccessType.Takeover && invitingUser.UsesKeyConnector)
{
throw new BadRequestException("You cannot use Emergency Access Takeover because you are using Key Connector.");
}
var emergencyAccess = new EmergencyAccess
{
@ -171,6 +176,11 @@ namespace Bit.Core.Services
}
var grantor = await _userRepository.GetByIdAsync(confirmingUserId);
if (emergencyAccess.Type == EmergencyAccessType.Takeover && grantor.UsesKeyConnector)
{
throw new BadRequestException("You cannot use Emergency Access Takeover because you are using Key Connector.");
}
var grantee = await _userRepository.GetByIdAsync(emergencyAccess.GranteeId.Value);
emergencyAccess.Status = EmergencyAccessStatusType.Confirmed;
@ -188,7 +198,16 @@ namespace Bit.Core.Services
{
throw new BadRequestException("Emergency Access not valid.");
}
if (emergencyAccess.Type == EmergencyAccessType.Takeover)
{
var grantor = await _userService.GetUserByIdAsync(emergencyAccess.GrantorId);
if (grantor.UsesKeyConnector)
{
throw new BadRequestException("You cannot use Emergency Access Takeover because you are using Key Connector.");
}
}
await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);
}
@ -202,6 +221,13 @@ namespace Bit.Core.Services
throw new BadRequestException("Emergency Access not valid.");
}
var grantor = await _userRepository.GetByIdAsync(emergencyAccess.GrantorId);
if (emergencyAccess.Type == EmergencyAccessType.Takeover && grantor.UsesKeyConnector)
{
throw new BadRequestException("You cannot takeover an account that is using Key Connector.");
}
var now = DateTime.UtcNow;
emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated;
emergencyAccess.RevisionDate = now;
@ -209,8 +235,6 @@ namespace Bit.Core.Services
emergencyAccess.LastNotificationDate = now;
await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);
var grantor = await _userRepository.GetByIdAsync(emergencyAccess.GrantorId);
await _mailService.SendEmergencyAccessRecoveryInitiated(emergencyAccess, NameOrEmail(initiatingUser), grantor.Email);
}
@ -277,7 +301,12 @@ namespace Bit.Core.Services
}
var grantor = await _userRepository.GetByIdAsync(emergencyAccess.GrantorId);
if (emergencyAccess.Type == EmergencyAccessType.Takeover && grantor.UsesKeyConnector)
{
throw new BadRequestException("You cannot takeover an account that is using Key Connector.");
}
return (emergencyAccess, grantor);
}

View File

@ -247,6 +247,16 @@ namespace Bit.Core.Services
}
}
if (!newPlan.HasKeyConnector && organization.UseKeyConnector)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
if (ssoConfig != null && ssoConfig.GetData().KeyConnectorEnabled)
{
throw new BadRequestException("Your new plan does not allow the Key Connector feature. " +
"Disable your Key Connector.");
}
}
if (!newPlan.HasResetPassword && organization.UseResetPassword)
{
var resetPasswordPolicy =
@ -295,6 +305,7 @@ namespace Bit.Core.Services
organization.Use2fa = newPlan.Has2fa;
organization.UseApi = newPlan.HasApi;
organization.UseSso = newPlan.HasSso;
organization.UseKeyConnector = newPlan.HasKeyConnector;
organization.UseResetPassword = newPlan.HasResetPassword;
organization.SelfHost = newPlan.HasSelfHost;
organization.UsersGetPremium = newPlan.UsersGetPremium || upgrade.PremiumAccessAddon;
@ -687,6 +698,7 @@ namespace Bit.Core.Services
MaxStorageGb = _globalSettings.SelfHosted ? 10240 : license.MaxStorageGb, // 10 TB
UsePolicies = license.UsePolicies,
UseSso = license.UseSso,
UseKeyConnector = license.UseKeyConnector,
UseGroups = license.UseGroups,
UseDirectory = license.UseDirectory,
UseEvents = license.UseEvents,
@ -865,6 +877,16 @@ namespace Bit.Core.Services
}
}
if (!license.UseKeyConnector && organization.UseKeyConnector)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
if (ssoConfig != null && ssoConfig.GetData().KeyConnectorEnabled)
{
throw new BadRequestException($"Your organization currently has Key Connector enabled. " +
$"Your new license does not allow for the use of Key Connector. Disable your Key Connector.");
}
}
if (!license.UseResetPassword && organization.UseResetPassword)
{
var resetPasswordPolicy =
@ -895,6 +917,7 @@ namespace Bit.Core.Services
organization.UseApi = license.UseApi;
organization.UsePolicies = license.UsePolicies;
organization.UseSso = license.UseSso;
organization.UseKeyConnector = license.UseKeyConnector;
organization.UseResetPassword = license.UseResetPassword;
organization.SelfHost = license.SelfHost;
organization.UsersGetPremium = license.UsersGetPremium;
@ -2156,7 +2179,7 @@ namespace Bit.Core.Services
private async Task ValidateDeleteOrganizationAsync(Organization organization)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
if (ssoConfig?.GetData()?.UseKeyConnector == true)
if (ssoConfig?.GetData()?.KeyConnectorEnabled == true)
{
throw new BadRequestException("You cannot delete an Organization that is using Key Connector.");
}

View File

@ -54,37 +54,27 @@ namespace Bit.Core.Services
case PolicyType.SingleOrg:
if (!policy.Enabled)
{
var requireSso =
await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.RequireSso);
if (requireSso?.Enabled == true)
{
throw new BadRequestException("Single Sign-On Authentication policy is enabled.");
}
var vaultTimeout =
await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.MaximumVaultTimeout);
if (vaultTimeout?.Enabled == true)
{
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.");
}
await RequiredBySsoAsync(org);
await RequiredByVaultTimeoutAsync(org);
await RequiredByKeyConnectorAsync(org);
}
break;
case PolicyType.RequireSso:
if (policy.Enabled)
{
await DependsOnSingleOrgAsync(org);
}
else
{
await RequiredByKeyConnectorAsync(org);
}
break;
case PolicyType.MaximumVaultTimeout:
if (policy.Enabled)
{
var singleOrg = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.SingleOrg);
if (singleOrg?.Enabled != true)
{
throw new BadRequestException("Single Organization policy not enabled.");
}
await DependsOnSingleOrgAsync(org);
}
break;
}
@ -144,5 +134,42 @@ namespace Bit.Core.Services
await _policyRepository.UpsertAsync(policy);
await _eventService.LogPolicyEventAsync(policy, Enums.EventType.Policy_Updated);
}
private async Task DependsOnSingleOrgAsync(Organization org)
{
var singleOrg = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.SingleOrg);
if (singleOrg?.Enabled != true)
{
throw new BadRequestException("Single Organization policy not enabled.");
}
}
private async Task RequiredBySsoAsync(Organization org)
{
var requireSso = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.RequireSso);
if (requireSso?.Enabled == true)
{
throw new BadRequestException("Single Sign-On Authentication policy is enabled.");
}
}
private async Task RequiredByKeyConnectorAsync(Organization org)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(org.Id);
if (ssoConfig?.GetData()?.KeyConnectorEnabled == true)
{
throw new BadRequestException("Key Connector is enabled.");
}
}
private async Task RequiredByVaultTimeoutAsync(Organization org)
{
var vaultTimeout = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.MaximumVaultTimeout);
if (vaultTimeout?.Enabled == true)
{
throw new BadRequestException("Maximum Vault Timeout policy is enabled.");
}
}
}
}

View File

@ -30,7 +30,7 @@ namespace Bit.Core.Services
_eventService = eventService;
}
public async Task SaveAsync(SsoConfig config)
public async Task SaveAsync(SsoConfig config, Organization organization)
{
var now = DateTime.UtcNow;
config.RevisionDate = now;
@ -39,14 +39,14 @@ namespace Bit.Core.Services
config.CreationDate = now;
}
var useKeyConnector = config.GetData().UseKeyConnector;
var useKeyConnector = config.GetData().KeyConnectorEnabled;
if (useKeyConnector)
{
await VerifyDependenciesAsync(config);
await VerifyDependenciesAsync(config, organization);
}
var oldConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(config.OrganizationId);
var disabledKeyConnector = oldConfig?.GetData()?.UseKeyConnector == true && !useKeyConnector;
var disabledKeyConnector = oldConfig?.GetData()?.KeyConnectorEnabled == true && !useKeyConnector;
if (disabledKeyConnector && await AnyOrgUserHasKeyConnectorEnabledAsync(config.OrganizationId))
{
throw new BadRequestException("Key Connector cannot be disabled at this moment.");
@ -63,12 +63,27 @@ namespace Bit.Core.Services
return userDetails.Any(u => u.UsesKeyConnector);
}
private async Task VerifyDependenciesAsync(SsoConfig config)
private async Task VerifyDependenciesAsync(SsoConfig config, Organization organization)
{
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.SingleOrg);
if (policy is not { Enabled: true })
if (!organization.UseKeyConnector)
{
throw new BadRequestException("KeyConnector requires Single Organization to be enabled.");
throw new BadRequestException("Organization cannot use Key Connector.");
}
var singleOrgPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.SingleOrg);
if (singleOrgPolicy is not { Enabled: true })
{
throw new BadRequestException("Key Connector requires the Single Organization policy to be enabled.");
}
var ssoPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.RequireSso);
if (ssoPolicy is not { Enabled: true })
{
throw new BadRequestException("Key Connector requires the Single Sign-On Authentication policy to be enabled.");
}
if (!config.Enabled) {
throw new BadRequestException("You must enable SSO to use Key Connector.");
}
}
@ -81,10 +96,10 @@ namespace Bit.Core.Services
await _eventService.LogOrganizationEventAsync(organization, e);
}
var useKeyConnector = config.GetData().UseKeyConnector;
if (oldConfig?.GetData()?.UseKeyConnector != useKeyConnector)
var keyConnectorEnabled = config.GetData().KeyConnectorEnabled;
if (oldConfig?.GetData()?.KeyConnectorEnabled != keyConnectorEnabled)
{
var e = useKeyConnector
var e = keyConnectorEnabled
? EventType.Organization_EnabledKeyConnector
: EventType.Organization_DisabledKeyConnector;
await _eventService.LogOrganizationEventAsync(organization, e);

View File

@ -646,7 +646,7 @@ namespace Bit.Core.Services
if (user.UsesKeyConnector)
{
Logger.LogWarning("Already uses key connector.");
Logger.LogWarning("Already uses Key Connector.");
return IdentityResult.Failed(_identityErrorDescriber.UserAlreadyHasPassword());
}
@ -671,7 +671,7 @@ namespace Bit.Core.Services
if (user.UsesKeyConnector)
{
Logger.LogWarning("Already uses key connector.");
Logger.LogWarning("Already uses Key Connector.");
return IdentityResult.Failed(_identityErrorDescriber.UserAlreadyHasPassword());
}
@ -740,7 +740,7 @@ namespace Bit.Core.Services
if (user.UsesKeyConnector)
{
throw new BadRequestException("Cannot reset password of a user with key connector.");
throw new BadRequestException("Cannot reset password of a user with Key Connector.");
}
var result = await UpdatePasswordHash(user, newMasterPassword);
@ -1387,7 +1387,7 @@ namespace Bit.Core.Services
if (!user.UsesKeyConnector)
{
throw new BadRequestException("Not using key connector.");
throw new BadRequestException("Not using Key Connector.");
}
var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultEmailProvider,

View File

@ -412,6 +412,7 @@ namespace Bit.Core.Utilities
Has2fa = true,
HasApi = true,
HasSso = true,
HasKeyConnector = true,
HasResetPassword = true,
UsersGetPremium = true,
@ -450,6 +451,7 @@ namespace Bit.Core.Utilities
HasApi = true,
HasSelfHost = true,
HasSso = true,
HasKeyConnector = true,
HasResetPassword = true,
UsersGetPremium = true,

View File

@ -40,7 +40,8 @@
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@OwnersNotifiedOfAutoscaling DATETIME2(7),
@MaxAutoscaleSeats INT
@MaxAutoscaleSeats INT,
@UseKeyConnector BIT = 0
AS
BEGIN
SET NOCOUNT ON
@ -88,7 +89,8 @@ BEGIN
[CreationDate],
[RevisionDate],
[OwnersNotifiedOfAutoscaling],
[MaxAutoscaleSeats]
[MaxAutoscaleSeats],
[UseKeyConnector]
)
VALUES
(
@ -133,6 +135,7 @@ BEGIN
@CreationDate,
@RevisionDate,
@OwnersNotifiedOfAutoscaling,
@MaxAutoscaleSeats
@MaxAutoscaleSeats,
@UseKeyConnector
)
END

View File

@ -15,6 +15,7 @@ BEGIN
END AS [Using2fa],
[UsersGetPremium],
[UseSso],
[UseKeyConnector],
[UseResetPassword],
[Enabled]
FROM

View File

@ -40,7 +40,8 @@
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@OwnersNotifiedOfAutoscaling DATETIME2(7),
@MaxAutoscaleSeats INT
@MaxAutoscaleSeats INT,
@UseKeyConnector BIT = 0
AS
BEGIN
SET NOCOUNT ON
@ -88,7 +89,8 @@ BEGIN
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate,
[OwnersNotifiedOfAutoscaling] = @OwnersNotifiedOfAutoscaling,
[MaxAutoscaleSeats] = @MaxAutoscaleSeats
[MaxAutoscaleSeats] = @MaxAutoscaleSeats,
[UseKeyConnector] = @UseKeyConnector
WHERE
[Id] = @Id
END

View File

@ -41,6 +41,7 @@
[RevisionDate] DATETIME2 (7) NOT NULL,
[OwnersNotifiedOfAutoscaling] DATETIME2(7) NULL,
[MaxAutoscaleSeats] INT NULL,
[UseKeyConnector] BIT NOT NULL,
CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC)
);

View File

@ -7,6 +7,7 @@ SELECT
O.[Enabled],
O.[UsePolicies],
O.[UseSso],
O.[UseKeyConnector],
O.[UseGroups],
O.[UseDirectory],
O.[UseEvents],

View File

@ -7,6 +7,7 @@ SELECT
O.[Enabled],
O.[UsePolicies],
O.[UseSso],
O.[UseKeyConnector],
O.[UseGroups],
O.[UseDirectory],
O.[UseEvents],

View File

@ -0,0 +1,117 @@
using AutoFixture.Xunit2;
using Bit.Api.Controllers;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using NSubstitute;
using System.Threading.Tasks;
using System.Security.Claims;
using System;
using Bit.Core.Models.Data;
using Xunit;
namespace Bit.Api.Test.Controllers
{
public class OrganizationsControllerTests: IDisposable
{
private readonly GlobalSettings _globalSettings;
private readonly ICurrentContext _currentContext;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationService _organizationService;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IPaymentService _paymentService;
private readonly IPolicyRepository _policyRepository;
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly ISsoConfigService _ssoConfigService;
private readonly IUserService _userService;
private readonly OrganizationsController _sut;
public OrganizationsControllerTests()
{
_currentContext = Substitute.For<ICurrentContext>();
_globalSettings = Substitute.For<GlobalSettings>();
_organizationRepository = Substitute.For<IOrganizationRepository>();
_organizationService = Substitute.For<IOrganizationService>();
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
_paymentService = Substitute.For<IPaymentService>();
_policyRepository = Substitute.For<IPolicyRepository>();
_ssoConfigRepository = Substitute.For<ISsoConfigRepository>();
_ssoConfigService = Substitute.For<ISsoConfigService>();
_userService = Substitute.For<IUserService>();
_sut = new OrganizationsController(_organizationRepository, _organizationUserRepository,
_policyRepository, _organizationService, _userService, _paymentService, _currentContext,
_ssoConfigRepository, _ssoConfigService, _globalSettings);
}
public void Dispose()
{
_sut?.Dispose();
}
[Theory, AutoData]
public async Task OrganizationsController_UserCannotLeaveOrganizationThatProvidesKeyConnector(
Guid orgId, User user)
{
var ssoConfig = new SsoConfig
{
Id = default,
Data = new SsoConfigurationData
{
KeyConnectorEnabled = true,
}.Serialize(),
Enabled = true,
OrganizationId = orgId,
};
user.UsesKeyConnector = true;
_currentContext.OrganizationUser(orgId).Returns(true);
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(user.Id);
_currentContext.User.Returns(user);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => _sut.Leave(orgId.ToString()));
Assert.Contains("You cannot leave this Organization because you are using its Key Connector.",
exception.Message);
await _organizationService.DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default);
}
[Theory]
[InlineAutoData(true, false)]
[InlineAutoData(false, true)]
[InlineAutoData(false, false)]
public async Task OrganizationsController_UserCanLeaveOrganizationThatDoesntProvideKeyConnector(
bool keyConnectorEnabled, bool userUsesKeyConnector, Guid orgId, User user)
{
var ssoConfig = new SsoConfig
{
Id = default,
Data = new SsoConfigurationData
{
KeyConnectorEnabled = keyConnectorEnabled,
}.Serialize(),
Enabled = true,
OrganizationId = orgId,
};
user.UsesKeyConnector = userUsesKeyConnector;
_currentContext.OrganizationUser(orgId).Returns(true);
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(user.Id);
_currentContext.User.Returns(user);
await _organizationService.DeleteUserAsync(orgId, user.Id);
await _organizationService.Received(1).DeleteUserAsync(orgId, user.Id);
}
}
}

View File

@ -25,6 +25,7 @@ namespace Bit.Core.Test.Repositories.EntityFramework.EqualityComparers
x.MaxCollections.Equals(y.MaxCollections) &&
x.UsePolicies.Equals(y.UsePolicies) &&
x.UseSso.Equals(y.UseSso) &&
x.UseKeyConnector.Equals(y.UseKeyConnector) &&
x.UseGroups.Equals(y.UseGroups) &&
x.UseDirectory.Equals(y.UseDirectory) &&
x.UseEvents.Equals(y.UseEvents) &&

View File

@ -0,0 +1,117 @@
using Bit.Core.Exceptions;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.AutoFixture.Attributes;
using Bit.Core.Test.AutoFixture;
using NSubstitute;
using System.Threading.Tasks;
using System;
using Xunit;
namespace Bit.Core.Test.Services
{
public class EmergencyAccessServiceTests
{
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task InviteAsync_UserWithKeyConnectorCannotUseTakeover(
SutProvider<EmergencyAccessService> sutProvider, User invitingUser, string email, int waitTime)
{
invitingUser.UsesKeyConnector = true;
sutProvider.GetDependency<IUserService>().CanAccessPremium(invitingUser).Returns(true);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.InviteAsync(invitingUser, email, Enums.EmergencyAccessType.Takeover, waitTime));
Assert.Contains("You cannot use Emergency Access Takeover because you are using Key Connector", exception.Message);
await sutProvider.GetDependency<IEmergencyAccessRepository>().DidNotReceiveWithAnyArgs().CreateAsync(default);
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task ConfirmUserAsync_UserWithKeyConnectorCannotUseTakeover(
SutProvider<EmergencyAccessService> sutProvider, User confirmingUser, string key)
{
confirmingUser.UsesKeyConnector = true;
var emergencyAccess = new EmergencyAccess
{
Status = Enums.EmergencyAccessStatusType.Accepted,
GrantorId = confirmingUser.Id,
Type = Enums.EmergencyAccessType.Takeover,
};
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(confirmingUser.Id).Returns(confirmingUser);
sutProvider.GetDependency<IEmergencyAccessRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(emergencyAccess);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.ConfirmUserAsync(new Guid(), key, confirmingUser.Id));
Assert.Contains("You cannot use Emergency Access Takeover because you are using Key Connector", exception.Message);
await sutProvider.GetDependency<IEmergencyAccessRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task SaveAsync_UserWithKeyConnectorCannotUseTakeover(
SutProvider<EmergencyAccessService> sutProvider, User savingUser)
{
savingUser.UsesKeyConnector = true;
var emergencyAccess = new EmergencyAccess
{
Type = Enums.EmergencyAccessType.Takeover,
GrantorId = savingUser.Id,
};
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(savingUser.Id).Returns(savingUser);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(emergencyAccess, savingUser.Id));
Assert.Contains("You cannot use Emergency Access Takeover because you are using Key Connector", exception.Message);
await sutProvider.GetDependency<IEmergencyAccessRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task InitiateAsync_UserWithKeyConnectorCannotUseTakeover(
SutProvider<EmergencyAccessService> sutProvider, User initiatingUser, User grantor)
{
grantor.UsesKeyConnector = true;
var emergencyAccess = new EmergencyAccess
{
Status = Enums.EmergencyAccessStatusType.Confirmed,
GranteeId = initiatingUser.Id,
GrantorId = grantor.Id,
Type = Enums.EmergencyAccessType.Takeover,
};
sutProvider.GetDependency<IEmergencyAccessRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(emergencyAccess);
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(grantor.Id).Returns(grantor);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser));
Assert.Contains("You cannot takeover an account that is using Key Connector", exception.Message);
await sutProvider.GetDependency<IEmergencyAccessRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task TakeoverAsync_UserWithKeyConnectorCannotUseTakeover(
SutProvider<EmergencyAccessService> sutProvider, User requestingUser, User grantor)
{
grantor.UsesKeyConnector = true;
var emergencyAccess = new EmergencyAccess
{
GrantorId = grantor.Id,
GranteeId = requestingUser.Id,
Status = Enums.EmergencyAccessStatusType.RecoveryApproved,
Type = Enums.EmergencyAccessType.Takeover,
};
sutProvider.GetDependency<IEmergencyAccessRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(emergencyAccess);
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(grantor.Id).Returns(grantor);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.TakeoverAsync(new Guid(), requestingUser));
Assert.Contains("You cannot takeover an account that is using Key Connector", exception.Message);
}
}
}

View File

@ -910,7 +910,7 @@ namespace Bit.Core.Test.Services
SsoConfig ssoConfig)
{
ssoConfig.Enabled = true;
ssoConfig.SetData(new SsoConfigurationData { UseKeyConnector = true });
ssoConfig.SetData(new SsoConfigurationData { KeyConnectorEnabled = true });
var ssoConfigRepository = sutProvider.GetDependency<ISsoConfigRepository>();
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();

View File

@ -126,12 +126,16 @@ namespace Bit.Core.Test.Services
.UpsertAsync(default);
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task SaveAsync_SingleOrg_KeyConnectorEnabled_ThrowsBadRequest(
[PolicyFixtures.Policy(Enums.PolicyType.SingleOrg)] Core.Models.Table.Policy policy,
[Theory]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization) }, Enums.PolicyType.SingleOrg)]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization) }, Enums.PolicyType.RequireSso)]
public async Task SaveAsync_PolicyRequiredByKeyConnector_DisablePolicy_ThrowsBadRequest(
Enums.PolicyType policyType,
Policy policy,
SutProvider<PolicyService> sutProvider)
{
policy.Enabled = false;
policy.Type = policyType;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
@ -140,7 +144,7 @@ namespace Bit.Core.Test.Services
});
var ssoConfig = new SsoConfig { Enabled = true };
var data = new SsoConfigurationData { UseKeyConnector = true };
var data = new SsoConfigurationData { KeyConnectorEnabled = true };
ssoConfig.SetData(data);
sutProvider.GetDependency<ISsoConfigRepository>()
@ -153,7 +157,7 @@ namespace Bit.Core.Test.Services
Substitute.For<IOrganizationService>(),
Guid.NewGuid()));
Assert.Contains("KeyConnector is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Key Connector is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()

View File

@ -15,9 +15,9 @@ namespace Bit.Core.Test.Services
public class SsoConfigServiceTests
{
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task SaveAsync_ExistingItem_UpdatesRevisionDateOnly(SutProvider<SsoConfigService> sutProvider)
public async Task SaveAsync_ExistingItem_UpdatesRevisionDateOnly(SutProvider<SsoConfigService> sutProvider,
Organization organization)
{
var utcNow = DateTime.UtcNow;
var ssoConfig = new SsoConfig
@ -25,7 +25,7 @@ namespace Bit.Core.Test.Services
Id = 1,
Data = "{}",
Enabled = true,
OrganizationId = Guid.NewGuid(),
OrganizationId = organization.Id,
CreationDate = utcNow.AddDays(-10),
RevisionDate = utcNow.AddDays(-10),
};
@ -33,7 +33,7 @@ namespace Bit.Core.Test.Services
sutProvider.GetDependency<ISsoConfigRepository>()
.UpsertAsync(ssoConfig).Returns(Task.CompletedTask);
await sutProvider.Sut.SaveAsync(ssoConfig);
await sutProvider.Sut.SaveAsync(ssoConfig, organization);
await sutProvider.GetDependency<ISsoConfigRepository>().Received()
.UpsertAsync(ssoConfig);
@ -43,7 +43,8 @@ namespace Bit.Core.Test.Services
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task SaveAsync_NewItem_UpdatesCreationAndRevisionDate(SutProvider<SsoConfigService> sutProvider)
public async Task SaveAsync_NewItem_UpdatesCreationAndRevisionDate(SutProvider<SsoConfigService> sutProvider,
Organization organization)
{
var utcNow = DateTime.UtcNow;
@ -52,7 +53,7 @@ namespace Bit.Core.Test.Services
Id = default,
Data = "{}",
Enabled = true,
OrganizationId = Guid.NewGuid(),
OrganizationId = organization.Id,
CreationDate = utcNow.AddDays(-10),
RevisionDate = utcNow.AddDays(-10),
};
@ -60,7 +61,7 @@ namespace Bit.Core.Test.Services
sutProvider.GetDependency<ISsoConfigRepository>()
.UpsertAsync(ssoConfig).Returns(Task.CompletedTask);
await sutProvider.Sut.SaveAsync(ssoConfig);
await sutProvider.Sut.SaveAsync(ssoConfig, organization);
await sutProvider.GetDependency<ISsoConfigRepository>().Received()
.UpsertAsync(ssoConfig);
@ -70,16 +71,20 @@ namespace Bit.Core.Test.Services
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task SaveAsync_PreventDisablingKeyConnector(SutProvider<SsoConfigService> sutProvider, Guid orgId)
public async Task SaveAsync_PreventDisablingKeyConnector(SutProvider<SsoConfigService> sutProvider,
Organization organization)
{
var utcNow = DateTime.UtcNow;
var oldSsoConfig = new SsoConfig
{
Id = 1,
Data = "{\"useKeyConnector\": true}",
Data = new SsoConfigurationData
{
KeyConnectorEnabled = true,
}.Serialize(),
Enabled = true,
OrganizationId = orgId,
OrganizationId = organization.Id,
CreationDate = utcNow.AddDays(-10),
RevisionDate = utcNow.AddDays(-10),
};
@ -89,19 +94,19 @@ namespace Bit.Core.Test.Services
Id = 1,
Data = "{}",
Enabled = true,
OrganizationId = orgId,
OrganizationId = organization.Id,
CreationDate = utcNow.AddDays(-10),
RevisionDate = utcNow,
};
var ssoConfigRepository = sutProvider.GetDependency<ISsoConfigRepository>();
ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(oldSsoConfig);
ssoConfigRepository.GetByOrganizationIdAsync(organization.Id).Returns(oldSsoConfig);
ssoConfigRepository.UpsertAsync(newSsoConfig).Returns(Task.CompletedTask);
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyDetailsByOrganizationAsync(orgId)
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyDetailsByOrganizationAsync(organization.Id)
.Returns(new[] { new OrganizationUserUserDetails { UsesKeyConnector = true } });
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(newSsoConfig));
() => sutProvider.Sut.SaveAsync(newSsoConfig, organization));
Assert.Contains("Key Connector cannot be disabled at this moment.", exception.Message);
@ -111,16 +116,19 @@ namespace Bit.Core.Test.Services
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task SaveAsync_AllowDisablingKeyConnectorWhenNoUserIsUsingIt(
SutProvider<SsoConfigService> sutProvider, Guid orgId)
SutProvider<SsoConfigService> sutProvider, Organization organization)
{
var utcNow = DateTime.UtcNow;
var oldSsoConfig = new SsoConfig
{
Id = 1,
Data = "{\"useKeyConnector\": true}",
Data = new SsoConfigurationData
{
KeyConnectorEnabled = true,
}.Serialize(),
Enabled = true,
OrganizationId = orgId,
OrganizationId = organization.Id,
CreationDate = utcNow.AddDays(-10),
RevisionDate = utcNow.AddDays(-10),
};
@ -130,42 +138,181 @@ namespace Bit.Core.Test.Services
Id = 1,
Data = "{}",
Enabled = true,
OrganizationId = orgId,
OrganizationId = organization.Id,
CreationDate = utcNow.AddDays(-10),
RevisionDate = utcNow,
};
var ssoConfigRepository = sutProvider.GetDependency<ISsoConfigRepository>();
ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(oldSsoConfig);
ssoConfigRepository.GetByOrganizationIdAsync(organization.Id).Returns(oldSsoConfig);
ssoConfigRepository.UpsertAsync(newSsoConfig).Returns(Task.CompletedTask);
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyDetailsByOrganizationAsync(orgId)
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyDetailsByOrganizationAsync(organization.Id)
.Returns(new[] { new OrganizationUserUserDetails { UsesKeyConnector = false } });
await sutProvider.Sut.SaveAsync(newSsoConfig);
await sutProvider.Sut.SaveAsync(newSsoConfig, organization);
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task SaveAsync_KeyConnector_SingleOrgNotEnabled(SutProvider<SsoConfigService> sutProvider)
public async Task SaveAsync_KeyConnector_SingleOrgNotEnabled_Throws(SutProvider<SsoConfigService> sutProvider,
Organization organization)
{
var utcNow = DateTime.UtcNow;
var ssoConfig = new SsoConfig
{
Id = default,
Data = "{\"useKeyConnector\": true}",
Data = new SsoConfigurationData
{
KeyConnectorEnabled = true,
}.Serialize(),
Enabled = true,
OrganizationId = Guid.NewGuid(),
OrganizationId = organization.Id,
CreationDate = utcNow.AddDays(-10),
RevisionDate = utcNow.AddDays(-10),
};
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(ssoConfig));
() => sutProvider.Sut.SaveAsync(ssoConfig, organization));
Assert.Contains("KeyConnector requires Single Organization to be enabled.", exception.Message);
Assert.Contains("Key Connector requires the Single Organization policy to be enabled.", exception.Message);
await sutProvider.GetDependency<ISsoConfigRepository>().DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task SaveAsync_KeyConnector_SsoPolicyNotEnabled_Throws(SutProvider<SsoConfigService> sutProvider,
Organization organization)
{
var utcNow = DateTime.UtcNow;
var ssoConfig = new SsoConfig
{
Id = default,
Data = new SsoConfigurationData
{
KeyConnectorEnabled = true,
}.Serialize(),
Enabled = true,
OrganizationId = organization.Id,
CreationDate = utcNow.AddDays(-10),
RevisionDate = utcNow.AddDays(-10),
};
sutProvider.GetDependency<IPolicyRepository>().GetByOrganizationIdTypeAsync(
Arg.Any<Guid>(), Enums.PolicyType.SingleOrg).Returns(new Policy
{
Enabled = true
});
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(ssoConfig, organization));
Assert.Contains("Key Connector requires the Single Sign-On Authentication policy to be enabled.", exception.Message);
await sutProvider.GetDependency<ISsoConfigRepository>().DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task SaveAsync_KeyConnector_SsoConfigNotEnabled_Throws(SutProvider<SsoConfigService> sutProvider,
Organization organization)
{
var utcNow = DateTime.UtcNow;
var ssoConfig = new SsoConfig
{
Id = default,
Data = new SsoConfigurationData
{
KeyConnectorEnabled = true,
}.Serialize(),
Enabled = false,
OrganizationId = organization.Id,
CreationDate = utcNow.AddDays(-10),
RevisionDate = utcNow.AddDays(-10),
};
sutProvider.GetDependency<IPolicyRepository>().GetByOrganizationIdTypeAsync(
Arg.Any<Guid>(), Arg.Any<Enums.PolicyType>()).Returns(new Policy
{
Enabled = true
});
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(ssoConfig, organization));
Assert.Contains("You must enable SSO to use Key Connector.", exception.Message);
await sutProvider.GetDependency<ISsoConfigRepository>().DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task SaveAsync_KeyConnector_KeyConnectorAbilityNotEnabled_Throws(SutProvider<SsoConfigService> sutProvider,
Organization organization)
{
var utcNow = DateTime.UtcNow;
organization.UseKeyConnector = false;
var ssoConfig = new SsoConfig
{
Id = default,
Data = new SsoConfigurationData
{
KeyConnectorEnabled = true,
}.Serialize(),
Enabled = true,
OrganizationId = organization.Id,
CreationDate = utcNow.AddDays(-10),
RevisionDate = utcNow.AddDays(-10),
};
sutProvider.GetDependency<IPolicyRepository>().GetByOrganizationIdTypeAsync(
Arg.Any<Guid>(), Arg.Any<Enums.PolicyType>()).Returns(new Policy
{
Enabled = true,
});
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(ssoConfig, organization));
Assert.Contains("Organization cannot use Key Connector.", exception.Message);
await sutProvider.GetDependency<ISsoConfigRepository>().DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task SaveAsync_KeyConnector_Success(SutProvider<SsoConfigService> sutProvider,
Organization organization)
{
var utcNow = DateTime.UtcNow;
organization.UseKeyConnector = true;
var ssoConfig = new SsoConfig
{
Id = default,
Data = new SsoConfigurationData
{
KeyConnectorEnabled = true,
}.Serialize(),
Enabled = true,
OrganizationId = organization.Id,
CreationDate = utcNow.AddDays(-10),
RevisionDate = utcNow.AddDays(-10),
};
sutProvider.GetDependency<IPolicyRepository>().GetByOrganizationIdTypeAsync(
Arg.Any<Guid>(), Arg.Any<Enums.PolicyType>()).Returns(new Policy
{
Enabled = true,
});
await sutProvider.Sut.SaveAsync(ssoConfig, organization);
await sutProvider.GetDependency<ISsoConfigRepository>().ReceivedWithAnyArgs()
.UpsertAsync(default);
}
}
}

View File

@ -0,0 +1,420 @@
IF COL_LENGTH('[dbo].[Organization]', 'UseKeyConnector') IS NULL
BEGIN
ALTER TABLE
[dbo].[Organization]
ADD
[UseKeyConnector] BIT NULL
END
GO
UPDATE
[dbo].[Organization]
SET
[UseKeyConnector] = 0
WHERE
[UseKeyConnector] IS NULL
GO
ALTER TABLE
[dbo].[Organization]
ALTER COLUMN
[UseKeyConnector] BIT NOT NULL
GO
IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'OrganizationView')
BEGIN
DROP VIEW [dbo].[OrganizationView]
END
GO
CREATE VIEW [dbo].[OrganizationView]
AS
SELECT
*
FROM
[dbo].[Organization]
GO
IF OBJECT_ID('[dbo].[Organization_Create]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[Organization_Create]
END
GO
CREATE PROCEDURE [dbo].[Organization_Create]
@Id UNIQUEIDENTIFIER OUTPUT,
@Identifier NVARCHAR(50),
@Name NVARCHAR(50),
@BusinessName NVARCHAR(50),
@BusinessAddress1 NVARCHAR(50),
@BusinessAddress2 NVARCHAR(50),
@BusinessAddress3 NVARCHAR(50),
@BusinessCountry VARCHAR(2),
@BusinessTaxNumber NVARCHAR(30),
@BillingEmail NVARCHAR(256),
@Plan NVARCHAR(50),
@PlanType TINYINT,
@Seats INT,
@MaxCollections SMALLINT,
@UsePolicies BIT,
@UseSso BIT,
@UseGroups BIT,
@UseDirectory BIT,
@UseEvents BIT,
@UseTotp BIT,
@Use2fa BIT,
@UseApi BIT,
@UseResetPassword BIT,
@SelfHost BIT,
@UsersGetPremium BIT,
@Storage BIGINT,
@MaxStorageGb SMALLINT,
@Gateway TINYINT,
@GatewayCustomerId VARCHAR(50),
@GatewaySubscriptionId VARCHAR(50),
@ReferenceData VARCHAR(MAX),
@Enabled BIT,
@LicenseKey VARCHAR(100),
@ApiKey VARCHAR(30),
@PublicKey VARCHAR(MAX),
@PrivateKey VARCHAR(MAX),
@TwoFactorProviders NVARCHAR(MAX),
@ExpirationDate DATETIME2(7),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@OwnersNotifiedOfAutoscaling DATETIME2(7),
@MaxAutoscaleSeats INT,
@UseKeyConnector BIT = 0
AS
BEGIN
SET NOCOUNT ON
INSERT INTO [dbo].[Organization]
(
[Id],
[Identifier],
[Name],
[BusinessName],
[BusinessAddress1],
[BusinessAddress2],
[BusinessAddress3],
[BusinessCountry],
[BusinessTaxNumber],
[BillingEmail],
[Plan],
[PlanType],
[Seats],
[MaxCollections],
[UsePolicies],
[UseSso],
[UseGroups],
[UseDirectory],
[UseEvents],
[UseTotp],
[Use2fa],
[UseApi],
[UseResetPassword],
[SelfHost],
[UsersGetPremium],
[Storage],
[MaxStorageGb],
[Gateway],
[GatewayCustomerId],
[GatewaySubscriptionId],
[ReferenceData],
[Enabled],
[LicenseKey],
[ApiKey],
[PublicKey],
[PrivateKey],
[TwoFactorProviders],
[ExpirationDate],
[CreationDate],
[RevisionDate],
[OwnersNotifiedOfAutoscaling],
[MaxAutoscaleSeats],
[UseKeyConnector]
)
VALUES
(
@Id,
@Identifier,
@Name,
@BusinessName,
@BusinessAddress1,
@BusinessAddress2,
@BusinessAddress3,
@BusinessCountry,
@BusinessTaxNumber,
@BillingEmail,
@Plan,
@PlanType,
@Seats,
@MaxCollections,
@UsePolicies,
@UseSso,
@UseGroups,
@UseDirectory,
@UseEvents,
@UseTotp,
@Use2fa,
@UseApi,
@UseResetPassword,
@SelfHost,
@UsersGetPremium,
@Storage,
@MaxStorageGb,
@Gateway,
@GatewayCustomerId,
@GatewaySubscriptionId,
@ReferenceData,
@Enabled,
@LicenseKey,
@ApiKey,
@PublicKey,
@PrivateKey,
@TwoFactorProviders,
@ExpirationDate,
@CreationDate,
@RevisionDate,
@OwnersNotifiedOfAutoscaling,
@MaxAutoscaleSeats,
@UseKeyConnector
)
END
GO
IF OBJECT_ID('[dbo].[Organization_ReadAbilities]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[Organization_ReadAbilities]
END
GO
CREATE PROCEDURE [dbo].[Organization_ReadAbilities]
AS
BEGIN
SET NOCOUNT ON
SELECT
[Id],
[UseEvents],
[Use2fa],
CASE
WHEN [Use2fa] = 1 AND [TwoFactorProviders] IS NOT NULL AND [TwoFactorProviders] != '{}' THEN
1
ELSE
0
END AS [Using2fa],
[UsersGetPremium],
[UseSso],
[UseKeyConnector],
[UseResetPassword],
[Enabled]
FROM
[dbo].[Organization]
END
GO
IF OBJECT_ID('[dbo].[Organization_Update]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[Organization_Update]
END
GO
CREATE PROCEDURE [dbo].[Organization_Update]
@Id UNIQUEIDENTIFIER,
@Identifier NVARCHAR(50),
@Name NVARCHAR(50),
@BusinessName NVARCHAR(50),
@BusinessAddress1 NVARCHAR(50),
@BusinessAddress2 NVARCHAR(50),
@BusinessAddress3 NVARCHAR(50),
@BusinessCountry VARCHAR(2),
@BusinessTaxNumber NVARCHAR(30),
@BillingEmail NVARCHAR(256),
@Plan NVARCHAR(50),
@PlanType TINYINT,
@Seats INT,
@MaxCollections SMALLINT,
@UsePolicies BIT,
@UseSso BIT,
@UseGroups BIT,
@UseDirectory BIT,
@UseEvents BIT,
@UseTotp BIT,
@Use2fa BIT,
@UseApi BIT,
@UseResetPassword BIT,
@SelfHost BIT,
@UsersGetPremium BIT,
@Storage BIGINT,
@MaxStorageGb SMALLINT,
@Gateway TINYINT,
@GatewayCustomerId VARCHAR(50),
@GatewaySubscriptionId VARCHAR(50),
@ReferenceData VARCHAR(MAX),
@Enabled BIT,
@LicenseKey VARCHAR(100),
@ApiKey VARCHAR(30),
@PublicKey VARCHAR(MAX),
@PrivateKey VARCHAR(MAX),
@TwoFactorProviders NVARCHAR(MAX),
@ExpirationDate DATETIME2(7),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@OwnersNotifiedOfAutoscaling DATETIME2(7),
@MaxAutoscaleSeats INT,
@UseKeyConnector BIT = 0
AS
BEGIN
SET NOCOUNT ON
UPDATE
[dbo].[Organization]
SET
[Identifier] = @Identifier,
[Name] = @Name,
[BusinessName] = @BusinessName,
[BusinessAddress1] = @BusinessAddress1,
[BusinessAddress2] = @BusinessAddress2,
[BusinessAddress3] = @BusinessAddress3,
[BusinessCountry] = @BusinessCountry,
[BusinessTaxNumber] = @BusinessTaxNumber,
[BillingEmail] = @BillingEmail,
[Plan] = @Plan,
[PlanType] = @PlanType,
[Seats] = @Seats,
[MaxCollections] = @MaxCollections,
[UsePolicies] = @UsePolicies,
[UseSso] = @UseSso,
[UseGroups] = @UseGroups,
[UseDirectory] = @UseDirectory,
[UseEvents] = @UseEvents,
[UseTotp] = @UseTotp,
[Use2fa] = @Use2fa,
[UseApi] = @UseApi,
[UseResetPassword] = @UseResetPassword,
[SelfHost] = @SelfHost,
[UsersGetPremium] = @UsersGetPremium,
[Storage] = @Storage,
[MaxStorageGb] = @MaxStorageGb,
[Gateway] = @Gateway,
[GatewayCustomerId] = @GatewayCustomerId,
[GatewaySubscriptionId] = @GatewaySubscriptionId,
[ReferenceData] = @ReferenceData,
[Enabled] = @Enabled,
[LicenseKey] = @LicenseKey,
[ApiKey] = @ApiKey,
[PublicKey] = @PublicKey,
[PrivateKey] = @PrivateKey,
[TwoFactorProviders] = @TwoFactorProviders,
[ExpirationDate] = @ExpirationDate,
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate,
[OwnersNotifiedOfAutoscaling] = @OwnersNotifiedOfAutoscaling,
[MaxAutoscaleSeats] = @MaxAutoscaleSeats,
[UseKeyConnector] = @UseKeyConnector
WHERE
[Id] = @Id
END
GO
IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'OrganizationUserOrganizationDetailsView')
BEGIN
DROP VIEW [dbo].[OrganizationUserOrganizationDetailsView]
END
GO
CREATE VIEW [dbo].[OrganizationUserOrganizationDetailsView]
AS
SELECT
OU.[UserId],
OU.[OrganizationId],
O.[Name],
O.[Enabled],
O.[UsePolicies],
O.[UseSso],
O.[UseKeyConnector],
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 OBJECT_ID('[dbo].[ProviderUserProviderOrganizationDetailsView]') IS NOT NULL
BEGIN
DROP VIEW [dbo].[ProviderUserProviderOrganizationDetailsView]
END
GO
CREATE VIEW [dbo].[ProviderUserProviderOrganizationDetailsView]
AS
SELECT
PU.[UserId],
PO.[OrganizationId],
O.[Name],
O.[Enabled],
O.[UsePolicies],
O.[UseSso],
O.[UseKeyConnector],
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],
PO.[Key],
O.[PublicKey],
O.[PrivateKey],
PU.[Status],
PU.[Type],
PO.[ProviderId],
PU.[Id] ProviderUserId,
P.[Name] ProviderName
FROM
[dbo].[ProviderUser] PU
INNER JOIN
[dbo].[ProviderOrganization] PO ON PO.[ProviderId] = PU.[ProviderId]
INNER JOIN
[dbo].[Organization] O ON O.[Id] = PO.[OrganizationId]
INNER JOIN
[dbo].[Provider] P ON P.[Id] = PU.[ProviderId]

View File

@ -14,6 +14,7 @@ SELECT
O.[PlanType],
O.[UsePolicies],
O.[UseSso],
O.[UserKeyConnector],
O.[UseGroups],
O.[UseDirectory],
O.[UseEvents],
@ -53,3 +54,4 @@ LEFT JOIN
[dbo].[SsoConfig] SS ON SS.[OrganizationId] = OU.[OrganizationId]
LEFT JOIN
[dbo].[OrganizationSponsorship] OS ON OS.[SponsoringOrganizationUserID] = OU.[Id]
GO

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -565,6 +565,9 @@ namespace Bit.MySqlMigrations.Migrations
b.Property<bool>("UseGroups")
.HasColumnType("tinyint(1)");
b.Property<bool>("UseKeyConnector")
.HasColumnType("tinyint(1)");
b.Property<bool>("UsePolicies")
.HasColumnType("tinyint(1)");

View File

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<UserSecretsId>9f1cd3e0-70f2-4921-8068-b2538fd7c3f7</UserSecretsId>
</PropertyGroup>
<ItemGroup>

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -569,6 +569,9 @@ namespace Bit.PostgresMigrations.Migrations
b.Property<bool>("UseGroups")
.HasColumnType("boolean");
b.Property<bool>("UseKeyConnector")
.HasColumnType("boolean");
b.Property<bool>("UsePolicies")
.HasColumnType("boolean");

View File

@ -97,5 +97,19 @@ namespace Bit.Setup
Helpers.ShowBanner(_context, "WARNING", message, ConsoleColor.Yellow);
}
}
public void BuildForUpdater()
{
if (_context.Config.EnableKeyConnector && !File.Exists("/bitwarden/key-connector/bwkc.pfx"))
{
Directory.CreateDirectory("/bitwarden/key-connector/");
var keyConnectorCertPassword = Helpers.GetValueFromEnvFile("key-connector",
"keyConnectorSettings__certificate__filesystemPassword");
Helpers.Exec("openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout bwkc.key " +
"-out bwkc.crt -subj \"/CN=Bitwarden Key Connector\" -days 36500");
Helpers.Exec("openssl pkcs12 -export -out /bitwarden/key-connector/bwkc.pfx -inkey bwkc.key " +
$"-in bwkc.crt -passout pass:{keyConnectorCertPassword}");
}
}
}
}

View File

@ -100,6 +100,9 @@ namespace Bit.Setup
"Learn more: https://nginx.org/en/docs/http/ngx_http_realip_module.html")]
public List<string> RealIps { get; set; }
[Description("Enable Key Connector (https://bitwarden.com/help/article/deploy-key-connector)")]
public bool EnableKeyConnector { get; set; } = false;
[YamlIgnore]
public string Domain
{

View File

@ -50,6 +50,7 @@ namespace Bit.Setup
ComposeVersion = context.Config.ComposeVersion;
}
MssqlDataDockerVolume = context.Config.DatabaseDockerVolume;
EnableKeyConnector = context.Config.EnableKeyConnector;
HttpPort = context.Config.HttpPort;
HttpsPort = context.Config.HttpsPort;
if (!string.IsNullOrWhiteSpace(context.CoreVersion))
@ -64,6 +65,7 @@ namespace Bit.Setup
public string ComposeVersion { get; set; } = "3";
public bool MssqlDataDockerVolume { get; set; }
public bool EnableKeyConnector { get; set; }
public string HttpPort { get; set; }
public string HttpsPort { get; set; }
public bool HasPort => !string.IsNullOrWhiteSpace(HttpPort) || !string.IsNullOrWhiteSpace(HttpsPort);

View File

@ -14,6 +14,7 @@ namespace Bit.Setup
private IDictionary<string, string> _mssqlValues;
private IDictionary<string, string> _globalOverrideValues;
private IDictionary<string, string> _mssqlOverrideValues;
private IDictionary<string, string> _keyConnectorOverrideValues;
public EnvironmentFileBuilder(Context context)
{
@ -45,6 +46,7 @@ namespace Bit.Setup
Init();
LoadExistingValues(_globalOverrideValues, "/bitwarden/env/global.override.env");
LoadExistingValues(_mssqlOverrideValues, "/bitwarden/env/mssql.override.env");
LoadExistingValues(_keyConnectorOverrideValues, "/bitwarden/env/key-connector.override.env");
if (_context.Config.PushNotifications &&
_globalOverrideValues.ContainsKey("globalSettings__pushRelayBaseUri") &&
@ -107,6 +109,18 @@ namespace Bit.Setup
{
["SA_PASSWORD"] = dbPassword,
};
_keyConnectorOverrideValues = new Dictionary<string, string>
{
["keyConnectorSettings__webVaultUri"] = _context.Config.Url,
["keyConnectorSettings__identityServerUri"] = "http://identity:5000",
["keyConnectorSettings__database__provider"] = "json",
["keyConnectorSettings__database__jsonFilePath"] = "/etc/bitwarden/key-connector/data.json",
["keyConnectorSettings__rsaKey__provider"] = "certificate",
["keyConnectorSettings__certificate__provider"] = "filesystem",
["keyConnectorSettings__certificate__filesystemPath"] = "/etc/bitwarden/key-connector/bwkc.pfx",
["keyConnectorSettings__certificate__filesystemPassword"] = Helpers.SecureRandomString(32, alpha: true, numeric: true),
};
}
private void LoadExistingValues(IDictionary<string, string> _values, string file)
@ -179,6 +193,16 @@ namespace Bit.Setup
}
Helpers.Exec("chmod 600 /bitwarden/env/mssql.override.env");
if (_context.Config.EnableKeyConnector)
{
using (var sw = File.CreateText("/bitwarden/env/key-connector.override.env"))
{
sw.Write(template(new TemplateModel(_keyConnectorOverrideValues)));
}
Helpers.Exec("chmod 600 /bitwarden/env/key-connector.override.env");
}
// Empty uid env file. Only used on Linux hosts.
if (!File.Exists("/bitwarden/env/uid.env"))
{

View File

@ -70,6 +70,7 @@ namespace Bit.Setup
{
Captcha = context.Config.Captcha;
Ssl = context.Config.Ssl;
EnableKeyConnector = context.Config.EnableKeyConnector;
Domain = context.Config.Domain;
Url = context.Config.Url;
RealIps = context.Config.RealIps;
@ -117,6 +118,7 @@ namespace Bit.Setup
public bool Captcha { get; set; }
public bool Ssl { get; set; }
public bool EnableKeyConnector { get; set; }
public string Domain { get; set; }
public string Url { get; set; }
public string CertificatePath { get; set; }

View File

@ -291,6 +291,9 @@ namespace Bit.Setup
var environmentFileBuilder = new EnvironmentFileBuilder(_context);
environmentFileBuilder.BuildForUpdater();
var certBuilder = new CertBuilder(_context);
certBuilder.BuildForUpdater();
var nginxBuilder = new NginxConfigBuilder(_context);
nginxBuilder.BuildForUpdater();

View File

@ -194,6 +194,22 @@ services:
networks:
- default
- public
{{#if EnableKeyConnector}}
key-connector:
image: bitwarden/key-connector:latest
container_name: bitwarden-key-connector
restart: always
volumes:
- ../key-connector:/etc/bitwarden/key-connector
- ../ca-certificates:/etc/bitwarden/ca-certificates
- ../logs/key-connector:/etc/bitwarden/logs
env_file:
- ../env/key-connector.override.env
networks:
- default
- public
{{/if}}
{{#if MssqlDataDockerVolume}}
volumes:

View File

@ -166,4 +166,10 @@ server {
include /etc/nginx/security-headers.conf;
add_header X-Frame-Options SAMEORIGIN;
}
{{#if EnableKeyConnector}}
location /key-connector/ {
proxy_pass http://key-connector:5000/;
}
{{/if}}
}