Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions documentation/collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ All these triples then get passed to the ODRL evaluator when policies need to be
Any policy that targets a collection ID will apply to all resources that are part of that collection.
If the relation was reversed, the relation object would be `[ owl:inverseOf <http://www.w3.org/ns/ldp#contains> ]`.

### Finding collection identifiers

Currently, there is no API yet to request a list of all the automatically registered collections described above.
As a workaround, the generated collection identifiers are fixed, based on the relevant identifiers.
A collection with source `http://example.com/container/` and relation `http://www.w3.org/ns/ldp#contains`,
would have as collection identifier `collection:http://example.com/container/:http://www.w3.org/ns/ldp#contains`.
In case of a reverse relationship, this would instead be
`collection:http://www.w3.org/ns/ldp#contains:http://example.com/container/`.
These are the identifiers to then use as targets in a policy.

## Updating collection triples

Every time a resource is updated, the corresponding collection triples are updated accordingly.
Expand Down
6 changes: 5 additions & 1 deletion packages/uma/src/routes/ResourceRegistration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,11 @@ export class ResourceRegistrationRequestHandler extends HttpHandler {
if (collections.length > 0) {
delete add[key];
} else {
addQuads.push(...this.generateCollectionTriples(entry));
// TODO: currently generating fixed collection ID as there is no API to get them
addQuads.push(...this.generateCollectionTriples(entry, DF.namedNode(`collection:${entry.reverse ?
entry.relation.value + ':' + entry.source.value :
entry.source.value + ':' + entry.relation.value
}`)));
}
}

