Skip to content

Let demo, local, and test run only alone and guard every profile against the demo users (B06) - #16

Closed
AsyncAssassin wants to merge 2 commits into
mainfrom
fix/profile-combination-guard
Closed

AsyncAssassin wants to merge 2 commits into
mainfrom
fix/profile-combination-guard

Conversation

@AsyncAssassin

@AsyncAssassin AsyncAssassin commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Summary

B06 from the merged review of v0.4.0 (prod,demo bypassed the demo-user guard); 0.4.1 is released once all fixes are merged. A regression review of the first version showed that the guard only knew the literal prod, so this PR closes the whole class:

  • demo, local, and test run only alone: SPRING_PROFILES_ACTIVE=prod,demo on a fresh database let the demo seeder write demo-operator after the guard had checked, and staging,demo or staging,local had the same gap without any guard. ProfileCombinationGuard, an environment post-processor registered in META-INF/spring.factories, now refuses any of these three next to another profile before the application context exists, and before a placeholder of the other profiles is resolved, so prod,local without the prod secrets names the combination instead of failing on ASSET_SYNC_DB_URL. The demo-only beans keep plain @Profile("demo"); this guard is the single point that keeps them out of other profiles.
  • Every profile with authentication except demo refuses the demo users: ProdDemoUserGuard ran only under prod. It now runs in prod, e2e, a custom profile such as staging, and a start without any profile. It is @Lazy(false), so spring.main.lazy-initialization cannot skip it. It lets a database without the users table start instead of failing on its own query. Its message names the default profile when none is active and recommends an empty database, because a database the demo ran on also holds the demo dataset.
  • The prod operator account cannot take a demo user name: the next start would have refused its own admin.

CHANGELOG.md lists both under [Unreleased] → Security.

Verification

  • ./gradlew clean check: 280 tests, 0 failures, 2 skipped (the env-gated Alchemy live smoke).
  • ProfileCombinationGuardTests pins the profile matrix. It covers which sets start, which are refused, and that the demo-user guard runs in exactly the sets that may start, except demo.
  • ProdProfileBootIntegrationTests boots second contexts against the test container:
    • prod,demo, prod,local, prod,test, staging,demo, and staging,local are refused with their exact profiles, against a separate empty database that must stay empty;
    • prod,local without the prod secrets is refused;
    • prod, staging, and no profile refuse the demo users;
    • staging starts on a database without the users table;
    • an admin named demo-operator fails.
  • Mutation check: without the spring.factories registration, prod,demo starts and the no-secrets case fails on a placeholder, so both tests go red.
  • The first version of this PR was also verified live: prod,demo and prod,local stop before migrations, and staging and prod refuse a database a demo start seeded.

This PR and #15 both add a section under [Unreleased] in CHANGELOG.md, so whichever merges second needs a one-file rebase. The other files merge cleanly with #15.

…ected profile

With SPRING_PROFILES_ACTIVE=prod,demo on a fresh database the
demo-user guard ran first and found nothing, then the demo seeder wrote
demo-reader and demo-operator with their public passwords, and the
service ran with them until the next restart; the simulator path was
open without credentials as well. prod combined with local failed, but
on whichever bean it missed first. A bean factory post-processor now
stops prod together with demo, local, or test before any bean is
created, and names the active profiles. The demo seeder, the simulator,
and the open /simulator path also require demo without prod.

The demo-user guard was bound to the literal prod profile, so any other
protected profile, e2e or a custom one such as staging, accepted the
public demo passwords on a database that once ran demo. It now runs in
every profile except demo, local, and test.
…r guard

Review of the profile guard found that it only knew the literal prod:
staging,demo still seeded the public demo users with the demo-user
guard switched off, and staging,local started without authentication.
It also ran after the configuration conditions, so prod,local without
the prod secrets failed on an unresolved placeholder instead of naming
the combination. The guard is now an environment post-processor that
refuses demo, local, or test next to any other profile before the
application context exists; the demo-only beans go back to plain demo,
with this guard as the single point that keeps them out of prod.

The demo-user guard is never lazy, skips a database without the users
table instead of failing on its own query, names the default profile
when none is active, and recommends an empty database, because a
database the demo ran on also holds the demo dataset. The prod operator
account cannot take a demo user name. The tests cover the profile
matrix, start the refused combinations against an empty database that
must stay empty, and assert the exact profiles in every message.
@AsyncAssassin AsyncAssassin changed the title Refuse prod mixed with demo, local, or test and guard every protected profile Let demo, local, and test run only alone and guard every profile against the demo users (B06) Sep 23, 2026
@AsyncAssassin

Copy link
Copy Markdown
Owner Author

Combined into #23 together with the other batch-1 fixes, so they merge without a rebase per PR. This description keeps the detailed evidence for its fix.

@AsyncAssassin
AsyncAssassin deleted the fix/profile-combination-guard branch September 24, 2026 05:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant