Skip to content

Don't bootstrap a dummy LDAP identity provider when unconfigured - #4061

Open
egorpak528 wants to merge 4 commits into
cloudfoundry:developfrom
egorpak528:fix/TNZ-125736-skip-ldap-bootstrap-without-config
Open

Don't bootstrap a dummy LDAP identity provider when unconfigured#4061
egorpak528 wants to merge 4 commits into
cloudfoundry:developfrom
egorpak528:fix/TNZ-125736-skip-ldap-bootstrap-without-config

Conversation

@egorpak528

@egorpak528 egorpak528 commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Problem

IdentityProviderBootstrap.addLdapProvider() unconditionally creates an IdentityProvider row with origin_key="ldap" in each zone on every startup, regardless of whether real LDAP configuration is supplied. Only the active flag depends on real config (ldapProfile && json.isConfigured()) - the row itself is always persisted.

Since identity_provider enforces a unique (identity_zone_id, origin_key) index, this causes two problems for anyone trying to manage LDAP identity providers via the REST API:

  1. POST /identity-providers with type=ldap collides with the existing dummy row - only PUT against it is possible, so a real, independently-managed LDAP IDP can never be created fresh.
  2. A ldap: config supplied with no real connection details (e.g. only override: false) does not prevent the dummy row from being created on a fresh zone/install - override only controls whether an already-existing row gets updated on a later boot, not whether one gets created in the first place. So ldap.override: false is not a usable workaround.

There's a second, deeper layer to this: even with the app-level fix below, identity_provider rows for uaa/login-server/ldap/keystone are also unconditionally inserted by a much older migration, V2_0_2__BootstrapIdentityZones (2014) - on every database, fresh or upgraded, since Flyway replays the full migration history regardless. That insert predates any application code and can't be prevented by fixing IdentityProviderBootstrap alone.

Fix

Two parts, addressing both layers:

1. IdentityProviderBootstrap.addLdapProvider() - skip creating the bootstrap provider when there's no real configuration (LdapIdentityProviderDefinition.isConfigured(), i.e. no baseUrl - so a ldap: block with only control flags like override still counts as unconfigured) and no provider already exists for this zone. A provider that already exists - from a prior real config, the REST API, or the legacy Flyway migration - is left untouched; it's only new, never-configured zones that no longer get a dummy row on boot.

2. A new migration, V4_114__DeleteUnconfiguredBootstrapIdentityProviders - deletes the login-server/ldap/keystone rows V2_0_2__BootstrapIdentityZones left behind, but only if they're still genuinely unconfigured (config IS NULL, or for ldap specifically, a parsed config with no baseUrl). uaa is never touched - it's the mandatory internal auth source. Because Flyway replays every migration on every database, this closes the loop for both timeframes at once: a fresh install inserts-then-immediately-deletes these rows within the same initial migrate() run (never persisting), while an already-provisioned database gets them cleaned up retroactively on its next upgrade. A real, actively-configured provider is left alone either way.

