1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-28 13:15:12 +01:00

Merge branch 'main' into PM-11162-assign-to-collection-perm-update

This commit is contained in:
jng 2024-11-08 12:39:24 -05:00
commit 5c672ecdc1
No known key found for this signature in database
GPG Key ID: AF822623CAD19C85
36 changed files with 9464 additions and 43 deletions

19
.github/renovate.json vendored
View File

@ -29,7 +29,7 @@
"commitMessagePrefix": "[deps] DevOps:" "commitMessagePrefix": "[deps] DevOps:"
}, },
{ {
"matchPackageNames": ["DnsClient", "Quartz"], "matchPackageNames": ["DnsClient"],
"description": "Admin Console owned dependencies", "description": "Admin Console owned dependencies",
"commitMessagePrefix": "[deps] AC:", "commitMessagePrefix": "[deps] AC:",
"reviewers": ["team:team-admin-console-dev"] "reviewers": ["team:team-admin-console-dev"]
@ -42,14 +42,7 @@
}, },
{ {
"matchPackageNames": [ "matchPackageNames": [
"AspNetCoreRateLimit",
"AspNetCoreRateLimit.Redis",
"Azure.Data.Tables",
"Azure.Extensions.AspNetCore.DataProtection.Blobs", "Azure.Extensions.AspNetCore.DataProtection.Blobs",
"Azure.Messaging.EventGrid",
"Azure.Messaging.ServiceBus",
"Azure.Storage.Blobs",
"Azure.Storage.Queues",
"DuoUniversal", "DuoUniversal",
"Fido2.AspNet", "Fido2.AspNet",
"Duende.IdentityServer", "Duende.IdentityServer",
@ -128,8 +121,16 @@
}, },
{ {
"matchPackageNames": [ "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.Authentication.JwtBearer",
"Microsoft.AspNetCore.Http" "Microsoft.AspNetCore.Http",
"Quartz"
], ],
"description": "Platform owned dependencies", "description": "Platform owned dependencies",
"commitMessagePrefix": "[deps] Platform:", "commitMessagePrefix": "[deps] Platform:",

View File

@ -7,18 +7,25 @@ on:
- "main" - "main"
- "rc" - "rc"
- "hotfix-rc" - "hotfix-rc"
pull_request: pull_request_target:
types: [opened, synchronize]
env: env:
_AZ_REGISTRY: "bitwardenprod.azurecr.io" _AZ_REGISTRY: "bitwardenprod.azurecr.io"
jobs: jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
lint: lint:
name: Lint name: Lint
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Check out repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up .NET - name: Set up .NET
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
@ -29,8 +36,7 @@ jobs:
build-artifacts: build-artifacts:
name: Build artifacts name: Build artifacts
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: needs: lint
- lint
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -68,6 +74,8 @@ jobs:
steps: steps:
- name: Check out repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up .NET - name: Set up .NET
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
@ -120,7 +128,9 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
permissions: permissions:
security-events: write security-events: write
needs: build-artifacts needs:
- build-artifacts
- check-run
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -173,6 +183,8 @@ jobs:
steps: steps:
- name: Check out repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Check branch to publish - name: Check branch to publish
env: env:
@ -292,6 +304,8 @@ jobs:
steps: steps:
- name: Check out repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up .NET - name: Set up .NET
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
@ -305,9 +319,9 @@ jobs:
run: az acr login -n $_AZ_REGISTRY --only-show-errors run: az acr login -n $_AZ_REGISTRY --only-show-errors
- name: Make Docker stubs - name: Make Docker stubs
if: github.ref == 'refs/heads/main' || if: |
github.ref == 'refs/heads/rc' || github.event_name != 'pull_request_target'
github.ref == 'refs/heads/hotfix-rc' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
run: | run: |
# Set proper setup image based on branch # Set proper setup image based on branch
case "$GITHUB_REF" in case "$GITHUB_REF" in
@ -347,13 +361,17 @@ jobs:
cd docker-stub/EU; zip -r ../../docker-stub-EU.zip *; cd ../.. cd docker-stub/EU; zip -r ../../docker-stub-EU.zip *; cd ../..
- name: Make Docker stub checksums - 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: | run: |
sha256sum docker-stub-US.zip > docker-stub-US-sha256.txt sha256sum docker-stub-US.zip > docker-stub-US-sha256.txt
sha256sum docker-stub-EU.zip > docker-stub-EU-sha256.txt sha256sum docker-stub-EU.zip > docker-stub-EU-sha256.txt
- name: Upload Docker stub US artifact - 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 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
name: docker-stub-US.zip name: docker-stub-US.zip
@ -361,7 +379,9 @@ jobs:
if-no-files-found: error if-no-files-found: error
- name: Upload Docker stub EU artifact - 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 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
name: docker-stub-EU.zip name: docker-stub-EU.zip
@ -369,7 +389,9 @@ jobs:
if-no-files-found: error if-no-files-found: error
- name: Upload Docker stub US checksum artifact - 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 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
name: docker-stub-US-sha256.txt name: docker-stub-US-sha256.txt
@ -377,7 +399,9 @@ jobs:
if-no-files-found: error if-no-files-found: error
- name: Upload Docker stub EU checksum artifact - 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 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
name: docker-stub-EU-sha256.txt name: docker-stub-EU-sha256.txt
@ -467,6 +491,8 @@ jobs:
steps: steps:
- name: Check out repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up .NET - name: Set up .NET
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
@ -501,6 +527,7 @@ jobs:
self-host-build: self-host-build:
name: Trigger 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 runs-on: ubuntu-22.04
needs: build-docker needs: build-docker
steps: steps:
@ -533,7 +560,7 @@ jobs:
trigger-k8s-deploy: trigger-k8s-deploy:
name: 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 runs-on: ubuntu-22.04
needs: build-docker needs: build-docker
steps: steps:
@ -567,7 +594,9 @@ jobs:
trigger-ee-updates: trigger-ee-updates:
name: Trigger Ephemeral Environment 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 runs-on: ubuntu-24.04
needs: build-docker needs: build-docker
steps: steps:
@ -613,9 +642,8 @@ jobs:
steps: steps:
- name: Check if any job failed - name: Check if any job failed
if: | if: |
(github.ref == 'refs/heads/main' github.event_name != 'pull_request_target'
|| github.ref == 'refs/heads/rc' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|| github.ref == 'refs/heads/hotfix-rc')
&& contains(needs.*.result, 'failure') && contains(needs.*.result, 'failure')
run: exit 1 run: exit 1

View File

@ -70,6 +70,11 @@ jobs:
docker compose --profile mssql --profile postgres --profile mysql up -d docker compose --profile mssql --profile postgres --profile mysql up -d
shell: pwsh 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 # 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 - name: Sleep
run: sleep 15s run: sleep 15s
@ -103,6 +108,12 @@ jobs:
env: env:
CONN_STR: "server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev;Allow User Variables=true" 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 - name: Migrate Postgres
working-directory: "util/PostgresMigrations" working-directory: "util/PostgresMigrations"
run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:PostgreSql:ConnectionString="$CONN_STR"' run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:PostgreSql:ConnectionString="$CONN_STR"'
@ -130,6 +141,9 @@ jobs:
# Default Sqlite # Default Sqlite
BW_TEST_DATABASES__3__TYPE: "Sqlite" BW_TEST_DATABASES__3__TYPE: "Sqlite"
BW_TEST_DATABASES__3__CONNECTIONSTRING: "Data Source=${{ runner.temp }}/test.db" 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" run: dotnet test --logger "trx;LogFileName=infrastructure-test-results.trx"
shell: pwsh shell: pwsh
@ -137,6 +151,10 @@ jobs:
if: failure() if: failure()
run: 'docker logs $(docker ps --quiet --filter "name=mysql")' 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 - name: Print Postgres Logs
if: failure() if: failure()
run: 'docker logs $(docker ps --quiet --filter "name=postgres")' run: 'docker logs $(docker ps --quiet --filter "name=postgres")'

View File

@ -110,7 +110,6 @@ public static class RolePermissionMapping
Permission.User_Licensing_View, Permission.User_Licensing_View,
Permission.User_Billing_View, Permission.User_Billing_View,
Permission.User_Billing_LaunchGateway, Permission.User_Billing_LaunchGateway,
Permission.User_Delete,
Permission.Org_List_View, Permission.Org_List_View,
Permission.Org_OrgInformation_View, Permission.Org_OrgInformation_View,
Permission.Org_GeneralDetails_View, Permission.Org_GeneralDetails_View,

View File

@ -252,6 +252,12 @@ public class OrganizationsController : Controller
throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving."); 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); await _removeOrganizationUserCommand.RemoveUserAsync(id, user.Id);
} }

View File

@ -99,7 +99,10 @@ public class CiphersController : Controller
throw new NotFoundException(); 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")] [HttpGet("{id}/full-details")]
@ -653,10 +656,10 @@ public class CiphersController : Controller
[HttpPut("{id}/collections-admin")] [HttpPut("{id}/collections-admin")]
[HttpPost("{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 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 || if (cipher == null || !cipher.OrganizationId.HasValue ||
!await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id })) !await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))
@ -674,6 +677,11 @@ public class CiphersController : Controller
} }
await _cipherService.SaveCollectionsAsync(cipher, collectionIds, userId, true); 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")] [HttpPost("bulk-collections")]

