feat(common): ESPI FB-scope parser + Function Block catalog (#122 PR A)#136
Merged
Merged
Conversation
Foundation for grant→subscription resolution and resource-server enforcement. New package org.greenbuttonalliance.espi.common.scope: - FunctionBlock: authoritative catalog of NAESB ESPI 4.0 §REQ.21.4.2.1.3.1 ScopeFBTerms (id, category, optional ServiceKind, spec title) with tolerant byId/categoryOf/serviceKindOf lookups. Only active FBs are enumerated; DEPRECATED_IDS records spec-deprecated ids so they resolve to DEPRECATED rather than UNKNOWN. - FunctionBlockCategory: BASE / COMMODITY / ENERGY_DATA_SHAPE / CUSTOMER_PII (load-bearing) + INTERACTION / BULK_TRANSFER / AUTHORIZATION / ADMINISTRATION / PLATFORM / DEPRECATED / UNKNOWN. - EspiScope: FB-agnostic record parser for the FB=…;IntervalDuration=…; BlockDuration=…;HistoryLength=… grammar, deriving commodityServiceKinds(), functionBlocksIn(category), includesCustomerPii(), includesEnergyData(). Spec accuracy (per GBA-confirmed table): FB_71 does not exist (customer/PII = 54–62); FB_56/57 retitled; SFTP bulk FB_34/FB_66 deprecated (REST 35/67 remain); FB_29 Temperature is COMMODITY with no ServiceKind (XSD gap). The parser carries any FB id (future/deprecated/unknown) without error; the catalog is consulted only for semantics. openespi-authserver stays scope-opaque and keeps no dependency on this package — FB semantics live solely in the DC. 50 unit tests (EspiScopeTest, FunctionBlockTest), all green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
6 tasks
dfcoffin
added a commit
that referenced
this pull request
May 31, 2026
Adds the data-custodian side of the AS↔DC back-channel that the GBA
Authorization Server calls at token-mint time to materialize a
customer-approved grant into a persistent Authorization aggregate.
Service (openespi-common)
- SubscriptionProvisioningService — interface + SubscriptionProvisionCommand
/ SubscriptionProvisionResult records.
- SubscriptionProvisioningServiceImpl — parses the granted scope via
EspiScope (PR A), looks up the registered client, the retail customer,
and the customer-selected usage points, then builds the Authorization
aggregate (PR B1 N:1 model). Three branch shapes:
* energy-only → 1 resource Subscription
* energy + PII → resource + customer Subscription
* PII-only → customer Subscription, no resource_uri
- Aggregate-root persistence: one authorizationRepository.save carries
Subscriptions through via cascade=ALL (PR B1).
REST surface (openespi-datacustodian)
- POST /internal/backchannel/v1/subscriptions
- Request: {correlation_id, client_id, granted_scope, retail_customer_id,
selected_usage_point_ids[], customer_resource_uri?}
- Response (201): {authorization_id, resource_subscription_id?,
customer_subscription_id?, resource_uri?, authorization_uri,
customer_resource_uri?}
- 400 returns an OAuth2-style {error, error_description} on validation
failure or service-level rejection.
Security
- BackchannelSecurityConfiguration — dedicated SecurityFilterChain at
HIGHEST_PRECEDENCE matching /internal/**, HTTP Basic against a private
DaoAuthenticationProvider (no top-level UserDetailsService bean — the
public OAuth2 resource-server chain in SecurityConfiguration is
unaffected). CSRF/CORS disabled, STATELESS.
- Implementation contract, NOT ESPI standard. Network-level isolation
(internal-interface bind / ingress allow-list) is expected on top of
this in production; mTLS is a future enhancement tracked separately.
Config
- espi.backchannel.client-id / client-secret in application.yml,
overridable via BACKCHANNEL_CLIENT_ID / BACKCHANNEL_CLIENT_SECRET.
Tests
- SubscriptionProvisioningServiceImplTest — 12 tests covering happy
paths (energy-only, energy+PII, PII-only), every validation rejection
path (empty grant, missing/extra customer_resource_uri, unknown
client/customer/usage-point, cross-customer usage-point, unparseable
scope, blank required fields), and aggregate persistence shape.
- SubscriptionProvisioningControllerTest — 7 MockMvc tests covering
unauthenticated 401, wrong-credentials 401, happy-path 201 with
canonical URIs, missing/blank field 400, service-rejection 400 with
error body, and snake_case DTO round-trip.
Verification
- openespi-common: 848 tests pass.
- openespi-datacustodian: 97 tests pass (+1 pre-existing @disabled skip).
Refs: #122. Builds on #136 (PR A) and #137 (PR B1).
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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.
PR A — ESPI FB-scope parser + Function Block catalog
Foundation PR for the #122 grant→subscription wiring. Adds the shared value object and authoritative Function Block catalog that every downstream PR (B–F) consumes. No behavior change to existing code — purely additive in
openespi-common.What's here
New package
org.greenbuttonalliance.espi.common.scope:FunctionBlock— authoritative in-code catalog of NAESB ESPI 4.0 §REQ.21.4.2.1.3.1 ScopeFBTerms. Each constant carriesid,FunctionBlockCategory, optionalServiceKind, and the spec title. StaticbyId/categoryOf/serviceKindOflookups tolerate uncatalogued ids. Only active FBs are enumerated;DEPRECATED_IDSrecords spec-deprecated ids so they resolve toDEPRECATEDrather thanUNKNOWN.FunctionBlockCategory—BASE/COMMODITY/ENERGY_DATA_SHAPE/CUSTOMER_PII(the load-bearing four that drive resolution + enforcement) plusINTERACTION/BULK_TRANSFER/AUTHORIZATION/ADMINISTRATION/PLATFORM/DEPRECATED/UNKNOWN.EspiScope— FB-agnostic immutable record parser for theFB=4_5_15;IntervalDuration=3600;BlockDuration=monthly;HistoryLength=13grammar. Carries any FB id (future/deprecated/unknown) without error and derivescommodityServiceKinds(),functionBlocksIn(category),includesCustomerPii(),includesEnergyData().Spec accuracy (GBA-confirmed table, May 2026)
COMMODITYwith noServiceKind— the ESPI XSDServiceKindenumeration has no temperature value (documented standard gap).SCOPE_FB_36_*in DataCustodianSecurityConfiguration— flagged for the enforcement PR (F) to fix.Design notes
openespi-authserverscope-opaque with no dependency onopenespi-common— all ESPI FB semantics live solely in the Data Custodian, per the AS↔DC contract pinned on Phase 2.0 — Stand up auth-server in dev with opaque tokens + subscription-bound introspection (Phase 2 precursor) #122.Tests
50 unit tests (
EspiScopeTest,FunctionBlockTest), all green — including catalog guard tests (id uniqueness/range, deprecated-vs-active disjointness, commodity→ServiceKind mapping with the FB_29 exception) and parser robustness (case-insensitive keys, whitespace/stray-segment tolerance, null/blank + non-integer rejection, immutability).Refs #122.
🤖 Generated with Claude Code