Skip to content

Commit f41b664

Browse files
committed
feat: Use a ReadOnlyStore for returning policy data
This prevents unnecessarily copying all rules when trying to read data
1 parent 0a8e321 commit f41b664

11 files changed

Lines changed: 63 additions & 42 deletions

File tree

packages/uma/src/controller/BaseController.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ConflictHttpError } from '@solid/community-server';
2-
import { UCRulesStorage } from "../ucp/storage/UCRulesStorage";
2+
import { ReadOnlyStore, UCRulesStorage } from '../ucp/storage/UCRulesStorage';
33
import { getLoggerFor } from 'global-logger-factory';
44
import { Parser, Store } from 'n3';
55
import { writeStore } from "../util/ConvertUtil";
@@ -18,8 +18,8 @@ export abstract class BaseController {
1818
protected readonly store: UCRulesStorage,
1919
protected sanitizePost: (store: Store, clientID: string) => Promise<{ result: Store, id: string }>,
2020
protected sanitizeDelete: (store: Store, entityID: string, clientID: string) => Promise<void>,
21-
protected sanitizeGets: (store: Store, clientID: string) => Promise<Store>,
22-
protected sanitizeGet: (store: Store, entityID: string, clientID: string) => Promise<Store>,
21+
protected sanitizeGets: (store: ReadOnlyStore, clientID: string) => Promise<ReadOnlyStore>,
22+
protected sanitizeGet: (store: ReadOnlyStore, entityID: string, clientID: string) => Promise<ReadOnlyStore>,
2323
protected sanitizePatch: (store: Store, entityID: string, clientID: string, patchInformation: string) => Promise<void>
2424
) { }
2525

@@ -30,7 +30,7 @@ export abstract class BaseController {
3030
* @returns results serialized in Turtle and status code 200,
3131
* or an empty body with status 404 if nothing was found
3232
*/
33-
private async get(sanitizeGet: () => Promise<Store>): Promise<{ message: string, status: number }> {
33+
private async get(sanitizeGet: () => Promise<ReadOnlyStore>): Promise<{ message: string, status: number }> {
3434
const store = await sanitizeGet();
3535

3636
const message = store.size > 0 ? await writeStore(store) : '';
@@ -92,7 +92,7 @@ export abstract class BaseController {
9292
* - 204 if deletion was successful
9393
*/
9494
public async deleteEntity(entityID: string, clientID: string): Promise<{ status: number }> {
95-
const filteredStore = new Store(await this.store.getStore());
95+
const filteredStore = new Store(await this.store.getStore() as Store);
9696
await this.sanitizeDelete(filteredStore, entityID, clientID);
9797
const diff = (await this.store.getStore()).difference(filteredStore);
9898
await this.store.removeData(diff as Store);
@@ -114,12 +114,12 @@ export abstract class BaseController {
114114
*/
115115
public async patchEntity(entityID: string, patchInformation: string, clientID: string, isolate: boolean = true): Promise<HttpHandlerResponse<string>> {
116116
let response: HttpHandlerResponse<string> = { status: 204, body: '' };
117-
let filteredStore = new Store(await this.store.getStore());
117+
let filteredStore = new Store(await this.store.getStore() as Store);
118118
let omitStore: Store;
119119

120120
if (isolate) { // requires isolating all information about the entity provided, as e.g. the patchinformation has a query to be executed
121-
filteredStore = await this.sanitizeGet(filteredStore, entityID, clientID);
122-
omitStore = new Store(await this.store.getStore());
121+
filteredStore = await this.sanitizeGet(filteredStore, entityID, clientID) as Store;
122+
omitStore = new Store(await this.store.getStore() as Store);
123123
omitStore.removeQuads([ ...filteredStore]);
124124
}
125125

@@ -130,14 +130,14 @@ export abstract class BaseController {
130130
// * bonus: filters out extra quads
131131
// ! drawback: PATCH may still be used to DELETE all information about the entity
132132
// TODO: check if PATCH is smth we want for all resources, make patchEntity optional otherwise
133-
filteredStore = await this.sanitizeGet(filteredStore, entityID, clientID) || filteredStore;
133+
filteredStore = await this.sanitizeGet(filteredStore, entityID, clientID) as Store || filteredStore;
134134
omitStore!.addAll(filteredStore);
135135
filteredStore = omitStore!;
136136
}
137137

138138
const originalStore = await this.store.getStore();
139139
const remove = originalStore.difference(filteredStore);
140-
const add = filteredStore.difference(originalStore);
140+
const add = filteredStore.difference(originalStore as Store);
141141

142142
if (remove.size > 0) {
143143
await this.store.removeData(remove as Store);

packages/uma/src/policies/authorizers/SimpleOdrlAuthorizer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { DataFactory as DF, Quad_Subject, Store } from 'n3';
44
import { ODRL } from 'odrl-evaluator';
55
import { CLIENTID, WEBID } from '../../credentials/Claims';
66
import { ClaimSet } from '../../credentials/ClaimSet';
7-
import { UCRulesStorage } from '../../ucp/storage/UCRulesStorage';
7+
import { ReadOnlyStore, UCRulesStorage } from '../../ucp/storage/UCRulesStorage';
88
import { Permission } from '../../views/Permission';
99
import { Authorizer } from './Authorizer';
1010

@@ -63,7 +63,7 @@ export class SimpleOdrlAuthorizer implements Authorizer {
6363
return permissions;
6464
}
6565

66-
protected getPermissions(policies: Store, claims: ClaimSet, resource: string, scope: string):
66+
protected getPermissions(policies: ReadOnlyStore, claims: ClaimSet, resource: string, scope: string):
6767
Permission[] | undefined {
6868
this.logger.info(`Evaluating Request ${scope}, ${resource} with claims ${JSON.stringify(claims)}`);
6969
const targets = [ DF.namedNode(resource), ...policies.getObjects(resource, ODRL.terms.partOf, null)];
@@ -153,7 +153,7 @@ export class SimpleOdrlAuthorizer implements Authorizer {
153153
* and undefined if any constraint is too complex to evaluate.
154154
* Only supports purpose (for client ID) and dateTime constraints.
155155
*/
156-
protected validateConstraints(rule: Quad_Subject, policies: Store, claims: ClaimSet): boolean | undefined {
156+
protected validateConstraints(rule: Quad_Subject, policies: ReadOnlyStore, claims: ClaimSet): boolean | undefined {
157157
const constraints = policies.getObjects(rule, ODRL.terms.constraint, null).map(constraint => ({
158158
leftOperand: policies.getObjects(constraint, ODRL.terms.leftOperand, null)[0],
159159
operator: policies.getObjects(constraint, ODRL.terms.operator, null)[0],

packages/uma/src/routes/Collection.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { ODRL } from 'odrl-evaluator';
1616
import { WEBID } from '../credentials/Claims';
1717
import { CredentialParser } from '../credentials/CredentialParser';
1818
import { Verifier } from '../credentials/verify/Verifier';
19-
import { UCRulesStorage } from '../ucp/storage/UCRulesStorage';
19+
import { ReadOnlyStore, UCRulesStorage } from '../ucp/storage/UCRulesStorage';
2020
import { DC, ODRL_P, OWL } from '../ucp/util/Vocabularies';
2121
import { writeStore } from '../util/ConvertUtil';
2222
import {
@@ -178,7 +178,7 @@ export class CollectionRequestHandler extends HttpHandler {
178178
/**
179179
* Verifies if the user is allowed to modify the given collection.
180180
*/
181-
protected async verifyOwnership(subject: NamedNode, userId: string, store?: Store): Promise<void> {
181+
protected async verifyOwnership(subject: NamedNode, userId: string, store?: ReadOnlyStore): Promise<void> {
182182
const userNode = DF.namedNode(userId);
183183
store = store ?? await this.policies.getStore();
184184

packages/uma/src/routes/ResourceRegistration.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import { getLoggerFor } from 'global-logger-factory';
1414
import { DataFactory as DF, NamedNode, Quad, Quad_Subject, Store } from 'n3';
1515
import { randomUUID } from 'node:crypto';
16-
import { UCRulesStorage } from '../ucp/storage/UCRulesStorage';
16+
import { ReadOnlyStore, UCRulesStorage } from '../ucp/storage/UCRulesStorage';
1717
import { DC, ODRL, ODRL_P, OWL } from '../ucp/util/Vocabularies';
1818
import {
1919
HttpHandler,
@@ -248,7 +248,7 @@ export class ResourceRegistrationRequestHandler extends HttpHandler {
248248
* @param previous - The previous {@link ResourceDescription}, in case this is an update.
249249
*/
250250
protected async updateCollections(
251-
policyStore: Store,
251+
policyStore: ReadOnlyStore,
252252
id: string,
253253
owner: string,
254254
description: ResourceDescription,
@@ -307,7 +307,7 @@ export class ResourceRegistrationRequestHandler extends HttpHandler {
307307
* @param previous - The previous {@link ResourceDescription}, in case this is an update.
308308
*/
309309
protected async updateRelations(
310-
policyStore: Store,
310+
policyStore: ReadOnlyStore,
311311
id: string,
312312
description: ResourceDescription,
313313
previous?: ResourceDescription
@@ -417,7 +417,7 @@ export class ResourceRegistrationRequestHandler extends HttpHandler {
417417
* @param entries - {@link CollectionMetadata} objects to parse.
418418
* @param policyStore - {@link Store} with the relevant triples to update.
419419
*/
420-
protected generatePartOfTriples(part: NamedNode, entries: CollectionMetadata[], policyStore: Store): Quad[] {
420+
protected generatePartOfTriples(part: NamedNode, entries: CollectionMetadata[], policyStore: ReadOnlyStore): Quad[] {
421421
const quads: Quad[] = [];
422422
for (const entry of entries) {
423423
const collectionIds = this.findCollectionIds(entry, policyStore);
@@ -439,7 +439,7 @@ export class ResourceRegistrationRequestHandler extends HttpHandler {
439439
* @param entry - Relevant {@link CollectionMetadata}.
440440
* @param data - {@link Store} in which to find the matching triples.
441441
*/
442-
protected findCollectionIds(entry: CollectionMetadata, data: Store): Quad_Subject[] {
442+
protected findCollectionIds(entry: CollectionMetadata, data: ReadOnlyStore): Quad_Subject[] {
443443
const sourceMatches = data.getSubjects(ODRL.terms.source, entry.source, null);
444444
if (entry.reverse) {
445445
const blankQuads = sourceMatches.flatMap((subject): Quad[] =>

packages/uma/src/ucp/storage/DirectoryUCRulesStorage.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { extractQuadsRecursive } from '../util/Util';
2-
import { UCRulesStorage } from "./UCRulesStorage";
2+
import { ReadOnlyStore, UCRulesStorage } from './UCRulesStorage';
33
import * as path from 'path'
44
import * as fs from 'fs'
55
import { Parser, Store, Writer } from 'n3';
@@ -28,9 +28,9 @@ export class DirectoryUCRulesStorage implements UCRulesStorage {
2828
this.baseIRI = baseIRI;
2929
}
3030

31-
public async getStore(): Promise<Store> {
31+
public async getStore(): Promise<ReadOnlyStore> {
3232
if (this.filesRead) {
33-
return new Store(this.store);
33+
return this.store;
3434
}
3535

3636
const parser = new Parser({ baseIRI: this.baseIRI });
@@ -51,13 +51,13 @@ export class DirectoryUCRulesStorage implements UCRulesStorage {
5151
}
5252

5353

54-
public async removeData(data: Store): Promise<void> {
54+
public async removeData(data: ReadOnlyStore): Promise<void> {
5555
// Make sure the files have been read into memory
5656
await this.getStore();
5757
this.store.removeQuads(data.getQuads(null, null, null, null));
5858
}
5959

60-
public async getRule(identifier: string): Promise<Store> {
60+
public async getRule(identifier: string): Promise<ReadOnlyStore> {
6161
const allRules = await this.getStore()
6262
return extractQuadsRecursive(allRules, identifier);
6363
}

packages/uma/src/ucp/storage/MemoryUCRulesStorage.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { DataFactory, Store } from 'n3';
22
import { extractQuadsRecursive } from '../util/Util';
3-
import { UCRulesStorage } from './UCRulesStorage';
3+
import { ReadOnlyStore, UCRulesStorage } from './UCRulesStorage';
44

55
const { namedNode } = DataFactory;
66

@@ -11,16 +11,16 @@ export class MemoryUCRulesStorage implements UCRulesStorage {
1111
this.store = new Store();
1212
}
1313

14-
public async getStore(): Promise<Store> {
15-
return new Store(this.store);
14+
public async getStore(): Promise<ReadOnlyStore> {
15+
return this.store;
1616
}
1717

1818

19-
public async addRule(rule: Store): Promise<void> {
19+
public async addRule(rule: ReadOnlyStore): Promise<void> {
2020
this.store.addQuads(rule.getQuads(null, null, null, null))
2121
}
2222

23-
public async getRule(identifier: string): Promise<Store> {
23+
public async getRule(identifier: string): Promise<ReadOnlyStore> {
2424
// currently doesn't check whether it is actually an odrl:rule
2525
return extractQuadsRecursive(this.store, identifier)
2626
}
@@ -36,7 +36,7 @@ export class MemoryUCRulesStorage implements UCRulesStorage {
3636
this.deleteRule(ruleID);
3737
}
3838

39-
public async removeData(data: Store): Promise<void> {
39+
public async removeData(data: ReadOnlyStore): Promise<void> {
4040
this.store.removeQuads(data.getQuads(null, null, null, null));
4141
}
4242
}

packages/uma/src/ucp/storage/UCRulesStorage.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,40 @@
11
import { Store } from "n3";
22

3+
/**
4+
* A read-only view of an N3 Store.
5+
* All write/mutation methods are excluded.
6+
*/
7+
export type ReadOnlyStore = Omit<
8+
Store,
9+
| 'addAll'
10+
| 'deleteMatches'
11+
| 'add'
12+
| 'addQuad'
13+
| 'addQuads'
14+
| 'delete'
15+
| 'import'
16+
| 'removeQuad'
17+
| 'removeQuads'
18+
| 'remove'
19+
| 'removeMatches'
20+
| 'deleteGraph'
21+
| 'clear'
22+
>;
23+
324
export interface UCRulesStorage {
4-
getStore: () => Promise<Store>;
25+
getStore: () => Promise<ReadOnlyStore>;
526
/**
627
* Add a single Usage Control Rule to the storage
728
* @param rule
829
* @returns
930
*/
10-
addRule: (rule: Store) => Promise<void>;
31+
addRule: (rule: ReadOnlyStore) => Promise<void>;
1132
/**
1233
* Get a Usage Control Rule from the storage
1334
* @param identifier
1435
* @returns
1536
*/
16-
getRule: (identifier: string) => Promise<Store>;
37+
getRule: (identifier: string) => Promise<ReadOnlyStore>;
1738
/**
1839
* Delete a Usage Control Rule from the storage
1940
* @param identifier
@@ -30,5 +51,5 @@ export interface UCRulesStorage {
3051
* Removes specific triples from the storage.
3152
* @param data
3253
*/
33-
removeData: (data: Store) => Promise<void>;
54+
removeData: (data: ReadOnlyStore) => Promise<void>;
3455
}

packages/uma/src/ucp/util/Util.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Store } from "n3";
2+
import { ReadOnlyStore } from '../storage/UCRulesStorage';
23

34
/**
45
* A recursive search algorithm that gives all quads that a subject can reach (working with circles)
@@ -7,7 +8,7 @@ import { Store } from "n3";
78
* @param subjectIRI
89
* @param existing IRIs that already have done the recursive search (IRIs in there must not be searched for again)
910
*/
10-
export function extractQuadsRecursive(store: Store, subjectIRI: string, existing?: string[]): Store {
11+
export function extractQuadsRecursive(store: ReadOnlyStore, subjectIRI: string, existing?: string[]): Store {
1112
const tempStore = new Store();
1213
const subjectIRIQuads = store.getQuads(subjectIRI, null, null, null);
1314

packages/uma/src/util/ConvertUtil.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { Prefixes, Store, Writer } from 'n3';
1+
import { Prefixes, Writer } from 'n3';
22
import { parse, stringify } from 'node:querystring';
33
import { NamedNode } from '@rdfjs/types';
4+
import { ReadOnlyStore } from '../ucp/storage/UCRulesStorage';
45

56
/**
67
* Converts a x-www-form-urlencoded string to a JSON object.
@@ -49,7 +50,7 @@ export function isIri(input: string): boolean {
4950
/**
5051
* Write an N3 store to a string (in turtle format)
5152
*/
52-
export async function writeStore(store: Store, prefixes: Prefixes<NamedNode | string> = {}): Promise<string> {
53+
export async function writeStore(store: ReadOnlyStore, prefixes: Prefixes<NamedNode | string> = {}): Promise<string> {
5354
const writer = new Writer({ format: 'text/turtle', prefixes });
5455
writer.addQuads(store.getQuads(null, null, null, null));
5556

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Store } from "n3";
1+
import { ReadOnlyStore } from '../../ucp/storage/UCRulesStorage';
22

33
/**
44
* Check whether all subjects in the new store
@@ -11,7 +11,7 @@ import { Store } from "n3";
1111
* @param newStore the store containing new data
1212
* @returns true if no subjects are already defined, false otherwise
1313
*/
14-
export const noAlreadyDefinedSubjects = (store: Store, newStore: Store): boolean =>
14+
export const noAlreadyDefinedSubjects = (store: ReadOnlyStore, newStore: ReadOnlyStore): boolean =>
1515
newStore.getSubjects(null, null, null)
1616
.every((subject) => store.countQuads(subject, null, null, null) === 0);
1717
export class ConflictError extends Error {

0 commit comments

Comments
 (0)