Skip to content

fix(security): load the system account from DS activation, not Shiro onInit() (GOSS-025) - #53

Merged
craigpnnl merged 2 commits into
GridOPTICS:developfrom
craigpnnl:fix/goss-025-systemrealm-activation
Aug 2, 2026
Merged

fix(security): load the system account from DS activation, not Shiro onInit() (GOSS-025)#53
craigpnnl merged 2 commits into
GridOPTICS:developfrom
craigpnnl:fix/goss-025-systemrealm-activation

Conversation

@craigpnnl

Copy link
Copy Markdown
Contributor

Fixes the GOSS-025 bootstrap failure where SystemBasedRealm activates with an
empty account map, so the built-in system account cannot authenticate to the
broker.

Symptom

Failed to add Connection ... due to Realm [pnnl.goss.core.security.system.SystemBasedRealm@22587507]
was unable to find account data for the submitted AuthenticationToken
[org.apache.shiro.authc.UsernamePasswordToken - system, rememberMe=true].

WARN [pnnl.goss.core.server.impl.GridOpticsServer] - Error Connecting to ActiveMQ
  at pnnl.goss.core.server.impl.GridOpticsServer.start(GridOpticsServer.java:463)

Cascade: empty system realm -> ActiveMQ refuses the connection -> GridOpticsServer
service factory returns null -> LogManagerImpl cannot resolve ServerControl ->
FieldBusManagerImpl cannot resolve LogManager and bails. Reproduced 4 of 4 in a
local container stack, with the same realm instance empty throughout each run.

Root cause

SystemBasedRealm populated userMap only from onInit(), Shiro's Initializable
hook. 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(), normally
driven by LifecycleUtils.init() from an ini or Spring bootstrap. No OSGi Declarative
Services path reaches it. RealmSecurityManager.setRealms() -> afterRealmsSet() only
calls setCacheManager and setEventBus. There are zero init() / LifecycleUtils /
Initializable call sites in goss-core.

The @Activate method, 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,
doMultiRealmAuthentication runs under the default AtLeastOneSuccessfulStrategy and
PropertyBasedRealm authenticates the same system account from userfile.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.java and
GridOpticsServer.java are byte-identical between v15.0.5 and v16.0.0.

Changes

  • SystemBasedRealm: a single idempotent loadSystemAccount(). @Activate, @Modified,
    the SecurityConfig bind/unbind/updated callbacks, and onInit() 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.
  • The SecurityConfig field reference became a dynamic greedy method reference with
    unbind and updated. The credential lives in the pnnl.goss.security PID rather than
    this component's own PID, so @Modified alone never saw a pnnl.goss.security.cfg edit.
  • Fail closed: a missing SecurityConfig, or a blank goss.system.manager or
    goss.system.manager.password, throws IllegalStateException naming the property and
    file. Thrown from @Activate this aborts DS activation, so the realm service is never
    published 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 empty
    at RealmSecurityManager.setRealms(RealmSecurityManager.java:80).

The permission string granted to the system account is unchanged.

Tests

Three new test classes, 19 new tests. RED before the fix included hasIdentifier("system")
false immediately after activate(), and the IllegalArgumentException above reproduced
from 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 a
future edit to the production grant fails the test. Descriptor-level assertions cover
activate, modified, configuration-policy=require, and configuration-pid, since those
are what the container actually invokes.

Result: 55 passed, 0 failed (baseline 36, all previously passing). Full ./gradlew build
succeeds. Two pre-existing build warnings are unrelated and unchanged.

Follow-ups, deliberately not addressed here

  • The permission string queue:*,topic:*,temp-queue:*,fusion:*:read,fusion:*:write is
    passed as a single string to addStringPermission. WildcardPermission splits on :
    before ,, producing eight parts rather than the five permissions it reads as, so it
    likely 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.SystemRealm appears to be dead code and hardcodes "*"
    against the literal key "system" regardless of the configured username.

craig8 added 2 commits August 1, 2026 17:30
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
@craigpnnl
craigpnnl merged commit cddaae6 into GridOPTICS:develop Aug 2, 2026
8 checks passed
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.

2 participants