View File

@ -151,6 +151,9 @@ public static class FeatureFlagKeys
public const string GeneratorToolsModernization = "generator-tools-modernization"; public const string GeneratorToolsModernization = "generator-tools-modernization";
public const string NewDeviceVerification = "new-device-verification"; public const string NewDeviceVerification = "new-device-verification";
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; 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() public static List<string> GetAllKeys()
{ {

View File

@ -39,6 +39,7 @@ public class CurrentContext : ICurrentContext
public virtual int? BotScore { get; set; } public virtual int? BotScore { get; set; }
public virtual string ClientId { get; set; } public virtual string ClientId { get; set; }
public virtual Version ClientVersion { get; set; } public virtual Version ClientVersion { get; set; }
public virtual bool ClientVersionIsPrerelease { get; set; }
public virtual IdentityClientType IdentityClientType { get; set; } public virtual IdentityClientType IdentityClientType { get; set; }
public virtual Guid? ServiceAccountOrganizationId { get; set; } public virtual Guid? ServiceAccountOrganizationId { get; set; }
@ -97,6 +98,11 @@ public class CurrentContext : ICurrentContext
{ {
ClientVersion = cVersion; ClientVersion = cVersion;
} }
if (httpContext.Request.Headers.TryGetValue("Is-Prerelease", out var clientVersionIsPrerelease))
{
ClientVersionIsPrerelease = clientVersionIsPrerelease == "1";
}
} }
public async virtual Task BuildAsync(ClaimsPrincipal user, GlobalSettings globalSettings) public async virtual Task BuildAsync(ClaimsPrincipal user, GlobalSettings globalSettings)

