Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
ecd6839
Basic POST /policies created
lennertdr Jul 4, 2025
846fcac
Practical addRule implementation to test the POST endpoint
lennertdr Jul 4, 2025
1c377a7
added Get One Policy endpoint, need a way to fix the ID (encoding?)
lennertdr Jul 4, 2025
e60eb75
getOnePolicy works if a good encoding of ID's is implemented
lennertdr Jul 4, 2025
6f0916f
follow implementation from Main
lennertdr Jul 4, 2025
a183107
test content type
lennertdr Jul 7, 2025
89eb900
feat: Allow APIs with raw input
joachimvh Jul 7, 2025
f2de8ad
Format checks removed, they are already in N3 Parser
lennertdr Jul 7, 2025
56727e8
Merge remote-tracking branch 'upstream/main' into policyEndpoints
lennertdr Jul 7, 2025
f6dc977
fix: Correctly handle multiple routing classes
joachimvh Jul 7, 2025
8295dbf
Change to Memory structure for better testing on this branch
lennertdr Jul 7, 2025
3d110ce
Merge remote-tracking branch 'upstream/main' into policyEndpoints
lennertdr Jul 7, 2025
3ee80a6
POST with proper content types
lennertdr Jul 7, 2025
90ff950
Memory based tests
lennertdr Jul 7, 2025
64ae909
Merge remote-tracking branch 'origin/policyEndpoints'
lennertdr Jul 8, 2025
b5dab70
import fix
lennertdr Jul 8, 2025
2dad0f7
GET /uma/policies/<id> first finished implementation
lennertdr Jul 8, 2025
2f43a30
Add extra checks to POST
lennertdr Jul 8, 2025
73d32d8
More generic url handling and POST with sanitize function (to be comp…
lennertdr Jul 8, 2025
9e1102b
excessive documentation
lennertdr Jul 8, 2025
e11cc43
test doc and very primitive way to detect fails
lennertdr Jul 8, 2025
9f78328
DELETE endpoint implemented, still needs tests
lennertdr Jul 8, 2025
944b52e
fix: Export OperationLogger
joachimvh Jul 1, 2025
4c05231
fix: Prevent contract creation errors from stopping the request
joachimvh Jul 1, 2025
1b08acb
fix: Request subject resource permissions for auxiliaries
joachimvh Jul 1, 2025
0f0e025
feat: Allow relative URIs when using DirectoryUCRulesStorage
joachimvh Jul 1, 2025
0d91822
refactor: Remove unused seeding fields
joachimvh Jul 1, 2025
1e3c9e1
feat: Make containerURL of ContainerUCRulesStorage configurable
joachimvh Jul 1, 2025
3e7d7b6
feat: Make App the root configured component
joachimvh Jul 1, 2025
b7588c7
chore: Remove unused dependencies
joachimvh Jul 1, 2025
a746efe
chore: Replace ts-node completely with tsx
joachimvh Jul 1, 2025
3f8c388
test: Add integration tests with vitest
joachimvh Jul 1, 2025
4cde160
test: Add testing to CI
joachimvh Jul 2, 2025
de2fab0
chore: Build startup scripts instead of using tsx
joachimvh Jul 8, 2025
3be468a
Tests for DELETE endpoint
lennertdr Jul 9, 2025
de05066
edit policy setup
lennertdr Jul 9, 2025
778ca15
Basic edit implementation
lennertdr Jul 9, 2025
f06de50
added simple tests for PATCH
lennertdr Jul 9, 2025
aecd278
patch + tests
lennertdr Jul 9, 2025
f0ea4c7
extra check for PATCH
lennertdr Jul 10, 2025
c807bb7
remove console.logs
lennertdr Jul 10, 2025
308e6c1
Seperate rule definitions for a policy based on the client
lennertdr Jul 10, 2025
f98fbc9
PATCH safety fix, GET duplicate fix
lennertdr Jul 10, 2025
15d4619
cleanup, fix PUT, less redundant GET
lennertdr Jul 10, 2025
d16071b
extra PUT checks, extra documentation
lennertdr Jul 10, 2025
a67b3d4
doc layout fix
lennertdr Jul 10, 2025
615a9a9
detailed documentation
lennertdr Jul 11, 2025
d66d1f4
Stronger POST checks
lennertdr Jul 11, 2025
bb08a83
DELETE idea, need to adjust tests
lennertdr Jul 11, 2025
29b8eaa
fixed small bug
lennertdr Jul 14, 2025
4c1cc6f
Merge remote-tracking branch 'upstream/main', kept memory storage in …
lennertdr Jul 14, 2025
9c189ec
doc update
lennertdr Jul 14, 2025
7cbbedc
doc update
lennertdr Jul 14, 2025
0ba8846
typos
lennertdr Jul 14, 2025
36934e8
temporary header against CORS, not the right solution
lennertdr Jul 16, 2025
ed76ee6
script to seed for specific id
lennertdr Jul 16, 2025
155169e
options for other requests
lennertdr Jul 17, 2025
6eaf6f2
some requested changes
lennertdr Jul 25, 2025
b8de385
undo wrong import
lennertdr Jul 28, 2025
10c7247
quick workaround
lennertdr Jul 28, 2025
dc950da
demo script
lennertdr Jul 30, 2025
80bf03b
script shortcut
lennertdr Jul 30, 2025
e6044f7
added test again
lennertdr Jul 31, 2025
eb605dc
Removed logs and finetuned docs
lennertdr Jul 31, 2025
46c177f
docfix
lennertdr Jul 31, 2025
9a5f4e8
TODO's
lennertdr Jul 31, 2025
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
31 changes: 29 additions & 2 deletions packages/ucp/src/storage/DirectoryUCRulesStorage.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { UCRulesStorage } from "./UCRulesStorage";
import * as path from 'path'
import * as fs from 'fs'
import { Store } from "n3";
import { Store, Writer } from "n3";
import { parseAsN3Store } from "koreografeye";

export class DirectoryUCRulesStorage implements UCRulesStorage {
private directoryPath: string;
private addedRulesPath: string;

/**
*
* @param directoryPath The absolute path to a directory
Expand All @@ -16,6 +18,8 @@ export class DirectoryUCRulesStorage implements UCRulesStorage {
if (!fs.lstatSync(directoryPath).isDirectory()) {
throw Error(`${directoryPath} does not resolve to a directory`)
}

this.addedRulesPath = path.join(this.directoryPath, 'addedRules.ttl');
}

public async getStore(): Promise<Store> {
Expand All @@ -30,8 +34,31 @@ export class DirectoryUCRulesStorage implements UCRulesStorage {
}


/**
* TEST IMPLEMENTATION - This is just to test the POST :uma/policies endpoint
*
* @param rule The quads to be added
*/
public async addRule(rule: Store): Promise<void> {
throw Error('not implemented');
const writer = new Writer({ format: 'Turtle' });

writer.addQuads(rule.getQuads(null, null, null, null));

const serializedTurtle: string = await new Promise((resolve, reject) => {
writer.end((error, result) => {
if (error) reject(error);
else resolve(result);
});
});

// Append or create the file
if (!fs.existsSync(this.addedRulesPath)) {
fs.writeFileSync(this.addedRulesPath, serializedTurtle);
} else {
fs.appendFileSync(this.addedRulesPath, '\n' + serializedTurtle);
}

console.log(`[${new Date().toISOString()}] - Added new rule to ${this.addedRulesPath}`);
}
public async getRule(identifier: string): Promise<Store> {
throw Error('not implemented');
Expand Down
58 changes: 37 additions & 21 deletions packages/uma/config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,28 +89,44 @@
"@id": "urm:uma:default:JsonHttpErrorHandler",
"@type": "JsonHttpErrorHandler",
"handler": {
"@id": "urm:uma:default:JsonFormHttpHandler",
"@type": "JsonFormHttpHandler",
"handler": {
"@id": "urn:uma:default:RoutedHttpRequestHandler",
"@type": "RoutedHttpRequestHandler",
"routes": [
{ "@id": "urn:uma:default:UmaConfigRoute" },
{ "@id": "urn:uma:default:JwksRoute" },
{ "@id": "urn:uma:default:TokenRoute" },
{ "@id": "urn:uma:default:PolicyRoute" },
{ "@id": "urn:uma:default:PermissionRegistrationRoute" },
{ "@id": "urn:uma:default:ResourceRegistrationRoute" },
{ "@id": "urn:uma:default:ResourceRegistrationOpsRoute" },
{ "@id": "urn:uma:default:IntrospectionRoute" },
{ "@id": "urn:uma:default:LogRoute" },
{ "@id": "urn:uma:default:VCRoute" },
{ "@id": "urn:uma:default:ContractRoute" }
],
"defaultHandler": {
"@type": "DefaultRequestHandler"
"@id": "urm:uma:default:RouteHandler",
"@type": "WaterfallHandler",
"handlers": [
{
"comment": "Handles all JSON and form encoded input/output.",
"@id": "urm:uma:default:JsonFormHttpHandler",
"@type": "JsonFormHttpHandler",
"handler": {
"@id": "urn:uma:default:JsonRoutedHttpRequestHandler",
"@type": "RoutedHttpRequestHandler",
"routes": [
{ "@id": "urn:uma:default:UmaConfigRoute" },
{ "@id": "urn:uma:default:JwksRoute" },
{ "@id": "urn:uma:default:TokenRoute" },
{ "@id": "urn:uma:default:PermissionRegistrationRoute" },
{ "@id": "urn:uma:default:ResourceRegistrationRoute" },
{ "@id": "urn:uma:default:ResourceRegistrationOpsRoute" },
{ "@id": "urn:uma:default:IntrospectionRoute" },
{ "@id": "urn:uma:default:GetOnePolicyRoute"}
]
}
},
{
"comment": "Handles all remaining output. These handlers have to handle input/output parsing themselves. TODO: At some point we want more generic conneg.",
"@id": "urn:uma:default:RawRoutedHttpRequestHandler",
"@type": "RoutedHttpRequestHandler",
"routes": [
{ "@id": "urn:uma:default:ContractRoute" },
{ "@id": "urn:uma:default:LogRoute" },
{ "@id": "urn:uma:default:VCRoute" },
{ "@id": "urn:uma:default:PolicyRoute" }
]
},
{
"@type": "StaticThrowHandler",
"error": { "@type": "NotFoundHttpError" }
}
}
]
}
}
},
Expand Down
5 changes: 1 addition & 4 deletions packages/uma/config/policies/authorizers/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,7 @@
"eyePath": { "@id": "urn:uma:variables:eyePath" },
"policies": {
"@id": "urn:uma:default:RulesStorage",
"@type": "DirectoryUCRulesStorage",
"directoryPath": {
"@id": "urn:uma:variables:policyDir"
}
"@type": "MemoryUCRulesStorage"
}
}
}
Expand Down
17 changes: 16 additions & 1 deletion packages/uma/config/routes/policies.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"@id": "urn:uma:default:PolicyRoute",
"@type": "HttpHandlerRoute",
"methods": [
"GET"
"GET",
"POST"
],
"handler": {
"@type": "PolicyRequestHandler",
Expand All @@ -16,6 +17,20 @@
}
},
"path": "/uma/policies"
},
{
"@id": "urn:uma:default:GetOnePolicyRoute",
"@type": "HttpHandlerRoute",
"methods": [
"GET"
],
"handler": {
"@type": "PolicyRequestHandler",
"storage": {
"@id": "urn:uma:default:RulesStorage"
}
},
"path": "/uma/policies/{id}"
}
]
}
7 changes: 7 additions & 0 deletions packages/uma/config/rules/odrl/addedRules.ttl
Copy link
Contributor

Choose a reason for hiding this comment

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

is this an artefact of using the earliest POST method in combination with the FileStorage solution?
If so, I think this can be removed

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<http://example.org/usagePolicy4> a <http://www.w3.org/ns/odrl/2/Agreement>;
<http://www.w3.org/ns/odrl/2/permission> <http://example.org/permission4>.
<http://example.org/permission4> a <http://www.w3.org/ns/odrl/2/Permission>;
<http://www.w3.org/ns/odrl/2/action> <http://www.w3.org/ns/odrl/2/read>;
<http://www.w3.org/ns/odrl/2/target> <http://localhost:3000/alice/other/resource.txt>;
<http://www.w3.org/ns/odrl/2/assignee> <https://woslabbi.pod.knows.idlab.ugent.be/profile/card#me>;
<http://www.w3.org/ns/odrl/2/assigner> <https://pod.example.com/profile/card#me>.
6 changes: 4 additions & 2 deletions packages/uma/src/routes/Policy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { BadRequestHttpError, getLoggerFor, MethodNotAllowedHttpError } from "@solid/community-server";
import { UCRulesStorage } from "@solidlab/ucp";
import { HttpHandlerContext, HttpHandlerResponse, HttpHandler, HttpHandlerRequest } from "../util/http/models/HttpHandler";
import { getPolicies as getPolicies } from "../util/routeSpecific/policies/GetPolicies";
import { getPolicies } from "../util/routeSpecific/policies/GetPolicies";
import { addPolicies } from "../util/routeSpecific/policies/CreatePolicies";

/**
* Endpoint to handle policies, this implementation gives all policies that have the
Expand All @@ -22,7 +23,7 @@ export class PolicyRequestHandler extends HttpHandler {
* (To be altered with actual Solid-OIDC)
*
* @param request the request with the client 'id' as body
* @returns the client id
* @returns the client webID
*/
protected getCredentials(request: HttpHandlerRequest): string {
const header = request.headers['authorization'];
Expand All @@ -44,6 +45,7 @@ export class PolicyRequestHandler extends HttpHandler {

switch (request.method) {
case 'GET': return getPolicies(request, store, client);
case 'POST': return addPolicies(request, store, this.storage, client);
// TODO: add other endpoints
default: throw new MethodNotAllowedHttpError();
}
Expand Down
41 changes: 24 additions & 17 deletions packages/uma/src/util/http/server/RoutedHttpRequestHandler.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { getLoggerFor } from '@solid/community-server';
import {
getLoggerFor,
InternalServerError,
MethodNotAllowedHttpError,
NotImplementedHttpError
} from '@solid/community-server';
import Template from 'uri-template-lite';
import { HttpHandler, HttpHandlerContext, HttpHandlerResponse } from '../models/HttpHandler';
import { HttpHandlerRoute } from '../models/HttpHandlerRoute';

type RouteMatch = { parameters: Record<string, string>, route: HttpHandlerRoute };

/**
* A {@link HttpHandler} handling requests based on routes in a given list of {@link HttpHandlerRoute}s.
* Route paths can contain variables as described in RFC 6570.
Expand All @@ -18,16 +25,17 @@ import { HttpHandlerRoute } from '../models/HttpHandlerRoute';
* E.g., `http://example.com/foo/bar` will match the route template `/bar`.
*/
export class RoutedHttpRequestHandler extends HttpHandler {
protected readonly logger = getLoggerFor(this);

protected readonly routeMap: Map<Template, HttpHandlerRoute>;
protected readonly logger = getLoggerFor(this);
protected readonly handledRequests: WeakMap<HttpHandlerContext, RouteMatch>;


/**
* Creates a RoutedHttpRequestHandler, super calls the HttpHandler class and expects a list of HttpHandlerControllers.
*/
constructor(
routes: HttpHandlerRoute[],
protected readonly defaultHandler?: HttpHandler,
onlyMatchTail = false,
) {
super();
Expand All @@ -37,9 +45,10 @@ export class RoutedHttpRequestHandler extends HttpHandler {
// Add a catchall variable to the front if only the URL tail needs to be matched.
this.routeMap.set(new Template(onlyMatchTail ? `{_prefix}${route.path}` : route.path), route);
}
this.handledRequests = new WeakMap();
}

async handle(context: HttpHandlerContext): Promise<HttpHandlerResponse> {
public async canHandle(context: HttpHandlerContext): Promise<void> {
const request = context.request;
const path = request.url.pathname;

Expand All @@ -55,26 +64,24 @@ export class RoutedHttpRequestHandler extends HttpHandler {
}

if (!match) {
if (this.defaultHandler) {
this.logger.info(`No matching route found, calling default handler. ${path}`);
return this.defaultHandler.handleSafe(context);
} else {
this.logger.error(`No matching route found. ${path}`);
return { body: '', headers: {}, status: 404 };
}
throw new NotImplementedHttpError();
}

this.logger.debug(`Route matched: ${JSON.stringify({ path, parameters: match.parameters })}`);

if (match.route.methods && !match.route.methods.includes(request.method)) {
this.logger.info(`Operation not supported. Supported operations: ${ match.route.methods }`);
return {
status: 405,
headers: { 'allow': match.route.methods.join(', ') },
body: '',
};
throw new MethodNotAllowedHttpError([ request.method ]);
}
this.handledRequests.set(context, match);
}

public async handle(context: HttpHandlerContext): Promise<HttpHandlerResponse> {
const match = this.handledRequests.get(context);
if (!match) {
throw new InternalServerError('Calling handle without successful canHandle');
}
request.parameters = { ...request.parameters, ...match.parameters };
context.request.parameters = { ...context.request.parameters, ...match.parameters };

return match.route.handler.handleSafe(context);
}
Expand Down
64 changes: 63 additions & 1 deletion packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts
Original file line number Diff line number Diff line change
@@ -1 +1,63 @@
//TODO
import { Store } from "n3";
import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler";
import { namedNode, odrlAssigner } from "./PolicyUtil";
import { BadRequestHttpError, InternalServerError } from "@solid/community-server";
import { parseStringAsN3Store } from "koreografeye";
import { UCRulesStorage } from "@solidlab/ucp";

export async function addPolicies(request: HttpHandlerRequest, store: Store, storage: UCRulesStorage, clientId: string): Promise<HttpHandlerResponse<any>> {

// 1. Parse the requested policy

const contentType = request.headers['content-type'] ?? 'turtle';
// Regex check for content type (awaiting server implementation)
if (!/(?:n3|trig|turtle|nquads?|ntriples?)$/i.test(contentType)) {
throw new BadRequestHttpError(`Content-Type ${contentType} is not supported.`);
}

console.log("Requested Policy:", request.body)
let requestedPolicy;
if (Buffer.isBuffer(request.body)) {
requestedPolicy = request.body.toString('utf-8');
console.log('RDF body:', requestedPolicy);
} else {
throw new Error("Expected Buffer body");
}
let parsedPolicy: Store;
try {
parsedPolicy = await parseStringAsN3Store(requestedPolicy, { format: contentType });
} catch (error) {
throw new BadRequestHttpError(`Policy string can not be parsed: ${error}`)
}

// 2. Check if assigner is client
const matchingClient = parsedPolicy.getQuads(null, odrlAssigner, namedNode(clientId), null);
if (matchingClient.length === 0) {
throw new BadRequestHttpError(`Policy is not authorized correctly`);
}

// Making sure there are no rules added with other assigners then yourself
const allAssigners = parsedPolicy.getQuads(null, odrlAssigner, null, null);
if (allAssigners.length !== matchingClient.length) {
throw new BadRequestHttpError(`Policy is incorrectly built`);
}

// TODO: 3. Perform other validity checks

// Check if assigner of the policy has access to the target
// Check if there is at least one permission/prohibition/duty
// Check if every rule has a target
// ...

// 4. Add the policy to the rule storage
try {
await storage.addRule(parsedPolicy);
} catch (error) {
throw new InternalServerError("Failed to add policy");
}


return {
status: 201
}
}
Loading