diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a4970654d1..6b16b9aa2c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -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:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 63a3a3888b..c56f642c71 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -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 }}
diff --git a/SETUP.md b/SETUP.md
index d001290a55..3f0cd199bb 100644
--- a/SETUP.md
+++ b/SETUP.md
@@ -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
diff --git a/src/Admin/Models/OrganizationEditModel.cs b/src/Admin/Models/OrganizationEditModel.cs
index 30b6f897d2..fe9c445cb5 100644
--- a/src/Admin/Models/OrganizationEditModel.cs
+++ b/src/Admin/Models/OrganizationEditModel.cs
@@ -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;
diff --git a/src/Admin/Views/Organizations/Edit.cshtml b/src/Admin/Views/Organizations/Edit.cshtml
index bc1c7ad571..c154ea2457 100644
--- a/src/Admin/Views/Organizations/Edit.cshtml
+++ b/src/Admin/Views/Organizations/Edit.cshtml
@@ -215,6 +215,10 @@
+
+
+
+
diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs
index 3d90231373..e85f05343e 100644
--- a/src/Api/Controllers/OrganizationsController.cs
+++ b/src/Api/Controllers/OrganizationsController.cs
@@ -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);
}
diff --git a/src/Core/IdentityServer/CustomTokenRequestValidator.cs b/src/Core/IdentityServer/CustomTokenRequestValidator.cs
index 0b9eb8c613..876706ce96 100644
--- a/src/Core/IdentityServer/CustomTokenRequestValidator.cs
+++ b/src/Core/IdentityServer/CustomTokenRequestValidator.cs
@@ -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
diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationSsoRequestModel.cs b/src/Core/Models/Api/Request/Organizations/OrganizationSsoRequestModel.cs
index f87ddfdf7d..c52740c5b0 100644
--- a/src/Core/Models/Api/Request/Organizations/OrganizationSsoRequestModel.cs
+++ b/src/Core/Models/Api/Request/Organizations/OrganizationSsoRequestModel.cs
@@ -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,
diff --git a/src/Core/Models/Api/Response/OrganizationResponseModel.cs b/src/Core/Models/Api/Response/OrganizationResponseModel.cs
index 7454a37d4a..bee98103ef 100644
--- a/src/Core/Models/Api/Response/OrganizationResponseModel.cs
+++ b/src/Core/Models/Api/Response/OrganizationResponseModel.cs
@@ -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; }
diff --git a/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs b/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs
index a16cf1ddb5..a596a12dc9 100644
--- a/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs
+++ b/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs
@@ -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; }
}
}
diff --git a/src/Core/Models/Api/Response/ProfileProviderOrganizationResponseModel.cs b/src/Core/Models/Api/Response/ProfileProviderOrganizationResponseModel.cs
index c877fc6352..f760c78454 100644
--- a/src/Core/Models/Api/Response/ProfileProviderOrganizationResponseModel.cs
+++ b/src/Core/Models/Api/Response/ProfileProviderOrganizationResponseModel.cs
@@ -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;
diff --git a/src/Core/Models/Business/OrganizationLicense.cs b/src/Core/Models/Business/OrganizationLicense.cs
index cd68527e3d..d493855e6d 100644
--- a/src/Core/Models/Business/OrganizationLicense.cs
+++ b/src/Core/Models/Business/OrganizationLicense.cs
@@ -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);
+ ///
+ /// Represents the current version of the license format. Should be updated whenever new fields are added.
+ ///
+ 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
diff --git a/src/Core/Models/Data/OrganizationAbility.cs b/src/Core/Models/Data/OrganizationAbility.cs
index 0433f38805..e02ab6a18c 100644
--- a/src/Core/Models/Data/OrganizationAbility.cs
+++ b/src/Core/Models/Data/OrganizationAbility.cs
@@ -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; }
}
}
diff --git a/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs b/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs
index 11cf0a820a..78313c2c23 100644
--- a/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs
+++ b/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs
@@ -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; }
diff --git a/src/Core/Models/Data/Provider/ProviderUserOrganizationDetails.cs b/src/Core/Models/Data/Provider/ProviderUserOrganizationDetails.cs
index 135617ddd4..38c5bb2106 100644
--- a/src/Core/Models/Data/Provider/ProviderUserOrganizationDetails.cs
+++ b/src/Core/Models/Data/Provider/ProviderUserOrganizationDetails.cs
@@ -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; }
diff --git a/src/Core/Models/Data/SsoConfigurationData.cs b/src/Core/Models/Data/SsoConfigurationData.cs
index a7f2e3f99a..b8745e6049 100644
--- a/src/Core/Models/Data/SsoConfigurationData.cs
+++ b/src/Core/Models/Data/SsoConfigurationData.cs
@@ -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
diff --git a/src/Core/Models/StaticStore/Plan.cs b/src/Core/Models/StaticStore/Plan.cs
index b03e35d4cd..2bed65c7fe 100644
--- a/src/Core/Models/StaticStore/Plan.cs
+++ b/src/Core/Models/StaticStore/Plan.cs
@@ -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; }
diff --git a/src/Core/Models/Table/Organization.cs b/src/Core/Models/Table/Organization.cs
index de65fbc83b..644017bac6 100644
--- a/src/Core/Models/Table/Organization.cs
+++ b/src/Core/Models/Table/Organization.cs
@@ -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; }
diff --git a/src/Core/Repositories/EntityFramework/OrganizationRepository.cs b/src/Core/Repositories/EntityFramework/OrganizationRepository.cs
index ea5cbc50fb..3c73c646df 100644
--- a/src/Core/Repositories/EntityFramework/OrganizationRepository.cs
+++ b/src/Core/Repositories/EntityFramework/OrganizationRepository.cs
@@ -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();
}
}
diff --git a/src/Core/Repositories/EntityFramework/Queries/OrganizationUserOrganizationDetailsViewQuery.cs b/src/Core/Repositories/EntityFramework/Queries/OrganizationUserOrganizationDetailsViewQuery.cs
index 17914fea88..2442191d3b 100644
--- a/src/Core/Repositories/EntityFramework/Queries/OrganizationUserOrganizationDetailsViewQuery.cs
+++ b/src/Core/Repositories/EntityFramework/Queries/OrganizationUserOrganizationDetailsViewQuery.cs
@@ -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,
diff --git a/src/Core/Repositories/EntityFramework/Queries/ProviderUserOrganizationDetailsViewQuery.cs b/src/Core/Repositories/EntityFramework/Queries/ProviderUserOrganizationDetailsViewQuery.cs
index 436f8ee339..c21c68fcc5 100644
--- a/src/Core/Repositories/EntityFramework/Queries/ProviderUserOrganizationDetailsViewQuery.cs
+++ b/src/Core/Repositories/EntityFramework/Queries/ProviderUserOrganizationDetailsViewQuery.cs
@@ -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,
diff --git a/src/Core/Services/ISsoConfigService.cs b/src/Core/Services/ISsoConfigService.cs
index 9154dcccab..3c1d06d755 100644
--- a/src/Core/Services/ISsoConfigService.cs
+++ b/src/Core/Services/ISsoConfigService.cs
@@ -5,6 +5,6 @@ namespace Bit.Core.Services
{
public interface ISsoConfigService
{
- Task SaveAsync(SsoConfig config);
+ Task SaveAsync(SsoConfig config, Organization organization);
}
}
diff --git a/src/Core/Services/Implementations/EmergencyAccessService.cs b/src/Core/Services/Implementations/EmergencyAccessService.cs
index ca88348afe..a647c07717 100644
--- a/src/Core/Services/Implementations/EmergencyAccessService.cs
+++ b/src/Core/Services/Implementations/EmergencyAccessService.cs
@@ -62,10 +62,15 @@ namespace Bit.Core.Services
public async Task
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);
}
diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs
index 40f7ee2ecc..c6dd807ebb 100644
--- a/src/Core/Services/Implementations/OrganizationService.cs
+++ b/src/Core/Services/Implementations/OrganizationService.cs
@@ -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.");
}
diff --git a/src/Core/Services/Implementations/PolicyService.cs b/src/Core/Services/Implementations/PolicyService.cs
index c38ea9fde7..8f7fbf1302 100644
--- a/src/Core/Services/Implementations/PolicyService.cs
+++ b/src/Core/Services/Implementations/PolicyService.cs
@@ -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.");
+ }
+ }
}
}
diff --git a/src/Core/Services/Implementations/SsoConfigService.cs b/src/Core/Services/Implementations/SsoConfigService.cs
index 429c470756..40fe6a87e1 100644
--- a/src/Core/Services/Implementations/SsoConfigService.cs
+++ b/src/Core/Services/Implementations/SsoConfigService.cs
@@ -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);
diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs
index 215e895858..a5b30de095 100644
--- a/src/Core/Services/Implementations/UserService.cs
+++ b/src/Core/Services/Implementations/UserService.cs
@@ -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,
diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs
index d905bd0a7f..3de47c1c84 100644
--- a/src/Core/Utilities/StaticStore.cs
+++ b/src/Core/Utilities/StaticStore.cs
@@ -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,
diff --git a/src/Sql/dbo/Stored Procedures/Organization_Create.sql b/src/Sql/dbo/Stored Procedures/Organization_Create.sql
index 22e190b1c8..3f467e4a60 100644
--- a/src/Sql/dbo/Stored Procedures/Organization_Create.sql
+++ b/src/Sql/dbo/Stored Procedures/Organization_Create.sql
@@ -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
diff --git a/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql b/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql
index 88463a2dc5..cf5219f088 100644
--- a/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql
+++ b/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql
@@ -15,6 +15,7 @@ BEGIN
END AS [Using2fa],
[UsersGetPremium],
[UseSso],
+ [UseKeyConnector],
[UseResetPassword],
[Enabled]
FROM
diff --git a/src/Sql/dbo/Stored Procedures/Organization_Update.sql b/src/Sql/dbo/Stored Procedures/Organization_Update.sql
index 2d1eb4fb08..a0ff29c7e2 100644
--- a/src/Sql/dbo/Stored Procedures/Organization_Update.sql
+++ b/src/Sql/dbo/Stored Procedures/Organization_Update.sql
@@ -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
diff --git a/src/Sql/dbo/Tables/Organization.sql b/src/Sql/dbo/Tables/Organization.sql
index cd1ffcae69..a40a462b2c 100644
--- a/src/Sql/dbo/Tables/Organization.sql
+++ b/src/Sql/dbo/Tables/Organization.sql
@@ -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)
);
diff --git a/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql b/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql
index c65c3f3287..288ae8612c 100644
--- a/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql
+++ b/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql
@@ -7,6 +7,7 @@ SELECT
O.[Enabled],
O.[UsePolicies],
O.[UseSso],
+ O.[UseKeyConnector],
O.[UseGroups],
O.[UseDirectory],
O.[UseEvents],
diff --git a/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql b/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql
index 94740127eb..27daa48006 100644
--- a/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql
+++ b/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql
@@ -7,6 +7,7 @@ SELECT
O.[Enabled],
O.[UsePolicies],
O.[UseSso],
+ O.[UseKeyConnector],
O.[UseGroups],
O.[UseDirectory],
O.[UseEvents],
diff --git a/test/Api.Test/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/Controllers/OrganizationsControllerTests.cs
new file mode 100644
index 0000000000..6b69a7e971
--- /dev/null
+++ b/test/Api.Test/Controllers/OrganizationsControllerTests.cs
@@ -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();
+ _globalSettings = Substitute.For();
+ _organizationRepository = Substitute.For();
+ _organizationService = Substitute.For();
+ _organizationUserRepository = Substitute.For();
+ _paymentService = Substitute.For();
+ _policyRepository = Substitute.For();
+ _ssoConfigRepository = Substitute.For();
+ _ssoConfigService = Substitute.For();
+ _userService = Substitute.For();
+
+ _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()).Returns(user.Id);
+ _currentContext.User.Returns(user);
+
+ var exception = await Assert.ThrowsAsync(
+ () => _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()).Returns(user.Id);
+ _currentContext.User.Returns(user);
+
+ await _organizationService.DeleteUserAsync(orgId, user.Id);
+ await _organizationService.Received(1).DeleteUserAsync(orgId, user.Id);
+ }
+ }
+}
+
diff --git a/test/Core.Test/Repositories/EntityFramework/EqualityComparers/OrganizationCompare.cs b/test/Core.Test/Repositories/EntityFramework/EqualityComparers/OrganizationCompare.cs
index d8f51febf1..04fa1b10a9 100644
--- a/test/Core.Test/Repositories/EntityFramework/EqualityComparers/OrganizationCompare.cs
+++ b/test/Core.Test/Repositories/EntityFramework/EqualityComparers/OrganizationCompare.cs
@@ -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) &&
diff --git a/test/Core.Test/Services/EmergencyAccessServiceTests.cs b/test/Core.Test/Services/EmergencyAccessServiceTests.cs
new file mode 100644
index 0000000000..f3812f20f1
--- /dev/null
+++ b/test/Core.Test/Services/EmergencyAccessServiceTests.cs
@@ -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 sutProvider, User invitingUser, string email, int waitTime)
+ {
+ invitingUser.UsesKeyConnector = true;
+ sutProvider.GetDependency().CanAccessPremium(invitingUser).Returns(true);
+
+ var exception = await Assert.ThrowsAsync(
+ () => 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().DidNotReceiveWithAnyArgs().CreateAsync(default);
+ }
+
+ [Theory, CustomAutoData(typeof(SutProviderCustomization))]
+ public async Task ConfirmUserAsync_UserWithKeyConnectorCannotUseTakeover(
+ SutProvider 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().GetByIdAsync(confirmingUser.Id).Returns(confirmingUser);
+ sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess);
+
+ var exception = await Assert.ThrowsAsync(
+ () => 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().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
+ }
+
+ [Theory, CustomAutoData(typeof(SutProviderCustomization))]
+ public async Task SaveAsync_UserWithKeyConnectorCannotUseTakeover(
+ SutProvider sutProvider, User savingUser)
+ {
+ savingUser.UsesKeyConnector = true;
+ var emergencyAccess = new EmergencyAccess
+ {
+ Type = Enums.EmergencyAccessType.Takeover,
+ GrantorId = savingUser.Id,
+ };
+
+ sutProvider.GetDependency().GetUserByIdAsync(savingUser.Id).Returns(savingUser);
+
+ var exception = await Assert.ThrowsAsync(
+ () => 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().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
+ }
+
+ [Theory, CustomAutoData(typeof(SutProviderCustomization))]
+ public async Task InitiateAsync_UserWithKeyConnectorCannotUseTakeover(
+ SutProvider 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().GetByIdAsync(Arg.Any()).Returns(emergencyAccess);
+ sutProvider.GetDependency().GetByIdAsync(grantor.Id).Returns(grantor);
+
+ var exception = await Assert.ThrowsAsync(
+ () => sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser));
+
+ Assert.Contains("You cannot takeover an account that is using Key Connector", exception.Message);
+ await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
+ }
+
+ [Theory, CustomAutoData(typeof(SutProviderCustomization))]
+ public async Task TakeoverAsync_UserWithKeyConnectorCannotUseTakeover(
+ SutProvider 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().GetByIdAsync(Arg.Any()).Returns(emergencyAccess);
+ sutProvider.GetDependency().GetByIdAsync(grantor.Id).Returns(grantor);
+
+ var exception = await Assert.ThrowsAsync(
+ () => sutProvider.Sut.TakeoverAsync(new Guid(), requestingUser));
+
+ Assert.Contains("You cannot takeover an account that is using Key Connector", exception.Message);
+ }
+ }
+}
diff --git a/test/Core.Test/Services/OrganizationServiceTests.cs b/test/Core.Test/Services/OrganizationServiceTests.cs
index 880ced0417..bf7b2d9625 100644
--- a/test/Core.Test/Services/OrganizationServiceTests.cs
+++ b/test/Core.Test/Services/OrganizationServiceTests.cs
@@ -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();
var organizationRepository = sutProvider.GetDependency();
var applicationCacheService = sutProvider.GetDependency();
diff --git a/test/Core.Test/Services/PolicyServiceTests.cs b/test/Core.Test/Services/PolicyServiceTests.cs
index 9c063e1393..ac83d0ff75 100644
--- a/test/Core.Test/Services/PolicyServiceTests.cs
+++ b/test/Core.Test/Services/PolicyServiceTests.cs
@@ -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 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()
@@ -153,7 +157,7 @@ namespace Bit.Core.Test.Services
Substitute.For(),
Guid.NewGuid()));
- Assert.Contains("KeyConnector is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("Key Connector is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency()
.DidNotReceiveWithAnyArgs()
diff --git a/test/Core.Test/Services/SsoConfigServiceTests.cs b/test/Core.Test/Services/SsoConfigServiceTests.cs
index 1bdb42148e..77eeb56750 100644
--- a/test/Core.Test/Services/SsoConfigServiceTests.cs
+++ b/test/Core.Test/Services/SsoConfigServiceTests.cs
@@ -15,9 +15,9 @@ namespace Bit.Core.Test.Services
public class SsoConfigServiceTests
{
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
- public async Task SaveAsync_ExistingItem_UpdatesRevisionDateOnly(SutProvider sutProvider)
+ public async Task SaveAsync_ExistingItem_UpdatesRevisionDateOnly(SutProvider 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()
.UpsertAsync(ssoConfig).Returns(Task.CompletedTask);
- await sutProvider.Sut.SaveAsync(ssoConfig);
+ await sutProvider.Sut.SaveAsync(ssoConfig, organization);
await sutProvider.GetDependency().Received()
.UpsertAsync(ssoConfig);
@@ -43,7 +43,8 @@ namespace Bit.Core.Test.Services
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
- public async Task SaveAsync_NewItem_UpdatesCreationAndRevisionDate(SutProvider sutProvider)
+ public async Task SaveAsync_NewItem_UpdatesCreationAndRevisionDate(SutProvider 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()
.UpsertAsync(ssoConfig).Returns(Task.CompletedTask);
- await sutProvider.Sut.SaveAsync(ssoConfig);
+ await sutProvider.Sut.SaveAsync(ssoConfig, organization);
await sutProvider.GetDependency().Received()
.UpsertAsync(ssoConfig);
@@ -70,16 +71,20 @@ namespace Bit.Core.Test.Services
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
- public async Task SaveAsync_PreventDisablingKeyConnector(SutProvider sutProvider, Guid orgId)
+ public async Task SaveAsync_PreventDisablingKeyConnector(SutProvider 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();
- ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(oldSsoConfig);
+ ssoConfigRepository.GetByOrganizationIdAsync(organization.Id).Returns(oldSsoConfig);
ssoConfigRepository.UpsertAsync(newSsoConfig).Returns(Task.CompletedTask);
- sutProvider.GetDependency().GetManyDetailsByOrganizationAsync(orgId)
+ sutProvider.GetDependency().GetManyDetailsByOrganizationAsync(organization.Id)
.Returns(new[] { new OrganizationUserUserDetails { UsesKeyConnector = true } });
var exception = await Assert.ThrowsAsync(
- () => 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 sutProvider, Guid orgId)
+ SutProvider 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();
- ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(oldSsoConfig);
+ ssoConfigRepository.GetByOrganizationIdAsync(organization.Id).Returns(oldSsoConfig);
ssoConfigRepository.UpsertAsync(newSsoConfig).Returns(Task.CompletedTask);
- sutProvider.GetDependency().GetManyDetailsByOrganizationAsync(orgId)
+ sutProvider.GetDependency().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 sutProvider)
+ public async Task SaveAsync_KeyConnector_SingleOrgNotEnabled_Throws(SutProvider 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(
- () => 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().DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
}
+
+ [Theory, CustomAutoData(typeof(SutProviderCustomization))]
+ public async Task SaveAsync_KeyConnector_SsoPolicyNotEnabled_Throws(SutProvider 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().GetByOrganizationIdTypeAsync(
+ Arg.Any(), Enums.PolicyType.SingleOrg).Returns(new Policy
+ {
+ Enabled = true
+ });
+
+ var exception = await Assert.ThrowsAsync(
+ () => sutProvider.Sut.SaveAsync(ssoConfig, organization));
+
+ Assert.Contains("Key Connector requires the Single Sign-On Authentication policy to be enabled.", exception.Message);
+
+ await sutProvider.GetDependency().DidNotReceiveWithAnyArgs()
+ .UpsertAsync(default);
+ }
+
+ [Theory, CustomAutoData(typeof(SutProviderCustomization))]
+ public async Task SaveAsync_KeyConnector_SsoConfigNotEnabled_Throws(SutProvider 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().GetByOrganizationIdTypeAsync(
+ Arg.Any(), Arg.Any()).Returns(new Policy
+ {
+ Enabled = true
+ });
+
+ var exception = await Assert.ThrowsAsync(
+ () => sutProvider.Sut.SaveAsync(ssoConfig, organization));
+
+ Assert.Contains("You must enable SSO to use Key Connector.", exception.Message);
+
+ await sutProvider.GetDependency().DidNotReceiveWithAnyArgs()
+ .UpsertAsync(default);
+ }
+
+ [Theory, CustomAutoData(typeof(SutProviderCustomization))]
+ public async Task SaveAsync_KeyConnector_KeyConnectorAbilityNotEnabled_Throws(SutProvider 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().GetByOrganizationIdTypeAsync(
+ Arg.Any(), Arg.Any()).Returns(new Policy
+ {
+ Enabled = true,
+ });
+
+ var exception = await Assert.ThrowsAsync(
+ () => sutProvider.Sut.SaveAsync(ssoConfig, organization));
+
+ Assert.Contains("Organization cannot use Key Connector.", exception.Message);
+
+ await sutProvider.GetDependency().DidNotReceiveWithAnyArgs()
+ .UpsertAsync(default);
+ }
+
+ [Theory, CustomAutoData(typeof(SutProviderCustomization))]
+ public async Task SaveAsync_KeyConnector_Success(SutProvider 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().GetByOrganizationIdTypeAsync(
+ Arg.Any(), Arg.Any()).Returns(new Policy
+ {
+ Enabled = true,
+ });
+
+ await sutProvider.Sut.SaveAsync(ssoConfig, organization);
+
+ await sutProvider.GetDependency().ReceivedWithAnyArgs()
+ .UpsertAsync(default);
+ }
}
}
diff --git a/util/Migrator/DbScripts/2021-11-12_00_KeyConnectorFlag.sql b/util/Migrator/DbScripts/2021-11-12_00_KeyConnectorFlag.sql
new file mode 100644
index 0000000000..b216601e56
--- /dev/null
+++ b/util/Migrator/DbScripts/2021-11-12_00_KeyConnectorFlag.sql
@@ -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]
diff --git a/util/Migrator/DbScripts/2021-11-15_00_MergeKeyConnectorAndFFE.sql b/util/Migrator/DbScripts/2021-11-18_00_MergeKeyConnectorAndFFE.sql
similarity index 98%
rename from util/Migrator/DbScripts/2021-11-15_00_MergeKeyConnectorAndFFE.sql
rename to util/Migrator/DbScripts/2021-11-18_00_MergeKeyConnectorAndFFE.sql
index d5f202a125..343fa447bb 100644
--- a/util/Migrator/DbScripts/2021-11-15_00_MergeKeyConnectorAndFFE.sql
+++ b/util/Migrator/DbScripts/2021-11-18_00_MergeKeyConnectorAndFFE.sql
@@ -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
diff --git a/util/MySqlMigrations/Migrations/20211115145402_KeyConnectorFlag.Designer.cs b/util/MySqlMigrations/Migrations/20211115145402_KeyConnectorFlag.Designer.cs
new file mode 100644
index 0000000000..287ac5bdac
--- /dev/null
+++ b/util/MySqlMigrations/Migrations/20211115145402_KeyConnectorFlag.Designer.cs
@@ -0,0 +1,1498 @@
+//
+using System;
+using Bit.Core.Repositories.EntityFramework;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace Bit.MySqlMigrations.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20211115145402_KeyConnectorFlag")]
+ partial class KeyConnectorFlag
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("Relational:MaxIdentifierLength", 64)
+ .HasAnnotation("ProductVersion", "5.0.9");
+
+ modelBuilder.Entity("Bit.Core.Models.EntityFramework.Cipher", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("Attachments")
+ .HasColumnType("longtext");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Data")
+ .HasColumnType("longtext");
+
+ b.Property("DeletedDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Favorites")
+ .HasColumnType("longtext");
+
+ b.Property("Folders")
+ .HasColumnType("longtext");
+
+ b.Property("OrganizationId")
+ .HasColumnType("char(36)");
+
+ b.Property("Reprompt")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Type")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("UserId")
+ .HasColumnType("char(36)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OrganizationId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Cipher");
+ });
+
+ modelBuilder.Entity("Bit.Core.Models.EntityFramework.Collection", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("ExternalId")
+ .HasMaxLength(300)
+ .HasColumnType("varchar(300)");
+
+ b.Property("Name")
+ .HasColumnType("longtext");
+
+ b.Property("OrganizationId")
+ .HasColumnType("char(36)");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OrganizationId");
+
+ b.ToTable("Collection");
+ });
+
+ modelBuilder.Entity("Bit.Core.Models.EntityFramework.CollectionCipher", b =>
+ {
+ b.Property("CollectionId")
+ .HasColumnType("char(36)");
+
+ b.Property("CipherId")
+ .HasColumnType("char(36)");
+
+ b.HasKey("CollectionId", "CipherId");
+
+ b.HasIndex("CipherId");
+
+ b.ToTable("CollectionCipher");
+ });
+
+ modelBuilder.Entity("Bit.Core.Models.EntityFramework.CollectionGroup", b =>
+ {
+ b.Property("CollectionId")
+ .HasColumnType("char(36)");
+
+ b.Property("GroupId")
+ .HasColumnType("char(36)");
+
+ b.Property("HidePasswords")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("ReadOnly")
+ .HasColumnType("tinyint(1)");
+
+ b.HasKey("CollectionId", "GroupId");
+
+ b.HasIndex("GroupId");
+
+ b.ToTable("CollectionGroups");
+ });
+
+ modelBuilder.Entity("Bit.Core.Models.EntityFramework.CollectionUser", b =>
+ {
+ b.Property("CollectionId")
+ .HasColumnType("char(36)");
+
+ b.Property("OrganizationUserId")
+ .HasColumnType("char(36)");
+
+ b.Property("HidePasswords")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("ReadOnly")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UserId")
+ .HasColumnType("char(36)");
+
+ b.HasKey("CollectionId", "OrganizationUserId");
+
+ b.HasIndex("OrganizationUserId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("CollectionUsers");
+ });
+
+ modelBuilder.Entity("Bit.Core.Models.EntityFramework.Device", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("char(36)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Identifier")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("Name")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("PushToken")
+ .HasMaxLength(255)
+ .HasColumnType("varchar(255)");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Type")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("UserId")
+ .HasColumnType("char(36)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Device");
+ });
+
+ modelBuilder.Entity("Bit.Core.Models.EntityFramework.EmergencyAccess", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("varchar(256)");
+
+ b.Property("GranteeId")
+ .HasColumnType("char(36)");
+
+ b.Property("GrantorId")
+ .HasColumnType("char(36)");
+
+ b.Property("KeyEncrypted")
+ .HasColumnType("longtext");
+
+ b.Property("LastNotificationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("RecoveryInitiatedDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Status")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("Type")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("WaitTimeDays")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GranteeId");
+
+ b.HasIndex("GrantorId");
+
+ b.ToTable("EmergencyAccess");
+ });
+
+ modelBuilder.Entity("Bit.Core.Models.EntityFramework.Event", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("ActingUserId")
+ .HasColumnType("char(36)");
+
+ b.Property("CipherId")
+ .HasColumnType("char(36)");
+
+ b.Property("CollectionId")
+ .HasColumnType("char(36)");
+
+ b.Property("Date")
+ .HasColumnType("datetime(6)");
+
+ b.Property("DeviceType")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("GroupId")
+ .HasColumnType("char(36)");
+
+ b.Property("IpAddress")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("OrganizationId")
+ .HasColumnType("char(36)");
+
+ b.Property("OrganizationUserId")
+ .HasColumnType("char(36)");
+
+ b.Property("PolicyId")
+ .HasColumnType("char(36)");
+
+ b.Property("ProviderId")
+ .HasColumnType("char(36)");
+
+ b.Property("ProviderOrganizationId")
+ .HasColumnType("char(36)");
+
+ b.Property("ProviderUserId")
+ .HasColumnType("char(36)");
+
+ b.Property("Type")
+ .HasColumnType("int");
+
+ b.Property("UserId")
+ .HasColumnType("char(36)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Event");
+ });
+
+ modelBuilder.Entity("Bit.Core.Models.EntityFramework.Folder", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Name")
+ .HasColumnType("longtext");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("UserId")
+ .HasColumnType("char(36)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Folder");
+ });
+
+ modelBuilder.Entity("Bit.Core.Models.EntityFramework.Grant", b =>
+ {
+ b.Property("Key")
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("ClientId")
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("ConsumedDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Data")
+ .HasColumnType("longtext");
+
+ b.Property("Description")
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("ExpirationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("SessionId")
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)");
+
+ b.Property("SubjectId")
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("Type")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.HasKey("Key");
+
+ b.ToTable("Grant");
+ });
+
+ modelBuilder.Entity("Bit.Core.Models.EntityFramework.Group", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("AccessAll")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("ExternalId")
+ .HasMaxLength(300)
+ .HasColumnType("varchar(300)");
+
+ b.Property("Name")
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)");
+
+ b.Property("OrganizationId")
+ .HasColumnType("char(36)");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OrganizationId");
+
+ b.ToTable("Group");
+ });
+
+ modelBuilder.Entity("Bit.Core.Models.EntityFramework.GroupUser", b =>
+ {
+ b.Property("GroupId")
+ .HasColumnType("char(36)");
+
+ b.Property("OrganizationUserId")
+ .HasColumnType("char(36)");
+
+ b.Property("UserId")
+ .HasColumnType("char(36)");
+
+ b.HasKey("GroupId", "OrganizationUserId");
+
+ b.HasIndex("OrganizationUserId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("GroupUser");
+ });
+
+ modelBuilder.Entity("Bit.Core.Models.EntityFramework.Installation", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("varchar(256)");
+
+ b.Property("Enabled")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("Key")
+ .HasMaxLength(150)
+ .HasColumnType("varchar(150)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Installation");
+ });
+
+ modelBuilder.Entity("Bit.Core.Models.EntityFramework.Organization", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("ApiKey")
+ .HasMaxLength(30)
+ .HasColumnType("varchar(30)");
+
+ b.Property("BillingEmail")
+ .HasMaxLength(256)
+ .HasColumnType("varchar(256)");
+
+ b.Property("BusinessAddress1")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("BusinessAddress2")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("BusinessAddress3")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("BusinessCountry")
+ .HasMaxLength(2)
+ .HasColumnType("varchar(2)");
+
+ b.Property("BusinessName")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("BusinessTaxNumber")
+ .HasMaxLength(30)
+ .HasColumnType("varchar(30)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Enabled")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("ExpirationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Gateway")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("GatewayCustomerId")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("GatewaySubscriptionId")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("Identifier")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("LicenseKey")
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)");
+
+ b.Property("MaxAutoscaleSeats")
+ .HasColumnType("int");
+
+ b.Property("MaxCollections")
+ .HasColumnType("smallint");
+
+ b.Property("MaxStorageGb")
+ .HasColumnType("smallint");
+
+ b.Property("Name")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("OwnersNotifiedOfAutoscaling")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Plan")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("PlanType")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("PrivateKey")
+ .HasColumnType("longtext");
+
+ b.Property("PublicKey")
+ .HasColumnType("longtext");
+
+ b.Property("ReferenceData")
+ .HasColumnType("longtext");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Seats")
+ .HasColumnType("int");
+
+ b.Property("SelfHost")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("Storage")
+ .HasColumnType("bigint");
+
+ b.Property("TwoFactorProviders")
+ .HasColumnType("longtext");
+
+ b.Property("Use2fa")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseApi")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseDirectory")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseEvents")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseGroups")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseKeyConnector")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UsePolicies")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseResetPassword")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseSso")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseTotp")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UsersGetPremium")
+ .HasColumnType("tinyint(1)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Organization");
+ });
+
+ modelBuilder.Entity("Bit.Core.Models.EntityFramework.OrganizationUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("AccessAll")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("varchar(256)");
+
+ b.Property("ExternalId")
+ .HasMaxLength(300)
+ .HasColumnType("varchar(300)");
+
+ b.Property("Key")
+ .HasColumnType("longtext");
+
+ b.Property("OrganizationId")
+ .HasColumnType("char(36)");
+
+ b.Property("Permissions")
+ .HasColumnType("longtext");
+
+ b.Property("ResetPasswordKey")
+ .HasColumnType("longtext");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Status")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("Type")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("UserId")
+ .HasColumnType("char(36)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OrganizationId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("OrganizationUser");
+ });
+
+ modelBuilder.Entity("Bit.Core.Models.EntityFramework.Policy", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Data")
+ .HasColumnType("longtext");
+
+ b.Property("Enabled")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("OrganizationId")
+ .HasColumnType("char(36)");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Type")
+ .HasColumnType("tinyint unsigned");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OrganizationId");
+
+ b.ToTable("Policy");
+ });
+
+ modelBuilder.Entity("Bit.Core.Models.EntityFramework.Provider.Provider", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("BillingEmail")
+ .HasColumnType("longtext");
+
+ b.Property("BusinessAddress1")
+ .HasColumnType("longtext");
+
+ b.Property