View File

@ -29,12 +29,13 @@ public interface ICurrentContext
int? BotScore { get; set; } int? BotScore { get; set; }
string ClientId { get; set; } string ClientId { get; set; }
Version ClientVersion { get; set; } Version ClientVersion { get; set; }
bool ClientVersionIsPrerelease { get; set; }
Task BuildAsync(HttpContext httpContext, GlobalSettings globalSettings); Task BuildAsync(HttpContext httpContext, GlobalSettings globalSettings);
Task BuildAsync(ClaimsPrincipal user, GlobalSettings globalSettings); Task BuildAsync(ClaimsPrincipal user, GlobalSettings globalSettings);
Task SetContextAsync(ClaimsPrincipal user); Task SetContextAsync(ClaimsPrincipal user);
Task<bool> OrganizationUser(Guid orgId); Task<bool> OrganizationUser(Guid orgId);
Task<bool> OrganizationAdmin(Guid orgId); Task<bool> OrganizationAdmin(Guid orgId);
Task<bool> OrganizationOwner(Guid orgId); Task<bool> OrganizationOwner(Guid orgId);

View File

@ -21,8 +21,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" /> <PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.401.30" /> <PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.401.35" />
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.40" /> <PackageReference Include="AWSSDK.SQS" Version="3.7.400.45" />
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" /> <PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" /> <PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" /> <PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />

View File

@ -20,6 +20,7 @@ public class LaunchDarklyFeatureService : IFeatureService
private const string _contextKindServiceAccount = "service-account"; private const string _contextKindServiceAccount = "service-account";
private const string _contextAttributeClientVersion = "client-version"; private const string _contextAttributeClientVersion = "client-version";
private const string _contextAttributeClientVersionIsPrerelease = "client-version-is-prerelease";
private const string _contextAttributeDeviceType = "device-type"; private const string _contextAttributeDeviceType = "device-type";
private const string _contextAttributeClientType = "client-type"; private const string _contextAttributeClientType = "client-type";
private const string _contextAttributeOrganizations = "organizations"; private const string _contextAttributeOrganizations = "organizations";
@ -145,6 +146,7 @@ public class LaunchDarklyFeatureService : IFeatureService
if (_currentContext.ClientVersion != null) if (_currentContext.ClientVersion != null)
{ {
builder.Set(_contextAttributeClientVersion, _currentContext.ClientVersion.ToString()); builder.Set(_contextAttributeClientVersion, _currentContext.ClientVersion.ToString());
builder.Set(_contextAttributeClientVersionIsPrerelease, _currentContext.ClientVersionIsPrerelease);
} }
if (_currentContext.DeviceType.HasValue) if (_currentContext.DeviceType.HasValue)

View File

@ -1,11 +1,15 @@
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Utilities; using Bit.Core.Utilities;
#nullable enable
namespace Bit.Core.Tools.Entities;
public class PasswordHealthReportApplication : ITableObject<Guid>, IRevisable public class PasswordHealthReportApplication : ITableObject<Guid>, IRevisable
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid OrganizationId { 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 CreationDate { get; set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; set; } = DateTime.UtcNow; public DateTime RevisionDate { get; set; } = DateTime.UtcNow;

View File

@ -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);
}

View File

@ -121,8 +121,8 @@ public class TwoFactorAuthenticationValidator(
var twoFactorResultDict = new Dictionary<string, object> var twoFactorResultDict = new Dictionary<string, object>
{ {
{ "TwoFactorProviders", null }, { "TwoFactorProviders", providers.Keys }, // backwards compatibility
{ "TwoFactorProviders2", providers }, // backwards compatibility { "TwoFactorProviders2", providers },
}; };
// If we have email as a 2FA provider, we might need an SsoEmail2fa Session Token // If we have email as a 2FA provider, we might need an SsoEmail2fa Session Token

View File

@ -58,6 +58,7 @@ public static class DapperServiceCollectionExtensions
services.AddSingleton<INotificationStatusRepository, NotificationStatusRepository>(); services.AddSingleton<INotificationStatusRepository, NotificationStatusRepository>();
services services
.AddSingleton<IClientOrganizationMigrationRecordRepository, ClientOrganizationMigrationRecordRepository>(); .AddSingleton<IClientOrganizationMigrationRecordRepository, ClientOrganizationMigrationRecordRepository>();
services.AddSingleton<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();
if (selfHosted) if (selfHosted)
{ {

View File

@ -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();
}
}
}

View File

