Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
bec3f78
feat: add Construct-X Policy Hub client and import functionality
gostev-p Aug 28, 2026
e14f09e
chore: remove connector-uebersicht.md from repo, keep it local-onl
gostev-p Sep 24, 2026
41842cc
feat: add DataUsageStartDate constraint function and related support …
gostev-p Sep 28, 2026
4c875fd
feat: enhance UsagePurpose support for ACCESS policies (add use_case …
gostev-p Sep 28, 2026
cc32b25
feat: update FrameworkAgreementConstraintFunction to support new nota…
gostev-p Sep 28, 2026
f65cc08
feat: refine UsagePolicyValidator to enforce "at least one" constrain…
gostev-p Sep 28, 2026
159f7dd
feat: enhance PolicyHub integration to include human-readable policy …
gostev-p Oct 1, 2026
c5d214c
create and update requests in bruno collection
gostev-p Oct 1, 2026
27904bf
feat: refine UsagePurpose handling to support ACCESS policies, fix NP…
gostev-p Oct 5, 2026
2b66266
test: add PolicyHubClientExtensionTest to verify behavior with and wi…
gostev-p Oct 5, 2026
b0f064a
correct licenses company naming
gostev-p Oct 5, 2026
7677d11
docs: remove known limitation regarding multi-value right operands se…
gostev-p Oct 14, 2026
9a1da0c
docs: update README.md
gostev-p Oct 16, 2026
42a0a68
refactor: update copyright information, remove unused classes, and im…
gostev-p Oct 16, 2026
1fe8d90
feat: add Construct-X policy functions and validators for DataUsage a…
gostev-p Oct 16, 2026
b652b8e
feat: implement support for odrl:or logical constraint in Construct-X…
gostev-p Oct 16, 2026
7d22188
update README for con-x-policy extension
gostev-p Oct 19, 2026
4d54a7c
feat: update dependencies and add new validator-spi module
gostev-p Oct 19, 2026
2b8666d
feat: implement periodic sync for policies from the Policy Hub
gostev-p Oct 22, 2026
8733266
refactor PolicyHubSyncService
gostev-p Oct 22, 2026
81c9d55
refactor: reorganize package structure for Policy Hub services
gostev-p Oct 22, 2026
52b6aad
feat: add ConnectorConstraintFunction for participant selection in po…
gostev-p Oct 29, 2026
61ed15e
revert original bruno collection to their default configuration
gostev-p Oct 29, 2026
53ae6b7
create a bruno collection copy for the policy hub import scenario
gostev-p Oct 29, 2026
de042af
feat: implement handling for unreachable Policy Hub during sync opera…
gostev-p Nov 5, 2026
7f9ba61
extend con-x-policy extension to provide datasubset constraint mapping
gostev-p Nov 5, 2026
7ede303
update bruno collection for policy-hub import
gostev-p Nov 5, 2026
e8e6f6b
Merge branch 'develop' into feat/policy-import-extension
gostev-p Nov 5, 2026
e45f7e2
update bruno collection for policy-hub import remove old requests
gostev-p Nov 6, 2026
b606dec
fix docker-compose.yaml
gostev-p Nov 6, 2026
ca22768
update READMEs
gostev-p Nov 6, 2026
3ce567f
update doc im bruno collection for SyncPoliciesFromHub.bru
gostev-p Nov 6, 2026
fd64c95
add new tests
gostev-p Nov 6, 2026
41c6882
enhance error handling for Policy Hub sync and import operations
gostev-p Nov 6, 2026
5efb7e7
add support for credential attributes in basic-abac extension for acc…
gostev-p Nov 6, 2026
0b70e98
add catalog-scope date constraints for access policies in Construct-X
gostev-p Nov 6, 2026
3f3712a
fix: remove BDRS/BPN dependency from the Construct-X control plane
gostev-p Nov 12, 2026
bc59984
feat: evaluate the Membership constraint through basic-abac
gostev-p Nov 12, 2026
8763cdc
refactor: update Construct-X policy extension to remove Catena-X depe…
gostev-p Nov 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,53 @@ dependencies {

implementation("org.eclipse.edc:vault-hashicorp:$edcVersion")

implementation(project(":edc-extensions:policy-hub-client"))

implementation("org.eclipse.tractusx.edc:agreements:$txVersion")
implementation("org.eclipse.tractusx.edc:retirement-evaluation-store-sql:$txVersion")
implementation(project(":edc-extensions:agreements:retirement-evaluation-bootstrapping"))
implementation(project(":edc-extensions:dynamic-issuers"))
implementation(project(":edc-extensions:basic-abac"))


// Construct-X's own policy extension (con-x-policy, ConxPolicyExtension): the ODRL policy functions and the
// policy definition validation of the Construct-X connector. Required so the control plane knows the left
// operands (Membership, UsagePurpose, FrameworkAgreement, DataSubset, ...) emitted by the Construct-X Policy
// Hub; without them edc.policy.validation.enabled=true already rejects the policy on creation.
// The Catena-X policy extension (CxPolicyExtension) is NOT used: con-x-policy only reuses individual classes of
// the unmodified cx-policy module as a library, and the ShadowJar below removes CxPolicyExtension from the runtime.
// Deliberately no Catena-X BPN/BDRS extensions (bpn-validation, bdrs-client): Construct-X uses DIDs as
// participant ids and con-x-policy does not register the BusinessPartnerGroup/BusinessPartnerNumber functions.
implementation(project(":edc-extensions:con-x-policy"))

// Hotfix version overrides
implementation("com.fasterxml.jackson.core:jackson-core:2.22.3")
implementation("com.fasterxml.jackson.core:jackson-databind:2.22.4")
}

configurations.runtimeClasspath {
// No Catena-X BPN/BDRS code in the Construct-X runtime, not even interfaces: the unmodified cx-policy library
// depends on these two SPI modules, but only for the BusinessPartnerGroup/BusinessPartnerNumber functions and
// CxPolicyExtension, none of which con-x-policy uses or loads.
exclude(group = "org.eclipse.tractusx.edc", module = "bdrs-client-spi")
exclude(group = "org.eclipse.tractusx.edc", module = "bpn-validation-spi")
}

/**
* Merges `META-INF/services` files like `mergeServiceFiles()`, but drops the given service implementations.
*/
class ExcludingServiceFileTransformer(private val excluded: Set<String>) :
com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer() {
override fun transform(context: com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext) {
val lines = context.inputStream.bufferedReader().use { it.readLines() }.filterNot { it.trim() in excluded }
super.transform(context.copy(inputStream = lines.joinToString("\n").byteInputStream()))
}
}

tasks.withType<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar> {
mergeServiceFiles()
// Only Construct-X's own ConxPolicyExtension (con-x-policy) may run. The Catena-X CxPolicyExtension arrives on the
// classpath with the cx-policy library classes; drop it from the service file so it is never loaded (EDC composes
// policy validators, so it would otherwise still reject what Construct-X accepts).
transform(ExcludingServiceFileTransformer(setOf("org.eclipse.tractusx.edc.policy.cx.CxPolicyExtension")))
duplicatesStrategy = DuplicatesStrategy.INCLUDE
archiveFileName.set("con-x-controlplane-postgresql-hashicorp-vault.jar")
transform(com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer())
Expand All @@ -61,4 +92,4 @@ tasks.named("build") {

application {
mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,53 @@ dependencies {
implementation("org.eclipse.edc:controlplane-feature-sql-bom:$edcVersion")

implementation(project(":edc-extensions:sql-vault"))
implementation(project(":edc-extensions:policy-hub-client"))

implementation("org.eclipse.tractusx.edc:agreements:$txVersion")
implementation("org.eclipse.tractusx.edc:retirement-evaluation-store-sql:$txVersion")
implementation(project(":edc-extensions:agreements:retirement-evaluation-bootstrapping"))
implementation(project(":edc-extensions:dynamic-issuers"))
implementation(project(":edc-extensions:basic-abac"))


// Construct-X's own policy extension (con-x-policy, ConxPolicyExtension): the ODRL policy functions and the
// policy definition validation of the Construct-X connector. Required so the control plane knows the left
// operands (Membership, UsagePurpose, FrameworkAgreement, DataSubset, ...) emitted by the Construct-X Policy
// Hub; without them edc.policy.validation.enabled=true already rejects the policy on creation.
// The Catena-X policy extension (CxPolicyExtension) is NOT used: con-x-policy only reuses individual classes of
// the unmodified cx-policy module as a library, and the ShadowJar below removes CxPolicyExtension from the runtime.
// Deliberately no Catena-X BPN/BDRS extensions (bpn-validation, bdrs-client): Construct-X uses DIDs as
// participant ids and con-x-policy does not register the BusinessPartnerGroup/BusinessPartnerNumber functions.
implementation(project(":edc-extensions:con-x-policy"))

// Hotfix version overrides
implementation("com.fasterxml.jackson.core:jackson-core:2.22.3")
implementation("com.fasterxml.jackson.core:jackson-databind:2.22.4")
}

configurations.runtimeClasspath {
// No Catena-X BPN/BDRS code in the Construct-X runtime, not even interfaces: the unmodified cx-policy library
// depends on these two SPI modules, but only for the BusinessPartnerGroup/BusinessPartnerNumber functions and
// CxPolicyExtension, none of which con-x-policy uses or loads.
exclude(group = "org.eclipse.tractusx.edc", module = "bdrs-client-spi")
exclude(group = "org.eclipse.tractusx.edc", module = "bpn-validation-spi")
}

/**
* Merges `META-INF/services` files like `mergeServiceFiles()`, but drops the given service implementations.
*/
class ExcludingServiceFileTransformer(private val excluded: Set<String>) :
com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer() {
override fun transform(context: com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext) {
val lines = context.inputStream.bufferedReader().use { it.readLines() }.filterNot { it.trim() in excluded }
super.transform(context.copy(inputStream = lines.joinToString("\n").byteInputStream()))
}
}

tasks.withType<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar> {
mergeServiceFiles()
// Only Construct-X's own ConxPolicyExtension (con-x-policy) may run. The Catena-X CxPolicyExtension arrives on the
// classpath with the cx-policy library classes; drop it from the service file so it is never loaded (EDC composes
// policy validators, so it would otherwise still reject what Construct-X accepts).
transform(ExcludingServiceFileTransformer(setOf("org.eclipse.tractusx.edc.policy.cx.CxPolicyExtension")))
duplicatesStrategy = DuplicatesStrategy.INCLUDE
archiveFileName.set("con-x-controlplane-postgresql-vault.jar")
transform(com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer())
Expand All @@ -60,4 +91,4 @@ tasks.named("build") {

application {
mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime")
}
}
26 changes: 26 additions & 0 deletions edc-controlplane/edc-controlplane-construct-x/local/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,32 @@ are interested
in a more detailed explanation of these interactions, please see
the [EDC Samples](https://github.com/eclipse-edc/Samples/tree/main/transfer).

#### Importing policies from the Construct-X Policy Hub (optional)

The provider control plane can pull policies directly from a locally running
[Construct-X Policy Hub](https://github.com/project-construct-x/policy-hub) instead of you creating them by hand.
Start the Policy Hub stack (its own `docker-compose.yml`, backend on port 8080), run the `identities` folder of
`con-x-local-test` as described above, and then use the separate Bruno collection
[`bruno/con-x-policy-hub-import`](bruno/con-x-policy-hub-import). It contains only the Policy Hub requests. Set
`HUB_POLICY_ID` in its environment to the UUID of the hub policy and run `ImportPolicyFromHub`. That calls
`POST /management/v1alpha/policyhub/policies/{id}/import` on the provider control plane, which fetches the ODRL
rendering from the hub (URL and vault-stored basic-auth credentials are configured via the `constructx.policyhub.*`
settings in the `docker-compose.yaml`) and stores it as a policy definition under that UUID. `Check Imported Policy`,
`List All Policies` and `SyncPoliciesFromHub` inspect and refresh the imported policies.

To offer an asset under the imported policy, run `CreateAsset` from `con-x-local-test` and set `accessPolicyId` and
`contractPolicyId` in its `CreateContractDefinition` request to the same `HUB_POLICY_ID`; the catalog, negotiation and
transfer requests of `con-x-local-test` then work as usual. If you run without the Policy Hub, use the policy requests
in `con-x-local-test` as they are. For a step-by-step walkthrough see the
[`policy-hub-client` README](../../../edc-extensions/policy-hub-client/README.md#how-an-import-runs).

The Docker images of the control plane **and** the data plane must both be built from the same checkout
(`./gradlew :edc-controlplane:edc-controlplane-construct-x:con-x-controlplane-postgresql-hashicorp-vault:dockerize` and
`./gradlew :edc-dataplane:edc-dataplane-construct-x:con-x-dataplane-postgresql-hashicorp-vault:dockerize`).
The compose file only references these images (`image:`, no `build:`), so `docker compose up --build` does not
rebuild them. A stale data plane image next to a newer control plane makes transfers fail with a `500` on
`/control/v1/dataflows/start`.

NOTE: The requests in the `transactions/consumer` folder are definitely not suited for being run in batch mode. Instead,
especially before you run the `InitiateNegotiation` or the `Get EDR` requests, it is strongly recommended that you make sure
that a healthy timespan (like 2 seconds) lies between them and the respective previous request. This is because the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,25 @@ create_and_store_aes_key() {
# create AES keys for wallets
create_and_store_aes_key "issuer-wallet"
create_and_store_aes_key "consumer-wallet"
create_and_store_aes_key "provider-wallet"
create_and_store_aes_key "provider-wallet"

# Basic-auth credentials the provider control plane uses to read policies from the
# Construct-X Policy Hub. Format is "user:password"; the connector base64-encodes it itself.
# These are the dev-profile defaults of the Policy Hub (application-dev.yaml).
store_policy_hub_credentials() {
local alias="policy-hub-credentials"
local credentials="${POLICY_HUB_CREDENTIALS:-admin:admin}"

jq -n --arg content "$credentials" '{data:{content:$content}}' | \
curl -sSf \
-H "X-Vault-Token: $TOKEN" \
-H "Content-Type: application/json" \
-X POST \
--data-binary @- \
"$VAULT/v1/secret/data/${alias}" \
|| { echo "Failed to create policy hub credentials entry"; exit 1; }

echo "Policy Hub credentials stored at secret/data/${alias}"
}

store_policy_hub_credentials
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"version": "1",
"name": "con-x-policy-hub-import",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
vars {
PROVIDER_MANAGEMENT: http://localhost:39010/management
HUB_POLICY_ID: 73dd2da1-1ee9-4c2b-9358-51298700a912
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
meta {
name: transactions
}

auth {
mode: inherit
}

docs {
Requests for importing and inspecting Construct-X Policy Hub policies on the provider.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
meta {
name: Check Imported Policy
type: http
seq: 4
}

get {
url: {{PROVIDER_MANAGEMENT}}/v4/policydefinitions/{{HUB_POLICY_ID}}
body: none
auth: inherit
}

headers {
X-Api-Key: prov-management-api-key
}

settings {
encodeUrl: true
timeout: 0
forwardAuthorizationHeader: false
}

docs {
Reads back the policy imported via ImportPolicyFromHub. The import stores the policy under the
Policy Hub's UUID, which is HUB_POLICY_ID, so no other variable is needed. The response's
"privateProperties.policyHubName" carries the Policy Hub's human-readable policy name.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
meta {
name: ImportPolicyFromHub
type: http
seq: 3
}

post {
url: {{PROVIDER_MANAGEMENT}}/v1alpha/policyhub/policies/{{HUB_POLICY_ID}}/import
body: none
auth: inherit
}

script:post-response {
bru.setVar("importedPolicyId", res.getBody()['@id']);
bru.setVar("importedPolicyName", res.getBody()['policyHubName']);
}

settings {
encodeUrl: true
timeout: 0
}

docs {
Pulls a policy from the Construct-X Policy Hub into the provider control plane. The control
plane fetches GET {hub}/api/v1/policies/{{HUB_POLICY_ID}}/odrl itself (URL and vault-based
basic-auth credentials are configured via constructx.policyhub.* in the docker-compose.yaml)
and stores it as a local policy definition. Repeating the request updates the definition.

The returned "@id" is the Policy Hub's UUID (same value as HUB_POLICY_ID), not the Policy
Hub's human-readable policy name - the stored policy definition is addressed by that UUID in
the connector. The human-readable name (e.g. "meine-policy-date") is returned separately as
"policyHubName", so it stays easy to tell which Policy Hub policy a given connector-side UUID
corresponds to.

The policy is stored under the Policy Hub UUID (= HUB_POLICY_ID), so a contract definition
created in the con-x-local-test collection references it with accessPolicyId /
contractPolicyId = HUB_POLICY_ID. The post-response script additionally keeps "@id" and
"policyHubName" in the runtime variables "importedPolicyId" / "importedPolicyName" for
readability; they only exist within the current Bruno session.

Prerequisite: the Policy Hub stack (policy-hub/docker-compose.yml) is running on port 8080.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
meta {
name: List All Policies
type: http
seq: 6
}

post {
url: {{PROVIDER_MANAGEMENT}}/v3/policydefinitions/request
body: json
auth: inherit
}

headers {
X-Api-Key: prov-management-api-key
}

body:json {
{
"@context": {
"@vocab": "https://w3id.org/edc/v0.0.1/ns/"
},
"@type": "QuerySpec",
"offset": 0,
"limit": 50
}
}

settings {
encodeUrl: true
timeout: 0
forwardAuthorizationHeader: false
}

docs {
Lists every policy definition stored in the provider connector - imported from the Policy Hub
via ImportPolicyFromHub as well as hand-written ones (e.g. those created in the con-x-local-test collection). Reads the
connector database only, never the Policy Hub. Imported policies are recognisable by
"privateProperties.policyHubName". Adjust "offset"/"limit" to paginate or add a
"filterExpression" to narrow the result.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
meta {
name: SyncPoliciesFromHub
type: http
seq: 7
}

post {
url: {{PROVIDER_MANAGEMENT}}/v1alpha/policyhub/policies/sync
body: none
auth: inherit
}

settings {
encodeUrl: true
timeout: 0
}

docs {
Re-imports every policy definition previously imported from the Construct-X Policy Hub right
away - the same run the control plane otherwise performs periodically (every hour by default,
see constructx.policyhub.sync.*). Only the connector's own imports (those carrying
"privateProperties.policyHubName") are synced; hand-written policies are left
alone. Runs synchronously, no body.

Response: counts of "checked", "updated", "unchanged" and "failed" policy definitions. A policy
is only written when its content in the hub changed. Failed policies (e.g. hub returns 404 or
the new version fails validation) are kept unchanged - the control plane log shows the reason as
a warning ("Policy Hub sync (manual): keeping policy definition ... unchanged: ...").

If the Policy Hub itself is unreachable (connection refused, timeout, 5xx, 408, 429), this
request does not wait for it: it aborts right away with 503 Service Unavailable, and the log
shows "Policy Hub sync (manual) aborted: Policy Hub unreachable (...)". Only the periodic sync
waits for the hub (up to 60 s) and, if it stays away, retries the run every 5 minutes. To try
it: docker stop policyhub-backend, run this request, then docker start policyhub-backend.

If the Policy Hub rejects the connector's credentials (401/403, e.g. a wrong secret under the
vault alias policy-hub-credentials), the run aborts at once without waiting and this request
answers 502 Bad Gateway; the log shows "Policy Hub sync (manual) aborted: Policy Hub rejected
the credentials (...)". The periodic sync aborts the same way and tries again after 5 minutes,
so a corrected secret takes effect without a restart.

To try it: run ImportPolicyFromHub, change the policy in the Policy Hub, run this request and
check with Check Imported Policy that the new version and
"privateProperties.policyHubImportedAt" are there.

Prerequisite: the Policy Hub stack (policy-hub/docker-compose.yml) is running on port 8080.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
meta {
name: provider
seq: 1
}

headers {
x-api-key: prov-management-api-key
}

auth {
mode: inherit
}

docs {
Policy Hub import requests for the provider connector. Everything else (identities, assets,
contract definitions, catalog, negotiation, transfer) is done with the con-x-local-test
collection - run its identities folder first.
}
Loading
Loading