Expand Down
14 changes: 8 additions & 6 deletions packages/uma/src/util/routeSpecific/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,12 @@ const executeGet = async (
const buildPolicyRetrievalQuery = (policyID: string, resourceOwner: string) => `
PREFIX odrl: <http://www.w3.org/ns/odrl/2/>

SELECT DISTINCT ?policy ?perm
SELECT DISTINCT ?policy ?perm ?target
WHERE {
?policy odrl:uid <${policyID}> ;
odrl:permission ?perm .
?perm odrl:assigner <${resourceOwner}> .
?perm odrl:assigner <${resourceOwner}> ;
odrl:target ?target .

{
?policy a odrl:Agreement .
Expand All @@ -85,7 +86,7 @@ const buildPolicyRetrievalQuery = (policyID: string, resourceOwner: string) => `
* @returns a store containing the policy and its permissions
*/
export const getPolicy = (store: Store, policyID: string, resourceOwner: string) =>
executeGet(store, buildPolicyRetrievalQuery(policyID, resourceOwner), ['policy', 'perm']);
executeGet(store, buildPolicyRetrievalQuery(policyID, resourceOwner), ['policy', 'perm', 'target']);

/**
* Build a query to retrieve all policies for a given client.
Expand All @@ -97,10 +98,11 @@ export const getPolicy = (store: Store, policyID: string, resourceOwner: string)
const buildPoliciesRetrievalQuery = (resourceOwner: string) => `
PREFIX odrl: <http://www.w3.org/ns/odrl/2/>

SELECT DISTINCT ?policy ?perm
SELECT DISTINCT ?policy ?perm ?target
WHERE {
?policy odrl:permission ?perm .
?perm odrl:assigner <${resourceOwner}> .
?perm odrl:assigner <${resourceOwner}> ;
odrl:target ?target .

{
?policy a odrl:Agreement .
Expand All @@ -118,7 +120,7 @@ const buildPoliciesRetrievalQuery = (resourceOwner: string) => `
* @returns a store containing all policies and their permissions
*/
export const getPolicies = (store: Store, resourceOwner: string) =>
executeGet(store, buildPoliciesRetrievalQuery(resourceOwner), ['perm']);
executeGet(store, buildPoliciesRetrievalQuery(resourceOwner), ['perm', 'target']);

// TODO: slight improvement over existing solution so constraints get returned but definitely not ideal yet
function permissionToQuads(store: Store, permission: Quad_Subject): Quad[] {
Expand Down
2 changes: 1 addition & 1 deletion packages/uma/src/util/routeSpecific/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const executePost = async (
const buildPolicyCreationQuery = (resourceOwner: string) => `
PREFIX odrl: <http://www.w3.org/ns/odrl/2/>

SELECT DISTINCT ?p ?r
SELECT DISTINCT ?p ?r ?target
WHERE {
{
?p a odrl:Agreement ;
Expand Down
38 changes: 20 additions & 18 deletions packages/uma/test/unit/routes/ResourceRegistration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,10 @@ describe('ResourceRegistration', (): void => {
});

it('stores newly created asset collections.', async(): Promise<void> => {
const crypto = await import('node:crypto');
let count = 0;
vi.mocked(crypto.randomUUID).mockImplementation(() => `${++count}` as any);
// TODO: not needed while collection identifiers are fixed
// const crypto = await import('node:crypto');
// let count = 0;
// vi.mocked(crypto.randomUUID).mockImplementation(() => `${++count}` as any);
input.request.body!.resource_defaults = { pred: [ 'scope' ], '@reverse': { 'rPred': [ 'otherScope' ]}};
await expect(handler.handle(input)).resolves.toEqual({
status: 201,
Expand All @@ -125,12 +126,12 @@ describe('ResourceRegistration', (): void => {
expect(policies.addRule).toHaveBeenCalledTimes(1);
const newStore = policies.addRule.mock.calls[0][0];
expect(newStore).toBeRdfIsomorphic([
DF.quad(DF.namedNode('collection:1'), RDF.terms.type, ODRL.terms.AssetCollection),
DF.quad(DF.namedNode('collection:1'), ODRL.terms.source, DF.namedNode('name')),
DF.quad(DF.namedNode('collection:1'), ODRL_P.terms.relation, DF.namedNode('pred')),
DF.quad(DF.namedNode('collection:2'), RDF.terms.type, ODRL.terms.AssetCollection),
DF.quad(DF.namedNode('collection:2'), ODRL.terms.source, DF.namedNode('name')),
DF.quad(DF.namedNode('collection:2'), ODRL_P.terms.relation, DF.blankNode('n3-0')),
DF.quad(DF.namedNode('collection:name:pred'), RDF.terms.type, ODRL.terms.AssetCollection),
DF.quad(DF.namedNode('collection:name:pred'), ODRL.terms.source, DF.namedNode('name')),
DF.quad(DF.namedNode('collection:name:pred'), ODRL_P.terms.relation, DF.namedNode('pred')),
DF.quad(DF.namedNode('collection:rPred:name'), RDF.terms.type, ODRL.terms.AssetCollection),
DF.quad(DF.namedNode('collection:rPred:name'), ODRL.terms.source, DF.namedNode('name')),
DF.quad(DF.namedNode('collection:rPred:name'), ODRL_P.terms.relation, DF.blankNode('n3-0')),
DF.quad(DF.blankNode('n3-0'), OWL.terms.inverseOf, DF.namedNode('rPred')),
]);
});
Expand Down Expand Up @@ -221,9 +222,10 @@ describe('ResourceRegistration', (): void => {
});

it('stores newly created asset collections.', async(): Promise<void> => {
const crypto = await import('node:crypto');
let count = 0;
vi.mocked(crypto.randomUUID).mockImplementation(() => `${++count}` as any);
// TODO: not needed while collection identifiers are fixed
// const crypto = await import('node:crypto');
// let count = 0;
// vi.mocked(crypto.randomUUID).mockImplementation(() => `${++count}` as any);
input.request.body!.resource_defaults = { pred: [ 'scope' ], '@reverse': { 'rPred': [ 'otherScope' ]}};
await expect(handler.handle(input)).resolves.toEqual({
status: 200,
Expand All @@ -232,12 +234,12 @@ describe('ResourceRegistration', (): void => {
expect(policies.addRule).toHaveBeenCalledTimes(1);
const newStore = policies.addRule.mock.calls[0][0];
expect(newStore).toBeRdfIsomorphic([
DF.quad(DF.namedNode('collection:1'), RDF.terms.type, ODRL.terms.AssetCollection),
DF.quad(DF.namedNode('collection:1'), ODRL.terms.source, DF.namedNode('name')),
DF.quad(DF.namedNode('collection:1'), ODRL_P.terms.relation, DF.namedNode('pred')),
DF.quad(DF.namedNode('collection:2'), RDF.terms.type, ODRL.terms.AssetCollection),
DF.quad(DF.namedNode('collection:2'), ODRL.terms.source, DF.namedNode('name')),
DF.quad(DF.namedNode('collection:2'), ODRL_P.terms.relation, DF.blankNode('n3-0')),
DF.quad(DF.namedNode('collection:name:pred'), RDF.terms.type, ODRL.terms.AssetCollection),
DF.quad(DF.namedNode('collection:name:pred'), ODRL.terms.source, DF.namedNode('name')),
DF.quad(DF.namedNode('collection:name:pred'), ODRL_P.terms.relation, DF.namedNode('pred')),
DF.quad(DF.namedNode('collection:rPred:name'), RDF.terms.type, ODRL.terms.AssetCollection),
DF.quad(DF.namedNode('collection:rPred:name'), ODRL.terms.source, DF.namedNode('name')),
DF.quad(DF.namedNode('collection:rPred:name'), ODRL_P.terms.relation, DF.blankNode('n3-0')),
DF.quad(DF.blankNode('n3-0'), OWL.terms.inverseOf, DF.namedNode('rPred')),
]);
});
Expand Down
91 changes: 91 additions & 0 deletions test/integration/Collections.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { App, joinUrl } from '@solid/community-server';
import { setGlobalLoggerFactory, WinstonLoggerFactory } from 'global-logger-factory';
import path from 'node:path';
import { getDefaultCssVariables, getPorts, instantiateFromConfig } from '../util/ServerUtil';
import { generateCredentials, umaFetch } from '../util/UmaUtil';

const [ cssPort, umaPort ] = getPorts('Collections');

describe('A server with collections', (): void => {
const owner = `http://localhost:${cssPort}/alice/profile/card#me`;
const user = `http://example.com/bob`;
let umaApp: App;
let cssApp: App;

beforeAll(async(): Promise<void> => {
setGlobalLoggerFactory(new WinstonLoggerFactory('off'));

umaApp = await instantiateFromConfig(
'urn:uma:default:App',
path.join(__dirname, '../../packages/uma/config/default.json'),
{
'urn:uma:variables:port': umaPort,
'urn:uma:variables:baseUrl': `http://localhost:${umaPort}/uma`,
'urn:uma:variables:eyePath': 'eye',
'urn:uma:variables:backupFilePath': '',
}
);

cssApp = await instantiateFromConfig(
'urn:solid-server:default:App',
path.join(__dirname, '../../packages/css/config/default.json'),
{
...getDefaultCssVariables(cssPort),
'urn:solid-server:default:variable:seedConfig': path.join(__dirname, '../../packages/css/config/seed.json'),
},
);

await Promise.all([ umaApp.start(), cssApp.start() ]);
});

afterAll(async(): Promise<void> => {
await Promise.all([ umaApp.stop(), cssApp.stop() ]);
});

it('can register client credentials for the user/RS combination.', async(): Promise<void> => {
await generateCredentials({
webId: owner,
authorizationServer: `http://localhost:${umaPort}/uma`,
resourceServer: `http://localhost:${cssPort}/`,
email: 'alice@example.org',
password: 'abc123'
});
});

it('can create a policy targeting an asset collection.', async(): Promise<void> => {
// TODO: hardcoded collection identifier due to lack of collection API
const policy = `
@prefix ex: <http://example.org/> .
@prefix ldp: <http://www.w3.org/ns/ldp#>.
@prefix odrl: <http://www.w3.org/ns/odrl/2/>.
@prefix odrl_p: <https://w3id.org/force/odrl3proposal#>.

ex:policy a odrl:Set ;
odrl:uid ex:policy ;
odrl:permission ex:permission .

ex:permission a odrl:Permission ;
odrl:assignee <${user}> ;
odrl:assigner <${owner}> ;
odrl:action odrl:read ;
odrl:target <collection:http://localhost:${cssPort}/alice/:http://www.w3.org/ns/ldp#contains> .`

const url = `http://localhost:${umaPort}/uma/policies`;
let response = await fetch(url, {
method: 'POST',
headers: { authorization: `WebID ${encodeURIComponent(owner)}`, 'content-type': 'text/turtle' },
body: policy,
});
expect(response.status).toBe(201);

response = await fetch(joinUrl(url, encodeURIComponent('http://example.org/policy')), {
headers: { authorization: `WebID ${encodeURIComponent(owner)}` },
});
console.log(await response.text());
});

it('can access a resource in the asset collection.', async(): Promise<void> => {
const response = await umaFetch(`http://localhost:${cssPort}/alice/README`, {}, user);
expect(response.status).toBe(200);
});
});
1 change: 1 addition & 0 deletions test/util/ServerUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const portNames = [
'Aggregation',
'AggregationSource',
'Base',
'Collections',
'Demo',
'ODRL',
'OIDC',
Expand Down