@ -7,6 +7,7 @@ using Bit.Infrastructure.EntityFramework.Converters;
using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.Models;
using Bit.Infrastructure.EntityFramework.NotificationCenter.Models; using Bit.Infrastructure.EntityFramework.NotificationCenter.Models;
using Bit.Infrastructure.EntityFramework.SecretsManager.Models; using Bit.Infrastructure.EntityFramework.SecretsManager.Models;
using Bit.Infrastructure.EntityFramework.Tools.Models;
using Bit.Infrastructure.EntityFramework.Vault.Models; using Bit.Infrastructure.EntityFramework.Vault.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
@ -75,6 +76,7 @@ public class DatabaseContext : DbContext
public DbSet<Notification> Notifications { get; set; } public DbSet<Notification> Notifications { get; set; }
public DbSet<NotificationStatus> NotificationStatuses { get; set; } public DbSet<NotificationStatus> NotificationStatuses { get; set; }
public DbSet<ClientOrganizationMigrationRecord> ClientOrganizationMigrationRecords { get; set; } public DbSet<ClientOrganizationMigrationRecord> ClientOrganizationMigrationRecords { get; set; }
public DbSet<PasswordHealthReportApplication> PasswordHealthReportApplications { get; set; }
protected override void OnModelCreating(ModelBuilder builder) protected override void OnModelCreating(ModelBuilder builder)
{ {

View File

@ -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));
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -51,7 +51,6 @@ public class OrganizationsControllerTests : IDisposable
private readonly IProviderBillingService _providerBillingService; private readonly IProviderBillingService _providerBillingService;
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory; private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly OrganizationsController _sut; private readonly OrganizationsController _sut;
public OrganizationsControllerTests() public OrganizationsControllerTests()
@ -123,7 +122,8 @@ public class OrganizationsControllerTests : IDisposable
_currentContext.OrganizationUser(orgId).Returns(true); _currentContext.OrganizationUser(orgId).Returns(true);
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig); _ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); _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)); var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Leave(orgId));
Assert.Contains("Your organization's Single Sign-On settings prevent you from leaving.", 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); 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] [Theory]
[InlineAutoData(true, false)] [InlineAutoData(true, false)]
[InlineAutoData(false, true)] [InlineAutoData(false, true)]
@ -157,6 +187,8 @@ public class OrganizationsControllerTests : IDisposable
_currentContext.OrganizationUser(orgId).Returns(true); _currentContext.OrganizationUser(orgId).Returns(true);
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig); _ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); _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); await _sut.Leave(orgId);

View File

@ -24,6 +24,7 @@ public class LaunchDarklyFeatureServiceTests
var currentContext = Substitute.For<ICurrentContext>(); var currentContext = Substitute.For<ICurrentContext>();
currentContext.UserId.Returns(Guid.NewGuid()); currentContext.UserId.Returns(Guid.NewGuid());
currentContext.ClientVersion.Returns(new Version(AssemblyHelpers.GetVersion())); currentContext.ClientVersion.Returns(new Version(AssemblyHelpers.GetVersion()));
currentContext.ClientVersionIsPrerelease.Returns(true);
currentContext.DeviceType.Returns(Enums.DeviceType.ChromeBrowser); currentContext.DeviceType.Returns(Enums.DeviceType.ChromeBrowser);
var client = Substitute.For<ILdClient>(); var client = Substitute.For<ILdClient>();

View File

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2"> <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@ -9,6 +9,7 @@ using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
using Bit.Infrastructure.EntityFramework.Auth.Models; using Bit.Infrastructure.EntityFramework.Auth.Models;
using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.Models;
using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Infrastructure.EntityFramework.Tools.Models;
using Bit.Infrastructure.EntityFramework.Vault.Models; using Bit.Infrastructure.EntityFramework.Vault.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -89,6 +90,7 @@ public class EfRepositoryListBuilder<T> : ISpecimenBuilder where T : BaseEntityF
cfg.AddProfile<TaxRateMapperProfile>(); cfg.AddProfile<TaxRateMapperProfile>();
cfg.AddProfile<TransactionMapperProfile>(); cfg.AddProfile<TransactionMapperProfile>();
cfg.AddProfile<UserMapperProfile>(); cfg.AddProfile<UserMapperProfile>();
cfg.AddProfile<PasswordHealthReportApplicationProfile>();
}) })
.CreateMapper())); .CreateMapper()));

