fix(security): load the system account from DS activation, not Shiro onInit() (GOSS-025) - #53
Merged
craigpnnl merged 2 commits intoAug 2, 2026
Conversation
SystemBasedRealm populates its account map only from Shiro's onInit() hook. onInit() is protected and reachable only from Initializable.init(), which nothing in goss-core, Felix DS, or shiro-core 2.0.0 ever calls on a DS-managed realm: RealmSecurityManager.setRealms() reaches only afterRealmsSet() -> applyCacheManagerToRealms(), not init(). The realm therefore activates with a permanently empty userMap, and the system principal cannot authenticate against it. SystemBasedRealmActivationTest asserts the realm resolves the system account after @activate alone, with the exact broker permission string, plus config reload and fail-closed behavior when the credential source is unusable. ActivatorRealmRemovalTest covers the shutdown defect on the same path: removing the last realm calls setRealms(emptySet), which Shiro rejects with IllegalArgumentException out of realmRemoved. RED: 8 failing across the two new classes, including "Realms collection argument cannot be empty." at Activator.java:123. Refs GOSS-025
…onInit() SystemBasedRealm built its only account inside onInit(), Shiro's Initializable hook. In this deployment nothing calls it. onInit() is protected and reachable only from AuthenticatingRealm.init(), which is normally driven by LifecycleUtils.init() from an ini or Spring bootstrap. Verified against shiro-core 2.0.0 bytecode: RealmSecurityManager.setRealms() reaches afterRealmsSet() -> applyCacheManagerToRealms() and applyEventBusToRealms(), never init(); LifecycleUtils appears only in destroy(). goss-core has no init() or LifecycleUtils call site. The realm therefore activated with a permanently empty userMap. The failure only surfaced where SystemBasedRealm was the sole registered realm: the reported message comes from ModularRealmAuthenticator's doSingleRealmAuthentication, which runs only when getRealms().size() == 1. With PropertyBasedRealm also present, AtLeastOneSuccessfulStrategy let the system principal authenticate through that realm instead, which is why a complete bundle set masked the defect. Changes: * The account load moves into a single idempotent loadSystemAccount(), and activate(), the modified callback, the SecurityConfig bind and updated callbacks, and onInit() all delegate to it. onInit() is kept wired so the realm stays correct under a host that does drive Shiro's lifecycle. * loadSystemAccount() clears before rebuilding, so a renamed manager user cannot leave the previous principal authenticating. * Missing SecurityConfig, or a blank goss.system.manager or goss.system.manager.password, now throws IllegalStateException naming the property and file. Out of activate() that aborts DS activation, so the realm service is never published, the Activator's mandatory realm.type=system reference stays unsatisfied, the SecurityManager service is never published, and no broker connection is made against an empty realm. * The SecurityConfig reference becomes a dynamic greedy method reference with unbind and updated callbacks. The manager credential comes from the pnnl.goss.security PID via SecurityConfigImpl, not from this component's own PID, so an edit to pnnl.goss.security.cfg only updates that service's registration properties. A field reference gave no callback for that and the realm kept a superseded credential. Unbind clears the account map. The system permission string, the ConfigurationPolicy.REQUIRE contract on pnnl.goss.core.security.systemrealm (GOSS-004), and the realm.type=system marker (GADP-012) are all unchanged. Also guards Activator.registerAllRealms() against an empty realm set. Shiro rejects setRealms(emptySet) with IllegalArgumentException, so removing the last realm threw out of realmRemoved() on every teardown. The guard declines the write and logs; it does not widen the authorization surface, because the realm reference is AT_LEAST_ONE and DS deactivates the component and unpublishes the SecurityManager service along with it. GREEN: 55 tests in pnnl.goss.core, up from 36. Full gradle build passes. Refs GOSS-025
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the GOSS-025 bootstrap failure where
SystemBasedRealmactivates with anempty account map, so the built-in
systemaccount cannot authenticate to thebroker.
Symptom
Cascade: empty system realm -> ActiveMQ refuses the connection -> GridOpticsServer
service factory returns null -> LogManagerImpl cannot resolve
ServerControl->FieldBusManagerImpl cannot resolve
LogManagerand bails. Reproduced 4 of 4 in alocal container stack, with the same realm instance empty throughout each run.
Root cause
SystemBasedRealmpopulateduserMaponly fromonInit(), Shiro'sInitializablehook. Verified against the actual dependency (shiro-core 2.0.0,
pnnl.goss.core/bnd.bnd:12):onInit()is invoked from exactly one place,AuthenticatingRealm.init(), normallydriven by
LifecycleUtils.init()from an ini or Spring bootstrap. No OSGi DeclarativeServices path reaches it.
RealmSecurityManager.setRealms()->afterRealmsSet()onlycalls
setCacheManagerandsetEventBus. There are zeroinit()/LifecycleUtils/Initializablecall sites in goss-core.The
@Activatemethod, which DS does guarantee, only logged.So the system realm's account map has been empty in every OSGi run since it was written.
Why this was not noticed until now
The message "unable to find account data for the submitted AuthenticationToken" occurs
in exactly one shiro-core class:
ModularRealmAuthenticator.doSingleRealmAuthentication,reached only when
getRealms().size() == 1. When a second realm is registered,doMultiRealmAuthenticationruns under the defaultAtLeastOneSuccessfulStrategyandPropertyBasedRealmauthenticates the samesystemaccount fromuserfile.cfg.The empty system realm was always masked by the property-file realm. An environment that
registered only the system realm removed the mask and exposed it.
This is not a regression in any recent release:
SystemBasedRealm.javaandGridOpticsServer.javaare byte-identical between v15.0.5 and v16.0.0.Changes
SystemBasedRealm: a single idempotentloadSystemAccount().@Activate,@Modified,the
SecurityConfigbind/unbind/updated callbacks, andonInit()all delegate to it,so no entry point is left broken. The map is cleared before rebuild, so a renamed
manager user cannot leave the previous principal able to authenticate.
SecurityConfigfield reference became a dynamic greedy method reference withunbindandupdated. The credential lives in thepnnl.goss.securityPID rather thanthis component's own PID, so
@Modifiedalone never saw apnnl.goss.security.cfgedit.SecurityConfig, or a blankgoss.system.managerorgoss.system.manager.password, throwsIllegalStateExceptionnaming the property andfile. Thrown from
@Activatethis aborts DS activation, so the realm service is neverpublished and the broker never connects against an empty realm. Previously this
condition produced a silently empty realm.
Activator.registerAllRealms()declines an empty realm set instead of throwing.Removing the last realm previously produced, on the shutdown path:
java.lang.IllegalArgumentException: Realms collection argument cannot be emptyat
RealmSecurityManager.setRealms(RealmSecurityManager.java:80).The permission string granted to the
systemaccount is unchanged.Tests
Three new test classes, 19 new tests. RED before the fix included
hasIdentifier("system")false immediately after
activate(), and theIllegalArgumentExceptionabove reproducedfrom
Activator.realmRemoved.Assertions check field values rather than absence of a crash: primary principal, credential,
and
getStringPermissions()matched exactly against the literal permission string, so afuture edit to the production grant fails the test. Descriptor-level assertions cover
activate,modified,configuration-policy=require, andconfiguration-pid, since thoseare what the container actually invokes.
Result: 55 passed, 0 failed (baseline 36, all previously passing). Full
./gradlew buildsucceeds. Two pre-existing build warnings are unrelated and unchanged.
Follow-ups, deliberately not addressed here
queue:*,topic:*,temp-queue:*,fusion:*:read,fusion:*:writeispassed as a single string to
addStringPermission.WildcardPermissionsplits on:before
,, producing eight parts rather than the five permissions it reads as, so itlikely does not grant what was intended. Preserved verbatim here because changing an
authorization grant belongs in its own reviewed change.
pnnl.goss.core.security.impl.SystemRealmappears to be dead code and hardcodes"*"against the literal key
"system"regardless of the configured username.