Repository navigation
[#1196] Accept the DNS aliases of the realms as valid host names, so that a realm alias no longer needs its own entry in the FQDN map - #1197
Conversation
maximthomas
left a comment
There was a problem hiding this comment.
praise: The snapshot logic is careful, and the two tests that guard it really do catch regressions.
RealmDnsAliases.reload()takesconfigVersionbefore the read, so a change notified during the read forces another read.shouldReadRealmsAgainWhenRealmConfigurationChangesWhileTheyAreReadholds it: with the version taken after the read it goes red (1 of 15 tests fails).expose(RealmDnsAliases.class)is pinned too. Without it,RealmGuiceModuleTestfails withGuice/ChildBindingAlreadySet.FqdnValidator.isRealmDnsAliasruns only for a host that is not an fqdnMap value, and only afterAMSetupServlet.isCurrentConfigurationValid().
issue (blocking): init() marks itself initialised before the listener is registered. If the registration fails or comes late, the alias set stays frozen until restart.
openam-core/src/main/java/org/forgerock/openam/core/realms/RealmDnsAliases.java:82-89
compareAndSet(false, true) runs before idRepoServiceProvider.get().addListener(...), and nothing ever resets the flag. Three things can leave the instance without a listener for the life of the JVM, with no retry:
- the provider throws.
RealmGuiceModule.provideIdRepoServicewrapsSMSException/SSOExceptionin aRuntimeException, and the admin token action can also throw. addListenerreturns null.- a concurrent first caller loses the CAS. It then reads and stores a snapshot before the winner has registered, and a realm change in that gap is never counted.
From then on configVersion never moves, so the first stored snapshot is served for good. An alias the admin removes stays an accepted Host, which master rejects, and a new alias keeps getting the 302/400. The description says "a stale set is never kept".
private void init() {
if (initialised.get()) {
return;
}
synchronized (this) {
if (initialised.get()) {
return;
}
if (idRepoServiceProvider.get().addListener(new RealmsChangeListener()) == null) {
logger.warn("RealmDnsAliases: Failed to add ServiceListener, will retry on the next request");
return;
}
// Discards a snapshot read while no listener was registered
configVersion.incrementAndGet();
initialised.set(true);
}
}If the provider throws, initialised now stays false and the next call retries. The throw itself is the non-blocking issue below. shouldRegisterServiceListenerOnlyOnce stays green.
Pin: let addListener return null and then "LISTENER_ID", and in a second case let the idRepo provider throw once. In both, change aliasesByRealm between two contains() calls without notifying, and assert that the second call sees the change. Both cases are red on the head.
question (blocking): Should a host that is an fqdnMap key and also a realm alias still be redirected to its mapped FQDN?
openam-core/src/main/java/com/sun/identity/common/FqdnValidator.java:110, :126-131; openam-core/src/main/java/com/sun/identity/setup/ConfigureData.java:134-138
getFullyQualifiedHostName(h) reads fqdnMap.get(h) only when isHostnameValid(h) is false. isHostnameValid checks the map's values and, since this PR, the realm aliases, but never the map's keys. By default the root realm's aliases include the RDN of the root suffix, which is openam for dc=openam,dc=openidentityplatform,dc=org. Now take com.sun.identity.server.fqdnMap[openam]=openam.example.com, the documented short-name mapping. On master, GET http://openam:8080/openam/XUI/ gets a 302 to openam.example.com. On this head the request is served on the short host, where cookies scoped to .example.com do not apply. ConsoleServletBase.validateHost also loses the mapping. The description says that mapped hosts are not affected. That is true for map values but not for keys.
If it is intended, the description must say that such mappings stop redirecting, and the test suggested below must pin the head's result (null). If it is not intended, it is a blocking regression, and this is the fix:
return fqdnMap.values().contains(lowerCaseHostname)
|| (!fqdnMap.containsKey(lowerCaseHostname) && isRealmDnsAlias(lowerCaseHostname));fqdnMap keys are already lower-cased (FqdnValidator.java:86). getFullyQualifiedHostName and the console both go through isHostnameValid, so they follow.
issue (non-blocking): A RuntimeException from the alias lookup now escapes isHostnameValid. The result is a 500 instead of the rejection that the description promises.
openam-core/src/main/java/org/forgerock/openam/core/realms/RealmDnsAliases.java:74-80, :109; openam-core/src/main/java/com/sun/identity/common/FqdnValidator.java:113-117
reload() catches only SMSException. Neither contains, isRealmDnsAlias, AuthClientUtils.isValidFQDNRequest nor CoreWrapper.isValidFQDN catches anything else. The exception can come from the AMSecurityPropertiesException of AdminTokenAction through CoreGuiceModule.provideAdminSSOToken, or from the RuntimeException that the idRepo provider throws in init(). In both cases a request with an unmapped Host gets a container 500 from FQDNValidationFilter and an unchecked error from RealmContextFilter. Before this PR it got a 302 or a 400, because isHostnameValid could not throw. Catching in contains() covers both sources, init() included:
public boolean contains(String hostname) {
if (hostname == null) {
return false;
}
try {
init();
return getAliases().contains(hostname.toLowerCase());
} catch (RuntimeException e) {
logger.warn("RealmDnsAliases: Unable to read the DNS aliases of the realms", e);
return false;
}
}question (non-blocking): Is the admin console meant to be served on every realm-alias host?
openam-console/src/main/java/com/sun/identity/console/base/ConsoleServletBase.java:362; openam-core/src/main/java/com/sun/identity/authentication/client/AuthClientUtils.java:1585
validateHost calls getFullyQualifiedHostName, which now returns null for an alias. Take Host: acme.example.com, an alias of /acme, on /base/AMAdminFrame: master sends a 302 to the server host, while this head serves the console on the alias host. The description says "All three places that check the host are affected" and lists only the three filters. As a gap in the description this is minor. It becomes blocking if the console was meant to stay on the server host. If the change is intended, list ConsoleServletBase and AuthClientUtils.getValidFQDNResource in the description.
suggestion (non-blocking): No test pins what an fqdnMap key that is also a realm alias resolves to.
openam-core/src/test/java/com/sun/identity/common/FqdnValidatorTest.java:49-50, :77, :83
The setup maps fqdnMap[openam], but the realmDnsAliases mock answers false for openam. getFullyQualifiedHostName is called only for acme.example.com and attacker.example.org. Master's key-first order and this head's alias-first order both pass all 6 tests. The test below fails on the current head and passes with the fix above. If the head's result is intended, assert isNull() instead.
@Test
public void shouldRedirectFqdnMapKeyThatIsAlsoARealmDnsAlias() {
given(realmDnsAliases.contains("openam")).willReturn(true);
assertThat(FqdnValidator.getInstance().getFullyQualifiedHostName("openam")).isEqualTo("openam.example.com");
}suggestion (non-blocking): RealmDnsAliasesTest stubs out the whole realm read, so no test runs the code that reads aliases from the configuration.
openam-core/src/test/java/org/forgerock/openam/core/realms/RealmDnsAliasesTest.java:73-102
The anonymous subclass overrides both getSubRealms and getRealmAliases. As a result RealmDnsAliases.java:131-145 never runs and the organizationConfigManagerFactory mock is never called. All 21 tests would pass with getSubOrganizationNames("*", false), with ORGANIZATION_STATUS_ATTR read instead of ORGANIZATION_ALIAS_ATTR, or with the "/" prefix dropped. The current code is correct. Only one test is needed on a RealmDnsAliases that is not subclassed, and it fails against each of those mutants:
@Test
public void shouldReadDnsAliasesOfNestedRealmsFromTheConfiguration() throws Exception {
OrganizationConfigManager root = mock(OrganizationConfigManager.class);
OrganizationConfigManager sub = mock(OrganizationConfigManager.class);
given(organizationConfigManagerFactory.create(adminToken, "/")).willReturn(root);
given(organizationConfigManagerFactory.create(adminToken, "/acme/sub")).willReturn(sub);
given(root.getSubOrganizationNames("*", true)).willReturn(asSet("acme/sub"));
given(sub.getAttributes(IdConstants.REPO_SERVICE)).willReturn(
Collections.singletonMap(IdConstants.ORGANIZATION_ALIAS_ATTR, asSet("sub.acme.example.com")));
RealmDnsAliases aliases = new RealmDnsAliases(() -> adminToken, organizationConfigManagerFactory,
() -> idRepoService);
assertThat(aliases.contains("sub.acme.example.com")).isTrue();
}issue (non-blocking): In AMSDK coexistence mode, an alias change can leave the old aliases stored under the new version.
openam-core/src/main/java/com/sun/identity/sm/OrganizationConfigManager.java:1032-1045, :797-820
In coexistence mode, setAttributes first saves the SMS realm entry, which schedules the notification, and only then calls amsdk.setAttributes. getAttributes returns the merged AMSDK aliases. Suppose a contains() call lands between the two writes. It captures the new version, reads the old AMSDK value and stores it, so the new alias is rejected and the old one accepted until the next realm change. This contradicts "a stale set is never kept". Not run: it needs a coexistence deployment and a concurrent probe. A sentence in the Javadoc of RealmDnsAliases is enough.
9cafa0e to
dca0dee
Compare
|
Round 2, rebased onto current master. issue (blocking): One change to the pins. For the provider that throws once, "change question (blocking): an fqdnMap key that is also a realm alias. Not intended. Fixed with your condition in issue (non-blocking): a question (non-blocking): the admin console on realm-alias hosts. Intended. A realm alias now behaves as the workaround from #1196, suggestion (non-blocking): a test for an fqdnMap key that is also a realm alias. Added, see above. suggestion (non-blocking): a test that reads the aliases from the configuration. Added, but in a class of its own, issue (non-blocking): AMSDK coexistence mode. A sentence added to the Javadoc of
|
maximthomas
left a comment
There was a problem hiding this comment.
praise: The round-1 findings are fixed where the bugs were, and the fixes are measured.
FqdnValidator.isHostnameValid(FqdnValidator.java:111-112) keeps an FQDN map key redirected with!fqdnMap.containsKey, pinned byshouldRedirectFqdnMapKeyThatIsAlsoARealmDnsAlias.RealmDnsAliases.init()registers the listener undersynchronizedand bumpsconfigVersiononce it is registered; with thesynchronizedremoved,shouldNotReadRealmsWhileServiceListenerIsBeingRegisteredfailed 50 of 50 invocations (JDK 26).contains()catchesSMSException | RuntimeExceptionin one place (RealmDnsAliases.java:85), so a broken admin token or store rejects the host instead of answering 500.
issue (blocking): After the upgrade, the host names of deleted servers and sites become valid hosts, and nothing tells the operator.
openam-core/src/main/java/com/sun/identity/common/FqdnValidator.java:111-112, openam-core/src/main/java/com/sun/identity/common/configuration/ConfigurationBase.java:186-209, openam-core/src/main/java/com/sun/identity/common/configuration/ServerConfiguration.java:593-608, openam-core/src/main/java/com/sun/identity/common/configuration/SiteConfiguration.java:183-198
Server create, clone and import (ServerConfiguration.java:676, :1006, :1134), site create (SiteConfiguration.java:247) and setup (AMSetupServlet.java:1622-1630) add their host to the top-level realm's sunOrganizationAliases through updateOrganizationAlias(..., true). No caller passes false, and deleteServerInstance and deleteSite only remove the sub-configuration. So an install that once had http://old.example.com:8080/openam still lists old.example.com. At the base, Host: old.example.com was redirected to the default host; at this head, FQDNValidationFilter, LoginServlet, RealmContextFilter and the console accept it. If that name or its DNS record is no longer under the operator's control, the OPENAM-7340 check no longer protects it.
The description says the server host names become valid, but not the names of deleted servers. A note in the PR body would not reach operators either, because the release notes list only PR titles. Is accepting the deleted ones intended? If not, you can also remove the host on delete: call updateOrganizationAlias(ssoToken, <url>, false) after removeSubConfig, but only when no remaining server or site URL has the same host, because two servers on one host share the alias. That does not clean up the entries left by earlier deletes, so the note is needed either way. Not checked: whether a site's secondary URLs or a changed server URL also add aliases.
In openam-documentation/openam-doc-source/src/main/asciidoc/upgrade-guide/chap-upgrade-servers.adoc, next to the other "Starting with this release" blocks:
[IMPORTANT]
====
Starting with this release a host name that is a DNS alias of a realm is accepted as a valid host name, as if it had its own `com.sun.identity.server.fqdnMap[<alias>]=<alias>` entry, unless the FQDN map maps it to another host name. By default the DNS aliases of the top-level realm hold the host name of every server and site that was ever created, including servers and sites that were deleted later, and the RDN value of the root suffix (for example `openam`). Before promoting the upgrade, review the DNS aliases of every realm and remove the host names that are no longer in use or no longer under your control.
====suggestion (non-blocking): No test checks that the aliases are skipped for an FQDN map key.
openam-core/src/test/java/com/sun/identity/common/FqdnValidatorTest.java:95-100
shouldNotReadRealmDnsAliasesForHostInFqdnMap calls isHostnameValid only for map values (lb.example.com, openam.example.com), and the key test checks only the outcome. If the guard is reordered to isRealmDnsAlias(h) && !fqdnMap.containsKey(h), every outcome stays the same and all 7 tests pass (traced, not run). Under that order, a request for a short-name key host registers the listener and reads the realms.
assertThat(FqdnValidator.getInstance().isHostnameValid("openam")).isFalse();Pin: add this line to shouldNotReadRealmDnsAliasesForHostInFqdnMap, before verifyZeroInteractions(realmDnsAliases). openam is a key in the fixture's map, so both a reordered guard and a dropped guard call contains("openam"), and verifyZeroInteractions fails.
nitpick (non-blocking): The description says the host is rejected when the listener cannot be registered, but when addListener returns null the alias is accepted.
openam-core/src/main/java/org/forgerock/openam/core/realms/RealmDnsAliases.java:99-101, openam-core/src/test/java/org/forgerock/openam/core/realms/RealmDnsAliasesTest.java:238
ServiceConfigManager.addListener reports a failure only by returning null. init() then returns, contains() reads the realms, and RealmDnsAliasesTest.java:255-258 asserts isTrue() for it. That is correct, since the next successful registration bumps configVersion. Only an idRepo provider or admin token that throws makes the host rejected (:231-241). Two sentences in the description do not match this: "The listener is registered before the realms are read for the first time" and "If the listener cannot be registered ... the host is rejected". Also, shouldRejectHostWhenServiceListenerCannotBeRegistered tests a throwing provider, not a failed registration.
Suggested wording for the Fix bullet: "If the idRepo service or the admin token cannot be obtained, or the realms cannot be read, the host is rejected and the next request tries again. If addListener returns null, the realms are read without a listener and read again once a later request registers it." Also add "unless addListener returns null" to "The listener is registered before the realms are read for the first time". You could also rename the test to shouldRejectHostWhenIdRepoServiceCannotBeObtained.
dca0dee to
e3e42d2
Compare
|
Round 3, rebased onto current master. issue (blocking): the host names of deleted servers and sites become valid hosts. Not intended. Fixed as you suggested, plus one more path. On your "Not checked": a secondary site URL never adds an alias, since the only callers of The upgrade note is in Pinned by
suggestion (non-blocking): pin that the aliases are skipped for an FQDN map key. Added your line to nitpick (non-blocking): the description and
|
maximthomas
left a comment
There was a problem hiding this comment.
praise: the cleanup on delete keeps every host that is still in use, and the upgrade note covers what it cannot clean.
removeOrganizationAliaschecks every remaining server and site URL, secondary site URLs included, case-insensitively before it removes a host (ConfigurationBase.java:228-234).- The
[IMPORTANT]block inchap-upgrade-servers.adoctells operators that servers and sites deleted before the upgrade left their host names behind, and to add a hand-added alias such as a load balancer's host name again after deleting the last server or site that has it.
issue (non-blocking): removeOrganizationAlias keeps a deleted server's alias when its letter case differs from the URL deleted last.
openam-core/src/main/java/com/sun/identity/common/configuration/ConfigurationBase.java:231-235, :206-207
The remaining-URL check ignores case (:231), but the removal is delegated to updateOrganizationAlias(..., false), which matches exact case (:206-207), and the add path (:201) stores the case of the first server's URL. With http://A.example.com:8080/openam and http://a.example.com:9090/openam, delete A, then a: the alias A.example.com stays (OpenDJ matches sunKeyValue with caseIgnoreMatch, so only one case is stored), and RealmDnsAliases lower-cases it, so the host of two deleted servers stays a valid host. shouldKeepHostOfDeletedServerThatAnotherServerHasInOtherCase seeds only the lower-case alias and never deletes the last server.
Replace updateOrganizationAlias(ssoToken, url, false); at :235 with:
OrganizationConfigManager ocm = new OrganizationConfigManager(ssoToken, "/");
Map<String, Set<String>> allAttrs = ocm.getAttributes(ServiceManager.REALM_SERVICE);
Set<String> values = allAttrs.get(OrganizationConfigManager.SUNORG_ALIAS);
if (values != null && values.removeIf(hostName::equalsIgnoreCase)) {
ocm.setAttributes(ServiceManager.REALM_SERVICE, allAttrs);
}The add path (:201) can stay as it is: the case-insensitive removal clears every case variant it may have stored.
Pin: in RootRealmAliasesTest, seed A.example.com, create both servers, delete A and then a, and assert that the alias is gone.
issue (non-blocking): an exception from the alias cleanup turns a committed server or site delete into a failed one.
openam-core/src/main/java/com/sun/identity/common/configuration/ServerConfiguration.java:603-609, SiteConfiguration.java:200-202, :492
removeOrganizationAlias runs after removeSubConfig or accessPoint.setAttributes has committed, and nothing catches what it throws. That can be an SMSException from a store fault in getServers, getAllSiteURLs or the root-realm read or write. It can also be an NPE from getSiteURLs for any site that has no accesspoint subconfig, because createSite writes the site and its accesspoint in two separate writes. When that happens, ServersResourceProvider.delete answers 500 and a retry gets 404. ServerSiteModelImpl.deleteServers reports a failure for a server that is already gone and skips the rest of the selection. While a half-created site exists, this repeats on every server and site delete. At the base these deletes returned true. If the cleanup fails, the alias stays behind, as it did at the base, and the new upgrade note already covers that.
Make the cleanup best-effort in removeOrganizationAlias, which covers all three callers:
protected static void removeOrganizationAlias(SSOToken ssoToken, String url) {
String hostName = getHostName(url);
if (hostName == null) {
return;
}
try {
Set<String> urls = new HashSet<>(ServerConfiguration.getServers(ssoToken));
urls.addAll(SiteConfiguration.getAllSiteURLs(ssoToken));
for (String remainingUrl : urls) {
if (hostName.equalsIgnoreCase(getHostName(remainingUrl))) {
return;
}
}
updateOrganizationAlias(ssoToken, url, false);
} catch (SMSException | SSOException | RuntimeException e) {
Debug.getInstance(SetupConstants.DEBUG_NAME).warning("ConfigurationBase.removeOrganizationAlias: could not remove "
+ hostName + " from the DNS aliases of the top-level realm", e);
}
}This also needs import com.sun.identity.setup.SetupConstants; and import com.sun.identity.shared.debug.Debug;, as in ServerConfigXMLObserver.java:35-36, and the two @throws lines removed from its Javadoc. The callers keep their throws clauses. If you also take the previous fix, its lines replace the updateOrganizationAlias call inside the try.
…alid host names, so that a realm alias no longer needs its own entry in the FQDN map FqdnValidator now also accepts a host name that is a DNS alias of a realm. RealmDnsAliases keeps the aliases of all realms in memory and reads them again after the realm configuration changes, so a request with an unknown Host header does not search the configuration store. The aliases are only consulted for a host missing from the FQDN map, and only once OpenAM is configured. Fixes OpenIdentityPlatform#1196
…ere redirected even if it is a realm alias, and retry the listener registration of RealmDnsAliases after a failure - FqdnValidator consults the realm aliases only for a host that is not a key of the FQDN map, so a mapping such as fqdnMap[openam]=openam.example.com keeps redirecting although openam is an alias of the root realm by default. - RealmDnsAliases registers its listener under a lock and stays uninitialised until the registration succeeds, so a failed registration is retried and the aliases read meanwhile are read again; a concurrent first request waits for the registration. - RealmDnsAliases.contains rejects the host when the admin token, the idRepo service or the realms cannot be obtained, instead of letting an unchecked exception fail the request. - RealmDnsAliasesConfigurationTest reads the aliases through OrganizationConfigManager.
…or site from the aliases of the top-level realm, so that it does not stay a valid host name - ServerConfiguration.deleteServerInstance and SiteConfiguration.deleteSite remove the host name of the deleted server or site from the DNS aliases of the top-level realm, and SiteConfiguration.setSitePrimaryURL the host name of the replaced URL, unless the URL of a remaining server or site has it. - The upgrade guide says that the DNS aliases of the realms are now valid host names and asks to review the aliases left by earlier deletes. - FqdnValidatorTest pins that the aliases are not read for a key of the FQDN map; a RealmDnsAliasesTest test is renamed after what it checks.
…ite in any letter case, and keep the delete when the aliases cannot be updated - ConfigurationBase.updateOrganizationAlias removes the host name from the DNS aliases of the top-level realm regardless of letter case, so an alias stored in the case of another server's URL does not stay behind once the last server or site with that host is deleted. - ConfigurationBase.removeOrganizationAlias only logs a warning when the aliases cannot be read or updated, because the server, site or URL is already gone by then; the upgrade guide says so. - SiteConfiguration.deleteSite reads the primary URL after listServers, so a site without an access point is rejected with ConfigurationException again instead of a NullPointerException.
e3e42d2 to
f85eca8
Compare
|
Round 4, rebased onto current master. issue (non-blocking): the alias stays when its letter case differs from the URL deleted last. Fixed, one level lower than you suggested. issue (non-blocking): an exception from the alias cleanup turns a committed delete into a failed one. Fixed as you suggested.
One more case of the same kind lies outside
|
maximthomas
left a comment
There was a problem hiding this comment.
praise: this round closes both cleanup defects from round 3 without changing how a delete behaves.
ConfigurationBase.removeOrganizationAliascatchesSMSException | SSOException | RuntimeExceptionafter the server or site is gone (ConfigurationBase.java:237), so a committed delete is no longer reported as a failure.values.removeIf(hostName::equalsIgnoreCase)(ConfigurationBase.java:208) removes the host in whatever letter case the aliases hold it.deleteSitereads the primary URL afterlistServers, so a site without an access point is still rejected withConfigurationException, pinned byshouldRejectDeletingSiteWithoutAccessPointAsInvalidSite.
issue (non-blocking): deleteSite now fails with an NPE, and keeps the site, when the site's access point has no primary-url.
openam-core/src/main/java/com/sun/identity/common/configuration/SiteConfiguration.java:196, :389-391
getSitePrimaryURL calls ((Set) map.get(ATTR_PRIMARY_SITE_URL)).iterator().next(). In deleteSite it runs before sc.removeSubConfig(siteName), outside the catch in removeOrganizationAlias. ssoadm set-sub-cfg -o delete on the site's accesspoint removes primary-url with no schema check, and after that delete-site, REST and the console all fail on that site. BASE deleted it, because listServers reads only primary-siteid (ConfigurationBase.java:150-153) and belongToSite reads only the server's site. Reaching this state takes a hand-edited entry. setSitePrimaryURL already fails on the same entry at BASE (existingSet.contains), so this comment leaves it alone.
Set svrs = listServers(ssoToken, siteName);
Set primaryURLs = (Set) cfg.getSubConfig(SUBCONFIG_ACCESS_URL).getAttributes().get(ATTR_PRIMARY_SITE_URL);
String siteURL = (primaryURLs == null || primaryURLs.isEmpty())
? null : (String) primaryURLs.iterator().next();
if ((svrs != null) && !svrs.isEmpty()) {
removeServersFromSite(ssoToken, siteName, svrs);
}
sc.removeSubConfig(siteName);
deleted = true;
if (siteURL != null) {
removeOrganizationAlias(ssoToken, siteURL);
}Pin: a RootRealmAliasesTest case with a site whose access point has primary-siteid but no primary-url. deleteSite must return true and remove the site config. At this head it throws an NPE.
issue (non-blocking): removing the alias is an unguarded read-modify-write of the root realm's whole sunIdentityRepositoryService map.
openam-core/src/main/java/com/sun/identity/common/configuration/ConfigurationBase.java:199-209, :229-236
removeOrganizationAlias checks the remaining URLs. Then updateOrganizationAlias reads every attribute of the realm service, removes the host and writes the whole map back, with no lock or compare. Suppose a server delete overlaps an admin save of the root realm, for example a new DNS alias. The delete then writes back the map it read before that save, and the save is lost. A server created with the same host between the check and the write loses its alias the same way. Server and site hosts stay valid through NamingService.updateFqdnMappings, and the add path has the same pattern at BASE. No change is asked in this PR: a synchronized method would protect one JVM only.
issue (non-blocking): the upgrade guide promises a warning that a default install does not write.
openam-documentation/openam-doc-source/src/main/asciidoc/upgrade-guide/chap-upgrade-servers.adoc:111, openam-core/src/main/java/com/sun/identity/common/configuration/ConfigurationBase.java:238
The guide says that when the aliases cannot be updated, "OpenAM logs a warning in the amSetupServlet debug log". Debug.warning is written only when warningEnabled(), and the shipped level is com.iplanet.services.debug.level=error. So an operator who follows the guide finds nothing, and the host is still accepted. The sentence also gives a narrower cause than the code does: shouldDeleteSiteWhenAnotherSiteHasNoAccessPoint pins the host staying because another site's URLs cannot be read. (The round-3 review suggested warning itself.) Suggested guide text: "If the host name cannot be removed at that moment, for example because the DNS aliases or the URLs of the other servers and sites cannot be read or updated, the server or site is still deleted or its URL still changed, OpenAM logs an error in the amSetupServlet debug log, and the host name stays."
} catch (SMSException | SSOException | RuntimeException e) {
Debug.getInstance(SetupConstants.DEBUG_NAME).error("ConfigurationBase.removeOrganizationAlias: "
+ "Unable to remove " + hostName + " from the DNS aliases of the top-level realm", e);
}Or: keep warning, and have the guide say that the message is written only at debug level warning or message.
…rimary URL, and log a failed alias cleanup as an error - SiteConfiguration.deleteSite reads the primary URL from the access point itself and skips the alias cleanup when the URL is missing, so a site whose primary-url was removed by hand is deleted again, as before this change, instead of failing with a NullPointerException. - ConfigurationBase.removeOrganizationAlias logs a failed cleanup as an error, because the shipped debug level drops warnings and the host name stays a valid host name. The upgrade guide says so, and also names the URLs of the other servers and sites that cannot be read as a cause.
|
Round 5. The branch already sits on current master, so no rebase was needed. issue (non-blocking): issue (non-blocking): the alias removal is an unguarded read-modify-write of the root realm's issue (non-blocking): the upgrade guide promises a warning that a default install does not write. Fixed with the first option.
|
maximthomas
left a comment
There was a problem hiding this comment.
praise: This round removes the last way a site delete could fail on its own alias cleanup, and the upgrade guide matches the code.
SiteConfiguration.deleteSitereads the access point'sprimary-urlitself and handles both a missing and an empty value (SiteConfiguration.java:196-200), so a site without a primary URL is deleted and no alias removal is attempted for it.ConfigurationBase.removeOrganizationAliasnow logs at error (ConfigurationBase.java:238), so the failure reaches the defaultamSetupServletdebug log, andchap-upgrade-servers.adocsays "logs an error".
Fixes #1196
Problem
A request addressed to a realm DNS alias is rejected by the FQDN check before the realm is resolved, unless the alias also has its own
com.sun.identity.server.fqdnMap[...]entry. The valid host names come only from the FQDN map, the server host and the site hosts thatNamingService.updateFqdnMappingsadds, so a realm alias is never among them. Every place that checks the host is affected:FQDNValidationFilter(/XUI/*,/oauth2/authorize,/oauth2/device/user) redirects with 302 to the default host.LoginServlet(/UI/Login) redirects with 302 to the default host.RealmContextFilter(REST) answers 400FQDN "acme.example.com" is not valid.ConsoleServletBase(admin console) redirects to the server host.The strict check came with OPENAM-7340 to stop Host header injection, and the realm aliases were left out of it. The OpenAM guide does not ask for the extra FQDN map entry. PingAM, which continues ForgeRock AM, adds that entry automatically when an alias is saved in the admin UI.
Fix
FqdnValidator.isHostnameValidalso accepts a host name that is a DNS alias of a realm. It consults the aliases only for a host that is neither a value nor a key of the FQDN map, and only once OpenAM is configured (AMSetupServlet.isCurrentConfigurationValid(), asMonitoringConfigurationdoes). A host that the FQDN map maps to another host name is still redirected there even if it is a realm alias, so a short-name mapping such asfqdnMap[openam]=openam.example.comkeeps working althoughopenamis an alias of the root realm by default. Requests to mapped hosts and the configurator are not affected.getFullyQualifiedHostName, which the filters,LoginServlet(throughAuthClientUtils.getValidFQDNResource) andConsoleServletBasecall, followsisHostnameValid.RealmDnsAliaseskeeps the aliases of all realms in memory, in lower case. A listener onsunIdentityRepositoryService, the same oneCachingRealmLookuprelies on, makes it read them again after the realm configuration changes. The aliases are read from the realms themselves, so it does not matter whether an alias was changed through the admin console, REST orssoadm, and nothing is left behind in the FQDN map when an alias is removed.addListenerreturnsnull, and a concurrent first request waits for the registration. A change notified while the realms are being read makes the next call read them again. If the idRepo service or the admin token cannot be obtained, or the realms cannot be read, for any reason including an unchecked exception, the host is rejected and the next request tries again. IfaddListenerreturnsnull, the realms are read without a listener and read again once a later request registers it.OrganizationConfigManager.setAttributeswrites after the realm entry. Aliases read in between are kept until the next change to the realm configuration; the Javadoc ofRealmDnsAliasessays so.ServerConfiguration.deleteServerInstanceandSiteConfiguration.deleteSitenow remove the host name of the deleted server or site, andSiteConfiguration.setSitePrimaryURLthe host name of the replaced URL, unless the URL of a remaining server or site, primary or secondary, has the same host name. The host name is compared and removed regardless of letter case. A site whose access point has no primary URL is still deleted. If the host name cannot be removed at that moment, for example because the aliases or the URLs of the other servers and sites cannot be read or updated, the server or site is still deleted or its URL still changed, an error is logged and the host name stays. Host names left behind by servers and sites deleted before the upgrade stay, and a host name that the administrator added to these aliases by hand is removed together with the last server or site that has it. A new[IMPORTANT]block in the upgrade guide (chap-upgrade-servers.adoc) describes both and asks to review the aliases of every realm before promoting the upgrade.RealmGuiceModulebinds and exposesRealmDnsAliases, because the@Named(REPO_SERVICE)ServiceConfigManagerit needs is bound only inside that private module.An alias is configured by the administrator, just like an FQDN map entry, so the list of accepted host names stays under the administrator's control. A realm alias now behaves as the workaround entry
fqdnMap[<alias>]=<alias>did, the admin console included: the console is served on an alias host instead of being redirected to the server host, and an administrator still has to log in. By default the root realm's aliases hold the host names of the servers and sites and the RDN value of the root suffix (for exampleopenam), so these become valid host names as well, unless the FQDN map maps them to another host name.Test
RealmDnsAliasesTest(20 tests): aliases of the root realm and of subrealms, letter case, an alias added or removed and a realm created after a configuration change, one read while the configuration is unchanged, a change notified during the read, a failed read, an admin token or an idRepo service that throws, a listener registration retried after a failure with the aliases read before it discarded, and a concurrent first request that waits for the registration.RealmDnsAliasesConfigurationTest: the aliases of a nested realm are read throughOrganizationConfigManager. It runs under PowerMock with@SuppressStaticInitializationFor, because the static initialiser ofOrganizationConfigManagerneeds the configuration store.FqdnValidatorTest(7 tests): a realm alias is accepted and not redirected, an unknown host is still redirected to the default host, a key of the FQDN map that is also a realm alias is still redirected to the host it is mapped to, and the aliases are not consulted for a host that is a key or a value of the FQDN map or before OpenAM is configured.RealmGuiceModuleTest: the injector providesRealmDnsAliasesas a single instance.RootRealmAliasesTest(15 tests): deleting a server or a site, or replacing the primary URL of a site, removes its host name from the aliases of the top-level realm, also when the aliases hold it in another letter case, and keeps it while another server (also in other letter case), the primary or a secondary URL of a site, or the new URL of the site has it. A failure to read or update the aliases fails neither the delete of a server or site nor the change of the primary URL, a site without an access point is still rejected withConfigurationException, and a site whose access point has no primary URL is still deleted. It runs the realServerConfigurationandSiteConfigurationcode against an in-memory platform configuration and top-level realm, under PowerMock (whenNewforServiceConfigManagerandOrganizationConfigManager).expose(RealmDnsAliases.class)fails the Guice test withChildBindingAlreadySet. Not discarding the snapshot after the registration, or marking the instance initialised althoughaddListenerreturnednull, fails the retry test; registering withoutsynchronizedfails the concurrent-first-request test. Reading the subrealms non-recursively, readingsunOrganizationStatusinstead of the aliases, or dropping the/before a subrealm name failsRealmDnsAliasesConfigurationTest. Checking the realm aliases before the FQDN map keys failsshouldNotReadRealmDnsAliasesForHostInFqdnMap. InRootRealmAliasesTest, removing the host name without checking the remaining URLs, comparing host names case-sensitively, ignoring the site URLs or the server URLs, or removing it before the site or its old URL is gone each fails at least one test, and so do removing the alias only in the exact letter case, letting an exception from the cleanup escape, reading the primary URL of a site beforelistServers, and reading it withgetSitePrimaryURL, which throws for an access point without a primary URL.mvn -pl openam-core,openam-rest -am clean test: openam-core 2258 tests and openam-rest 285 tests, no failures.The change was not tried on a running OpenAM.