Skip to content

Support aggregator specification#80

Open
joachimvh wants to merge 5 commits intomainfrom
feat/aggregator
Open

Support aggregator specification#80
joachimvh wants to merge 5 commits intomainfrom
feat/aggregator

Conversation

@joachimvh
Copy link
Contributor

@joachimvh joachimvh commented Jan 30, 2026

https://spec.knows.idlab.ugent.be/aggregator-protocol/latest/

Implements everything that is needed to support the above specification. A full run of the process can be seen in the Aggregation.test.ts integration test.

Notes

  • The current implementation requires you to have permission for everything you want to do. E.g., a policy needs to grant explicit derivation-creation permission to an aggregator before it can request a derivation_resource_id, and similarly for derivation-read.
  • Main support is done by a new AggregatorNegotiator which adds the derivation_resource_id to relevant responses, and a AggregatorStrategy which handles the derivation-read permissions for derived resources.
  • A derivation_resource_id is generated by encoding the ID string. This way no additional mapping needs to be stored on the server. It can then be decoded by the server when needed.
  • The claim_tokens field when requesting a token now also supports an array of token/format entries.
  • I removed the ContractNegotiator from the configuration as it currently returns a mostly hardcoded contract and performs requests that always fail.
  • The derived_from field is supported when registering a resource.
  • A new storage is used to keep track of derivation_resource_id -> issuer links for derived_from entries in resource registrations. This way we can check if access tokens for these identifiers are issued by the correct issuer.
  • The UMA configuration is now also accessible through .well-known/openid-configuration. I only needed the jwks field to be accessible but it was easier to just link everything there. Reason being that when validating the access tokens used during the aggregator interactions, this is where the OIDC library will look to find the key to validate these tokens.
  • Reworked the ticket strategies a bit to make it more easier to create the aggregator strategy. If something like this is needed in the future I think it makes more sense to have a new look at this.
    • The credentials function is removed from the interface as the main authorizer did not support it anyway, and it introduced a lot of similar-ish code.
    • The claim-elimination strategy was removed for the same reason.
    • The ticket object no longer stores an array of opaque requirement functions.
    • The contents of a failed validation call are now the required_claims objects which are needed for the need_info error.
  • There is a new internal ACCESS claim which has as value an array of id/scopes entries, indicating which permissions were (already) granted to the client from other sources.

Other stuff that came up

  • Access tokens from AS server have aud: solid, but are not actual valid Solid OIDC tokens due to missing webid field, do we still want this?
  • The ODRLAuthorizer takes as input "CSS" modes, and rewrites them to "ODRL" modes, to then convert back in the output. Having looked into this now, this feels somewhat weird. IMO either the policies should use CSS modes (probably don't want this), or this conversion should happen on the RS side before the AS is contacted. I don't think it should be the responsibility of the AS to do this conversion.

@termontwouter
Copy link
Collaborator

To already give a quick answer the 'other stuff':

  • Audience in Solid-OIDC was totally not correct, so we should indeed change it to the URI of the RS.

  • The modes of the RS should indeed be used in the policies. I don't know why we do that rewrite...

Copy link
Collaborator

@termontwouter termontwouter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good qua implementation for the aggregator spec. Two questions, more related to unrelated changes:

  • What went wrong with the contractnegotiator? Did it never work, or when did it start to fail?

  • The TicketStrategy used to contain those two methods because they are two questions we want to ask of the evaluator... How does the process now calculate which requirements are missing?

@joachimvh
Copy link
Contributor Author

What went wrong with the contractnegotiator? Did it never work, or when did it start to fail?

It still works, as in, responses will be returned. But what it returns is a mostly hardcoded contract:

createContract(perms: Permission[]): ODRLContract {
// todo: un-mock this!!!
type Pair = { action: string, target: string };
const permissionPairs: Pair[] = perms.flatMap(
(perm): Pair[] => perm.resource_scopes.map(
(scope): Pair => ({ action: scope, target: perm.resource_id })));
const contract: ODRLContract = {
"@context": "http://www.w3.org/ns/odrl.jsonld",
"@type": "Agreement",
uid: `urn:uma:pacsoi:agreement:${randomUUID()}`,
"http://purl.org/dc/terms/description": "Agreement for HCP X to read Alice's health data for bariatric care.",
"https://w3id.org/dpv#hasLegalBasis": { "@id": "https://w3id.org/dpv/legal/eu/gdpr#eu-gdpr:A9-2-a" },
permission: permissionPairs.map(({ action, target }): ODRLPermission => ({
"@type": "Permission",
action: switchODRLandCSSPermission(action),
target: target,
assigner: 'http://localhost:3000/ruben/profile/card#me', // user WebID
assignee: 'http://localhost:3000/alice/profile/card#me', // target WebID
constraint: [ {
"@type": "Constraint",
leftOperand: "purpose",
operator: "eq",
rightOperand: { "@id": "http://example.org/bariatric-care" },
} ]
})),
}
return contract;
}

It then tries to store this contract in a hardcoded container which does not exist:
const instantiatedPolicyContainer = 'http://localhost:3000/ruben/settings/policies/instantiated/';
const policyCreationResponse = await fetch(instantiatedPolicyContainer, {
method: 'POST',
headers: { 'content-type': 'application/ld+json' },
body: JSON.stringify(contract),
});

So I didn't really see a point in keeping it in there in the current state, as it doesn't actually add anything. Parts of this code can be re-inserted when contracts are actually supported.

The TicketStrategy used to contain those two methods because they are two questions we want to ask of the evaluator... How does the process now calculate which requirements are missing?

While that is the idea behind the function that was removed, in practice this never happened since the ODRL authorizer did not support that call: it would throw an error when calling that function. Since that is the main authorizer the server is running on, this means it was never possible to find the missing requirements. I think it makes more sense to come back to this once we can actually answer that question. Potentially this can already partially be done by seeing in the ODRL report which parts of a policy were fulfilled and which were not. Although then we still need to see how to return that in a need_info error.

I did actually improve the need_info error a bit in this PR. The ODRL authorizer returns the permissions that were granted, and if that is only a subset of what was requested, the missing permissions will be in the need_info error in the resource_scopes field. Without the associated identifier though, if multiple resources were requested, as that is not part of the preset need_info fields. 😅

@termontwouter termontwouter self-requested a review February 5, 2026 14:48
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