View File

@ -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)
{ }
}

View File

@ -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);
}
}

View File

@ -5,9 +5,9 @@ SET
U.TwoFactorProviders = JSON_SET( U.TwoFactorProviders = JSON_SET(
JSON_SET( JSON_SET(
U.TwoFactorProviders, '$."2".MetaData.ClientSecret', U.TwoFactorProviders, '$."2".MetaData.ClientSecret',
JSON_UNQUOTE(U.TwoFactorProviders ->'$."2".MetaData.SKey')), JSON_UNQUOTE(JSON_EXTRACT(U.TwoFactorProviders,'$."2".MetaData.SKey'))),
'$."2".MetaData.ClientId', '$."2".MetaData.ClientId',
JSON_UNQUOTE(U.TwoFactorProviders -> '$."2".MetaData.IKey')) JSON_UNQUOTE(JSON_EXTRACT(U.TwoFactorProviders,'$."2".MetaData.IKey')))
WHERE WHERE
JSON_CONTAINS(TwoFactorProviders, JSON_CONTAINS(TwoFactorProviders,
'{"2":{}}') '{"2":{}}')
@ -20,9 +20,9 @@ SET
o.TwoFactorProviders = JSON_SET( o.TwoFactorProviders = JSON_SET(
JSON_SET( JSON_SET(
o.TwoFactorProviders, '$."6".MetaData.ClientSecret', o.TwoFactorProviders, '$."6".MetaData.ClientSecret',
JSON_UNQUOTE(o.TwoFactorProviders ->'$."6".MetaData.SKey')), JSON_UNQUOTE(JSON_EXTRACT(o.TwoFactorProviders,'$."6".MetaData.SKey'))),
'$."6".MetaData.ClientId', '$."6".MetaData.ClientId',
JSON_UNQUOTE(o.TwoFactorProviders -> '$."6".MetaData.IKey')) JSON_UNQUOTE(JSON_EXTRACT(o.TwoFactorProviders,'$."6".MetaData.IKey')))
WHERE WHERE
JSON_CONTAINS(o.TwoFactorProviders, JSON_CONTAINS(o.TwoFactorProviders,
'{"6":{}}') '{"6":{}}')

File diff suppressed because it is too large Load Diff

View File

@ -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)
{
}
}

View File

@ -1887,6 +1887,34 @@ namespace Bit.MySqlMigrations.Migrations
b.ToTable("ServiceAccount", (string)null); 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 => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -2578,6 +2606,17 @@ namespace Bit.MySqlMigrations.Migrations
b.Navigation("Organization"); 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 => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b =>
{ {
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")

File diff suppressed because it is too large Load Diff

View File

@ -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)
{
}
}

View File

@ -1893,6 +1893,34 @@ namespace Bit.PostgresMigrations.Migrations
b.ToTable("ServiceAccount", (string)null); 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 => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -2584,6 +2612,17 @@ namespace Bit.PostgresMigrations.Migrations
b.Navigation("Organization"); 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 => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b =>
{ {
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")

File diff suppressed because it is too large Load Diff

View File

@ -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)
{
}
}

View File

@ -1876,6 +1876,34 @@ namespace Bit.SqliteMigrations.Migrations
b.ToTable("ServiceAccount", (string)null); 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 => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -2567,6 +2595,17 @@ namespace Bit.SqliteMigrations.Migrations
b.Navigation("Organization"); 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 => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b =>
{ {
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")