Skip to content

Seeder/secrets manager scenes - #8255

Open
nthompson-bitwarden wants to merge 4 commits into
mainfrom
seeder/secrets-manager-scenes
Open

Seeder/secrets manager scenes#8255
nthompson-bitwarden wants to merge 4 commits into
mainfrom
seeder/secrets-manager-scenes

Conversation

@nthompson-bitwarden

Copy link
Copy Markdown
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/QA-2400

📔 Objective

Add Secrets Manager seeder scenes to the SeederApi so tests and local setups can persist real SM fixtures (projects, secrets, service accounts, access policies) against an SM-enabled org.

What changed:

  • Add four SM seeder scenes — OrganizationProjectScene, OrganizationSecretScene, OrganizationServiceAccountScene, and OrganizationAccessPolicyScene — that persist real rows via the commercial EF repositories. Each mirrors OrganizationCollectionScene: inject repositories and IManglerService, load and validate the org, encrypt fields under the org key, and return the mangle map.
  • Wire AddSecretsManagerEfRepositories into SeederApi Startup so the scenes override the OSS Noop registrations.
  • Guard all four scenes against non-Secrets-Manager orgs. Previously they claimed to target an SM-enabled org but never checked Organization.UseSecretsManager, so they could seed impossible fixtures (SM data on an SM-off org) and bad refs surfaced as raw FK 500s. The guard throws InvalidOperationException, which SceneExecutor/SeedController surface as a clean 400.
  • Extract the duplicated org-load + not-found + SM-enabled guard into a single GetSecretsManagerOrganizationOrThrowAsync extension on IOrganizationRepository, called by all four scenes. Exception messages are kept byte-identical so the BadRequest integration test and HTTP 400 mapping are unaffected.

Tests:

  • End-to-end integration test exercising all four scenes through POST /seed, asserting real DB rows with round-tripped encryption.
  • Integration test seeding a non-SM Enterprise org (via OrganizationOverrides.UseSecretsManager=false) asserting the project scene returns 400 and writes no row.

Add four SM seeder scenes (project, secret, service account, access
policy) that persist real rows via the commercial EF repositories, and
wire AddSecretsManagerEfRepositories into SeederApi Startup so the
scenes override the OSS Noop registrations.

Scenes mirror OrganizationCollectionScene: inject repositories and
IManglerService, load and validate the org, encrypt fields under the
org key, and return the mangle map. Adds an end-to-end integration
test exercising all four scenes through POST /seed and asserting real
DB rows with round-tripped encryption.
The Secrets Manager seeder scenes claimed to target an SM-enabled org but
never checked Organization.UseSecretsManager, so they could seed impossible
fixtures (SM data on an SM-off org), and bad refs surfaced as raw FK 500s.

Add a UseSecretsManager guard to OrganizationProjectScene,
OrganizationSecretScene, OrganizationServiceAccountScene, and
OrganizationAccessPolicyScene. The guard throws InvalidOperationException,
which SceneExecutor/SeedController surface as a 400 with a clean message.

Add an integration test seeding a non-SM Enterprise org (via
OrganizationOverrides.UseSecretsManager=false) and asserting the project
scene returns 400 and writes no row.
Extract the duplicated org-load + not-found + Secrets Manager-enabled
guard into a GetSecretsManagerOrganizationOrThrowAsync extension on
IOrganizationRepository, and call it from the four SM scenes. Keeps the
exception messages byte-identical so the BadRequest integration test and
HTTP 400 mapping are unaffected.
@nthompson-bitwarden nthompson-bitwarden added the t:misc Change Type - ¯\_(ツ)_/¯ Prefer using other type labels label Aug 25, 2026
@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 63.19%. Comparing base (aebb9c4) to head (7d98433).
⚠️ Report is 4 commits behind head on main.

❗ There is a different number of reports uploaded between BASE (aebb9c4) and HEAD (7d98433). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (aebb9c4) HEAD (7d98433)
2 1
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8255      +/-   ##
==========================================
- Coverage   68.88%   63.19%   -5.70%     
==========================================
  Files        2410     2409       -1     
  Lines      104442   104393      -49     
  Branches     9457     9453       -4     
==========================================
- Hits        71946    65971    -5975     
- Misses      30115    36164    +6049     
+ Partials     2381     2258     -123     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@nthompson-bitwarden nthompson-bitwarden added the ai-review Request a Claude code review label Aug 25, 2026
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Reviewed the four new Secrets Manager seeder scenes (OrganizationProjectScene, OrganizationSecretScene, OrganizationServiceAccountScene, OrganizationAccessPolicyScene), the shared GetSecretsManagerOrganizationOrThrowAsync guard, the commercial SM EF repository wiring in SeederApi/Startup.cs, and the end-to-end integration tests. The scenes mirror OrganizationCollectionScene, encryption stays client-side via RustSdkService under the caller-supplied org key, and the guard's InvalidOperationException maps to a clean 400 through SceneExecutor/SeedController. Verified AddSecretsManagerEfRepositories() is safe on the SQL Server path (AddDatabaseRepositories always calls SetupEntityFramework, matching Api/Startup.cs), and that DestroySceneCommand cleanup reaches the new rows — UserRepository.DeleteManyAsync clears user/service-account access policies and OrganizationRepository.DeleteInternalAsync clears Project, Secret, ServiceAccount, and the SM policy tables.

Code Review Details

No blocking findings.

The previously raised ♻️ finding on the unused Commercial.Core project reference was addressed in 7d98433; both packages.lock.json files now match the trimmed SeederApi.csproj.

