ACL: ConfigDbApiKeyStore confers roles from Cluster-scoped grants in ANY cluster #528

Closed
opened 2026-08-01 01:10:30 -04:00 by dohertj2 · 0 comments
Owner

Fixed in 3a6b2a1d (#520 stage 3). Filed for the record — this one reached master in fbe7403c.

ConfigDbApiKeyStore.LoadClusterGrantsAsync selected a principal's grants with
Where(a => a.Principal == principal && a.ScopeKind == NodeAclScopeKind.Cluster) and no ClusterId
predicate
. The ConfigDb holds the whole fleet's NodeAcl rows, so on a fused admin,driver node a
Cluster-scoped grant authored in any cluster conferred the server-wide WriteOperate /
AlarmAcknowledge role.

ArtifactApiKeyStore was never affected: it reads a snapshot DeploymentArtifact.ParseDataPlaneAuthority
has already narrowed with ClusterFilterMode.ScopeTo. The two stores' behaviour diverged while
ArtifactApiKeyStore's doc comment claimed it was "matching ConfigDbApiKeyStore" — it matched on scope
kind but not on cluster.

Fix. The store takes the node's own ClusterId, derived from AkkaClusterOptions via
RoleParser.IsClusterRole/ClusterIdFromRole (options, not IClusterRoleInfo — resolving that from the
SP is the documented boot-StackOverflow landmine). A null ClusterId applies no filter, mirroring
ClusterFilterMode.None, so single-cluster deployments are unchanged.

The cluster match is done in memory after the SQL fetch, deliberately: ParseDataPlaneAuthority matches
OrdinalIgnoreCase, and a.ClusterId == clusterId in SQL is decided by database collation instead. That is
not theoretical — Cluster:Roles binds verbatim (cluster-MAIN ⇒ MAIN) while the OTOPCUA_ROLES path
goes through RoleParser.Parse, which lowercases (⇒ main).

Regression test staged red→green: ConfigDbApiKeyStoreTests.A_cluster_scoped_grant_from_another_cluster_confers_nothing.

**Fixed in `3a6b2a1d` (#520 stage 3). Filed for the record — this one reached `master` in `fbe7403c`.** `ConfigDbApiKeyStore.LoadClusterGrantsAsync` selected a principal's grants with `Where(a => a.Principal == principal && a.ScopeKind == NodeAclScopeKind.Cluster)` and **no `ClusterId` predicate**. The ConfigDb holds the whole fleet's `NodeAcl` rows, so on a fused `admin,driver` node a `Cluster`-scoped grant authored in *any* cluster conferred the server-wide `WriteOperate` / `AlarmAcknowledge` role. `ArtifactApiKeyStore` was never affected: it reads a snapshot `DeploymentArtifact.ParseDataPlaneAuthority` has already narrowed with `ClusterFilterMode.ScopeTo`. The two stores' behaviour diverged while `ArtifactApiKeyStore`'s doc comment claimed it was "matching `ConfigDbApiKeyStore`" — it matched on scope kind but not on cluster. **Fix.** The store takes the node's own ClusterId, derived from `AkkaClusterOptions` via `RoleParser.IsClusterRole`/`ClusterIdFromRole` (options, not `IClusterRoleInfo` — resolving that from the SP is the documented boot-`StackOverflow` landmine). A null ClusterId applies no filter, mirroring `ClusterFilterMode.None`, so single-cluster deployments are unchanged. The cluster match is done **in memory after the SQL fetch**, deliberately: `ParseDataPlaneAuthority` matches `OrdinalIgnoreCase`, and `a.ClusterId == clusterId` in SQL is decided by database collation instead. That is not theoretical — `Cluster:Roles` binds verbatim (`cluster-MAIN` ⇒ `MAIN`) while the `OTOPCUA_ROLES` path goes through `RoleParser.Parse`, which lowercases (⇒ `main`). Regression test staged red→green: `ConfigDbApiKeyStoreTests.A_cluster_scoped_grant_from_another_cluster_confers_nothing`.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#528