mirror of
https://github.com/bitwarden/server.git
synced 2024-11-25 12:45:18 +01:00
Merge branch 'main' into PM-11162-assign-to-collection-perm-update
This commit is contained in:
commit
5c672ecdc1
19
.github/renovate.json
vendored
19
.github/renovate.json
vendored
@ -29,7 +29,7 @@
|
||||
"commitMessagePrefix": "[deps] DevOps:"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["DnsClient", "Quartz"],
|
||||
"matchPackageNames": ["DnsClient"],
|
||||
"description": "Admin Console owned dependencies",
|
||||
"commitMessagePrefix": "[deps] AC:",
|
||||
"reviewers": ["team:team-admin-console-dev"]
|
||||
@ -42,14 +42,7 @@
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"AspNetCoreRateLimit",
|
||||
"AspNetCoreRateLimit.Redis",
|
||||
"Azure.Data.Tables",
|
||||
"Azure.Extensions.AspNetCore.DataProtection.Blobs",
|
||||
"Azure.Messaging.EventGrid",
|
||||
"Azure.Messaging.ServiceBus",
|
||||
"Azure.Storage.Blobs",
|
||||
"Azure.Storage.Queues",
|
||||
"DuoUniversal",
|
||||
"Fido2.AspNet",
|
||||
"Duende.IdentityServer",
|
||||
@ -128,8 +121,16 @@
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"AspNetCoreRateLimit",
|
||||
"AspNetCoreRateLimit.Redis",
|
||||
"Azure.Data.Tables",
|
||||
"Azure.Messaging.EventGrid",
|
||||
"Azure.Messaging.ServiceBus",
|
||||
"Azure.Storage.Blobs",
|
||||
"Azure.Storage.Queues",
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer",
|
||||
"Microsoft.AspNetCore.Http"
|
||||
"Microsoft.AspNetCore.Http",
|
||||
"Quartz"
|
||||
],
|
||||
"description": "Platform owned dependencies",
|
||||
"commitMessagePrefix": "[deps] Platform:",
|
||||
|
62
.github/workflows/build.yml
vendored
62
.github/workflows/build.yml
vendored
@ -7,18 +7,25 @@ on:
|
||||
- "main"
|
||||
- "rc"
|
||||
- "hotfix-rc"
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
env:
|
||||
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
|
||||
|
||||
jobs:
|
||||
check-run:
|
||||
name: Check PR run
|
||||
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
@ -29,8 +36,7 @@ jobs:
|
||||
build-artifacts:
|
||||
name: Build artifacts
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- lint
|
||||
needs: lint
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@ -68,6 +74,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
@ -120,7 +128,9 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
security-events: write
|
||||
needs: build-artifacts
|
||||
needs:
|
||||
- build-artifacts
|
||||
- check-run
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@ -173,6 +183,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Check branch to publish
|
||||
env:
|
||||
@ -292,6 +304,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
@ -305,9 +319,9 @@ jobs:
|
||||
run: az acr login -n $_AZ_REGISTRY --only-show-errors
|
||||
|
||||
- name: Make Docker stubs
|
||||
if: github.ref == 'refs/heads/main' ||
|
||||
github.ref == 'refs/heads/rc' ||
|
||||
github.ref == 'refs/heads/hotfix-rc'
|
||||
if: |
|
||||
github.event_name != 'pull_request_target'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||
run: |
|
||||
# Set proper setup image based on branch
|
||||
case "$GITHUB_REF" in
|
||||
@ -347,13 +361,17 @@ jobs:
|
||||
cd docker-stub/EU; zip -r ../../docker-stub-EU.zip *; cd ../..
|
||||
|
||||
- name: Make Docker stub checksums
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
||||
if: |
|
||||
github.event_name != 'pull_request_target'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||
run: |
|
||||
sha256sum docker-stub-US.zip > docker-stub-US-sha256.txt
|
||||
sha256sum docker-stub-EU.zip > docker-stub-EU-sha256.txt
|
||||
|
||||
- name: Upload Docker stub US artifact
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
||||
if: |
|
||||
github.event_name != 'pull_request_target'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: docker-stub-US.zip
|
||||
@ -361,7 +379,9 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Docker stub EU artifact
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
||||
if: |
|
||||
github.event_name != 'pull_request_target'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: docker-stub-EU.zip
|
||||
@ -369,7 +389,9 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Docker stub US checksum artifact
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
||||
if: |
|
||||
github.event_name != 'pull_request_target'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: docker-stub-US-sha256.txt
|
||||
@ -377,7 +399,9 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Docker stub EU checksum artifact
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
||||
if: |
|
||||
github.event_name != 'pull_request_target'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: docker-stub-EU-sha256.txt
|
||||
@ -467,6 +491,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
@ -501,6 +527,7 @@ jobs:
|
||||
|
||||
self-host-build:
|
||||
name: Trigger self-host build
|
||||
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-22.04
|
||||
needs: build-docker
|
||||
steps:
|
||||
@ -533,7 +560,7 @@ jobs:
|
||||
|
||||
trigger-k8s-deploy:
|
||||
name: Trigger k8s deploy
|
||||
if: github.ref == 'refs/heads/main'
|
||||
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-22.04
|
||||
needs: build-docker
|
||||
steps:
|
||||
@ -567,7 +594,9 @@ jobs:
|
||||
|
||||
trigger-ee-updates:
|
||||
name: Trigger Ephemeral Environment updates
|
||||
if: github.ref != 'refs/heads/main' && contains(github.event.pull_request.labels.*.name, 'ephemeral-environment')
|
||||
if: |
|
||||
github.event_name == 'pull_request_target'
|
||||
&& contains(github.event.pull_request.labels.*.name, 'ephemeral-environment')
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-docker
|
||||
steps:
|
||||
@ -613,9 +642,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check if any job failed
|
||||
if: |
|
||||
(github.ref == 'refs/heads/main'
|
||||
|| github.ref == 'refs/heads/rc'
|
||||
|| github.ref == 'refs/heads/hotfix-rc')
|
||||
github.event_name != 'pull_request_target'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||
&& contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
|
18
.github/workflows/test-database.yml
vendored
18
.github/workflows/test-database.yml
vendored
@ -70,6 +70,11 @@ jobs:
|
||||
docker compose --profile mssql --profile postgres --profile mysql up -d
|
||||
shell: pwsh
|
||||
|
||||
- name: Add MariaDB for unified
|
||||
# Use a different port than MySQL
|
||||
run: |
|
||||
docker run --detach --name mariadb --env MARIADB_ROOT_PASSWORD=mariadb-password -p 4306:3306 mariadb:10
|
||||
|
||||
# I've seen the SQL Server container not be ready for commands right after starting up and just needing a bit longer to be ready
|
||||
- name: Sleep
|
||||
run: sleep 15s
|
||||
@ -103,6 +108,12 @@ jobs:
|
||||
env:
|
||||
CONN_STR: "server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev;Allow User Variables=true"
|
||||
|
||||
- name: Migrate MariaDB
|
||||
working-directory: "util/MySqlMigrations"
|
||||
run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:MySql:ConnectionString="$CONN_STR"'
|
||||
env:
|
||||
CONN_STR: "server=localhost;port=4306;uid=root;pwd=mariadb-password;database=vault_dev;Allow User Variables=true"
|
||||
|
||||
- name: Migrate Postgres
|
||||
working-directory: "util/PostgresMigrations"
|
||||
run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:PostgreSql:ConnectionString="$CONN_STR"'
|
||||
@ -130,6 +141,9 @@ jobs:
|
||||
# Default Sqlite
|
||||
BW_TEST_DATABASES__3__TYPE: "Sqlite"
|
||||
BW_TEST_DATABASES__3__CONNECTIONSTRING: "Data Source=${{ runner.temp }}/test.db"
|
||||
# Unified MariaDB
|
||||
BW_TEST_DATABASES__4__TYPE: "MySql"
|
||||
BW_TEST_DATABASES__4__CONNECTIONSTRING: "server=localhost;port=4306;uid=root;pwd=mariadb-password;database=vault_dev;Allow User Variables=true"
|
||||
run: dotnet test --logger "trx;LogFileName=infrastructure-test-results.trx"
|
||||
shell: pwsh
|
||||
|
||||
@ -137,6 +151,10 @@ jobs:
|
||||
if: failure()
|
||||
run: 'docker logs $(docker ps --quiet --filter "name=mysql")'
|
||||
|
||||
- name: Print MariaDB Logs
|
||||
if: failure()
|
||||
run: 'docker logs $(docker ps --quiet --filter "name=mariadb")'
|
||||
|
||||
- name: Print Postgres Logs
|
||||
if: failure()
|
||||
run: 'docker logs $(docker ps --quiet --filter "name=postgres")'
|
||||
|
@ -110,7 +110,6 @@ public static class RolePermissionMapping
|
||||
Permission.User_Licensing_View,
|
||||
Permission.User_Billing_View,
|
||||
Permission.User_Billing_LaunchGateway,
|
||||
Permission.User_Delete,
|
||||
Permission.Org_List_View,
|
||||
Permission.Org_OrgInformation_View,
|
||||
Permission.Org_GeneralDetails_View,
|
||||
|
@ -252,6 +252,12 @@ public class OrganizationsController : Controller
|
||||
throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving.");
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& (await _userService.GetOrganizationsManagingUserAsync(user.Id)).Any(x => x.Id == id))
|
||||
{
|
||||
throw new BadRequestException("Managed user account cannot leave managing organization. Contact your organization administrator for additional details.");
|
||||
}
|
||||
|
||||
await _removeOrganizationUserCommand.RemoveUserAsync(id, user.Id);
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,10 @@ public class CiphersController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp);
|
||||
var collectionCiphers = await _collectionCipherRepository.GetManyByOrganizationIdAsync(cipher.OrganizationId.Value);
|
||||
var collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);
|
||||
|
||||
return new CipherMiniDetailsResponseModel(cipher, _globalSettings, collectionCiphersGroupDict, cipher.OrganizationUseTotp);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/full-details")]
|
||||
@ -653,10 +656,10 @@ public class CiphersController : Controller
|
||||
|
||||
[HttpPut("{id}/collections-admin")]
|
||||
[HttpPost("{id}/collections-admin")]
|
||||
public async Task PutCollectionsAdmin(string id, [FromBody] CipherCollectionsRequestModel model)
|
||||
public async Task<CipherMiniDetailsResponseModel> PutCollectionsAdmin(string id, [FromBody] CipherCollectionsRequestModel model)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id));
|
||||
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(new Guid(id));
|
||||
|
||||
if (cipher == null || !cipher.OrganizationId.HasValue ||
|
||||
!await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))
|
||||
@ -674,6 +677,11 @@ public class CiphersController : Controller
|
||||
}
|
||||
|
||||
await _cipherService.SaveCollectionsAsync(cipher, collectionIds, userId, true);
|
||||
|
||||
var collectionCiphers = await _collectionCipherRepository.GetManyByOrganizationIdAsync(cipher.OrganizationId.Value);
|
||||
var collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);
|
||||
|
||||
return new CipherMiniDetailsResponseModel(cipher, _globalSettings, collectionCiphersGroupDict, cipher.OrganizationUseTotp);
|
||||
}
|
||||
|
||||
[HttpPost("bulk-collections")]
|
||||
|
@ -151,6 +151,9 @@ public static class FeatureFlagKeys
|
||||
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
||||
public const string NewDeviceVerification = "new-device-verification";
|
||||
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
|
||||
public const string IntegrationPage = "pm-14505-admin-console-integration-page";
|
||||
public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss";
|
||||
public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
{
|
||||
|
@ -39,6 +39,7 @@ public class CurrentContext : ICurrentContext
|
||||
public virtual int? BotScore { get; set; }
|
||||
public virtual string ClientId { get; set; }
|
||||
public virtual Version ClientVersion { get; set; }
|
||||
public virtual bool ClientVersionIsPrerelease { get; set; }
|
||||
public virtual IdentityClientType IdentityClientType { get; set; }
|
||||
public virtual Guid? ServiceAccountOrganizationId { get; set; }
|
||||
|
||||
@ -97,6 +98,11 @@ public class CurrentContext : ICurrentContext
|
||||
{
|
||||
ClientVersion = cVersion;
|
||||
}
|
||||
|
||||
if (httpContext.Request.Headers.TryGetValue("Is-Prerelease", out var clientVersionIsPrerelease))
|
||||
{
|
||||
ClientVersionIsPrerelease = clientVersionIsPrerelease == "1";
|
||||
}
|
||||
}
|
||||
|
||||
public async virtual Task BuildAsync(ClaimsPrincipal user, GlobalSettings globalSettings)
|
||||
|
@ -29,12 +29,13 @@ public interface ICurrentContext
|
||||
int? BotScore { get; set; }
|
||||
string ClientId { get; set; }
|
||||
Version ClientVersion { get; set; }
|
||||
bool ClientVersionIsPrerelease { get; set; }
|
||||
|
||||
Task BuildAsync(HttpContext httpContext, GlobalSettings globalSettings);
|
||||
Task BuildAsync(ClaimsPrincipal user, GlobalSettings globalSettings);
|
||||
|
||||
Task SetContextAsync(ClaimsPrincipal user);
|
||||
|
||||
|
||||
Task<bool> OrganizationUser(Guid orgId);
|
||||
Task<bool> OrganizationAdmin(Guid orgId);
|
||||
Task<bool> OrganizationOwner(Guid orgId);
|
||||
|
@ -21,8 +21,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.401.30" />
|
||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.40" />
|
||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.401.35" />
|
||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.45" />
|
||||
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
|
||||
|
@ -20,6 +20,7 @@ public class LaunchDarklyFeatureService : IFeatureService
|
||||
private const string _contextKindServiceAccount = "service-account";
|
||||
|
||||
private const string _contextAttributeClientVersion = "client-version";
|
||||
private const string _contextAttributeClientVersionIsPrerelease = "client-version-is-prerelease";
|
||||
private const string _contextAttributeDeviceType = "device-type";
|
||||
private const string _contextAttributeClientType = "client-type";
|
||||
private const string _contextAttributeOrganizations = "organizations";
|
||||
@ -145,6 +146,7 @@ public class LaunchDarklyFeatureService : IFeatureService
|
||||
if (_currentContext.ClientVersion != null)
|
||||
{
|
||||
builder.Set(_contextAttributeClientVersion, _currentContext.ClientVersion.ToString());
|
||||
builder.Set(_contextAttributeClientVersionIsPrerelease, _currentContext.ClientVersionIsPrerelease);
|
||||
}
|
||||
|
||||
if (_currentContext.DeviceType.HasValue)
|
||||
|
@ -1,11 +1,15 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Tools.Entities;
|
||||
|
||||
public class PasswordHealthReportApplication : ITableObject<Guid>, IRevisable
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid OrganizationId { get; set; }
|
||||
public string Uri { get; set; }
|
||||
public string? Uri { get; set; }
|
||||
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
||||
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
||||
|
||||
|
@ -0,0 +1,9 @@
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Tools.Entities;
|
||||
|
||||
namespace Bit.Core.Tools.Repositories;
|
||||
|
||||
public interface IPasswordHealthReportApplicationRepository : IRepository<PasswordHealthReportApplication, Guid>
|
||||
{
|
||||
Task<ICollection<PasswordHealthReportApplication>> GetByOrganizationIdAsync(Guid organizationId);
|
||||
}
|
@ -121,8 +121,8 @@ public class TwoFactorAuthenticationValidator(
|
||||
|
||||
var twoFactorResultDict = new Dictionary<string, object>
|
||||
{
|
||||
{ "TwoFactorProviders", null },
|
||||
{ "TwoFactorProviders2", providers }, // backwards compatibility
|
||||
{ "TwoFactorProviders", providers.Keys }, // backwards compatibility
|
||||
{ "TwoFactorProviders2", providers },
|
||||
};
|
||||
|
||||
// If we have email as a 2FA provider, we might need an SsoEmail2fa Session Token
|
||||
|
@ -58,6 +58,7 @@ public static class DapperServiceCollectionExtensions
|
||||
services.AddSingleton<INotificationStatusRepository, NotificationStatusRepository>();
|
||||
services
|
||||
.AddSingleton<IClientOrganizationMigrationRecordRepository, ClientOrganizationMigrationRecordRepository>();
|
||||
services.AddSingleton<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();
|
||||
|
||||
if (selfHosted)
|
||||
{
|
||||
|
@ -0,0 +1,33 @@
|
||||
using System.Data;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Infrastructure.Dapper.Repositories;
|
||||
using Dapper;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using ToolsEntities = Bit.Core.Tools.Entities;
|
||||
|
||||
namespace Bit.Infrastructure.Dapper.Tools.Repositories;
|
||||
|
||||
public class PasswordHealthReportApplicationRepository : Repository<ToolsEntities.PasswordHealthReportApplication, Guid>, IPasswordHealthReportApplicationRepository
|
||||
{
|
||||
public PasswordHealthReportApplicationRepository(GlobalSettings globalSettings)
|
||||
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
|
||||
{ }
|
||||
|
||||
public PasswordHealthReportApplicationRepository(string connectionString, string readOnlyConnectionString)
|
||||
: base(connectionString, readOnlyConnectionString)
|
||||
{ }
|
||||
|
||||
public async Task<ICollection<ToolsEntities.PasswordHealthReportApplication>> GetByOrganizationIdAsync(Guid organizationId)
|
||||
{
|
||||
using (var connection = new SqlConnection(ReadOnlyConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<ToolsEntities.PasswordHealthReportApplication>(
|
||||
$"[{Schema}].[PasswordHealthReportApplication_ReadByOrganizationId]",
|
||||
new { OrganizationId = organizationId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ using Bit.Infrastructure.EntityFramework.Converters;
|
||||
using Bit.Infrastructure.EntityFramework.Models;
|
||||
using Bit.Infrastructure.EntityFramework.NotificationCenter.Models;
|
||||
using Bit.Infrastructure.EntityFramework.SecretsManager.Models;
|
||||
using Bit.Infrastructure.EntityFramework.Tools.Models;
|
||||
using Bit.Infrastructure.EntityFramework.Vault.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@ -75,6 +76,7 @@ public class DatabaseContext : DbContext
|
||||
public DbSet<Notification> Notifications { get; set; }
|
||||
public DbSet<NotificationStatus> NotificationStatuses { get; set; }
|
||||
public DbSet<ClientOrganizationMigrationRecord> ClientOrganizationMigrationRecords { get; set; }
|
||||
public DbSet<PasswordHealthReportApplication> PasswordHealthReportApplications { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
|
@ -0,0 +1,24 @@
|
||||
using Bit.Infrastructure.EntityFramework.Tools.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Bit.Infrastructure.EntityFramework.Tools.Configurations;
|
||||
|
||||
public class PasswordHealthReportApplicationEntityTypeConfiguration : IEntityTypeConfiguration<PasswordHealthReportApplication>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PasswordHealthReportApplication> builder)
|
||||
{
|
||||
builder
|
||||
.Property(s => s.Id)
|
||||
.ValueGeneratedNever();
|
||||
|
||||
builder.HasIndex(s => s.Id)
|
||||
.IsClustered(true);
|
||||
|
||||
builder
|
||||
.HasIndex(s => s.OrganizationId)
|
||||
.IsClustered(false);
|
||||
|
||||
builder.ToTable(nameof(PasswordHealthReportApplication));
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
using AutoMapper;
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||
|
||||
namespace Bit.Infrastructure.EntityFramework.Tools.Models;
|
||||
|
||||
public class PasswordHealthReportApplication : Core.Tools.Entities.PasswordHealthReportApplication
|
||||
{
|
||||
public virtual Organization Organization { get; set; }
|
||||
}
|
||||
|
||||
public class PasswordHealthReportApplicationProfile : Profile
|
||||
{
|
||||
public PasswordHealthReportApplicationProfile()
|
||||
{
|
||||
CreateMap<Core.Tools.Entities.PasswordHealthReportApplication, PasswordHealthReportApplication>()
|
||||
.ReverseMap();
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
using AutoMapper;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Tools.Models;
|
||||
using LinqToDB;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using AdminConsoleEntities = Bit.Core.Tools.Entities;
|
||||
|
||||
namespace Bit.Infrastructure.EntityFramework.Tools.Repositories;
|
||||
|
||||
public class PasswordHealthReportApplicationRepository :
|
||||
Repository<AdminConsoleEntities.PasswordHealthReportApplication, PasswordHealthReportApplication, Guid>,
|
||||
IPasswordHealthReportApplicationRepository
|
||||
{
|
||||
public PasswordHealthReportApplicationRepository(IServiceScopeFactory serviceScopeFactory,
|
||||
IMapper mapper) : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.PasswordHealthReportApplications)
|
||||
{ }
|
||||
|
||||
public async Task<ICollection<AdminConsoleEntities.PasswordHealthReportApplication>> GetByOrganizationIdAsync(Guid organizationId)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var results = await dbContext.PasswordHealthReportApplications
|
||||
.Where(p => p.OrganizationId == organizationId)
|
||||
.ToListAsync();
|
||||
return Mapper.Map<ICollection<AdminConsoleEntities.PasswordHealthReportApplication>>(results);
|
||||
}
|
||||
}
|
||||
}
|
@ -51,7 +51,6 @@ public class OrganizationsControllerTests : IDisposable
|
||||
private readonly IProviderBillingService _providerBillingService;
|
||||
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
|
||||
private readonly OrganizationsController _sut;
|
||||
|
||||
public OrganizationsControllerTests()
|
||||
@ -123,7 +122,8 @@ public class OrganizationsControllerTests : IDisposable
|
||||
_currentContext.OrganizationUser(orgId).Returns(true);
|
||||
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||
_userService.GetOrganizationsManagingUserAsync(user.Id).Returns(new List<Organization> { null });
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Leave(orgId));
|
||||
|
||||
Assert.Contains("Your organization's Single Sign-On settings prevent you from leaving.",
|
||||
@ -132,6 +132,36 @@ public class OrganizationsControllerTests : IDisposable
|
||||
await _removeOrganizationUserCommand.DidNotReceiveWithAnyArgs().RemoveUserAsync(default, default);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task OrganizationsController_UserCannotLeaveOrganizationThatManagesUser(
|
||||
Guid orgId, User user)
|
||||
{
|
||||
var ssoConfig = new SsoConfig
|
||||
{
|
||||
Id = default,
|
||||
Data = new SsoConfigurationData
|
||||
{
|
||||
MemberDecryptionType = MemberDecryptionType.KeyConnector
|
||||
}.Serialize(),
|
||||
Enabled = true,
|
||||
OrganizationId = orgId,
|
||||
};
|
||||
var foundOrg = new Organization();
|
||||
foundOrg.Id = orgId;
|
||||
|
||||
_currentContext.OrganizationUser(orgId).Returns(true);
|
||||
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||
_userService.GetOrganizationsManagingUserAsync(user.Id).Returns(new List<Organization> { { foundOrg } });
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Leave(orgId));
|
||||
|
||||
Assert.Contains("Managed user account cannot leave managing organization. Contact your organization administrator for additional details.",
|
||||
exception.Message);
|
||||
|
||||
await _removeOrganizationUserCommand.DidNotReceiveWithAnyArgs().RemoveUserAsync(default, default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineAutoData(true, false)]
|
||||
[InlineAutoData(false, true)]
|
||||
@ -157,6 +187,8 @@ public class OrganizationsControllerTests : IDisposable
|
||||
_currentContext.OrganizationUser(orgId).Returns(true);
|
||||
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||
_userService.GetOrganizationsManagingUserAsync(user.Id).Returns(new List<Organization>());
|
||||
|
||||
await _sut.Leave(orgId);
|
||||
|
||||
|
@ -24,6 +24,7 @@ public class LaunchDarklyFeatureServiceTests
|
||||
var currentContext = Substitute.For<ICurrentContext>();
|
||||
currentContext.UserId.Returns(Guid.NewGuid());
|
||||
currentContext.ClientVersion.Returns(new Version(AssemblyHelpers.GetVersion()));
|
||||
currentContext.ClientVersionIsPrerelease.Returns(true);
|
||||
currentContext.DeviceType.Returns(Enums.DeviceType.ChromeBrowser);
|
||||
|
||||
var client = Substitute.For<ILdClient>();
|
||||
|
@ -10,7 +10,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
@ -9,6 +9,7 @@ using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
||||
using Bit.Infrastructure.EntityFramework.Auth.Models;
|
||||
using Bit.Infrastructure.EntityFramework.Models;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Tools.Models;
|
||||
using Bit.Infrastructure.EntityFramework.Vault.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -89,6 +90,7 @@ public class EfRepositoryListBuilder<T> : ISpecimenBuilder where T : BaseEntityF
|
||||
cfg.AddProfile<TaxRateMapperProfile>();
|
||||
cfg.AddProfile<TransactionMapperProfile>();
|
||||
cfg.AddProfile<UserMapperProfile>();
|
||||
cfg.AddProfile<PasswordHealthReportApplicationProfile>();
|
||||
})
|
||||
.CreateMapper()));
|
||||
|
||||
|
@ -0,0 +1,82 @@
|
||||
using AutoFixture;
|
||||
using AutoFixture.Kernel;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Tools.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
|
||||
namespace Bit.Infrastructure.EFIntegration.Test.AutoFixture;
|
||||
|
||||
internal class PasswordHealthReportApplicationBuilder : ISpecimenBuilder
|
||||
{
|
||||
public object Create(object request, ISpecimenContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var type = request as Type;
|
||||
if (type == null || type != typeof(PasswordHealthReportApplication))
|
||||
{
|
||||
return new NoSpecimen();
|
||||
}
|
||||
|
||||
var fixture = new Fixture();
|
||||
var obj = fixture.WithAutoNSubstitutions().Create<PasswordHealthReportApplication>();
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
internal class EfPasswordHealthReportApplication : ICustomization
|
||||
{
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
fixture.Customizations.Add(new IgnoreVirtualMembersCustomization());
|
||||
fixture.Customizations.Add(new GlobalSettingsBuilder());
|
||||
fixture.Customizations.Add(new PasswordHealthReportApplicationBuilder());
|
||||
fixture.Customizations.Add(new OrganizationBuilder());
|
||||
fixture.Customizations.Add(new EfRepositoryListBuilder<PasswordHealthReportApplicationRepository>());
|
||||
fixture.Customizations.Add(new EfRepositoryListBuilder<OrganizationRepository>());
|
||||
}
|
||||
}
|
||||
|
||||
internal class EfPasswordHealthReportApplicationApplicableToUser : ICustomization
|
||||
{
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
fixture.Customizations.Add(new IgnoreVirtualMembersCustomization());
|
||||
fixture.Customizations.Add(new GlobalSettingsBuilder());
|
||||
fixture.Customizations.Add(new PasswordHealthReportApplicationBuilder());
|
||||
fixture.Customizations.Add(new OrganizationBuilder());
|
||||
fixture.Customizations.Add(new EfRepositoryListBuilder<PasswordHealthReportApplicationRepository>());
|
||||
fixture.Customizations.Add(new EfRepositoryListBuilder<UserRepository>());
|
||||
fixture.Customizations.Add(new EfRepositoryListBuilder<OrganizationRepository>());
|
||||
fixture.Customizations.Add(new EfRepositoryListBuilder<OrganizationUserRepository>());
|
||||
fixture.Customizations.Add(new EfRepositoryListBuilder<ProviderRepository>());
|
||||
fixture.Customizations.Add(new EfRepositoryListBuilder<ProviderUserRepository>());
|
||||
fixture.Customizations.Add(new EfRepositoryListBuilder<ProviderOrganizationRepository>());
|
||||
}
|
||||
}
|
||||
|
||||
internal class EfPasswordHealthReportApplicationAutoDataAttribute : CustomAutoDataAttribute
|
||||
{
|
||||
public EfPasswordHealthReportApplicationAutoDataAttribute() : base(new SutProviderCustomization(), new EfPasswordHealthReportApplication())
|
||||
{ }
|
||||
}
|
||||
|
||||
internal class EfPasswordHealthReportApplicationApplicableToUserInlineAutoDataAttribute : InlineCustomAutoDataAttribute
|
||||
{
|
||||
public EfPasswordHealthReportApplicationApplicableToUserInlineAutoDataAttribute(params object[] values) :
|
||||
base(new[] { typeof(SutProviderCustomization), typeof(EfPasswordHealthReportApplicationApplicableToUser) }, values)
|
||||
{ }
|
||||
}
|
||||
|
||||
internal class InlineEfPasswordHealthReportApplicationAutoDataAttribute : InlineCustomAutoDataAttribute
|
||||
{
|
||||
public InlineEfPasswordHealthReportApplicationAutoDataAttribute(params object[] values) : base(new[] { typeof(SutProviderCustomization),
|
||||
typeof(EfPolicy) }, values)
|
||||
{ }
|
||||
}
|
@ -0,0 +1,269 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Test.AutoFixture.Attributes;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Infrastructure.EFIntegration.Test.AutoFixture;
|
||||
using Xunit;
|
||||
using EfRepo = Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using EfToolsRepo = Bit.Infrastructure.EntityFramework.Tools.Repositories;
|
||||
using SqlAdminConsoleRepo = Bit.Infrastructure.Dapper.Tools.Repositories;
|
||||
using SqlRepo = Bit.Infrastructure.Dapper.Repositories;
|
||||
|
||||
namespace Bit.Infrastructure.EFIntegration.Test.Tools.Repositories;
|
||||
|
||||
public class PasswordHealthReportApplicationRepositoryTests
|
||||
{
|
||||
[CiSkippedTheory, EfPasswordHealthReportApplicationAutoData]
|
||||
public async Task CreateAsync_Works_DataMatches(
|
||||
PasswordHealthReportApplication passwordHealthReportApplication,
|
||||
Organization organization,
|
||||
List<EfToolsRepo.PasswordHealthReportApplicationRepository> suts,
|
||||
List<EfRepo.OrganizationRepository> efOrganizationRepos,
|
||||
SqlAdminConsoleRepo.PasswordHealthReportApplicationRepository sqlPasswordHealthReportApplicationRepo,
|
||||
SqlRepo.OrganizationRepository sqlOrganizationRepo
|
||||
)
|
||||
{
|
||||
var passwordHealthReportApplicationRecords = new List<PasswordHealthReportApplication>();
|
||||
foreach (var sut in suts)
|
||||
{
|
||||
var i = suts.IndexOf(sut);
|
||||
|
||||
var efOrganization = await efOrganizationRepos[i].CreateAsync(organization);
|
||||
sut.ClearChangeTracking();
|
||||
|
||||
passwordHealthReportApplication.OrganizationId = efOrganization.Id;
|
||||
var postEfPasswordHeathReportApp = await sut.CreateAsync(passwordHealthReportApplication);
|
||||
sut.ClearChangeTracking();
|
||||
|
||||
var savedPasswordHealthReportApplication = await sut.GetByIdAsync(postEfPasswordHeathReportApp.Id);
|
||||
passwordHealthReportApplicationRecords.Add(savedPasswordHealthReportApplication);
|
||||
}
|
||||
|
||||
var sqlOrganization = await sqlOrganizationRepo.CreateAsync(organization);
|
||||
|
||||
passwordHealthReportApplication.OrganizationId = sqlOrganization.Id;
|
||||
var sqlPasswordHealthReportApplicationRecord = await sqlPasswordHealthReportApplicationRepo.CreateAsync(passwordHealthReportApplication);
|
||||
var savedSqlPasswordHealthReportApplicationRecord = await sqlPasswordHealthReportApplicationRepo.GetByIdAsync(sqlPasswordHealthReportApplicationRecord.Id);
|
||||
passwordHealthReportApplicationRecords.Add(savedSqlPasswordHealthReportApplicationRecord);
|
||||
|
||||
Assert.True(passwordHealthReportApplicationRecords.Count == 4);
|
||||
}
|
||||
|
||||
[CiSkippedTheory, EfPasswordHealthReportApplicationAutoData]
|
||||
public async Task RetrieveByOrganisation_Works(
|
||||
SqlAdminConsoleRepo.PasswordHealthReportApplicationRepository sqlPasswordHealthReportApplicationRepo,
|
||||
SqlRepo.OrganizationRepository sqlOrganizationRepo)
|
||||
{
|
||||
var (firstOrg, firstRecord) = await CreateSampleRecord(sqlOrganizationRepo, sqlPasswordHealthReportApplicationRepo);
|
||||
var (secondOrg, secondRecord) = await CreateSampleRecord(sqlOrganizationRepo, sqlPasswordHealthReportApplicationRepo);
|
||||
|
||||
var firstSetOfRecords = await sqlPasswordHealthReportApplicationRepo.GetByOrganizationIdAsync(firstOrg.Id);
|
||||
var nextSetOfRecords = await sqlPasswordHealthReportApplicationRepo.GetByOrganizationIdAsync(secondOrg.Id);
|
||||
|
||||
Assert.True(firstSetOfRecords.Count == 1 && firstSetOfRecords.First().OrganizationId == firstOrg.Id);
|
||||
Assert.True(nextSetOfRecords.Count == 1 && nextSetOfRecords.First().OrganizationId == secondOrg.Id);
|
||||
}
|
||||
|
||||
[CiSkippedTheory, EfPasswordHealthReportApplicationAutoData]
|
||||
public async Task ReplaceQuery_Works(
|
||||
List<EfToolsRepo.PasswordHealthReportApplicationRepository> suts,
|
||||
List<EfRepo.OrganizationRepository> efOrganizationRepos,
|
||||
SqlAdminConsoleRepo.PasswordHealthReportApplicationRepository sqlPasswordHealthReportApplicationRepo,
|
||||
SqlRepo.OrganizationRepository sqlOrganizationRepo)
|
||||
{
|
||||
var (org, pwdRecord) = await CreateSampleRecord(sqlOrganizationRepo, sqlPasswordHealthReportApplicationRepo);
|
||||
var exampleUri = "http://www.example.com";
|
||||
var exampleRevisionDate = new DateTime(2021, 1, 1);
|
||||
var dbRecords = new List<PasswordHealthReportApplication>();
|
||||
|
||||
foreach (var sut in suts)
|
||||
{
|
||||
var i = suts.IndexOf(sut);
|
||||
|
||||
// create a new organization for each repository
|
||||
var organization = await efOrganizationRepos[i].CreateAsync(org);
|
||||
|
||||
// map the organization Id and create the PasswordHealthReportApp record
|
||||
pwdRecord.OrganizationId = organization.Id;
|
||||
var passwordHealthReportApplication = await sut.CreateAsync(pwdRecord);
|
||||
|
||||
// update the record with new values
|
||||
passwordHealthReportApplication.Uri = exampleUri;
|
||||
passwordHealthReportApplication.RevisionDate = exampleRevisionDate;
|
||||
|
||||
// apply update to the database
|
||||
await sut.ReplaceAsync(passwordHealthReportApplication);
|
||||
sut.ClearChangeTracking();
|
||||
|
||||
// retrieve the data and add to the list for assertions
|
||||
var recordFromDb = await sut.GetByIdAsync(passwordHealthReportApplication.Id);
|
||||
sut.ClearChangeTracking();
|
||||
|
||||
dbRecords.Add(recordFromDb);
|
||||
}
|
||||
|
||||
// sql - create a new organization and PasswordHealthReportApplication record
|
||||
var (sqlOrg, sqlPwdRecord) = await CreateSampleRecord(sqlOrganizationRepo, sqlPasswordHealthReportApplicationRepo);
|
||||
var sqlPasswordHealthReportApplicationRecord = await sqlPasswordHealthReportApplicationRepo.GetByIdAsync(sqlPwdRecord.Id);
|
||||
|
||||
// sql - update the record with new values
|
||||
sqlPasswordHealthReportApplicationRecord.Uri = exampleUri;
|
||||
sqlPasswordHealthReportApplicationRecord.RevisionDate = exampleRevisionDate;
|
||||
await sqlPasswordHealthReportApplicationRepo.ReplaceAsync(sqlPasswordHealthReportApplicationRecord);
|
||||
|
||||
// sql - retrieve the data and add to the list for assertions
|
||||
var sqlDbRecord = await sqlPasswordHealthReportApplicationRepo.GetByIdAsync(sqlPasswordHealthReportApplicationRecord.Id);
|
||||
dbRecords.Add(sqlDbRecord);
|
||||
|
||||
// assertions
|
||||
// the Guids must be distinct across all records
|
||||
Assert.True(dbRecords.Select(_ => _.Id).Distinct().Count() == dbRecords.Count);
|
||||
|
||||
// the Uri and RevisionDate must match the updated values
|
||||
Assert.True(dbRecords.All(_ => _.Uri == exampleUri && _.RevisionDate == exampleRevisionDate));
|
||||
}
|
||||
|
||||
[CiSkippedTheory, EfPasswordHealthReportApplicationAutoData]
|
||||
public async Task Upsert_Works(
|
||||
List<EfToolsRepo.PasswordHealthReportApplicationRepository> suts,
|
||||
List<EfRepo.OrganizationRepository> efOrganizationRepos,
|
||||
SqlAdminConsoleRepo.PasswordHealthReportApplicationRepository sqlPasswordHealthReportApplicationRepo,
|
||||
SqlRepo.OrganizationRepository sqlOrganizationRepo)
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
var rawOrg = fixture.Build<Organization>().Create();
|
||||
var rawPwdRecord = fixture.Build<PasswordHealthReportApplication>()
|
||||
.With(_ => _.OrganizationId, rawOrg.Id)
|
||||
.Without(_ => _.Id)
|
||||
.Create();
|
||||
var exampleUri = "http://www.example.com";
|
||||
var exampleRevisionDate = new DateTime(2021, 1, 1);
|
||||
var dbRecords = new List<PasswordHealthReportApplication>();
|
||||
|
||||
foreach (var sut in suts)
|
||||
{
|
||||
var i = suts.IndexOf(sut);
|
||||
|
||||
// create a new organization for each repository
|
||||
var organization = await efOrganizationRepos[i].CreateAsync(rawOrg);
|
||||
|
||||
// map the organization Id and use Upsert to save new record
|
||||
rawPwdRecord.OrganizationId = organization.Id;
|
||||
rawPwdRecord.Id = default(Guid);
|
||||
await sut.UpsertAsync(rawPwdRecord);
|
||||
sut.ClearChangeTracking();
|
||||
|
||||
// retrieve the data and add to the list for assertions
|
||||
var passwordHealthReportApplication = await sut.GetByIdAsync(rawPwdRecord.Id);
|
||||
|
||||
// update the record with new values
|
||||
passwordHealthReportApplication.Uri = exampleUri;
|
||||
passwordHealthReportApplication.RevisionDate = exampleRevisionDate;
|
||||
|
||||
// apply update using Upsert to make changes to db
|
||||
await sut.UpsertAsync(passwordHealthReportApplication);
|
||||
sut.ClearChangeTracking();
|
||||
|
||||
// retrieve the data and add to the list for assertions
|
||||
var recordFromDb = await sut.GetByIdAsync(passwordHealthReportApplication.Id);
|
||||
dbRecords.Add(recordFromDb);
|
||||
|
||||
sut.ClearChangeTracking();
|
||||
}
|
||||
|
||||
// sql - create new records
|
||||
var organizationForSql = fixture.Create<Organization>();
|
||||
var passwordHealthReportApplicationForSql = fixture.Build<PasswordHealthReportApplication>()
|
||||
.With(_ => _.OrganizationId, organizationForSql.Id)
|
||||
.Without(_ => _.Id)
|
||||
.Create();
|
||||
|
||||
// sql - use Upsert to insert this data
|
||||
var sqlOrganization = await sqlOrganizationRepo.CreateAsync(organizationForSql);
|
||||
await sqlPasswordHealthReportApplicationRepo.UpsertAsync(passwordHealthReportApplicationForSql);
|
||||
var sqlPasswordHealthReportApplicationRecord = await sqlPasswordHealthReportApplicationRepo.GetByIdAsync(passwordHealthReportApplicationForSql.Id);
|
||||
|
||||
// sql - update the record with new values
|
||||
sqlPasswordHealthReportApplicationRecord.Uri = exampleUri;
|
||||
sqlPasswordHealthReportApplicationRecord.RevisionDate = exampleRevisionDate;
|
||||
await sqlPasswordHealthReportApplicationRepo.UpsertAsync(sqlPasswordHealthReportApplicationRecord);
|
||||
|
||||
// sql - retrieve the data and add to the list for assertions
|
||||
var sqlDbRecord = await sqlPasswordHealthReportApplicationRepo.GetByIdAsync(sqlPasswordHealthReportApplicationRecord.Id);
|
||||
dbRecords.Add(sqlDbRecord);
|
||||
|
||||
// assertions
|
||||
// the Guids must be distinct across all records
|
||||
Assert.True(dbRecords.Select(_ => _.Id).Distinct().Count() == dbRecords.Count);
|
||||
|
||||
// the Uri and RevisionDate must match the updated values
|
||||
Assert.True(dbRecords.All(_ => _.Uri == exampleUri && _.RevisionDate == exampleRevisionDate));
|
||||
}
|
||||
|
||||
[CiSkippedTheory, EfPasswordHealthReportApplicationAutoData]
|
||||
public async Task Delete_Works(
|
||||
List<EfToolsRepo.PasswordHealthReportApplicationRepository> suts,
|
||||
List<EfRepo.OrganizationRepository> efOrganizationRepos,
|
||||
SqlAdminConsoleRepo.PasswordHealthReportApplicationRepository sqlPasswordHealthReportApplicationRepo,
|
||||
SqlRepo.OrganizationRepository sqlOrganizationRepo)
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
var rawOrg = fixture.Build<Organization>().Create();
|
||||
var rawPwdRecord = fixture.Build<PasswordHealthReportApplication>()
|
||||
.With(_ => _.OrganizationId, rawOrg.Id)
|
||||
.Create();
|
||||
var dbRecords = new List<PasswordHealthReportApplication>();
|
||||
|
||||
foreach (var sut in suts)
|
||||
{
|
||||
var i = suts.IndexOf(sut);
|
||||
|
||||
// create a new organization for each repository
|
||||
var organization = await efOrganizationRepos[i].CreateAsync(rawOrg);
|
||||
|
||||
// map the organization Id and use Upsert to save new record
|
||||
rawPwdRecord.OrganizationId = organization.Id;
|
||||
rawPwdRecord = await sut.CreateAsync(rawPwdRecord);
|
||||
sut.ClearChangeTracking();
|
||||
|
||||
// apply update using Upsert to make changes to db
|
||||
await sut.DeleteAsync(rawPwdRecord);
|
||||
sut.ClearChangeTracking();
|
||||
|
||||
// retrieve the data and add to the list for assertions
|
||||
var recordFromDb = await sut.GetByIdAsync(rawPwdRecord.Id);
|
||||
dbRecords.Add(recordFromDb);
|
||||
|
||||
sut.ClearChangeTracking();
|
||||
}
|
||||
|
||||
// sql - create new records
|
||||
var (org, passwordHealthReportApplication) = await CreateSampleRecord(sqlOrganizationRepo, sqlPasswordHealthReportApplicationRepo);
|
||||
await sqlPasswordHealthReportApplicationRepo.DeleteAsync(passwordHealthReportApplication);
|
||||
var sqlDbRecord = await sqlPasswordHealthReportApplicationRepo.GetByIdAsync(passwordHealthReportApplication.Id);
|
||||
dbRecords.Add(sqlDbRecord);
|
||||
|
||||
// assertions
|
||||
// all records should be null - as they were deleted before querying
|
||||
Assert.True(dbRecords.Where(_ => _ == null).Count() == 4);
|
||||
}
|
||||
|
||||
private async Task<(Organization, PasswordHealthReportApplication)> CreateSampleRecord(
|
||||
IOrganizationRepository organizationRepo,
|
||||
IPasswordHealthReportApplicationRepository passwordHealthReportApplicationRepo
|
||||
)
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
var organization = fixture.Create<Organization>();
|
||||
var passwordHealthReportApplication = fixture.Build<PasswordHealthReportApplication>()
|
||||
.With(_ => _.OrganizationId, organization.Id)
|
||||
.Create();
|
||||
|
||||
organization = await organizationRepo.CreateAsync(organization);
|
||||
passwordHealthReportApplication = await passwordHealthReportApplicationRepo.CreateAsync(passwordHealthReportApplication);
|
||||
|
||||
return (organization, passwordHealthReportApplication);
|
||||
}
|
||||
}
|
@ -5,9 +5,9 @@ SET
|
||||
U.TwoFactorProviders = JSON_SET(
|
||||
JSON_SET(
|
||||
U.TwoFactorProviders, '$."2".MetaData.ClientSecret',
|
||||
JSON_UNQUOTE(U.TwoFactorProviders ->'$."2".MetaData.SKey')),
|
||||
JSON_UNQUOTE(JSON_EXTRACT(U.TwoFactorProviders,'$."2".MetaData.SKey'))),
|
||||
'$."2".MetaData.ClientId',
|
||||
JSON_UNQUOTE(U.TwoFactorProviders -> '$."2".MetaData.IKey'))
|
||||
JSON_UNQUOTE(JSON_EXTRACT(U.TwoFactorProviders,'$."2".MetaData.IKey')))
|
||||
WHERE
|
||||
JSON_CONTAINS(TwoFactorProviders,
|
||||
'{"2":{}}')
|
||||
@ -20,9 +20,9 @@ SET
|
||||
o.TwoFactorProviders = JSON_SET(
|
||||
JSON_SET(
|
||||
o.TwoFactorProviders, '$."6".MetaData.ClientSecret',
|
||||
JSON_UNQUOTE(o.TwoFactorProviders ->'$."6".MetaData.SKey')),
|
||||
JSON_UNQUOTE(JSON_EXTRACT(o.TwoFactorProviders,'$."6".MetaData.SKey'))),
|
||||
'$."6".MetaData.ClientId',
|
||||
JSON_UNQUOTE(o.TwoFactorProviders -> '$."6".MetaData.IKey'))
|
||||
JSON_UNQUOTE(JSON_EXTRACT(o.TwoFactorProviders,'$."6".MetaData.IKey')))
|
||||
WHERE
|
||||
JSON_CONTAINS(o.TwoFactorProviders,
|
||||
'{"6":{}}')
|
||||
|
2888
util/MySqlMigrations/Migrations/20241105195202_FixPasswordHealthReportApplication.Designer.cs
generated
Normal file
2888
util/MySqlMigrations/Migrations/20241105195202_FixPasswordHealthReportApplication.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.MySqlMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class FixPasswordHealthReportApplication : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// this file is not required, but the designer file is required
|
||||
// in order to keep the database models in sync with the database
|
||||
// without this - the unit tests will fail when run on your local machine
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
@ -1887,6 +1887,34 @@ namespace Bit.MySqlMigrations.Migrations
|
||||
b.ToTable("ServiceAccount", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("CreationDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<Guid>("OrganizationId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("RevisionDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Uri")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Id")
|
||||
.HasAnnotation("SqlServer:Clustered", true);
|
||||
|
||||
b.HasIndex("OrganizationId")
|
||||
.HasAnnotation("SqlServer:Clustered", false);
|
||||
|
||||
b.ToTable("PasswordHealthReportApplication", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@ -2578,6 +2606,17 @@ namespace Bit.MySqlMigrations.Migrations
|
||||
b.Navigation("Organization");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b =>
|
||||
{
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
||||
.WithMany()
|
||||
.HasForeignKey("OrganizationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Organization");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b =>
|
||||
{
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
||||
|
2894
util/PostgresMigrations/Migrations/20241105202053_FixPasswordHealthReportApplication.Designer.cs
generated
Normal file
2894
util/PostgresMigrations/Migrations/20241105202053_FixPasswordHealthReportApplication.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.PostgresMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class FixPasswordHealthReportApplication : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// this file is not required, but the designer file is required
|
||||
// in order to keep the database models in sync with the database
|
||||
// without this - the unit tests will fail when run on your local machine
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
@ -1893,6 +1893,34 @@ namespace Bit.PostgresMigrations.Migrations
|
||||
b.ToTable("ServiceAccount", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreationDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("OrganizationId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("RevisionDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Uri")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Id")
|
||||
.HasAnnotation("SqlServer:Clustered", true);
|
||||
|
||||
b.HasIndex("OrganizationId")
|
||||
.HasAnnotation("SqlServer:Clustered", false);
|
||||
|
||||
b.ToTable("PasswordHealthReportApplication", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@ -2584,6 +2612,17 @@ namespace Bit.PostgresMigrations.Migrations
|
||||
b.Navigation("Organization");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b =>
|
||||
{
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
||||
.WithMany()
|
||||
.HasForeignKey("OrganizationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Organization");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b =>
|
||||
{
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
||||
|
2877
util/SqliteMigrations/Migrations/20241105202413_FixPasswordHealthReportApplication.Designer.cs
generated
Normal file
2877
util/SqliteMigrations/Migrations/20241105202413_FixPasswordHealthReportApplication.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.SqliteMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class FixPasswordHealthReportApplication : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// this file is not required, but the designer file is required
|
||||
// in order to keep the database models in sync with the database
|
||||
// without this - the unit tests will fail when run on your local machine
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
@ -1876,6 +1876,34 @@ namespace Bit.SqliteMigrations.Migrations
|
||||
b.ToTable("ServiceAccount", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreationDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("OrganizationId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("RevisionDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Uri")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Id")
|
||||
.HasAnnotation("SqlServer:Clustered", true);
|
||||
|
||||
b.HasIndex("OrganizationId")
|
||||
.HasAnnotation("SqlServer:Clustered", false);
|
||||
|
||||
b.ToTable("PasswordHealthReportApplication", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@ -2567,6 +2595,17 @@ namespace Bit.SqliteMigrations.Migrations
|
||||
b.Navigation("Organization");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b =>
|
||||
{
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
||||
.WithMany()
|
||||
.HasForeignKey("OrganizationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Organization");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b =>
|
||||
{
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
||||
|
Loading…
Reference in New Issue
Block a user