Dependency Changes

Package Change Ecosystem
Commercial.Infrastructure.EntityFramework New project reference (SeederApi) NuGet/ProjectReference

Internal repo project reference, not a third-party dependency — no AppSec review required. Consistent with Api.csproj and Admin.csproj, which reference the same commercial project unconditionally. Both lock files were regenerated in step with the manifest.

Comment thread util/SeederApi/SeederApi.csproj Outdated
SeederApi only calls AddSecretsManagerEfRepositories(), which lives in
Commercial.Infrastructure.EntityFramework. Nothing in util/SeederApi or
test/SeederApi.IntegrationTest uses Bit.Commercial.Core, so the reference
only pulled Commercial.Core (and CsvHelper transitively) into the build and
container image. Regenerated both packages.lock.json files.
@nthompson-bitwarden
nthompson-bitwarden marked this pull request as ready for review August 25, 2026 21:44

@theMickster theMickster left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have refactoring work to do in the scenes and a couple things for thought/discussion. Thanks!

mangleMap: manglerService.GetMangleMap());
}

private static BaseAccessPolicy BuildPolicy(Grant grant) =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Kindly refactoring this code (and it's related enumerations) into a new factory in the util/Seeder/Factories folder. Doing this keeps the scene's responsibility to only being the orchestrator and not the creator of seeds, and moving this allows for code reuse by the CLI (or future scenes).

throw new InvalidOperationException($"Organization {request.OrganizationId} not found.");
}

var project = new Project

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Similar request here; kindly refactor this into a new Seeder/Factories class.
c5966eb#r3860957845

throw new InvalidOperationException($"Organization {request.OrganizationId} not found.");
}

var secret = new Secret

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Similar request here; kindly refactor this into a new Seeder/Factories class.
c5966eb#r3860957845

throw new InvalidOperationException($"Organization {request.OrganizationId} not found.");
}

var serviceAccount = new ServiceAccount

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Similar request here; kindly refactor this into a new Seeder/Factories class.
c5966eb#r3860957845

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 I don't love that we have added an extensions helper class to the namespace that contains scenes. I also really don't want to have this in an base class an inheritance coupling 🤮
Where else is a better home for this (and future) extensions?

Secondarily, I am also not convinced at first glance that this must be public; wouldn't marking this as internal be more accurate/applicable? 🧐

/// Creates a Secrets Manager secret (key/value/note encrypted with the organization's symmetric key)
/// for an existing Secrets Manager-enabled organization, optionally associating it with projects.
/// </summary>
public class OrganizationSecretScene(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓Should we also consider injecting IProjectRepository and perhaps call await projectRepository.ProjectsAreInOrganization(request.ProjectIds.ToList(), organization.Id) to guard that the project is properly bound to the correct organization?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ A local multi-agent Claude Code review brought to light the following finding. It's verbose, but I think it's correct that we slow down to analyze if/how we properly introduce this project reference into the Seeder and thus into the SeederApi.IntegrationTest test library.

Don't take Claude's word for word on the work to do, but let's be wise and double-check the recommendations.

Commercial project reference and SM repo registration are not gated behind the OSS build guard
`util/SeederApi/SeederApi.csproj:15`
**Caught by:** Code quality agent

This PR adds <ProjectReference Include="..\..\bitwarden_license\src\Commercial.Infrastructure.EntityFramework\Commercial.Infrastructure.EntityFramework.csproj" /> unconditionally, and util/SeederApi/Startup.cs:39 calls services.AddSecretsManagerEfRepositories() unconditionally.

Both established sibling sites gate exactly this dependency:

  • src/Api/Api.csproj:31-38 and src/Admin/Admin.csproj:21-28 wrap the same Commercial.Infrastructure.EntityFramework reference in <Choose><When Condition="!$(DefineConstants.Contains('OSS'))">.
  • src/Api/Startup.cs:204-212 and src/Admin/Startup.cs:98-103 select between services.AddOosServices() (Noop SM repositories) and services.AddSecretsManagerEfRepositories() via #if OSS / #else.

Two consequences:

  1. util/SeederApi sits in the AGPL portion of the tree and now has a hard, ungated compile-time dependency on bitwarden_license source. An OSS-defined build of SeederApi cannot restore or compile.
  2. test/SeederApi.IntegrationTest becomes the first project under ./test to transitively reference bitwarden_license (grep -rn bitwarden_license test --include='*.csproj' currently returns nothing), which blurs the dotnet test ./test ("OSS solution") vs dotnet test ./bitwarden_license/test ("Bitwarden solution") split in .github/workflows/test.yml:52-56.

Suggested fix: mirror the sibling pattern — move the ProjectReference into a <Choose><When Condition="!$(DefineConstants.Contains('OSS'))"> block and wrap the AddSecretsManagerEfRepositories() call in #if OSS / #else, taking services.AddOosServices() on the OSS side.

Note the fix is not purely mechanical: AddOosServices() (src/SharedWeb/Utilities/ServiceCollectionExtensions.cs:386-394) registers Noops for IProjectRepository, ISecretRepository, IServiceAccountRepository and ISecretVersionRepository, but not IAccessPolicyRepository. Since SeederApi's default host builder validates the container on build and AddScenes() registers every scene's concrete type, OrganizationAccessPolicyScene would fail startup validation on the OSS side unless a Noop IAccessPolicyRepository is added too. That gap should be resolved as part of the gating rather than sidestepped by leaving the dependency ungated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review t:misc Change Type - ¯\_(ツ)_/¯ Prefer using other type labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants