Skip to content

Commit 160c119

Browse files
committed
refactor: iterate over access request endpoint API
Ensure timestamp is generated within the server; Ensure DELETE on access requests can not be executed (who owns an access request?); iterate over documentation; Remove duplicated function; fix tests accordingly
1 parent 3e30fd8 commit 160c119

11 files changed

Lines changed: 142 additions & 119 deletions

File tree

documentation/access-request-management.md

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ The general flow of access requests and grants looks like this:
1010

1111
The document makes use of these parties and identifiers:
1212

13-
- **Resource Owner**: `https://pod.harrypodder.org/profile/card#me`
13+
- **Resource Owner**: `https://pod.example.com/profile/card#me`
1414
- **Authorization Server**: `http://localhost:4000`
1515
- **Resource Server**: `http://localhost:3000/resources`
1616
- **Requesting Party**: `https://example.pod.knows.idlab.ugent.be/profile/card#me`
@@ -21,10 +21,7 @@ The access request used in the examples below looks like this:
2121
```turtle
2222
@prefix sotw: <https://w3id.org/force/sotw#> .
2323
@prefix odrl: <http://www.w3.org/ns/odrl/2/> .
24-
@prefix dcterms: <http://purl.org/dc/terms/> .
25-
@prefix dct: <http://purl.org/dc/terms/> .
2624
@prefix ex: <http://example.org/> .
27-
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
2825
2926
ex:request a sotw:EvaluationRequest ;
3027
sotw:requestedTarget <http://localhost:3000/resources/resource.txt> ;
@@ -70,19 +67,16 @@ curl --location 'http://localhost:4000/uma/requests' \
7067
--header 'Content-Type: text/turtle' \
7168
--data-raw '
7269
@prefix sotw: <https://w3id.org/force/sotw#> .
73-
@prefix odrl: <https://www.w3.org/ns/odrl/2/> .
74-
@prefix dcterms: <https://purl.org/dc/terms/> .
75-
@prefix dct: <https://purl.org/dc/terms/> .
76-
@prefix ex: <https://example.org/> .
77-
@prefix xsd: <https://www.w3.org/2001/XMLSchema#> .
70+
@prefix odrl: <http://www.w3.org/ns/odrl/2/> .
71+
@prefix dcterms: <http://purl.org/dc/terms/> .
72+
@prefix ex: <http://example.org/> .
73+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
7874
7975
ex:request a sotw:EvaluationRequest ;
80-
dcterms:issued "2025-08-21T11:24:34.999Z"^^xsd:datetime ;
8176
sotw:requestedTarget <http://localhost:3000/resources/resource.txt> ;
82-
sotw:requestedAction odrl:read ;
77+
sotw:requestedAction odrl:write ;
8378
sotw:requestingParty <https://example.pod.knows.idlab.ugent.be/profile/card#me> ;
84-
ex:requestStatus ex:requested .
85-
'
79+
ex:requestStatus ex:requested .'
8680
```
8781

8882
## Reading access requests
@@ -105,8 +99,8 @@ The body must hold the content type `application/json`.
10599
The example below shows how to update the access request's status from `requested` to `accepted`:
106100

107101
```shell-session
108-
curl -X PATCH --location 'http://localhost:4000/uma/rquests/:id' \
109-
--header 'Authorization: https://example.pod.knows.idlab.ugent.be/profile/card#me' \
102+
curl -X PATCH --location 'http://localhost:4000/uma/requests/http%3A%2F%2Fexample.org%2Frequest' \
103+
--header 'Authorization: https://pod.example.com/profile/card#me' \
110104
--header 'Content-Type: application/json' \
111105
--data-raw '{ "status": "accepted" }' # can be changed to `denied` too.
112106
```
@@ -116,13 +110,11 @@ After this, the RP will be able to use the resource following the UMA protocol.
116110

117111
## Deleting access requests
118112

119-
By making a simple **DELETE** request on the `/uma/requests/:id` endpoint, an access request can be deleted.
120-
The id should be sufficiently encoded in the URL.
113+
Currently, access requests cannot be deleted. The reason being that it from a governance decision a decision need to be made who is allowed to delete it.
114+
115+
Is it the requesting party? Or is it the resource owner?
116+
From the start. It makes more sense for the RP. However, if the RO made a decision, it does not make sense that the RP can remove this.
121117

122-
```shell-session
123-
curl -X DELETE --location 'http://localhost:4000/uma/requests/:id' \
124-
--header 'Authorization: https://example.pod.knows.idlab.ugent.be/profile/card#me' \
125-
```
126118

127119
## Important Notes
128120

packages/uma/config/routes/accessrequests.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"OPTIONS",
3434
"PATCH",
3535
"GET",
36-
"DELETE"
36+
"DELETE",
37+
"PUT"
3738
],
3839
"handler": { "@id": "urn:uma:default:AccessRequestHandler" },
3940
"path": "/uma/requests/{id}"

packages/uma/src/controller/AccessRequestController.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,20 @@ export class AccessRequestController extends BaseController {
2525
patchAccessRequest,
2626
);
2727
}
28+
29+
/**
30+
* Deletes are not allowed on access requests.
31+
*
32+
* @param entityID ID pointing to the policy or access request
33+
* @param clientID ID of the resource owner (RO) or requesting party (RP) making the deletion
34+
* @returns a status code: 403
35+
*/
36+
public async deleteEntity(entityID: string, clientID: string): Promise<{ status: number }> {
37+
return { status: 403 };
38+
}
39+
40+
public async putEntity(data: string, entityID: string, clientID: string): Promise<{ status: number }> {
41+
return { status: 403 };
42+
}
43+
2844
}

packages/uma/src/controller/BaseController.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,19 +78,19 @@ export abstract class BaseController {
7878
* - 201 if creation was successful
7979
* - 409 if a conflict occurred (duplicate subject)
8080
*/
81-
public async addEntity(data: string, clientID: string): Promise<{ status: number }> {
81+
public async addEntity(data: string, clientID: string): Promise<{ status: number, message:string }> {
8282
const store = await parseStringAsN3Store(data);
8383

8484
try {
8585
const sanitizedStore = await this.sanitizePost(store, clientID);
8686
if (noAlreadyDefinedSubjects(await this.store.getStore(), sanitizedStore))
8787
this.store.addRule(sanitizedStore);
88-
else return { status: 409 }; // conflict
89-
} catch (e) {
90-
return { status: parseInt(e.message, 10) }; // the message of this error will contain the reason this query failed
88+
else return { status: 409, message: '' }; // conflict
89+
} catch (e) {
90+
return { status: e.statusCode || 500, message: e.message }; // the message of this error will contain the reason this query failed
9191
}
9292

93-
return { status: 201 }; // success
93+
return { status: 201, message: '' }; // success
9494
}
9595

9696
/**
@@ -147,10 +147,9 @@ export abstract class BaseController {
147147
}
148148

149149
/**
150-
* Apply a PUT to a single policy or access request identified by `entityID`µ.
150+
* Apply a PUT to a single policy or access request identified by `entityID`.
151151
*
152152
* Currently, this is only implemented for policies.
153-
* The {@link BaseHandler} should never call this function for access requests routes, as it isn't configured in the componentsjs file.
154153
*
155154
* @param data RDF data in Turtle/N3 format representing a policy or acccess request
156155
* @param entityID ID pointing to the policy or access request

packages/uma/src/routes/BaseHandler.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,13 @@ export abstract class BaseHandler extends HttpHandler {
170170
* @returns a response with status code 201 if successful, 409 if conflict occurred, or error otherwise
171171
* @throws BadRequestHttpError if request body is missing
172172
*/
173-
private async handlePost(request: HttpHandlerRequest<string>, clientID: string): Promise<HttpHandlerResponse<void>> {
173+
private async handlePost(request: HttpHandlerRequest<string>, clientID: string): Promise<HttpHandlerResponse<string>> {
174174
if (!request.body) throw new BadRequestHttpError();
175-
const { status } = await this.controller.addEntity(request.body.toString(), clientID);
175+
const { status, message } = await this.controller.addEntity(request.body.toString(), clientID);
176176

177177
return {
178-
status: status
178+
status: status,
179+
body: message
179180
};
180181
}
181182
}

packages/uma/src/util/routeSpecific/delete.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,5 +103,7 @@ const buildAccessRequestDeletionQuery = (requestID: string, requestingPartyOrRes
103103
* @param requestingPartyOrResourceOwner ID of the requesting party or resource owner
104104
* @returns a promise resolving when deletion is completed
105105
*/
106-
export const deleteAccessRequest = (store: Store, requestID: string, requestingPartyOrResourceOwner: string) =>
107-
executeDelete(store, buildAccessRequestDeletionQuery(requestID, requestingPartyOrResourceOwner));
106+
export const deleteAccessRequest = async (store: Store, requestID: string, requestingPartyOrResourceOwner: string): Promise<void> => {
107+
await executeDelete(store, buildAccessRequestDeletionQuery(requestID, requestingPartyOrResourceOwner));
108+
109+
}

packages/uma/src/util/routeSpecific/post.ts

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { Store } from "n3";
1+
import { Store, DataFactory } from "n3";
22
import {queryEngine} from './index';
3-
3+
import { BadRequestHttpError, ForbiddenHttpError, RDF, XSD } from "@solid/community-server";
4+
const {literal, namedNode} = DataFactory
45
/**
56
* Run a query against the store and extract exactly one matching subgraph.
67
*
@@ -90,7 +91,7 @@ const buildPolicyCreationQuery = (resourceOwner: string) => `
9091
*/
9192
export const postPolicy = async (store: Store, resourceOwner: string): Promise<Store> => {
9293
const isOwner = store.countQuads(null, 'http://www.w3.org/ns/odrl/2/assigner', resourceOwner, null) !== 0;
93-
if (!isOwner) throw new Error('403');
94+
if (!isOwner) throw new ForbiddenHttpError();
9495

9596
const result = await executePost(store, buildPolicyCreationQuery(resourceOwner), ["p", "r"]);
9697

@@ -132,20 +133,15 @@ const buildAccessRequestCreationQuery = (requestingParty: string) => `
132133
* @param requestingParty identifier of the client
133134
* @returns the validated request as a store
134135
*/
135-
export const postAccessRequest = (store: Store, requestingParty: string) =>
136-
executePost(store, buildAccessRequestCreationQuery(requestingParty), ["r"]);
136+
export const postAccessRequest = async (store: Store, requestingParty: string): Promise<Store> =>{
137+
const hasTime = store.countQuads(null, "http://purl.org/dc/terms/issued", null, null) !== 0;
138+
if (hasTime) throw new BadRequestHttpError("Time is managed by the server");
137139

138-
/**
139-
* Check whether all subjects in the new store
140-
* are absent from the existing store.
141-
*
142-
* This is used to ensure no pre-existing entities
143-
* are being redefined.
144-
*
145-
* @param store the original store
146-
* @param newStore the store containing new data
147-
* @returns true if no subjects are already defined, false otherwise
148-
*/
149-
export const noAlreadyDefinedSubjects = (store: Store, newStore: Store): boolean =>
150-
newStore.getSubjects(null, null, null)
151-
.every((subject) => store.countQuads(subject, null, null, null) === 0);
140+
const requestIds = store.getSubjects(RDF.type, "https://w3id.org/force/sotw#EvaluationRequest", null);
141+
if (requestIds.length !==1) {
142+
throw new BadRequestHttpError("Expected one acces request.");
143+
}
144+
145+
store.addQuad(requestIds[0], namedNode("http://purl.org/dc/terms/issued"), literal(new Date().toISOString(), XSD.terms.dateTime))
146+
return await executePost(store, buildAccessRequestCreationQuery(requestingParty), ["r"]);
147+
}
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
import { Store } from "n3";
22

3-
// check if there are no subjects in newStore that are already in Store
3+
/**
4+
* Check whether all subjects in the new store
5+
* are absent from the existing store.
6+
*
7+
* This is used to ensure no pre-existing entities
8+
* are being redefined.
9+
*
10+
* @param store the original store
11+
* @param newStore the store containing new data
12+
* @returns true if no subjects are already defined, false otherwise
13+
*/
414
export const noAlreadyDefinedSubjects = (store: Store, newStore: Store): boolean =>
515
newStore.getSubjects(null, null, null)
616
.every((subject) => store.countQuads(subject, null, null, null) === 0);
7-
817
export class ConflictError extends Error {
918
constructor() { super(`Resource already exists`); }
1019
}

0 commit comments

Comments
 (0)