@twin.org/dataspace-control-plane-service 0.0.3-next.41 → 0.0.3-next.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/dataspaceControlPlanePolicyRequester.js +1 -1
- package/dist/es/dataspaceControlPlanePolicyRequester.js.map +1 -1
- package/dist/es/dataspaceControlPlaneService.js +178 -329
- package/dist/es/dataspaceControlPlaneService.js.map +1 -1
- package/dist/es/models/IDataspaceControlPlaneServiceConstructorOptions.js.map +1 -1
- package/dist/types/dataspaceControlPlaneService.d.ts +5 -9
- package/dist/types/models/IDataspaceControlPlaneServiceConstructorOptions.d.ts +5 -6
- package/docs/changelog.md +28 -0
- package/docs/reference/classes/DataspaceControlPlaneService.md +5 -9
- package/docs/reference/interfaces/IDataspaceControlPlaneServiceConstructorOptions.md +9 -15
- package/locales/en.json +4 -8
- package/package.json +2 -2
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
// Copyright 2025 IOTA Stiftung.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0.
|
|
3
|
+
import { HttpUrlHelper } from "@twin.org/api-models";
|
|
4
|
+
import { ContextIdKeys, ContextIdStore, ContextIdHelper } from "@twin.org/context";
|
|
2
5
|
import { ArrayHelper, AlreadyExistsError, BaseError, ComponentFactory, Converter, GeneralError, Guards, Is, NotFoundError, ObjectHelper, RandomHelper, StringHelper, UnauthorizedError, Url, Urn, ValidationError } from "@twin.org/core";
|
|
3
6
|
import { JsonLdHelper } from "@twin.org/data-json-ld";
|
|
4
7
|
import { DataspaceAppFactory, DataspaceTransferFormat, TransferProcessRole, getJsonLdId, getJsonLdType } from "@twin.org/dataspace-models";
|
|
@@ -35,14 +38,6 @@ export class DataspaceControlPlaneService {
|
|
|
35
38
|
* @internal
|
|
36
39
|
*/
|
|
37
40
|
static _STALLED_NEGOTIATION_THRESHOLD_MS = 30 * 60 * 1000;
|
|
38
|
-
/**
|
|
39
|
-
* Matches the base64url encoding of a BLAKE2b-256 hash (32 bytes → 43 base64url
|
|
40
|
-
* chars, no padding). Used to discriminate the tenant-hash portion of a
|
|
41
|
-
* composite tenant identifier from a bare IOTA DID, whose `<id>` segment is
|
|
42
|
-
* always `0x<64-hex>` (66 chars) and cannot match.
|
|
43
|
-
* @internal
|
|
44
|
-
*/
|
|
45
|
-
static _TENANT_HASH_PATTERN = /^[\w-]{43}$/;
|
|
46
41
|
/**
|
|
47
42
|
* The logging component.
|
|
48
43
|
* @internal
|
|
@@ -77,7 +72,7 @@ export class DataspaceControlPlaneService {
|
|
|
77
72
|
*/
|
|
78
73
|
_transferProcessStorage;
|
|
79
74
|
/**
|
|
80
|
-
* Entity storage for
|
|
75
|
+
* Entity storage for Dataspace App Dataset entities.
|
|
81
76
|
* @internal
|
|
82
77
|
*/
|
|
83
78
|
_dataspaceAppDatasetStorage;
|
|
@@ -114,6 +109,11 @@ export class DataspaceControlPlaneService {
|
|
|
114
109
|
* @internal
|
|
115
110
|
*/
|
|
116
111
|
_taskScheduler;
|
|
112
|
+
/**
|
|
113
|
+
* Platform component.
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
_platformComponent;
|
|
117
117
|
/**
|
|
118
118
|
* Registered negotiation callbacks from upstream callers, keyed by registration key.
|
|
119
119
|
* @internal
|
|
@@ -135,17 +135,12 @@ export class DataspaceControlPlaneService {
|
|
|
135
135
|
* @internal
|
|
136
136
|
*/
|
|
137
137
|
_remoteControlPlaneComponentType;
|
|
138
|
-
/**
|
|
139
|
-
* The component type name for the hosting component.
|
|
140
|
-
* @internal
|
|
141
|
-
*/
|
|
142
|
-
_urlTransformerComponent;
|
|
143
138
|
/**
|
|
144
139
|
* Create a new instance of DataspaceControlPlaneService.
|
|
145
140
|
* @param options The options for the service.
|
|
146
141
|
*/
|
|
147
142
|
constructor(options) {
|
|
148
|
-
this._loggingComponent = ComponentFactory.getIfExists(options?.loggingComponentType
|
|
143
|
+
this._loggingComponent = ComponentFactory.getIfExists(options?.loggingComponentType);
|
|
149
144
|
// Retrieve PAP component with default
|
|
150
145
|
this._policyAdministrationPointComponent =
|
|
151
146
|
ComponentFactory.get(options?.policyAdministrationPointComponentType ?? "policy-administration-point");
|
|
@@ -164,6 +159,7 @@ export class DataspaceControlPlaneService {
|
|
|
164
159
|
? StringHelper.trimTrailingSlashes(StringHelper.trimLeadingSlashes(options.config.dataPlanePath))
|
|
165
160
|
: undefined;
|
|
166
161
|
this._taskScheduler = ComponentFactory.getIfExists(options?.taskSchedulerComponentType ?? "task-scheduler");
|
|
162
|
+
this._platformComponent = ComponentFactory.getIfExists(options?.platformComponentType ?? "platform");
|
|
167
163
|
// Data plane component is optional and resolved lazily. The control plane is initialised
|
|
168
164
|
// BEFORE the data plane in engine startup order, so `getRegisteredInstanceTypeOptional`
|
|
169
165
|
// returns undefined when the engine constructs us — the wiring override can't fire here.
|
|
@@ -172,14 +168,13 @@ export class DataspaceControlPlaneService {
|
|
|
172
168
|
// lookup at push time finds it regardless of init order.
|
|
173
169
|
this._dataPlaneComponentType =
|
|
174
170
|
options?.dataPlaneComponentType ?? "dataspace-data-plane-service";
|
|
175
|
-
this._urlTransformerComponent = ComponentFactory.get(options?.urlTransformerComponentType ?? "url-transformer");
|
|
176
171
|
this._negotiationCallbacks = new Map();
|
|
177
172
|
this._transferCallbacks = new Map();
|
|
178
173
|
this._internalTransferCallback = this.createInternalTransferCallback();
|
|
179
174
|
this._remoteControlPlaneComponentType =
|
|
180
175
|
options?.remoteControlPlaneComponentType ?? "dataspace-control-plane-rest-client";
|
|
181
176
|
const internalCallback = this.createInternalCallback();
|
|
182
|
-
this._policyRequester = new DataspaceControlPlanePolicyRequester(options?.loggingComponentType
|
|
177
|
+
this._policyRequester = new DataspaceControlPlanePolicyRequester(options?.loggingComponentType, internalCallback);
|
|
183
178
|
PolicyRequesterFactory.register(DataspaceControlPlaneService._REQUESTER_TYPE, () => this._policyRequester);
|
|
184
179
|
}
|
|
185
180
|
/**
|
|
@@ -227,10 +222,6 @@ export class DataspaceControlPlaneService {
|
|
|
227
222
|
}
|
|
228
223
|
/**
|
|
229
224
|
* The service needs to be started when the application is initialized.
|
|
230
|
-
* Populates the Federated Catalogue with datasets from registered apps
|
|
231
|
-
* and starts the stalled negotiation cleanup task. Also captures the node
|
|
232
|
-
* identity from ContextIdStore when tenant-token encryption is configured
|
|
233
|
-
* (required to derive the vault key name `${nodeId}/${signingKeyName}`).
|
|
234
225
|
* @param nodeLoggingComponentType The node logging component type.
|
|
235
226
|
*/
|
|
236
227
|
async start(nodeLoggingComponentType) {
|
|
@@ -254,49 +245,52 @@ export class DataspaceControlPlaneService {
|
|
|
254
245
|
let registeredCount = 0;
|
|
255
246
|
let errorCount = 0;
|
|
256
247
|
let totalDatasets = 0;
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
248
|
+
// The platform component execute is used so that the dataset publication runs in the
|
|
249
|
+
// tenant context of the hosting component, also works in single tenant mode
|
|
250
|
+
await this._platformComponent?.execute(async () => {
|
|
251
|
+
// The tenant context id is set here for each system tenant
|
|
252
|
+
let cursor;
|
|
253
|
+
do {
|
|
254
|
+
const page = await this._dataspaceAppDatasetStorage.query(undefined, undefined, undefined, cursor);
|
|
255
|
+
if (Is.arrayValue(page.entities)) {
|
|
256
|
+
for (const entity of page.entities) {
|
|
257
|
+
const appDataset = entity;
|
|
258
|
+
totalDatasets++;
|
|
259
|
+
try {
|
|
260
|
+
await this.publishAppDataset(appDataset);
|
|
261
|
+
registeredCount++;
|
|
262
|
+
await this._loggingComponent?.log({
|
|
263
|
+
level: "debug",
|
|
264
|
+
ts: Date.now(),
|
|
265
|
+
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
266
|
+
message: "datasetRegistered",
|
|
267
|
+
data: {
|
|
268
|
+
datasetId: appDataset.id,
|
|
269
|
+
appId: appDataset.appId,
|
|
270
|
+
tenantId: appDataset.tenantId
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
errorCount++;
|
|
276
|
+
await this._loggingComponent?.log({
|
|
277
|
+
level: "error",
|
|
278
|
+
ts: Date.now(),
|
|
279
|
+
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
280
|
+
message: "datasetPublishFailed",
|
|
281
|
+
error: BaseError.fromError(error),
|
|
282
|
+
data: {
|
|
283
|
+
datasetId: appDataset.id,
|
|
284
|
+
appId: appDataset.appId,
|
|
285
|
+
tenantId: appDataset.tenantId
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
295
289
|
}
|
|
296
290
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
291
|
+
cursor = page.cursor;
|
|
292
|
+
} while (Is.stringValue(cursor));
|
|
293
|
+
});
|
|
300
294
|
await this._loggingComponent?.log({
|
|
301
295
|
level: "info",
|
|
302
296
|
ts: Date.now(),
|
|
@@ -380,12 +374,11 @@ export class DataspaceControlPlaneService {
|
|
|
380
374
|
}
|
|
381
375
|
const assigneeIdentity = OdrlPolicyHelper.extractAssigneeIdentity(agreement);
|
|
382
376
|
const assigneeIds = ArrayHelper.fromObjectOrArray(assigneeIdentity);
|
|
383
|
-
// The agreement's assignee is stamped by the provider as the consumer's
|
|
384
|
-
|
|
385
|
-
if (!assigneeIds.includes(callerComposite)) {
|
|
377
|
+
// The agreement's assignee is stamped by the provider as the consumer's organization identity.
|
|
378
|
+
if (!assigneeIds.includes(trustInfo.identity)) {
|
|
386
379
|
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedForAgreement");
|
|
387
380
|
}
|
|
388
|
-
consumerIdentity =
|
|
381
|
+
consumerIdentity = trustInfo.identity;
|
|
389
382
|
const assignerIdentity = OdrlPolicyHelper.extractAssignerIdentity(agreement);
|
|
390
383
|
const assignerIds = ArrayHelper.fromObjectOrArray(assignerIdentity);
|
|
391
384
|
if (assignerIds.length > 1) {
|
|
@@ -400,8 +393,7 @@ export class DataspaceControlPlaneService {
|
|
|
400
393
|
return transformToTransferError(error, { consumerPid: request.consumerPid, providerPid });
|
|
401
394
|
}
|
|
402
395
|
const now = new Date();
|
|
403
|
-
const
|
|
404
|
-
const requestTenantId = requestContextIds?.[ContextIdKeys.Tenant];
|
|
396
|
+
const organizationIdentity = await this.resolveContextOrganizationId();
|
|
405
397
|
const storageEntity = {
|
|
406
398
|
id: Converter.bytesToHex(RandomHelper.generate(32)),
|
|
407
399
|
consumerPid: request.consumerPid,
|
|
@@ -417,7 +409,7 @@ export class DataspaceControlPlaneService {
|
|
|
417
409
|
policies,
|
|
418
410
|
callbackAddress: request.callbackAddress,
|
|
419
411
|
format: request.format,
|
|
420
|
-
|
|
412
|
+
organizationIdentity,
|
|
421
413
|
dataAddress: request.dataAddress,
|
|
422
414
|
dateCreated: now.toISOString(),
|
|
423
415
|
dateModified: now.toISOString()
|
|
@@ -484,8 +476,7 @@ export class DataspaceControlPlaneService {
|
|
|
484
476
|
const agreement = await this.lookupAgreement(agreementId);
|
|
485
477
|
const assigneeIdentity = OdrlPolicyHelper.extractAssigneeIdentity(agreement);
|
|
486
478
|
const assigneeIds = ArrayHelper.fromObjectOrArray(assigneeIdentity);
|
|
487
|
-
|
|
488
|
-
if (!assigneeIds.includes(callerComposite)) {
|
|
479
|
+
if (!assigneeIds.includes(trustInfo.identity)) {
|
|
489
480
|
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedForAgreement");
|
|
490
481
|
}
|
|
491
482
|
const assignerIdentity = OdrlPolicyHelper.extractAssignerIdentity(agreement);
|
|
@@ -498,11 +489,7 @@ export class DataspaceControlPlaneService {
|
|
|
498
489
|
const consumerPid = `urn:uuid:${RandomHelper.generateUuidV7()}`;
|
|
499
490
|
const callbackAddress = StringHelper.trimTrailingSlashes(publicOrigin);
|
|
500
491
|
// Fetch context once and reuse across the PUSH dataAddress, trust token, and storage entity.
|
|
501
|
-
const
|
|
502
|
-
const nodeIdentity = contextIds?.[ContextIdKeys.Node];
|
|
503
|
-
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "nodeIdentity", nodeIdentity);
|
|
504
|
-
const tenantId = contextIds?.[ContextIdKeys.Tenant];
|
|
505
|
-
const organizationIdentity = contextIds?.[ContextIdKeys.Organization];
|
|
492
|
+
const organizationIdentity = await this.resolveContextOrganizationId();
|
|
506
493
|
const transferRequestMessage = {
|
|
507
494
|
"@context": [DataspaceProtocolContexts.Context],
|
|
508
495
|
"@type": DataspaceProtocolTransferProcessTypes.TransferRequestMessage,
|
|
@@ -519,8 +506,8 @@ export class DataspaceControlPlaneService {
|
|
|
519
506
|
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "pushTransferDataPathNotConfigured", { consumerPid });
|
|
520
507
|
}
|
|
521
508
|
let inboxEndpoint = `${callbackAddress}/${this._dataPlanePath}/inbox`;
|
|
522
|
-
if (Is.stringValue(
|
|
523
|
-
inboxEndpoint =
|
|
509
|
+
if (Is.stringValue(organizationIdentity)) {
|
|
510
|
+
inboxEndpoint = HttpUrlHelper.addQueryStringParam(inboxEndpoint, ContextIdKeys.Organization, organizationIdentity);
|
|
524
511
|
}
|
|
525
512
|
transferRequestMessage.dataAddress = {
|
|
526
513
|
"@type": DataspaceProtocolTransferProcessTypes.DataAddress,
|
|
@@ -529,7 +516,7 @@ export class DataspaceControlPlaneService {
|
|
|
529
516
|
};
|
|
530
517
|
}
|
|
531
518
|
// Generate outbound trust token to authenticate this node to the provider.
|
|
532
|
-
const outboundToken = await this._trustComponent.generate(
|
|
519
|
+
const outboundToken = await this._trustComponent.generate(organizationIdentity, this._overrideTrustGeneratorType, { subject: { consumerPid, agreementId } });
|
|
533
520
|
// Create a remote REST client pointed at the provider endpoint and call requestTransfer.
|
|
534
521
|
const remoteControlPlane = ComponentFactory.create(this._remoteControlPlaneComponentType, { endpoint: providerEndpoint, pathPrefix: "" });
|
|
535
522
|
const result = await remoteControlPlane.requestTransfer(transferRequestMessage, outboundToken);
|
|
@@ -553,14 +540,14 @@ export class DataspaceControlPlaneService {
|
|
|
553
540
|
state: DataspaceProtocolTransferProcessStateType.REQUESTED,
|
|
554
541
|
agreementId,
|
|
555
542
|
datasetId,
|
|
556
|
-
consumerIdentity:
|
|
543
|
+
consumerIdentity: trustInfo.identity,
|
|
557
544
|
providerIdentity,
|
|
558
545
|
offerId: agreementId,
|
|
559
546
|
policies: [agreement],
|
|
560
547
|
callbackAddress,
|
|
561
548
|
format,
|
|
562
549
|
dataAddress: transferRequestMessage.dataAddress,
|
|
563
|
-
|
|
550
|
+
organizationIdentity,
|
|
564
551
|
dateCreated: now.toISOString(),
|
|
565
552
|
dateModified: now.toISOString()
|
|
566
553
|
};
|
|
@@ -608,14 +595,13 @@ export class DataspaceControlPlaneService {
|
|
|
608
595
|
}
|
|
609
596
|
try {
|
|
610
597
|
const { entity, role } = await this.lookupTransferByMessage(message);
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "transferWrongTenant");
|
|
598
|
+
if (trustInfo.identity !== entity.providerIdentity) {
|
|
599
|
+
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedAsProvider");
|
|
600
|
+
}
|
|
601
|
+
// Only the organization that owns this transfer can mutate it.
|
|
602
|
+
const callingOrganizationIdentity = await this.resolveContextOrganizationId();
|
|
603
|
+
if (entity.organizationIdentity !== callingOrganizationIdentity) {
|
|
604
|
+
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "transferWrongOrganization");
|
|
619
605
|
}
|
|
620
606
|
if (entity.state !== DataspaceProtocolTransferProcessStateType.REQUESTED &&
|
|
621
607
|
entity.state !== DataspaceProtocolTransferProcessStateType.SUSPENDED) {
|
|
@@ -665,27 +651,23 @@ export class DataspaceControlPlaneService {
|
|
|
665
651
|
if (!Is.stringValue(entity.providerIdentity)) {
|
|
666
652
|
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "providerIdentityMissing");
|
|
667
653
|
}
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
const accessToken = await this._trustComponent.generate(pushProviderId.nodeDid, this._overrideTrustGeneratorType, {
|
|
654
|
+
const pushProviderId = entity.providerIdentity;
|
|
655
|
+
const accessToken = await this._trustComponent.generate(pushProviderId, this._overrideTrustGeneratorType, {
|
|
671
656
|
subject: {
|
|
672
657
|
consumerPid: entity.consumerPid,
|
|
673
658
|
providerPid: entity.providerPid,
|
|
674
659
|
agreementId: entity.agreementId,
|
|
675
660
|
datasetId: entity.datasetId
|
|
676
661
|
}
|
|
677
|
-
}
|
|
662
|
+
});
|
|
678
663
|
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "accessToken", accessToken);
|
|
679
664
|
const tokenString = accessToken;
|
|
680
665
|
let fullEndpoint = `${publicOrigin}/${this._dataPlanePath}/inbox`;
|
|
681
|
-
// Bake the provider's
|
|
682
|
-
// inbound POST routes to the right
|
|
666
|
+
// Bake the provider's organization identity into the /inbox URL so the consumer's
|
|
667
|
+
// inbound POST routes to the right organization via TenantProcessor — mirrors the
|
|
683
668
|
// pull-mode endpoint baking below.
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
if (Is.stringValue(inboxTenantId1)) {
|
|
687
|
-
fullEndpoint = await this._urlTransformerComponent.addEncryptedQueryParamToUrl(fullEndpoint, "tenant", inboxTenantId1);
|
|
688
|
-
}
|
|
669
|
+
const organizationIdentity = await this.resolveContextOrganizationId();
|
|
670
|
+
fullEndpoint = HttpUrlHelper.addQueryStringParam(fullEndpoint, ContextIdKeys.Organization, organizationIdentity);
|
|
689
671
|
response.dataAddress = {
|
|
690
672
|
"@type": DataspaceProtocolTransferProcessTypes.DataAddress,
|
|
691
673
|
endpointType: DataspaceProtocolEndpointType.HttpsActivityStreamEndpoint,
|
|
@@ -725,29 +707,24 @@ export class DataspaceControlPlaneService {
|
|
|
725
707
|
}
|
|
726
708
|
// Provider signs the data access token with its own identity.
|
|
727
709
|
// The subject contains the transfer context claims that the data plane
|
|
728
|
-
// will verify when the consumer presents this token
|
|
729
|
-
// the composite providerIdentity to extract the signing nodeDid and
|
|
730
|
-
// the tenant-hash to embed as `tid`.
|
|
710
|
+
// will verify when the consumer presents this token
|
|
731
711
|
if (!Is.stringValue(entity.providerIdentity)) {
|
|
732
712
|
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "providerIdentityMissing");
|
|
733
713
|
}
|
|
734
|
-
const pullProviderId =
|
|
735
|
-
const accessToken = await this._trustComponent.generate(pullProviderId
|
|
714
|
+
const pullProviderId = entity.providerIdentity;
|
|
715
|
+
const accessToken = await this._trustComponent.generate(pullProviderId, this._overrideTrustGeneratorType, {
|
|
736
716
|
subject: {
|
|
737
717
|
consumerPid: entity.consumerPid,
|
|
738
718
|
providerPid: entity.providerPid,
|
|
739
719
|
agreementId: entity.agreementId,
|
|
740
720
|
datasetId: entity.datasetId
|
|
741
721
|
}
|
|
742
|
-
}
|
|
722
|
+
});
|
|
743
723
|
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "accessToken", accessToken);
|
|
744
724
|
const tokenString = accessToken;
|
|
745
725
|
let fullEndpoint = `${publicOrigin}/${this._dataPlanePath}`;
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
if (Is.stringValue(tenantId)) {
|
|
749
|
-
fullEndpoint = await this._urlTransformerComponent.addEncryptedQueryParamToUrl(fullEndpoint, "tenant", tenantId);
|
|
750
|
-
}
|
|
726
|
+
const organizationIdentity = await this.resolveContextOrganizationId();
|
|
727
|
+
fullEndpoint = HttpUrlHelper.addQueryStringParam(fullEndpoint, ContextIdKeys.Organization, organizationIdentity);
|
|
751
728
|
response.dataAddress = {
|
|
752
729
|
"@type": DataspaceProtocolTransferProcessTypes.DataAddress,
|
|
753
730
|
endpointType: DataspaceProtocolEndpointType.HttpsQueryEndpoint,
|
|
@@ -792,14 +769,11 @@ export class DataspaceControlPlaneService {
|
|
|
792
769
|
return transformToTransferError(new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "pushTransferDataPathNotConfigured", { consumerPid: entity.consumerPid }), message);
|
|
793
770
|
}
|
|
794
771
|
let fullEndpoint = `${publicOrigin}/${this._dataPlanePath}/inbox`;
|
|
795
|
-
// Bake the provider's
|
|
796
|
-
// inbound POST routes to the right
|
|
772
|
+
// Bake the provider's organization identity into the /inbox URL so the consumer's
|
|
773
|
+
// inbound POST routes to the right organization via TenantProcessor — mirrors the
|
|
797
774
|
// pull-mode endpoint baking.
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
if (Is.stringValue(inboxTenantId2)) {
|
|
801
|
-
fullEndpoint = await this._urlTransformerComponent.addEncryptedQueryParamToUrl(fullEndpoint, "tenant", inboxTenantId2);
|
|
802
|
-
}
|
|
775
|
+
const organizationIdentity = await this.resolveContextOrganizationId();
|
|
776
|
+
fullEndpoint = HttpUrlHelper.addQueryStringParam(fullEndpoint, ContextIdKeys.Organization, organizationIdentity);
|
|
803
777
|
response.dataAddress = {
|
|
804
778
|
"@type": DataspaceProtocolTransferProcessTypes.DataAddress,
|
|
805
779
|
endpointType: DataspaceProtocolEndpointType.HttpsActivityStreamEndpoint,
|
|
@@ -887,14 +861,13 @@ export class DataspaceControlPlaneService {
|
|
|
887
861
|
}
|
|
888
862
|
try {
|
|
889
863
|
const { entity, role } = await this.lookupTransferByMessage(message);
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "transferWrongTenant");
|
|
864
|
+
if (trustInfo.identity !== entity.consumerIdentity) {
|
|
865
|
+
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedAsConsumer");
|
|
866
|
+
}
|
|
867
|
+
// Only the organization that owns this transfer can mutate it.
|
|
868
|
+
const callingOrganizationId = await this.resolveContextOrganizationId();
|
|
869
|
+
if (entity.organizationIdentity !== callingOrganizationId) {
|
|
870
|
+
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "transferWrongOrganization");
|
|
898
871
|
}
|
|
899
872
|
// DSP idempotency: re-receiving TransferCompletionMessage for a transfer already
|
|
900
873
|
// in COMPLETED should return the same success response, not invalidStateForComplete.
|
|
@@ -981,14 +954,14 @@ export class DataspaceControlPlaneService {
|
|
|
981
954
|
}
|
|
982
955
|
try {
|
|
983
956
|
const { entity, role } = await this.lookupTransferByMessage(message);
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "
|
|
957
|
+
if (trustInfo.identity !== entity.consumerIdentity &&
|
|
958
|
+
trustInfo.identity !== entity.providerIdentity) {
|
|
959
|
+
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedForTransfer");
|
|
960
|
+
}
|
|
961
|
+
// S2 belt-and-braces: only the organization that owns this transfer can mutate it.
|
|
962
|
+
const callingOrganizationId = await this.resolveContextOrganizationId();
|
|
963
|
+
if (entity.organizationIdentity !== callingOrganizationId) {
|
|
964
|
+
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "transferWrongOrganization");
|
|
992
965
|
}
|
|
993
966
|
// DSP idempotency: re-receiving TransferSuspensionMessage for a transfer already
|
|
994
967
|
// in SUSPENDED should return success. Data-plane suspend already ran.
|
|
@@ -1079,14 +1052,14 @@ export class DataspaceControlPlaneService {
|
|
|
1079
1052
|
}
|
|
1080
1053
|
try {
|
|
1081
1054
|
const { entity, role } = await this.lookupTransferByMessage(message);
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "
|
|
1055
|
+
if (trustInfo.identity !== entity.consumerIdentity &&
|
|
1056
|
+
trustInfo.identity !== entity.providerIdentity) {
|
|
1057
|
+
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedForTransfer");
|
|
1058
|
+
}
|
|
1059
|
+
// Only the organization that owns this transfer can mutate it.
|
|
1060
|
+
const organizationIdentity = await this.resolveContextOrganizationId();
|
|
1061
|
+
if (entity.organizationIdentity !== organizationIdentity) {
|
|
1062
|
+
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "transferWrongOrganization");
|
|
1090
1063
|
}
|
|
1091
1064
|
// DSP idempotency: re-receiving TransferTerminationMessage for a transfer already
|
|
1092
1065
|
// in TERMINATED should return success. Data-plane teardown already ran.
|
|
@@ -1160,7 +1133,10 @@ export class DataspaceControlPlaneService {
|
|
|
1160
1133
|
const trustInfo = await TrustHelper.verifyTrust(this._trustComponent, trustPayload, "getTransferProcess");
|
|
1161
1134
|
try {
|
|
1162
1135
|
const { entity, role } = await this.lookupTransferByPid(pid);
|
|
1163
|
-
|
|
1136
|
+
if (trustInfo.identity !== entity.consumerIdentity &&
|
|
1137
|
+
trustInfo.identity !== entity.providerIdentity) {
|
|
1138
|
+
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedForTransfer");
|
|
1139
|
+
}
|
|
1164
1140
|
await this._loggingComponent?.log({
|
|
1165
1141
|
level: "info",
|
|
1166
1142
|
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
@@ -1218,7 +1194,8 @@ export class DataspaceControlPlaneService {
|
|
|
1218
1194
|
verifiedIdentity: trustInfo.identity
|
|
1219
1195
|
}
|
|
1220
1196
|
});
|
|
1221
|
-
const
|
|
1197
|
+
const organizationId = await this.resolveContextOrganizationId();
|
|
1198
|
+
const localTrustPayload = await this._trustComponent.generate(organizationId, this._overrideTrustGeneratorType, {});
|
|
1222
1199
|
const catalogResult = await this._federatedCatalogueComponent.get(datasetId, localTrustPayload);
|
|
1223
1200
|
if (isCatalogError(catalogResult)) {
|
|
1224
1201
|
if (isCatalogErrorName(catalogResult, NotFoundError.CLASS_NAME)) {
|
|
@@ -1391,23 +1368,14 @@ export class DataspaceControlPlaneService {
|
|
|
1391
1368
|
});
|
|
1392
1369
|
}
|
|
1393
1370
|
const agreement = await this.lookupAgreement(entity.agreementId);
|
|
1394
|
-
const
|
|
1395
|
-
const currentComposite = this.buildTenantIdentifier(contextIds?.[ContextIdKeys.Node], contextIds?.[ContextIdKeys.Tenant]);
|
|
1396
|
-
// Identity is `nodeDid:hash(tenantId)` (or just `nodeDid` in
|
|
1397
|
-
// single-tenant). Without a node DID in context we cannot derive any
|
|
1398
|
-
// caller identity at all.
|
|
1399
|
-
if (!Is.stringValue(currentComposite)) {
|
|
1400
|
-
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "organizationContextMissing", {
|
|
1401
|
-
consumerPid
|
|
1402
|
-
});
|
|
1403
|
-
}
|
|
1371
|
+
const organizationIdentity = await this.resolveContextOrganizationId();
|
|
1404
1372
|
const assignerIdentity = OdrlPolicyHelper.extractAssignerIdentity(agreement);
|
|
1405
1373
|
const assignerIds = ArrayHelper.fromObjectOrArray(assignerIdentity);
|
|
1406
|
-
if (!assignerIds.includes(
|
|
1374
|
+
if (!assignerIds.includes(organizationIdentity)) {
|
|
1407
1375
|
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "agreementAssignerMismatch", {
|
|
1408
1376
|
consumerPid,
|
|
1409
1377
|
agreementId: OdrlPolicyHelper.getUid(agreement),
|
|
1410
|
-
|
|
1378
|
+
organizationIdentity,
|
|
1411
1379
|
actualAssigner: assignerIds.join(", ")
|
|
1412
1380
|
});
|
|
1413
1381
|
}
|
|
@@ -1422,7 +1390,7 @@ export class DataspaceControlPlaneService {
|
|
|
1422
1390
|
state: entity.state,
|
|
1423
1391
|
consumerIdentity: entity.consumerIdentity,
|
|
1424
1392
|
agreementId: OdrlPolicyHelper.getUid(agreement),
|
|
1425
|
-
organizationId:
|
|
1393
|
+
organizationId: organizationIdentity
|
|
1426
1394
|
}
|
|
1427
1395
|
});
|
|
1428
1396
|
return {
|
|
@@ -1453,20 +1421,14 @@ export class DataspaceControlPlaneService {
|
|
|
1453
1421
|
});
|
|
1454
1422
|
}
|
|
1455
1423
|
const agreement = await this.lookupAgreement(entity.agreementId);
|
|
1456
|
-
const
|
|
1457
|
-
const currentComposite = this.buildTenantIdentifier(contextIds?.[ContextIdKeys.Node], contextIds?.[ContextIdKeys.Tenant]);
|
|
1458
|
-
if (!Is.stringValue(currentComposite)) {
|
|
1459
|
-
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "organizationContextMissingProvider", {
|
|
1460
|
-
providerPid
|
|
1461
|
-
});
|
|
1462
|
-
}
|
|
1424
|
+
const organizationIdentity = await this.resolveContextOrganizationId();
|
|
1463
1425
|
const assignerIdentity = OdrlPolicyHelper.extractAssignerIdentity(agreement);
|
|
1464
1426
|
const assignerIds = ArrayHelper.fromObjectOrArray(assignerIdentity);
|
|
1465
|
-
if (!assignerIds.includes(
|
|
1427
|
+
if (!assignerIds.includes(organizationIdentity)) {
|
|
1466
1428
|
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "agreementAssignerMismatchProvider", {
|
|
1467
1429
|
providerPid,
|
|
1468
1430
|
agreementId: OdrlPolicyHelper.getUid(agreement),
|
|
1469
|
-
|
|
1431
|
+
organizationIdentity,
|
|
1470
1432
|
actualAssigner: assignerIds.join(", ")
|
|
1471
1433
|
});
|
|
1472
1434
|
}
|
|
@@ -1496,7 +1458,7 @@ export class DataspaceControlPlaneService {
|
|
|
1496
1458
|
consumerIdentity: entity.consumerIdentity,
|
|
1497
1459
|
providerIdentity: entity.providerIdentity,
|
|
1498
1460
|
agreementId: OdrlPolicyHelper.getUid(agreement),
|
|
1499
|
-
organizationId:
|
|
1461
|
+
organizationId: organizationIdentity
|
|
1500
1462
|
}
|
|
1501
1463
|
});
|
|
1502
1464
|
return {
|
|
@@ -1512,7 +1474,7 @@ export class DataspaceControlPlaneService {
|
|
|
1512
1474
|
};
|
|
1513
1475
|
}
|
|
1514
1476
|
/**
|
|
1515
|
-
* Register a dataset for a dataspace app, owned by the calling
|
|
1477
|
+
* Register a dataset for a dataspace app, owned by the calling organization.
|
|
1516
1478
|
* @param id Optional explicit id. If omitted, derived from `dataset["@id"]`
|
|
1517
1479
|
* or generated.
|
|
1518
1480
|
* @param appId The dataspace app this dataset belongs to.
|
|
@@ -1537,8 +1499,7 @@ export class DataspaceControlPlaneService {
|
|
|
1537
1499
|
id: resolvedId
|
|
1538
1500
|
});
|
|
1539
1501
|
}
|
|
1540
|
-
const
|
|
1541
|
-
const nodeIdentity = await this.resolveNodeIdentity();
|
|
1502
|
+
const organizationIdentity = await this.resolveContextOrganizationId();
|
|
1542
1503
|
const existing = await this._dataspaceAppDatasetStorage.get(resolvedId);
|
|
1543
1504
|
if (!Is.empty(existing)) {
|
|
1544
1505
|
throw new AlreadyExistsError(DataspaceControlPlaneService.CLASS_NAME, "datasetAlreadyExists", resolvedId);
|
|
@@ -1546,8 +1507,9 @@ export class DataspaceControlPlaneService {
|
|
|
1546
1507
|
const now = new Date().toISOString();
|
|
1547
1508
|
const entity = {
|
|
1548
1509
|
id: resolvedId,
|
|
1549
|
-
|
|
1550
|
-
|
|
1510
|
+
organizationIdentity,
|
|
1511
|
+
// Required for out of bound dataset operations that need to correlate back to the owning tenant
|
|
1512
|
+
tenantId: await this.resolveContextTenantId(),
|
|
1551
1513
|
appId,
|
|
1552
1514
|
dataset: ObjectHelper.omit(dataset, ["@id"]),
|
|
1553
1515
|
dateCreated: now,
|
|
@@ -1559,19 +1521,19 @@ export class DataspaceControlPlaneService {
|
|
|
1559
1521
|
return resolvedId;
|
|
1560
1522
|
}
|
|
1561
1523
|
/**
|
|
1562
|
-
* Get a dataset record owned by the calling
|
|
1524
|
+
* Get a dataset record owned by the calling organization.
|
|
1563
1525
|
* @param id The stored dataset id.
|
|
1564
1526
|
* @returns The stored dataset record.
|
|
1565
1527
|
*/
|
|
1566
1528
|
async getAppDataset(id) {
|
|
1567
1529
|
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "id", id);
|
|
1568
|
-
const
|
|
1530
|
+
const organizationId = await this.resolveContextOrganizationId();
|
|
1569
1531
|
const entity = await this._dataspaceAppDatasetStorage.get(id);
|
|
1570
1532
|
if (Is.empty(entity)) {
|
|
1571
1533
|
throw new NotFoundError(DataspaceControlPlaneService.CLASS_NAME, "datasetNotFound", id);
|
|
1572
1534
|
}
|
|
1573
|
-
if (entity.
|
|
1574
|
-
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "
|
|
1535
|
+
if (entity.organizationIdentity !== organizationId) {
|
|
1536
|
+
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "datasetWrongOrganization");
|
|
1575
1537
|
}
|
|
1576
1538
|
return {
|
|
1577
1539
|
id: entity.id,
|
|
@@ -1582,20 +1544,18 @@ export class DataspaceControlPlaneService {
|
|
|
1582
1544
|
};
|
|
1583
1545
|
}
|
|
1584
1546
|
/**
|
|
1585
|
-
* List the dataspace app datasets owned by the calling
|
|
1547
|
+
* List the dataspace app datasets owned by the calling organization.
|
|
1586
1548
|
* @param cursor Optional pagination cursor.
|
|
1587
1549
|
* @param limit Optional maximum number of entries to return.
|
|
1588
1550
|
* @returns The stored datasets and the next-page cursor if more exist.
|
|
1589
1551
|
*/
|
|
1590
1552
|
async listAppDatasets(cursor, limit) {
|
|
1591
|
-
const
|
|
1592
|
-
const page = await this._dataspaceAppDatasetStorage.query(
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
}
|
|
1598
|
-
: undefined, undefined, undefined, cursor, limit);
|
|
1553
|
+
const organizationIdentity = await this.resolveContextOrganizationId();
|
|
1554
|
+
const page = await this._dataspaceAppDatasetStorage.query({
|
|
1555
|
+
property: "organizationIdentity",
|
|
1556
|
+
value: organizationIdentity,
|
|
1557
|
+
comparison: ComparisonOperator.Equals
|
|
1558
|
+
}, undefined, undefined, cursor, limit);
|
|
1599
1559
|
const entities = (page.entities ?? []).map(entity => ({
|
|
1600
1560
|
id: entity.id,
|
|
1601
1561
|
appId: entity.appId,
|
|
@@ -1609,7 +1569,7 @@ export class DataspaceControlPlaneService {
|
|
|
1609
1569
|
};
|
|
1610
1570
|
}
|
|
1611
1571
|
/**
|
|
1612
|
-
* Update a dataset record owned by the calling
|
|
1572
|
+
* Update a dataset record owned by the calling organization.
|
|
1613
1573
|
* @param id The stored dataset id.
|
|
1614
1574
|
* @param appId The dataspace app this dataset belongs to.
|
|
1615
1575
|
* @param dataset The dataset payload.
|
|
@@ -1618,13 +1578,13 @@ export class DataspaceControlPlaneService {
|
|
|
1618
1578
|
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "id", id);
|
|
1619
1579
|
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "appId", appId);
|
|
1620
1580
|
Guards.object(DataspaceControlPlaneService.CLASS_NAME, "dataset", dataset);
|
|
1621
|
-
const
|
|
1581
|
+
const organizationId = await this.resolveContextOrganizationId();
|
|
1622
1582
|
const existing = await this._dataspaceAppDatasetStorage.get(id);
|
|
1623
1583
|
if (Is.empty(existing)) {
|
|
1624
1584
|
throw new NotFoundError(DataspaceControlPlaneService.CLASS_NAME, "datasetNotFound", id);
|
|
1625
1585
|
}
|
|
1626
|
-
if (existing.
|
|
1627
|
-
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "
|
|
1586
|
+
if (existing.organizationIdentity !== organizationId) {
|
|
1587
|
+
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "datasetWrongOrganization");
|
|
1628
1588
|
}
|
|
1629
1589
|
const updated = {
|
|
1630
1590
|
...existing,
|
|
@@ -1637,20 +1597,20 @@ export class DataspaceControlPlaneService {
|
|
|
1637
1597
|
await this._dataspaceAppDatasetStorage.set(updated);
|
|
1638
1598
|
}
|
|
1639
1599
|
/**
|
|
1640
|
-
* Delete a dataspace app dataset owned by the calling
|
|
1600
|
+
* Delete a dataspace app dataset owned by the calling organization.
|
|
1641
1601
|
* @param id The stored app dataset id.
|
|
1642
1602
|
*/
|
|
1643
1603
|
async deleteAppDataset(id) {
|
|
1644
1604
|
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "id", id);
|
|
1645
|
-
const
|
|
1605
|
+
const organizationId = await this.resolveContextOrganizationId();
|
|
1646
1606
|
const existing = await this._dataspaceAppDatasetStorage.get(id);
|
|
1647
1607
|
if (Is.empty(existing)) {
|
|
1648
1608
|
throw new NotFoundError(DataspaceControlPlaneService.CLASS_NAME, "datasetNotFound", id);
|
|
1649
1609
|
}
|
|
1650
|
-
if (existing.
|
|
1651
|
-
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "
|
|
1610
|
+
if (existing.organizationIdentity !== organizationId) {
|
|
1611
|
+
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "datasetWrongOrganization");
|
|
1652
1612
|
}
|
|
1653
|
-
const localTrustPayload = await this.
|
|
1613
|
+
const localTrustPayload = await this._trustComponent.generate(existing.organizationIdentity, this._overrideTrustGeneratorType, {});
|
|
1654
1614
|
const removeResult = await this._federatedCatalogueComponent.remove(id, localTrustPayload);
|
|
1655
1615
|
if (isCatalogError(removeResult)) {
|
|
1656
1616
|
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "datasetRemoveFailed", {
|
|
@@ -1730,7 +1690,7 @@ export class DataspaceControlPlaneService {
|
|
|
1730
1690
|
providerIdentity: storageEntity.providerIdentity,
|
|
1731
1691
|
format: storageEntity.format,
|
|
1732
1692
|
callbackAddress: storageEntity.callbackAddress,
|
|
1733
|
-
|
|
1693
|
+
organizationIdentity: storageEntity.organizationIdentity,
|
|
1734
1694
|
dateCreated: new Date(storageEntity.dateCreated),
|
|
1735
1695
|
dateModified: new Date(storageEntity.dateModified),
|
|
1736
1696
|
policies: storageEntity.policies,
|
|
@@ -1756,7 +1716,7 @@ export class DataspaceControlPlaneService {
|
|
|
1756
1716
|
providerIdentity: entity.providerIdentity,
|
|
1757
1717
|
format: entity.format,
|
|
1758
1718
|
callbackAddress: entity.callbackAddress,
|
|
1759
|
-
|
|
1719
|
+
organizationIdentity: entity.organizationIdentity,
|
|
1760
1720
|
dateCreated: entity.dateCreated.toISOString(),
|
|
1761
1721
|
dateModified: entity.dateModified.toISOString(),
|
|
1762
1722
|
policies: entity.policies,
|
|
@@ -1783,57 +1743,6 @@ export class DataspaceControlPlaneService {
|
|
|
1783
1743
|
}
|
|
1784
1744
|
return agreement;
|
|
1785
1745
|
}
|
|
1786
|
-
/**
|
|
1787
|
-
* Build the caller's composite identity (`nodeDid:tenantIdHash`) from a
|
|
1788
|
-
* verified trust payload.
|
|
1789
|
-
* @param trustInfo The verification info from `TrustHelper.verifyTrust`.
|
|
1790
|
-
* @returns The composite identity.
|
|
1791
|
-
* @internal
|
|
1792
|
-
*/
|
|
1793
|
-
buildCallerComposite(trustInfo) {
|
|
1794
|
-
Guards.object(DataspaceControlPlaneService.CLASS_NAME, "trustInfo", trustInfo);
|
|
1795
|
-
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "trustInfo.identity", trustInfo.identity);
|
|
1796
|
-
return Is.stringValue(trustInfo.tenantId)
|
|
1797
|
-
? `${trustInfo.identity}:${trustInfo.tenantId}`
|
|
1798
|
-
: trustInfo.identity;
|
|
1799
|
-
}
|
|
1800
|
-
/**
|
|
1801
|
-
* Validate that the caller's verified identity matches the consumer of a transfer process.
|
|
1802
|
-
* @param callerComposite The caller's composite identity (`nodeDid:tenantIdHash`).
|
|
1803
|
-
* @param entity The transfer process entity.
|
|
1804
|
-
* @throws UnauthorizedError if the caller is not the consumer.
|
|
1805
|
-
* @internal
|
|
1806
|
-
*/
|
|
1807
|
-
validateCallerIsConsumer(callerComposite, entity) {
|
|
1808
|
-
if (callerComposite !== entity.consumerIdentity) {
|
|
1809
|
-
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedAsConsumer");
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
/**
|
|
1813
|
-
* Validate that the caller's verified identity matches the provider of a transfer process.
|
|
1814
|
-
* @param callerComposite The caller's composite identity (`nodeDid:tenantIdHash`).
|
|
1815
|
-
* @param entity The transfer process entity.
|
|
1816
|
-
* @throws UnauthorizedError if the caller is not the provider.
|
|
1817
|
-
* @internal
|
|
1818
|
-
*/
|
|
1819
|
-
validateCallerIsProvider(callerComposite, entity) {
|
|
1820
|
-
if (callerComposite !== entity.providerIdentity) {
|
|
1821
|
-
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedAsProvider");
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
|
-
/**
|
|
1825
|
-
* Validate that the caller's verified identity matches either the consumer or provider of a transfer process.
|
|
1826
|
-
* @param callerComposite The caller's composite identity (`nodeDid:tenantIdHash`).
|
|
1827
|
-
* @param entity The transfer process entity.
|
|
1828
|
-
* @throws UnauthorizedError if the caller is not a party to the transfer.
|
|
1829
|
-
* @internal
|
|
1830
|
-
*/
|
|
1831
|
-
validateCallerIsTransferParty(callerComposite, entity) {
|
|
1832
|
-
if (callerComposite !== entity.consumerIdentity &&
|
|
1833
|
-
callerComposite !== entity.providerIdentity) {
|
|
1834
|
-
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedForTransfer");
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
1746
|
/**
|
|
1838
1747
|
* Extract dataset ID from Agreement target.
|
|
1839
1748
|
* @param format Agreement.
|
|
@@ -1882,8 +1791,8 @@ export class DataspaceControlPlaneService {
|
|
|
1882
1791
|
async validateCatalogDataset(datasetId, agreement) {
|
|
1883
1792
|
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "datasetId", datasetId);
|
|
1884
1793
|
Guards.object(DataspaceControlPlaneService.CLASS_NAME, "agreement", agreement);
|
|
1885
|
-
|
|
1886
|
-
const localTrustPayload = await this.
|
|
1794
|
+
const organizationId = await this.resolveContextOrganizationId();
|
|
1795
|
+
const localTrustPayload = await this._trustComponent.generate(organizationId, this._overrideTrustGeneratorType, {});
|
|
1887
1796
|
const catalogResult = await this._federatedCatalogueComponent.get(datasetId, localTrustPayload);
|
|
1888
1797
|
if (isCatalogError(catalogResult)) {
|
|
1889
1798
|
if (isCatalogErrorName(catalogResult, NotFoundError.CLASS_NAME)) {
|
|
@@ -2090,52 +1999,11 @@ export class DataspaceControlPlaneService {
|
|
|
2090
1999
|
return dataset;
|
|
2091
2000
|
}
|
|
2092
2001
|
const contextIds = await ContextIdStore.getContextIds();
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
if (compositePublisher) {
|
|
2096
|
-
return { ...dataset, "dcterms:publisher": compositePublisher };
|
|
2002
|
+
if (contextIds?.[ContextIdKeys.Organization]) {
|
|
2003
|
+
return { ...dataset, "dcterms:publisher": contextIds[ContextIdKeys.Organization] };
|
|
2097
2004
|
}
|
|
2098
2005
|
return dataset;
|
|
2099
2006
|
}
|
|
2100
|
-
/**
|
|
2101
|
-
* Build the tenant-attributed identifier.
|
|
2102
|
-
* @param nodeId The node identity (from `ContextIdKeys.Node`).
|
|
2103
|
-
* @param tenantId The plaintext tenant id (from `ContextIdKeys.Tenant`), if any. Hashed before insertion into the composite.
|
|
2104
|
-
* @returns Composite identifier, or undefined if nodeId is missing.
|
|
2105
|
-
* @internal
|
|
2106
|
-
*/
|
|
2107
|
-
buildTenantIdentifier(nodeId, tenantId) {
|
|
2108
|
-
if (!Is.stringValue(nodeId)) {
|
|
2109
|
-
return undefined;
|
|
2110
|
-
}
|
|
2111
|
-
return Is.stringValue(tenantId) ? `${nodeId}:${TrustHelper.hashTenantId(tenantId)}` : nodeId;
|
|
2112
|
-
}
|
|
2113
|
-
/**
|
|
2114
|
-
* Parse a composite tenant identifier into its node and tenant-hash portions.
|
|
2115
|
-
* @param composite The composite identifier (`nodeDid` or `nodeDid:hash`).
|
|
2116
|
-
* @returns The parsed parts. `tenantIdHash` is undefined when the composite has no tenant portion (single-tenant node form).
|
|
2117
|
-
* @throws GuardError if the input is not a non-empty string.
|
|
2118
|
-
* @throws GeneralError if the input does not start with `did:` — the only valid composite identifier shapes carry a DID as the leading portion.
|
|
2119
|
-
* @internal
|
|
2120
|
-
*/
|
|
2121
|
-
parseTenantIdentifier(composite) {
|
|
2122
|
-
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "composite", composite);
|
|
2123
|
-
if (!composite.startsWith("did:")) {
|
|
2124
|
-
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "invalidCompositeIdentifier", { composite });
|
|
2125
|
-
}
|
|
2126
|
-
const lastColon = composite.lastIndexOf(":");
|
|
2127
|
-
if (lastColon === -1) {
|
|
2128
|
-
return { nodeDid: composite };
|
|
2129
|
-
}
|
|
2130
|
-
const candidateHash = composite.slice(lastColon + 1);
|
|
2131
|
-
if (DataspaceControlPlaneService._TENANT_HASH_PATTERN.test(candidateHash)) {
|
|
2132
|
-
return {
|
|
2133
|
-
nodeDid: composite.slice(0, lastColon),
|
|
2134
|
-
tenantIdHash: candidateHash
|
|
2135
|
-
};
|
|
2136
|
-
}
|
|
2137
|
-
return { nodeDid: composite };
|
|
2138
|
-
}
|
|
2139
2007
|
/**
|
|
2140
2008
|
* Resolve the data plane component or throw if it isn't registered. Push-mode transfers
|
|
2141
2009
|
* require the data plane; pull-only deployments may run without it.
|
|
@@ -2315,26 +2183,22 @@ export class DataspaceControlPlaneService {
|
|
|
2315
2183
|
}
|
|
2316
2184
|
// DATASPACE APP DATASET HANDLERS
|
|
2317
2185
|
/**
|
|
2318
|
-
* Publish a single dataspace app dataset to the federated catalogue
|
|
2186
|
+
* Publish a single dataspace app dataset to the federated catalogue.
|
|
2319
2187
|
* @param appDataset The stored dataspace app dataset entity.
|
|
2320
2188
|
* @internal
|
|
2321
2189
|
*/
|
|
2322
2190
|
async publishAppDataset(appDataset) {
|
|
2323
|
-
|
|
2324
|
-
//
|
|
2325
|
-
//
|
|
2326
|
-
const contextIds =
|
|
2327
|
-
const
|
|
2328
|
-
|
|
2329
|
-
...contextIds,
|
|
2330
|
-
[ContextIdKeys.Tenant]: appDataset.tenantId
|
|
2331
|
-
};
|
|
2332
|
-
await ContextIdStore.run(wrappedContextIds, async () => {
|
|
2191
|
+
const localTrustPayload = await this._trustComponent.generate(appDataset.organizationIdentity, this._overrideTrustGeneratorType);
|
|
2192
|
+
// Ensure that any dataspace app operations are run with the tenant id
|
|
2193
|
+
// of the dataset owner, so that propagates to any operations it performs
|
|
2194
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
2195
|
+
const tenantContextIds = { ...contextIds, [ContextIdKeys.Tenant]: appDataset.tenantId };
|
|
2196
|
+
await ContextIdStore.run(tenantContextIds, async () => {
|
|
2333
2197
|
const app = DataspaceAppFactory.get(appDataset.appId);
|
|
2334
2198
|
const datasetPayload = this.restampDatasetId(appDataset.dataset, appDataset.id);
|
|
2335
2199
|
const overrideHandler = app.datasetsHandled?.bind(app);
|
|
2336
2200
|
const rawDatasets = overrideHandler
|
|
2337
|
-
? await overrideHandler(datasetPayload
|
|
2201
|
+
? await overrideHandler(datasetPayload)
|
|
2338
2202
|
: [datasetPayload];
|
|
2339
2203
|
const datasets = await Promise.all(rawDatasets.map(async (d) => this.populateDefaults(d)));
|
|
2340
2204
|
for (const dataset of datasets) {
|
|
@@ -2351,38 +2215,23 @@ export class DataspaceControlPlaneService {
|
|
|
2351
2215
|
});
|
|
2352
2216
|
}
|
|
2353
2217
|
/**
|
|
2354
|
-
* Resolve the calling
|
|
2355
|
-
* @returns The owning
|
|
2218
|
+
* Resolve the calling organization from the request context.
|
|
2219
|
+
* @returns The owning organization identity.
|
|
2356
2220
|
* @internal
|
|
2357
2221
|
*/
|
|
2358
|
-
async
|
|
2222
|
+
async resolveContextOrganizationId() {
|
|
2359
2223
|
const contextIds = await ContextIdStore.getContextIds();
|
|
2360
|
-
|
|
2361
|
-
return
|
|
2224
|
+
ContextIdHelper.guard(contextIds, ContextIdKeys.Organization);
|
|
2225
|
+
return contextIds[ContextIdKeys.Organization];
|
|
2362
2226
|
}
|
|
2363
2227
|
/**
|
|
2364
|
-
* Resolve the
|
|
2365
|
-
* @returns The owning
|
|
2228
|
+
* Resolve the tenant identity from the current context.
|
|
2229
|
+
* @returns The owning tenant identity.
|
|
2366
2230
|
* @internal
|
|
2367
2231
|
*/
|
|
2368
|
-
async
|
|
2232
|
+
async resolveContextTenantId() {
|
|
2369
2233
|
const contextIds = await ContextIdStore.getContextIds();
|
|
2370
|
-
|
|
2371
|
-
if (!Is.stringValue(nodeId)) {
|
|
2372
|
-
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "datasetNodeContextRequired");
|
|
2373
|
-
}
|
|
2374
|
-
return nodeId;
|
|
2375
|
-
}
|
|
2376
|
-
/**
|
|
2377
|
-
* Generate a trust payload representing this node for internal FederatedCatalogue calls.
|
|
2378
|
-
* @param tenantId Optional tenant ID to embed in the token.
|
|
2379
|
-
* @returns The generated trust payload.
|
|
2380
|
-
* @internal
|
|
2381
|
-
*/
|
|
2382
|
-
async generateLocalTrustPayload(tenantId) {
|
|
2383
|
-
const nodeId = await this.resolveNodeIdentity();
|
|
2384
|
-
const contextIds = (await ContextIdStore.getContextIds()) ?? {};
|
|
2385
|
-
return this._trustComponent.generate(nodeId, this._overrideTrustGeneratorType, {}, Is.stringValue(tenantId) ? TrustHelper.hashTenantId(tenantId) : undefined, contextIds[ContextIdKeys.Organization]);
|
|
2234
|
+
return contextIds?.[ContextIdKeys.Tenant];
|
|
2386
2235
|
}
|
|
2387
2236
|
}
|
|
2388
2237
|
//# sourceMappingURL=dataspaceControlPlaneService.js.map
|