Verified no blast radius concerns before adding the migration: no FK constraints anywhere reference identity_provider(id) (the columns that once did were dropped back in V2_0_4), and the two real callers that look up these origins already handle total absence gracefully (DynamicZoneAwareAuthenticationManager#getProvider() falls back to a synthetic inactive stub; so does InvitationsController#acceptLdapInvitation()).

Testing

  • IdentityProviderBootstrapTest - added tests for the profile-alone and override-only-config cases; updated upgradeLDAPProvider and getGenericLdapConfig() (pre-existing helpers whose assumptions no longer held once dummy rows stopped being free - see commit messages for details).
  • DeleteUnconfiguredBootstrapIdentityProvidersTest - new, covers null-config deletion, semantic ldap/keystone "still unconfigured" detection, preservation of real configs, and defensive handling of unparseable JSON.
  • TestUtils.cleanAndSeedDb() updated to match the new post-migration baseline; re-verified against all 10 test files that depend on it (160 tests).

ai-assisted=yes

🤖 Generated with Claude Code

egorpak528 and others added 2 commits August 31, 2026 13:45
IdentityProviderBootstrap.addLdapProvider() unconditionally created an
IdentityProvider row with origin_key="ldap" on every startup, even
when no `ldap:` config was supplied and the `ldap` Spring profile was
merely active (or not active at all). Only the `active` flag depended
on real configuration; the row itself was always persisted.

Since identity_provider enforces a unique (identity_zone_id,
origin_key) index, this blocked creating a real LDAP IdentityProvider
via POST /identity-providers (only PUT against the existing dummy row
was possible), and risked clobbering a REST-managed LDAP IDP's config
on the next restart unless `ldap.override: false` was explicitly set -
which itself doesn't prevent the dummy row from being created on a
fresh zone, since override only decides whether an *existing* row
gets updated, not whether one gets created.

Skip creating the bootstrap provider entirely when there's no real
LDAP configuration (LdapIdentityProviderDefinition.isConfigured(),
i.e. no baseUrl - so a `ldap:` block containing only control flags
like `override` still counts as unconfigured) and no provider already
exists for this zone. A provider that already exists (from a prior
real config, the REST API, or the legacy Flyway migration) is left
untouched by this change - it's only new, never-configured zones that
no longer get a dummy row.

ai-assisted=yes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ldapProfileAloneDoesNotBootstrapDummyProvider and
ldapConfigWithOnlyOverrideFlagDoesNotBootstrapDummyProvider assumed a
clean identity_provider table, but @WithDatabaseContext doesn't roll
back between tests. Depending on JUnit's (unspecified) method
execution order, an earlier test in the class that bootstraps a real
LDAP provider (e.g. ldapOverrideFalse) could leave a row behind,
making `existing` non-null and causing these tests to fail because no
exception was thrown - exactly what happened on CI for PR cloudfoundry#4061
(mysql 8.0/8.4/9, postgresql 15/16/17/18, and the standalone hsqldb
run all failed identically).

Add the same TestUtils.cleanAndSeedDb(jdbcTemplate) call the
neighboring ldapOverrideFalse test already uses to guarantee a clean
slate before asserting.

ai-assisted=yes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
egorpak528 and others added 2 commits August 31, 2026 16:29
The IdentityProviderBootstrap guard added in the previous commit only
prevents *new* dummy rows from being created - it does nothing for
databases that already have them, because V2_0_2__BootstrapIdentityZones
(2014) unconditionally inserts a login-server/ldap/keystone/uaa
identity_provider row for the uaa zone on *every* database, fresh or
upgraded (Flyway replays the whole migration history regardless). That
insert predates any application code touching the row, so the app-level
guard alone can never make it stop existing - only a migration can.

Add V4_114__DeleteUnconfiguredBootstrapIdentityProviders (one shared
Java migration class, three thin per-engine subclasses, same pattern as
BootstrapIdentityZones itself) which deletes exactly the rows that are
still in their never-configured state:

- login-server, keystone: config is still the literal NULL the 2014
  migration inserted (nothing else has ever written to it under normal
  operation).
- ldap: needs a semantic check, not just NULL - IdentityProviderBootstrap
  overwrites the literal NULL with a structurally-real-but-empty JSON
  config on every boot today, so this parses it and checks
  LdapIdentityProviderDefinition#isConfigured() (baseUrl set).
- uaa: never touched. It's the mandatory internal auth source, kept
  live by IdentityProviderBootstrap#updateDefaultZoneUaaIDP().

Verified no blast radius concerns before writing this: no FK constraints
anywhere reference identity_provider(id) (the columns that once did,
users/group_membership.identity_provider_id, were dropped back in
V2_0_4__Identity_Provider_Adjustments), and the two real callers that
look up these origins already handle total absence gracefully
(DynamicZoneAwareAuthenticationManager#getProvider() falls back to a
synthetic inactive stub on EmptyResultDataAccessException; so does
InvitationsController#acceptLdapInvitation()).

Because Flyway replays every migration on every database, this closes
the loop for both timeframes at once: a brand-new install inserts then
immediately deletes these rows within the same initial migrate() run
(never persisting), while an already-provisioned database gets them
cleaned up retroactively the next time it upgrades - as long as they're
still genuinely unconfigured. A real, actively-configured LDAP or
Keystone provider is left untouched either way.

See TNZ-125736.

ai-assisted=yes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TestUtils.cleanAndSeedDb() is explicitly documented as seeding a
database "similar to how the real Flyway migration does it" - now
that a later migration removes the never-configured login-server/
ldap/keystone placeholder rows, a truly fresh, fully-migrated database
no longer has them, so the shared test helper needs to stop pretending
otherwise. Checked usage first: of the 10 call sites across the test
suite, only IdentityProviderBootstrapTest ever looks at those two
origins post-seed, and it always creates its own row via the code
under test rather than relying on the seeded placeholder - so this is
safe (confirmed by running all 10 affected test files).

That change surfaced two independent, pre-existing issues in
IdentityProviderBootstrapTest once the placeholder rows stopped being
free:

- upgradeLDAPProvider(): an assertion I added in the previous commit
  checked origin "ldap", but the row this test actually inserts (from
  a 2016 commit, 645b8a9, testing that bootstrap tolerates unknown
  legacy JSON attributes like "ldapdebug") uses origin_key "ldap2" -
  a different, deliberately unrelated row. That mismatch only ever
  passed because the old unconditional bootstrap code created *a*
  "ldap" row regardless of what else existed in the table. Fixed to
  check the row that's actually under test.

- getGenericLdapConfig(): the shared "generic" config used by
  ldapBootstrap() and ldapOverrideFalse() never set base.url, so
  LdapIdentityProviderDefinition#isConfigured() was false for it -
  harmless under the old unconditional-create behavior, but now trips
  the new "don't bootstrap when unconfigured" guard. Added a base.url
  so this helper actually represents the realistic, fully-configured
  LDAP setup its name claims.

Verified against the full IdentityProviderBootstrapTest suite plus all
other test files that use TestUtils.cleanAndSeedDb/restoreToDefaults
(160 tests, all passing).

See TNZ-125736.

ai-assisted=yes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant