From 83c151cac2a6e1122e12e514d6d7562c70ab06a6 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Wed, 16 Jul 2025 16:08:34 +0200 Subject: [PATCH 01/17] GET --- controller/src/classes/Controller.ts | 144 ++++++++---------- .../permissionManager/inrupt/WebIdManager.ts | 54 +++++-- controller/src/classes/utils/PolicyParser.ts | 124 +++++++++++++++ controller/src/types/modules.ts | 43 ++++++ loama/src/components/layouts/HeaderLayout.vue | 4 +- loama/src/lib/state.ts | 17 ++- 6 files changed, 279 insertions(+), 107 deletions(-) create mode 100644 controller/src/classes/utils/PolicyParser.ts diff --git a/controller/src/classes/Controller.ts b/controller/src/classes/Controller.ts index aa3aad1..bba4fd2 100644 --- a/controller/src/classes/Controller.ts +++ b/controller/src/classes/Controller.ts @@ -2,13 +2,14 @@ import { BaseSubject, Index, IndexItem, Permission, ResourcePermissions, Resourc import { IAccessRequest, IController, IInboxConstructor, IStore, IStoreConstructor, SubjectConfig, SubjectConfigs, SubjectKey, SubjectType } from "../types/modules"; import { AccessRequest } from "./accessRequests/AccessRequest"; import { InruptAccessRequest } from "./accessRequests/InruptAccessRequest"; +import { WebIdManager } from "./permissionManager/inrupt"; import { Mutex } from "./utils/Mutex"; export class Controller>> extends Mutex implements IController { - private index: IStore> - private resources: IStore + private index: IStore>; + private resources: IStore; private accessRequest: AccessRequest; - private subjectConfigs: SubjectConfigs + private subjectConfigs: SubjectConfigs; // TODO : Find a better way of constructing the controller with all the different modules constructor(storeConstructor: IStoreConstructor, inboxConstructor: IInboxConstructor, subjects: SubjectConfigs) { @@ -21,6 +22,8 @@ export class Controller> } private getSubjectConfig>(subject: T[K]): SubjectConfig { + console.log('Controller.getSubjectConfig called'); + console.log('Arguments:', { subject }); const subjectConfig = this.subjectConfigs[subject.type]; if (!subjectConfig) { throw new Error(`No config found for subject type ${subject.type}`); @@ -29,6 +32,8 @@ export class Controller> } private async getExistingRemotePermissions>(resourceUrl: string, subject: T[K]): Promise { + console.log('Controller.getExistingRemotePermissions called'); + console.log('Arguments:', { resourceUrl, subject }); const subjectConfig = this.getSubjectConfig(subject) const subjects = await subjectConfig.manager.getRemotePermissions(resourceUrl); const subjectPermission = subjects.find(entry => subjectConfig.resolver.checkMatch(entry.subject, subject)) @@ -38,6 +43,8 @@ export class Controller> private async getExistingPermissions>(resourceUrl: string, subject: T[K]): Promise { + console.log('Controller.getExistingPermissions called'); + console.log('Arguments:', { resourceUrl, subject }); const item = await this.getItem(resourceUrl, subject); if (item) { // Makeing sure the array is not a reference to the one stored in the index @@ -47,6 +54,8 @@ export class Controller> } private async updateItem>(resourceUrl: string, subject: SubjectType, permissions: Permission[], alwaysKeepItem = false) { + console.log('Controller.updateItem called'); + console.log('Arguments:', { resourceUrl, subject, permissions, alwaysKeepItem }); let item = await this.getItem(resourceUrl, subject); const { manager } = this.getSubjectConfig(subject) @@ -90,31 +99,43 @@ export class Controller> } AccessRequest(): IAccessRequest { + console.log('Controller.AccessRequest called'); + console.log('Arguments:', {}); return this.accessRequest; } async setPodUrl(podUrl: string) { + console.log('Controller.setPodUrl called'); + console.log('Arguments:', { podUrl }); this.index.setPodUrl(podUrl); this.resources.setPodUrl(podUrl); await this.accessRequest.setPodUrl(podUrl) } unsetPodUrl() { + console.log('Controller.unsetPodUrl called'); + console.log('Arguments:', {}); this.index.unsetPodUrl(); this.resources.unsetPodUrl(); this.accessRequest.unsetPodUrl(); } async getOrCreateIndex() { + console.log('Controller.getOrCreateIndex called'); + console.log('Arguments:', {}); return this.index.getOrCreate(); } getLabelForSubject>(subject: T[K]): string { + console.log('Controller.getLabelForSubject called'); + console.log('Arguments:', { subject }); const { resolver } = this.getSubjectConfig(subject); return resolver.toLabel(subject); } async getItem>(resourceUrl: string, subject: SubjectType): Promise | undefined> { + console.log('Controller.getItem called'); + console.log('Arguments:', { resourceUrl, subject }); const { resolver } = this.getSubjectConfig(subject); const index = await this.index.getCurrent() as Index; @@ -122,6 +143,8 @@ export class Controller> } async addPermission>(resourceUrl: string, addedPermission: Permission, subject: SubjectType) { + console.log('Controller.addPermission called'); + console.log('Arguments:', { resourceUrl, addedPermission, subject }); const release = await this.acquire(); try { let permissions = await this.getExistingPermissions(resourceUrl, subject); @@ -143,6 +166,8 @@ export class Controller> } async removeSubject>(resourceUrl: string, subject: SubjectType) { + console.log('Controller.removeSubject called'); + console.log('Arguments:', { resourceUrl, subject }); await this.updateItem(resourceUrl, subject, []); const subjectConfig = this.getSubjectConfig(subject); @@ -160,6 +185,8 @@ export class Controller> } async removePermission>(resourceUrl: string, removedPermission: Permission, subject: SubjectType) { + console.log('Controller.removePermission called'); + console.log('Arguments:', { resourceUrl, removedPermission, subject }); const release = await this.acquire(); try { let oldPermissions = await this.getExistingPermissions(resourceUrl, subject); @@ -180,6 +207,8 @@ export class Controller> } async enablePermissions>(resource: string, subject: SubjectType) { + console.log('Controller.enablePermissions called'); + console.log('Arguments:', { resource, subject }); let item = await this.getItem(resource, subject); if (!item) { // This point should never be reached @@ -194,6 +223,8 @@ export class Controller> } async disablePermissions>(resourceUrl: string, subject: SubjectType) { + console.log('Controller.disablePermissions called'); + console.log('Arguments:', { resourceUrl, subject }); let item = await this.getItem(resourceUrl, subject); if (!item) { throw new Error("Item not found to disable permissions from") @@ -208,94 +239,39 @@ export class Controller> } async getContainerPermissionList(containerUrl: string): Promise[]> { - const configs: SubjectConfig[] = Object.values(this.subjectConfigs); + console.log('Controller.getContainerPermissionList called'); + console.log('Arguments:', { containerUrl }); - const index = await this.index.getCurrent(); - const resourcesToSkip = index.items.filter(i => { - if (!i.resource.includes(containerUrl)) { - return false; - } - const strippedURI = i.resource.replace(containerUrl, ""); - const slashIdx = strippedURI.indexOf("/"); - if (slashIdx < 0) { - return true; - } - return !strippedURI.includes("/", slashIdx + 1) + // const targets = await new WebIdManager().getRemotePermissions(""); + // Use the subjectConfigs to get permissions for the container + // For each subjectConfig, call its manager.getRemotePermissions for the containerUrl + const configs: SubjectConfig[] = Object.values(this.subjectConfigs); + const results = await Promise.all( + configs.filter(c => (c.manager as WebIdManager).type === 'webID').map(c => c.manager.getRemotePermissions(containerUrl)) + ); + // Flatten the results and process as needed + const targets = results.flat(); + const resourcePermissions: ResourcePermissions[] = []; + + targets.forEach(async result => { + console.log("result", result) + resourcePermissions.push( + { + resourceUrl: result.subject.selector?.url ?? "if you see this, something went wrong", + canRequestAccess: await this.accessRequest.canRequestAccessToResource(result.subject.selector?.url), + permissionsPerSubject: [{ subject: result.subject, permissions: result.permissions, isEnabled: result.isEnabled }] + }) }); - const results = await Promise.allSettled(configs.map(c => c.manager.getContainerPermissionList(containerUrl, resourcesToSkip.map(i => i.resource)))) - const resourcesToClean = resourcesToSkip.filter(r => r.isEnabled).map(r => r.resource); - - if (results.length !== 0) { - index.items.push(...results.reduce]>[]>((arr, v) => { - if (v.status === "fulfilled") { - // Check if the resourceUrl is already present before pushing it into the array - v.value.forEach((r) => { - for (let i = 0; i < resourcesToClean.length; i++) { - const rtc = resourcesToClean[i]; - if (r.resourceUrl.includes(rtc)) { - resourcesToClean.splice(i, 1); - i--; - } - } - // @ts-expect-error - arr.push(...r.permissionsPerSubject.map(p => ({ - id: crypto.randomUUID(), - requestId: crypto.randomUUID(), - isEnabled: true, - permissions: p.permissions, - resource: r.resourceUrl, - subject: p.subject - }))) - }) - } - return arr; - }, [])); - } - - await Promise.allSettled(resourcesToClean.map(async r => { - for (let i = 0; i < index.items.length; i++) { - let entry = index.items[i]; - if (!entry.resource.includes(r)) { - continue - } - index.items.splice(i, 1); - } - })); - - await this.index.saveToRemote(); + console.log(resourcePermissions) - return await index.items.reduce[]>>(async (arr, v) => { - const resourcePath = v.resource.replace(containerUrl, ""); - const amountOfSlashes = resourcePath.replace(/[^\/]/g, "").length; - if ((amountOfSlashes == 1 && !resourcePath.endsWith("/")) || resourcePath.startsWith("/") || amountOfSlashes > 1) { - return arr; - } - let indexItems = await arr; - let existingInfo = indexItems.find((info) => info.resourceUrl === v.resource); - if (existingInfo) { - existingInfo.permissionsPerSubject.push({ - permissions: v.permissions, - subject: v.subject, - isEnabled: v.isEnabled, - }) - } else { - indexItems.push({ - resourceUrl: v.resource, - canRequestAccess: await this.accessRequest.canRequestAccessToResource(v.resource), - permissionsPerSubject: [{ - permissions: v.permissions, - subject: v.subject, - isEnabled: v.isEnabled, - }] - }) - } - return indexItems; - }, Promise.resolve([])); + return resourcePermissions } // NOTE: Do we want to force this to only use the index stored in the store? async getResourcePermissionList(resourceUrl: string) { + console.log('Controller.getResourcePermissionList called'); + console.log('Arguments:', { resourceUrl }); // Need to put it in a variable because the type declaration vanishes const configs: SubjectConfig[] = Object.values(this.subjectConfigs); const index = await this.index.getCurrent(); @@ -327,6 +303,8 @@ export class Controller> } isSubjectSupported>(subject: BaseSubject): IController> { + console.log('Controller.isSubjectSupported called'); + console.log('Arguments:', { subject }); if (!this.subjectConfigs[subject.type as unknown as keyof T]) { throw new Error(`Subject type ${subject.type} is not supported`); diff --git a/controller/src/classes/permissionManager/inrupt/WebIdManager.ts b/controller/src/classes/permissionManager/inrupt/WebIdManager.ts index 2c275ec..bc37814 100644 --- a/controller/src/classes/permissionManager/inrupt/WebIdManager.ts +++ b/controller/src/classes/permissionManager/inrupt/WebIdManager.ts @@ -1,10 +1,11 @@ import { getDefaultSession } from "@inrupt/solid-client-authn-browser"; -import { BaseSubject, IndexItem, Permission } from "../../../types"; +import { BaseSubject, IndexItem, Permission, SubjectPermissions } from "../../../types"; import { IPermissionManager, SubjectKey } from "../../../types/modules"; import { InruptPermissionManager } from "./InruptPermissionManager"; -import { getAgentAccessAll, setAgentAccess } from "@inrupt/solid-client/universal"; +import { setAgentAccess } from "@inrupt/solid-client/universal"; import { AccessModes, } from "@inrupt/solid-client"; -import { cacheBustedSessionFetch } from "../../../util"; +import { PolicyParser } from "../../utils/PolicyParser"; + export class WebIdManager>> extends InruptPermissionManager implements IPermissionManager { private async updateACL>(resource: string, subject: T[K], accessModes: Partial) { @@ -31,22 +32,47 @@ export class WebIdManager>(resourceUrl: string) { + async getRemotePermissions>(resourceUrl: string): Promise[]> { + // Extract our webID const session = getDefaultSession(); - const agentAccess = await getAgentAccessAll(resourceUrl, { fetch: cacheBustedSessionFetch(session) }); + const webId = session.info.webId; - if (!agentAccess) { - return []; + // We must be logged on + if (!webId) { + throw new Error("User not logged in"); } - return Object.entries(agentAccess).map(([url, access]) => ({ - // @ts-expect-error selector is required for webId + // Temporal + const url = "http://localhost:4000/uma/policies"; + + // Get all our policies + const response = await fetch(url, { + headers: { + "Authorization": webId, + "Accept": "text/turtle" + } + }); + + const turtleText = await response.text(); + console.log("Retrieved Turtle:", turtleText); + + // Use parser to extract an N3 Store + const parser = new PolicyParser(); + const store = parser.parseText(turtleText); + + // Parse to SubjectPermissions + const targets = parser.ownedPoliciesToObject(store); + + // Do we even need different subject types when always working with policy targets..... + return targets.map(target => ({ subject: { type: "webId", - selector: { url }, - } as T[K], - permissions: this.AccessModesToPermissions(access), - isEnabled: true, - })) + selector: { url: target.uri }, + } as unknown as T[K], + permissions: [...target.permissions], + isEnabled: true + })); } + + type = 'webID'; } diff --git a/controller/src/classes/utils/PolicyParser.ts b/controller/src/classes/utils/PolicyParser.ts new file mode 100644 index 0000000..bbdb134 --- /dev/null +++ b/controller/src/classes/utils/PolicyParser.ts @@ -0,0 +1,124 @@ +import { Permission } from "../../types/"; +import { IPolicy, ITarget } from "../../types/modules"; +import { DataFactory, Parser, Store } from "n3"; + +const { namedNode } = DataFactory + + +export class PolicyParser { + + public constructor() { } + + private ODRL = (something: string) => namedNode(`http://www.w3.org/ns/odrl/2/${something}`); + private fromODRL = (odrlString: string) => odrlString.split('/')[6]; + private defaultTarget = (uri: string): ITarget => ({ uri: uri, rules: new Set(), permissions: new Set(), policies: new Set() }) + + + public parseText = (text: string): Store => { + const parser = new Parser({ format: 'text/turtle' }); + const quads = parser.parse(text); + return new Store(quads); + } + + /** + * Extract the quads of one subject, and recursively add whatever their object is referring to + * @param store store to extract subject from + * @param subjectIRI + * @param existing IDs that have already been added to the store + * @returns detailed store of the original subject and all of their children + */ + private extractQuadsRecursive(store: Store, subjectIRI: string, existing: Set = new Set([subjectIRI])): Store { + // Add the direct quads to the store + const result = new Store(); + const subjectQuads = store.getQuads(subjectIRI, null, null, null); + result.addQuads(subjectQuads); + + // If objects are not already added, add their quads and their children + for (const quad of subjectQuads) { + if (!existing.has(quad.object.id)) { + existing.add(quad.object.id); + result.addQuads(this.extractQuadsRecursive(store, quad.object.id, existing).getQuads(null, null, null, null)); + } + } + return result; + } + + /** + * Function that returns the stored Target objects without sanitization + * This function assumes all policies are correct, and only contains information for the logged on client + * @param store the owned policies in a store + */ + public ownedPoliciesToObject = (store: Store): ITarget[] => { + + // 1. Find every target + const ruleTargetQuads = store.getQuads(null, this.ODRL('target'), null, null); + + // 2. Inspect the Rule of each target + const idToTarget = new Map(); + for (const targetQuad of ruleTargetQuads) { + const rule = targetQuad.subject; + + // Find out in what policy this rule occurs + // Since a valid policy only has unique ID's, we can just search for ' .' quads on the entire store + const policyIDs: Set = new Set(); + for (const relation of ['permission', 'prohibition', 'duty'].map(x => this.ODRL(x))) + store.getQuads(null, relation, rule, null).forEach(res => policyIDs.add(res.subject.id)); + if (policyIDs.size !== 1) + console.warn("Corrupted Policy"); + console.log(policyIDs) + const policyId = [...policyIDs][0]; + + // Get all quads of this rule + const ruleStore = this.extractQuadsRecursive(store, rule.id); + + // Find all permission information + const permissions = []; + for (const quad of ruleStore.getQuads(null, this.ODRL('action'), null, null)) { + // TODO: find a way to categorize all actions as one of the Permission types + const action = this.fromODRL(quad.object.id).toLowerCase(); + console.log('action', action) + + switch (action) { + case "use": + case "play": + case "read": + permissions.push(Permission.Read); + break; + + case "write": + case "update": + permissions.push(Permission.Write); + break; + + case "append": + permissions.push(Permission.Append); + break; + + case "control": + case "manage": + permissions.push(Permission.Control); + break; + + default: + console.warn(`Unrecognized ODRL action: ${action}`); + } + + } + + // For every target in this store, add the permissions + for (const quad of ruleStore.getQuads(null, this.ODRL('target'), null, null)) { + // If target id is not yet handled, set it to a default target object + if (!idToTarget.has(quad.object.id)) + idToTarget.set(quad.object.id, this.defaultTarget(quad.object.id)); + + const targetObject = idToTarget.get(quad.object.id); + + permissions.forEach(p => targetObject?.permissions.add(p)); + targetObject?.rules.add(rule.id); + targetObject?.policies.add(policyId); + } + } + + return Array.from(idToTarget.values()); + } +} \ No newline at end of file diff --git a/controller/src/types/modules.ts b/controller/src/types/modules.ts index db9b0f9..0ac96dd 100644 --- a/controller/src/types/modules.ts +++ b/controller/src/types/modules.ts @@ -140,3 +140,46 @@ export interface IPermissionManager>> { */ shouldDeleteOnAllRevoked(): boolean } + +// Temporal (?) interface to represent a policy +export interface IPolicy { + rules: IRule[]; + id: string; +} + +export type RuleType = 'permission' | 'prohibition' | 'duty'; + +// Temporal (?) interface to represent a rule within a policy +export interface IRule { + // What kind of rule is this + ruleType: RuleType; + + // Every rule has one assigner represented by its webID + assigner: string; + + // Multiple assignees possible + assignees: string[]; + + // What actions does this rule definine? + permissions: string[]; + + // target objects of the rule + targets: string[]; + + // ID + id: string; +} + +export interface ITarget { + // The uri of the target + uri: string; + + // The rule IDs where target is mentioned + rules: Set; + + // The policy IDs where target is mentioned + policies: Set; + + // the actions set to the target + permissions: Set; +} diff --git a/loama/src/components/layouts/HeaderLayout.vue b/loama/src/components/layouts/HeaderLayout.vue index 3aaf3b8..a015c7c 100644 --- a/loama/src/components/layouts/HeaderLayout.vue +++ b/loama/src/components/layouts/HeaderLayout.vue @@ -2,8 +2,8 @@ Resources - Request access - Access Requests + diff --git a/loama/src/lib/state.ts b/loama/src/lib/state.ts index 1e4fd60..8b787e7 100644 --- a/loama/src/lib/state.ts +++ b/loama/src/lib/state.ts @@ -21,7 +21,7 @@ export const usePodStore = defineStore("pod", { return state.podEntries.map(resourcePermissions => { const uri = resourcePermissions.resourceUrl.replace(store.usedPod, ''); const isContainer = uri.charAt(uri.length - 1) === '/' - const name = uriToName(uri, isContainer); + const name = uri//uriToName(uri, isContainer); const publicAccess = (resourcePermissions.permissionsPerSubject.find(s => s.subject.type === "public")?.permissions.length ?? 0) > 0; return { isContainer, @@ -38,13 +38,14 @@ export const usePodStore = defineStore("pod", { actions: { async loadResources(url: string) { this.podEntries = (await activeController.getContainerPermissionList(url)) - // Filter out the current resource - .filter(entry => entry.resourceUrl !== url) - // Filter out the things that are nested (?) - .filter(entry => { - const depth = entry.resourceUrl.replace(url, '').split('/'); - return depth.length <= 2; - }) + // Filter out the current resource + // .filter(entry => entry.resourceUrl !== url) + // Filter out the things that are nested (?) + // .filter(entry => { + // const depth = entry.resourceUrl.replace(url, '').split('/'); + // console.log(depth); + // return depth.length <= 2; + // }) }, async refreshEntryPermissions() { if (!this.selectedEntry) { From 1a04a28f56bd1c9f8945bd68846bdf86735aa3d9 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Thu, 17 Jul 2025 11:26:31 +0200 Subject: [PATCH 02/17] add targetId to display target and subject owners properly --- controller/src/classes/Controller.ts | 7 ++++--- .../src/classes/permissionManager/inrupt/WebIdManager.ts | 8 ++++++-- controller/src/types/index.ts | 1 + 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/controller/src/classes/Controller.ts b/controller/src/classes/Controller.ts index bba4fd2..7612d79 100644 --- a/controller/src/classes/Controller.ts +++ b/controller/src/classes/Controller.ts @@ -242,12 +242,13 @@ export class Controller> console.log('Controller.getContainerPermissionList called'); console.log('Arguments:', { containerUrl }); - // const targets = await new WebIdManager().getRemotePermissions(""); // Use the subjectConfigs to get permissions for the container // For each subjectConfig, call its manager.getRemotePermissions for the containerUrl + + // Eventually, we would only need to use the webIDManager... const configs: SubjectConfig[] = Object.values(this.subjectConfigs); const results = await Promise.all( - configs.filter(c => (c.manager as WebIdManager).type === 'webID').map(c => c.manager.getRemotePermissions(containerUrl)) + configs.map(c => c.manager.getRemotePermissions(containerUrl)) ); // Flatten the results and process as needed const targets = results.flat(); @@ -257,7 +258,7 @@ export class Controller> console.log("result", result) resourcePermissions.push( { - resourceUrl: result.subject.selector?.url ?? "if you see this, something went wrong", + resourceUrl: result.targetId ?? "if you see this, something went wrong", canRequestAccess: await this.accessRequest.canRequestAccessToResource(result.subject.selector?.url), permissionsPerSubject: [{ subject: result.subject, permissions: result.permissions, isEnabled: result.isEnabled }] }) diff --git a/controller/src/classes/permissionManager/inrupt/WebIdManager.ts b/controller/src/classes/permissionManager/inrupt/WebIdManager.ts index bc37814..8b45a45 100644 --- a/controller/src/classes/permissionManager/inrupt/WebIdManager.ts +++ b/controller/src/classes/permissionManager/inrupt/WebIdManager.ts @@ -36,6 +36,7 @@ export class WebIdManager ({ subject: { type: "webId", - selector: { url: target.uri }, + selector: { url: webId }, } as unknown as T[K], permissions: [...target.permissions], - isEnabled: true + isEnabled: true, + targetId: target.uri })); } diff --git a/controller/src/types/index.ts b/controller/src/types/index.ts index 7287883..3936990 100644 --- a/controller/src/types/index.ts +++ b/controller/src/types/index.ts @@ -43,6 +43,7 @@ export interface SubjectPermissions> { subject: T; permissions: Permission[]; isEnabled: boolean; + targetId?: string; } export interface ResourcePermissions> { From 6d7cb2bc8258444041119620e5177c830e0f1312 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Fri, 18 Jul 2025 12:04:41 +0200 Subject: [PATCH 03/17] Rework policy interpreter, it now deals with public and private subjects --- controller/src/classes/Controller.ts | 52 ++++---- .../inrupt/InruptPermissionManager.ts | 112 +++++++++++++----- .../permissionManager/inrupt/PublicManager.ts | 33 +----- .../permissionManager/inrupt/WebIdManager.ts | 74 ++++-------- controller/src/classes/utils/PolicyParser.ts | 109 ++++++++++------- controller/src/types/modules.ts | 28 ++++- 6 files changed, 217 insertions(+), 191 deletions(-) diff --git a/controller/src/classes/Controller.ts b/controller/src/classes/Controller.ts index 7612d79..c627b38 100644 --- a/controller/src/classes/Controller.ts +++ b/controller/src/classes/Controller.ts @@ -47,7 +47,7 @@ export class Controller> console.log('Arguments:', { resourceUrl, subject }); const item = await this.getItem(resourceUrl, subject); if (item) { - // Makeing sure the array is not a reference to the one stored in the index + // Making sure the array is not a reference to the one stored in the index return [...item.permissions] } return this.getExistingRemotePermissions(resourceUrl, subject); @@ -143,21 +143,30 @@ export class Controller> } async addPermission>(resourceUrl: string, addedPermission: Permission, subject: SubjectType) { - console.log('Controller.addPermission called'); - console.log('Arguments:', { resourceUrl, addedPermission, subject }); + console.log("add permission with: ", { resourceUrl, addedPermission, subject }) const release = await this.acquire(); try { - let permissions = await this.getExistingPermissions(resourceUrl, subject); - if (permissions.indexOf(addedPermission) !== -1) { - console.error("Permission already granted") - return permissions; - } + // 1. Collect already existing permissions + const permissions = subject.type === "webId" + ? (await this.getSubjectConfig(subject).manager.getRemotePermissions(resourceUrl)) + .filter(p => p.subject.type === "webId" && p.subject.selector!.url === subject.selector!.url) + .map(p => p.permissions)[0] ?? [] + : []; + + // 2. Let the manager add the permission, and return the + - permissions.push(addedPermission) - await this.updateItem(resourceUrl, subject, permissions) - return permissions; + // if (permissions.indexOf(addedPermission) !== -1) { + // console.error("Permission already granted") + // return permissions; + // } + + // permissions.push(addedPermission) + + // await this.updateItem(resourceUrl, subject, permissions) + return []; } catch (e) { throw e; } finally { @@ -248,25 +257,10 @@ export class Controller> // Eventually, we would only need to use the webIDManager... const configs: SubjectConfig[] = Object.values(this.subjectConfigs); const results = await Promise.all( - configs.map(c => c.manager.getRemotePermissions(containerUrl)) + configs.map(c => c.manager.getContainerPermissionList(containerUrl)) ); - // Flatten the results and process as needed - const targets = results.flat(); - const resourcePermissions: ResourcePermissions[] = []; - - targets.forEach(async result => { - console.log("result", result) - resourcePermissions.push( - { - resourceUrl: result.targetId ?? "if you see this, something went wrong", - canRequestAccess: await this.accessRequest.canRequestAccessToResource(result.subject.selector?.url), - permissionsPerSubject: [{ subject: result.subject, permissions: result.permissions, isEnabled: result.isEnabled }] - }) - }); - - console.log(resourcePermissions) - - return resourcePermissions + + return results.flat() } // NOTE: Do we want to force this to only use the index stored in the store? diff --git a/controller/src/classes/permissionManager/inrupt/InruptPermissionManager.ts b/controller/src/classes/permissionManager/inrupt/InruptPermissionManager.ts index c91d194..7c074a9 100644 --- a/controller/src/classes/permissionManager/inrupt/InruptPermissionManager.ts +++ b/controller/src/classes/permissionManager/inrupt/InruptPermissionManager.ts @@ -1,7 +1,10 @@ import { Access, AccessModes, getSolidDataset, getThingAll } from "@inrupt/solid-client"; import { SubjectPermissions, BaseSubject, IndexItem, Permission, ResourcePermissions } from "../../../types"; -import { SubjectKey } from "../../../types/modules"; +import { SubjectKey, TargetSubjects } from "../../../types/modules"; import { getDefaultSession } from "@inrupt/solid-client-authn-browser"; +import { PolicyParser } from "../../../classes/utils/PolicyParser"; +import { Store } from "n3"; +import { PolicyEditor } from "../../../classes/utils/PolicyEditor"; const ACCESS_MODES_TO_PERMISSION_MAPPING: Record = { read: Permission.Read, @@ -12,6 +15,7 @@ const ACCESS_MODES_TO_PERMISSION_MAPPING: Record `http://localhost:4000/uma/policies${encodedId}` /** * A permission manager implementation using the inrupt sdk to actually update the ACL @@ -75,38 +79,86 @@ export abstract class InruptPermissionManager>(resourceUrl: string): Promise[]> + protected async fetchPolicies(webId: string) { - async getContainerPermissionList(containerUrl: string, resourceToSkip: string[] = []) { - const session = getDefaultSession(); - // this request is cached, so it doesn't matter if it's emitted multiple times in a short span - const dataset = await getSolidDataset(containerUrl, { fetch: session.fetch }); - const results = await Promise.allSettled( - getThingAll(dataset) - .map(async (resource) => { - if (resourceToSkip.includes(resource.url)) { - return { - resourceUrl: resource.url, - // We can set this to false as this is the default & getting doubled checked in the controller - canRequestAccess: false, - permissionsPerSubject: [], - } - } - return { - resourceUrl: resource.url, - // We can set this to false as this is the default & getting doubled checked in the controller - canRequestAccess: false, - permissionsPerSubject: await this.getRemotePermissions(resource.url) - } - }) - ) - return results.reduce[]>((arr, v) => { - if (v.status == "fulfilled") { - arr.push(v.value); + // Get all our policies + const response = await fetch(UMA_URL(), { + headers: { + "Authorization": webId, + "Accept": "text/turtle" } - return arr; - }, []) + }); + + // Extract the target Ids + const turtleText = await response.text(); + console.log("Retrieved Turtle:", turtleText); + + // Use parser to extract an N3 Store + const parser = new PolicyParser(); + return parser.parseText(turtleText); + } + + + public async getRemotePermissions>(resourceUrl: string): Promise[]> { + // Extract our webID + const session = getDefaultSession(); + const webId = session.info.webId; + console.log("webId", webId) + + // We must be logged on + if (!webId) { + throw new Error("User not logged in"); + } + const store = await this.fetchPolicies(webId) + + // Use parser to extract an N3 Store + const parser = new PolicyParser(); + + // Parse to SubjectPermissions + const targets: TargetSubjects[] = parser.permissionsForOneResource(resourceUrl, store); + + const subjectPermissions: SubjectPermissions[] = []; + targets.forEach(target => { + // Add the owner information + subjectPermissions.push({ + subject: { + type: "webId", + selector: { url: target.assigner } + } as unknown as T[K], + permissions: [Permission.Append, Permission.Control, Permission.Read, Permission.Write], + isEnabled: true, + targetId: target.targetUrl + }) + + // Add the public information + if (target.public) subjectPermissions.push({ + subject: { + type: "public", + } as unknown as T[K], + permissions: Array.from(target.public.permissions), + isEnabled: true, // Not yet implemented + targetId: target.targetUrl + }) + + // Add the private subjects + if (target.private) target.private.forEach(subject => subjectPermissions.push({ + subject: { + type: "webId", + selector: { url: subject.subject } + } as unknown as T[K], + permissions: Array.from(subject.permissions), + isEnabled: true, // Not yet implemented + targetId: target.targetUrl + })) + } + ); + + return subjectPermissions; + } + + async getContainerPermissionList(containerUrl: string, resourceToSkip: string[] = []): Promise[]> { + return []; } shouldDeleteOnAllRevoked() { return true } diff --git a/controller/src/classes/permissionManager/inrupt/PublicManager.ts b/controller/src/classes/permissionManager/inrupt/PublicManager.ts index cd8e6b4..72a0e61 100644 --- a/controller/src/classes/permissionManager/inrupt/PublicManager.ts +++ b/controller/src/classes/permissionManager/inrupt/PublicManager.ts @@ -1,49 +1,22 @@ -import { AccessModes, getSolidDataset, getThingAll } from "@inrupt/solid-client"; import { BaseSubject, IndexItem, Permission, ResourcePermissions } from "../../../types"; import { IPermissionManager, SubjectKey } from "../../../types/modules"; import { InruptPermissionManager } from "./InruptPermissionManager"; -import { getDefaultSession } from "@inrupt/solid-client-authn-browser"; -import { getPublicAccess, setPublicAccess } from "@inrupt/solid-client/universal"; -import { cacheBustedSessionFetch } from "../../../util"; export class PublicManager>> extends InruptPermissionManager implements IPermissionManager { - private async updateACL>(resource: string, subject: T[K], accessModes: Partial) { - const session = getDefaultSession(); - await setPublicAccess(resource, accessModes, { - fetch: session.fetch - }) - } - //. NOTE: Currently, it doesn't do any recursive permission setting on containers async createPermissions>(resource: string, subject: T[K], permissions: Permission[]): Promise { - const accessModes = this.permissionsToAccessModes(permissions, []); - await this.updateACL(resource, subject, accessModes) + console.log("public manager: create permissions") } async deletePermissions>(resource: string, subject: T[K]) { - await this.updateACL(resource, subject, {}); + console.log("public manager: delete permissions") } async editPermissions>(resource: string, item: IndexItem, subject: T[K], permissions: Permission[]) { - const accessModes = this.editPermissionsToAccessModes(item, permissions); - await this.updateACL(resource, subject, accessModes) + console.log("public manager: edit permissions") } - async getRemotePermissions>(resourceUrl: string) { - const session = getDefaultSession(); - const publicAccess = await getPublicAccess(resourceUrl, { fetch: cacheBustedSessionFetch(session) }) - if (!publicAccess) { - return []; - } - return [{ - subject: { - type: "public", - } as T[K], - permissions: this.AccessModesToPermissions(publicAccess), - isEnabled: true, - }] - } } diff --git a/controller/src/classes/permissionManager/inrupt/WebIdManager.ts b/controller/src/classes/permissionManager/inrupt/WebIdManager.ts index 8b45a45..f74db24 100644 --- a/controller/src/classes/permissionManager/inrupt/WebIdManager.ts +++ b/controller/src/classes/permissionManager/inrupt/WebIdManager.ts @@ -1,38 +1,25 @@ import { getDefaultSession } from "@inrupt/solid-client-authn-browser"; -import { BaseSubject, IndexItem, Permission, SubjectPermissions } from "../../../types"; +import { BaseSubject, IndexItem, Permission, ResourcePermissions, SubjectPermissions } from "../../../types"; import { IPermissionManager, SubjectKey } from "../../../types/modules"; import { InruptPermissionManager } from "./InruptPermissionManager"; -import { setAgentAccess } from "@inrupt/solid-client/universal"; -import { AccessModes, } from "@inrupt/solid-client"; -import { PolicyParser } from "../../utils/PolicyParser"; +import { ODRL, PolicyParser } from "../../utils/PolicyParser"; +// Temporal export class WebIdManager>> extends InruptPermissionManager implements IPermissionManager { - private async updateACL>(resource: string, subject: T[K], accessModes: Partial) { - const session = getDefaultSession(); - if (!subject.selector?.url) { - throw new Error("Missing url selector on WebID subject") - } - await setAgentAccess(resource, subject.selector.url, accessModes, { - fetch: session.fetch - }) - } + //. NOTE: Currently, it doesn't do any recursive permission setting on containers async createPermissions>(resource: string, subject: T[K], permissions: Permission[]): Promise { - const accessModes = this.permissionsToAccessModes(permissions, []); - await this.updateACL(resource, subject, accessModes) } async deletePermissions>(resource: string, subject: T[K]) { - await this.updateACL(resource, subject, {}); } async editPermissions>(resource: string, item: IndexItem, subject: T[K], permissions: Permission[]) { - const accessModes = this.editPermissionsToAccessModes(item, permissions); - await this.updateACL(resource, subject, accessModes) } - async getRemotePermissions>(resourceUrl: string): Promise[]> { + + async getContainerPermissionList(containerUrl: string, resourceToSkip: string[] = []): Promise[]> { // Extract our webID const session = getDefaultSession(); const webId = session.info.webId; @@ -43,40 +30,21 @@ export class WebIdManager q.object.id))); + const resourcePermissions: ResourcePermissions[] = [] + for (const targetUrl of targetUrls) { + console.log(targetUrl) + const perms = await this.getRemotePermissions(targetUrl); + resourcePermissions.push({ + resourceUrl: targetUrl, + canRequestAccess: true, // TODO: based on proper access logic + permissionsPerSubject: perms + }) + } - // Do we even need different subject types when always working with policy targets..... - return targets.map(target => ({ - subject: { - type: "webId", - selector: { url: webId }, - } as unknown as T[K], - permissions: [...target.permissions], - isEnabled: true, - targetId: target.uri - })); + return resourcePermissions; } - - type = 'webID'; } diff --git a/controller/src/classes/utils/PolicyParser.ts b/controller/src/classes/utils/PolicyParser.ts index bbdb134..ee33d2c 100644 --- a/controller/src/classes/utils/PolicyParser.ts +++ b/controller/src/classes/utils/PolicyParser.ts @@ -1,18 +1,18 @@ import { Permission } from "../../types/"; -import { IPolicy, ITarget } from "../../types/modules"; +import { IPolicy, ISpecificTargetInfo, TargetSubjects } from "../../types/modules"; import { DataFactory, Parser, Store } from "n3"; const { namedNode } = DataFactory +export const ODRL = (something: string) => namedNode(`http://www.w3.org/ns/odrl/2/${something}`); + export class PolicyParser { public constructor() { } - private ODRL = (something: string) => namedNode(`http://www.w3.org/ns/odrl/2/${something}`); private fromODRL = (odrlString: string) => odrlString.split('/')[6]; - private defaultTarget = (uri: string): ITarget => ({ uri: uri, rules: new Set(), permissions: new Set(), policies: new Set() }) - + private defaultTarget = (uri: string, subject: string = ""): ISpecificTargetInfo => ({ uri: uri, permissions: new Set(), subject: subject, public: subject === "" }) public parseText = (text: string): Store => { const parser = new Parser({ format: 'text/turtle' }); @@ -46,47 +46,41 @@ export class PolicyParser { /** * Function that returns the stored Target objects without sanitization * This function assumes all policies are correct, and only contains information for the logged on client + * + * Currently, it does not check the rule type (permission, prohibition, duty) and it assumes permission * @param store the owned policies in a store + * @returns the target -> subjects -> permissions relation for all owned targets */ - public ownedPoliciesToObject = (store: Store): ITarget[] => { - - // 1. Find every target - const ruleTargetQuads = store.getQuads(null, this.ODRL('target'), null, null); - - // 2. Inspect the Rule of each target - const idToTarget = new Map(); - for (const targetQuad of ruleTargetQuads) { - const rule = targetQuad.subject; - - // Find out in what policy this rule occurs - // Since a valid policy only has unique ID's, we can just search for ' .' quads on the entire store - const policyIDs: Set = new Set(); - for (const relation of ['permission', 'prohibition', 'duty'].map(x => this.ODRL(x))) - store.getQuads(null, relation, rule, null).forEach(res => policyIDs.add(res.subject.id)); - if (policyIDs.size !== 1) - console.warn("Corrupted Policy"); - console.log(policyIDs) - const policyId = [...policyIDs][0]; - - // Get all quads of this rule - const ruleStore = this.extractQuadsRecursive(store, rule.id); - - // Find all permission information + public ownedPoliciesToObject = (store: Store, specifiedTarget: string = ""): TargetSubjects[] => { + + // 1. Get every odrl:target . quad, or only the rules targetting the specified target + // Note that multiple rules can refer to the same target, and one rule can refer to multiple targets + const relevantRuleSet: Set = new Set((specifiedTarget === "" + ? store.getQuads(null, ODRL('target'), null, null) + : store.getQuads(null, ODRL('target'), namedNode(specifiedTarget), null)) + .map(quad => quad.subject.id)); + + // 2. Add permission information for every target we find + // Every target ID corresponds with the subjects that each have some permissions etc. + const idToTarget: Map = new Map(); + for (const ruleId of relevantRuleSet) { + + // 2.1 Get the every quad defined by the rule (and their children recursively) + const ruleStore = this.extractQuadsRecursive(store, ruleId); + + // 2.2 List all relevant actions for this rule const permissions = []; - for (const quad of ruleStore.getQuads(null, this.ODRL('action'), null, null)) { - // TODO: find a way to categorize all actions as one of the Permission types + for (const quad of ruleStore.getQuads(null, ODRL('action'), null, null)) { + // TODO: find a way to categorize all actions as one of the Permission types const action = this.fromODRL(quad.object.id).toLowerCase(); console.log('action', action) switch (action) { - case "use": - case "play": case "read": permissions.push(Permission.Read); break; case "write": - case "update": permissions.push(Permission.Write); break; @@ -95,7 +89,6 @@ export class PolicyParser { break; case "control": - case "manage": permissions.push(Permission.Control); break; @@ -105,20 +98,50 @@ export class PolicyParser { } - // For every target in this store, add the permissions - for (const quad of ruleStore.getQuads(null, this.ODRL('target'), null, null)) { - // If target id is not yet handled, set it to a default target object - if (!idToTarget.has(quad.object.id)) - idToTarget.set(quad.object.id, this.defaultTarget(quad.object.id)); + // Get assigner ID + const assigner = ruleStore.getQuads(null, ODRL('assigner'), null, null)[0].object.id; + if (!assigner) throw new Error("Corrupted Policy"); + + // Get assignee IDs + const subjects: string[] = ruleStore.getQuads(null, ODRL('assignee'), null, null).map(quad => quad.object.id); + + for (const target of ruleStore.getQuads(null, ODRL('target'), null, null).map(quad => quad.object.id)) { + // 2.3 Set the target's assigner if not already done + if (!idToTarget.get(target)) idToTarget.set(target, { assigner: assigner, targetUrl: target }); - const targetObject = idToTarget.get(quad.object.id); + // 2.4 Add the private assignee information for every target in the rule + for (const subject of subjects) { + // If target does not have subjects yet, set a default object + if (!idToTarget.get(target)!.private) + idToTarget.get(target)!.private = new Map(); - permissions.forEach(p => targetObject?.permissions.add(p)); - targetObject?.rules.add(rule.id); - targetObject?.policies.add(policyId); + // If subject is new to the target, set its permissions to a new set + if (!idToTarget.get(target)!.private!.has(subject)) + idToTarget.get(target)!.private!.set(subject, { uri: target, subject: subject, public: false, permissions: new Set() }); + + // Add the permissions of this rule to the subject + const targetObject: ISpecificTargetInfo = idToTarget.get(target)!.private!.get(subject)!; + permissions.forEach(p => targetObject.permissions.add(p)); + } + + if (subjects.length === 0) { + // If there is no public permission set, add one + if (!idToTarget.get(target)!.public) + idToTarget.get(target)!.public = { uri: target, public: true, subject: "", permissions: new Set() }; + + // Add the permissions to the set + const publicPermissions: Set = idToTarget.get(target)!.public!.permissions; + permissions.forEach(p => publicPermissions.add(p)); + } } } + // Return the list of target info objects return Array.from(idToTarget.values()); } + + // Return the subject -> permissions relation for a target + public permissionsForOneResource(resourceUrl: string, store: Store): TargetSubjects[] { + return this.ownedPoliciesToObject(store, resourceUrl); + } } \ No newline at end of file diff --git a/controller/src/types/modules.ts b/controller/src/types/modules.ts index 0ac96dd..e284a37 100644 --- a/controller/src/types/modules.ts +++ b/controller/src/types/modules.ts @@ -170,16 +170,32 @@ export interface IRule { id: string; } -export interface ITarget { +// The interface to display the permissions for one subject on one target +export interface ISpecificTargetInfo { + + // Indicate whether its public + public: boolean; + // The uri of the target uri: string; - // The rule IDs where target is mentioned - rules: Set; - - // The policy IDs where target is mentioned - policies: Set; + // The client that has permission over this target + subject: string; // the actions set to the target permissions: Set; } + +// A target can have multiple private subjects and a public subject +export interface TargetSubjects { + targetUrl: string; + + // The map of subject names and their permissions on this target + private?: Map; + + // The public permission settings for this target + public?: ISpecificTargetInfo; + + // WebID of the target owner + assigner: string; +} From 025faf51e8bb78c9032de89b8b7c3f817ba86dfe Mon Sep 17 00:00:00 2001 From: lennertdr Date: Fri, 25 Jul 2025 15:28:30 +0200 Subject: [PATCH 04/17] edit policy perms --- controller/src/classes/Controller.ts | 183 ++++++++---------- .../permissionManager/inrupt/GroupManager.ts | 2 + .../inrupt/InruptPermissionManager.ts | 78 ++++++-- .../permissionManager/inrupt/PublicManager.ts | 9 +- .../permissionManager/inrupt/WebIdManager.ts | 41 +--- controller/src/classes/utils/PolicyEditor.ts | 156 +++++++++++++++ .../src/classes/utils/PolicyInterpreter.ts | 160 +++++++++++++++ controller/src/classes/utils/PolicyParser.ts | 130 ------------- controller/src/types/modules.ts | 10 +- 9 files changed, 489 insertions(+), 280 deletions(-) create mode 100644 controller/src/classes/utils/PolicyEditor.ts create mode 100644 controller/src/classes/utils/PolicyInterpreter.ts diff --git a/controller/src/classes/Controller.ts b/controller/src/classes/Controller.ts index c627b38..ac69ebe 100644 --- a/controller/src/classes/Controller.ts +++ b/controller/src/classes/Controller.ts @@ -1,3 +1,4 @@ +import { getDefaultSession } from "@inrupt/solid-client-authn-browser"; import { BaseSubject, Index, IndexItem, Permission, ResourcePermissions, Resources, SubjectPermissions } from "../types"; import { IAccessRequest, IController, IInboxConstructor, IStore, IStoreConstructor, SubjectConfig, SubjectConfigs, SubjectKey, SubjectType } from "../types/modules"; import { AccessRequest } from "./accessRequests/AccessRequest"; @@ -56,46 +57,46 @@ export class Controller> private async updateItem>(resourceUrl: string, subject: SubjectType, permissions: Permission[], alwaysKeepItem = false) { console.log('Controller.updateItem called'); console.log('Arguments:', { resourceUrl, subject, permissions, alwaysKeepItem }); - let item = await this.getItem(resourceUrl, subject); - const { manager } = this.getSubjectConfig(subject) - - if (item) { - await manager.editPermissions(resourceUrl, item, subject, permissions); - } else { - await manager.createPermissions(resourceUrl, subject, permissions); - - item = { - id: crypto.randomUUID(), - requestId: crypto.randomUUID(), - isEnabled: true, - permissions: permissions, - resource: resourceUrl, - subject: subject, - } - - const index = await this.index.getCurrent(); - index.items.push(item); - } - - if (!alwaysKeepItem && permissions.length === 0 && manager.shouldDeleteOnAllRevoked()) { - const index = await this.index.getCurrent(); - const idx = index.items.findIndex(i => i.id === item.id); - index.items.splice(idx, 1); - } else { - item.permissions = permissions; - - // The SOLID server OR the SDK does not directly push the update to the acl files for some reason - // Here we give it some time to save/push the changes - await new Promise(res => setTimeout(res, 500)); - // extra check what the ACL currently has stored as info. Will decrease the chance of the index going out of sync with the ACL file - const remotePermissions = await this.getExistingRemotePermissions(resourceUrl, subject); - if (remotePermissions !== permissions) { - console.debug("Permissions in index are out of sync with remote, updating index...", subject); - item.permissions = remotePermissions; - } - } - - await this.index.saveToRemote(); + // let item = await this.getItem(resourceUrl, subject); + // const { manager } = this.getSubjectConfig(subject) + + // if (item) { + // await manager.editPermissions(resourceUrl, item, subject, permissions); + // } else { + // await manager.createPermissions(resourceUrl, subject, permissions); + + // item = { + // id: crypto.randomUUID(), + // requestId: crypto.randomUUID(), + // isEnabled: true, + // permissions: permissions, + // resource: resourceUrl, + // subject: subject, + // } + + // const index = await this.index.getCurrent(); + // index.items.push(item); + // } + + // if (!alwaysKeepItem && permissions.length === 0 && manager.shouldDeleteOnAllRevoked()) { + // const index = await this.index.getCurrent(); + // const idx = index.items.findIndex(i => i.id === item.id); + // index.items.splice(idx, 1); + // } else { + // item.permissions = permissions; + + // // The SOLID server OR the SDK does not directly push the update to the acl files for some reason + // // Here we give it some time to save/push the changes + // await new Promise(res => setTimeout(res, 500)); + // // extra check what the ACL currently has stored as info. Will decrease the chance of the index going out of sync with the ACL file + // const remotePermissions = await this.getExistingRemotePermissions(resourceUrl, subject); + // if (remotePermissions !== permissions) { + // console.debug("Permissions in index are out of sync with remote, updating index...", subject); + // item.permissions = remotePermissions; + // } + // } + + // await this.index.saveToRemote(); } AccessRequest(): IAccessRequest { @@ -107,23 +108,24 @@ export class Controller> async setPodUrl(podUrl: string) { console.log('Controller.setPodUrl called'); console.log('Arguments:', { podUrl }); - this.index.setPodUrl(podUrl); - this.resources.setPodUrl(podUrl); - await this.accessRequest.setPodUrl(podUrl) + // this.index.setPodUrl(podUrl); + // this.resources.setPodUrl(podUrl); + // await this.accessRequest.setPodUrl(podUrl) } unsetPodUrl() { console.log('Controller.unsetPodUrl called'); console.log('Arguments:', {}); - this.index.unsetPodUrl(); - this.resources.unsetPodUrl(); - this.accessRequest.unsetPodUrl(); + // this.index.unsetPodUrl(); + // this.resources.unsetPodUrl(); + // this.accessRequest.unsetPodUrl(); } async getOrCreateIndex() { console.log('Controller.getOrCreateIndex called'); console.log('Arguments:', {}); - return this.index.getOrCreate(); + // return this.index.getOrCreate(); + return { id: "", items: [] } } getLabelForSubject>(subject: T[K]): string { @@ -136,10 +138,11 @@ export class Controller> async getItem>(resourceUrl: string, subject: SubjectType): Promise | undefined> { console.log('Controller.getItem called'); console.log('Arguments:', { resourceUrl, subject }); - const { resolver } = this.getSubjectConfig(subject); + // const { resolver } = this.getSubjectConfig(subject); - const index = await this.index.getCurrent() as Index; - return resolver.getItem(index, resourceUrl, subject.selector) + // const index = await this.index.getCurrent() as Index; + // return resolver.getItem(index, resourceUrl, subject.selector) + return {} as IndexItem } async addPermission>(resourceUrl: string, addedPermission: Permission, subject: SubjectType) { @@ -147,16 +150,12 @@ export class Controller> const release = await this.acquire(); try { - // 1. Collect already existing permissions - const permissions = subject.type === "webId" - ? (await this.getSubjectConfig(subject).manager.getRemotePermissions(resourceUrl)) - .filter(p => p.subject.type === "webId" && p.subject.selector!.url === subject.selector!.url) - .map(p => p.permissions)[0] ?? [] - : []; - - // 2. Let the manager add the permission, and return the - + // 1. Create a new permission for the subject + await this.getSubjectConfig(subject).manager.createPermissions(resourceUrl, subject, [addedPermission]) + // 2. Let the manager add the permission, return the updated version + const webId = getDefaultSession().info.webId!; + const permissions = await this.getSubjectConfig(subject).manager.getTargetPermissionsForUser(webId, subject.selector!.url, resourceUrl); // if (permissions.indexOf(addedPermission) !== -1) { // console.error("Permission already granted") @@ -166,7 +165,7 @@ export class Controller> // permissions.push(addedPermission) // await this.updateItem(resourceUrl, subject, permissions) - return []; + return permissions; //Promise[] } catch (e) { throw e; } finally { @@ -185,7 +184,7 @@ export class Controller> const item = subjectConfig.resolver.getItem(index, resourceUrl, subject.selector); if (!item) return; - await subjectConfig.manager.deletePermissions(resourceUrl, subject); + await subjectConfig.manager.deletePermissions(resourceUrl, subject, []); const idx = index.items.findIndex(i => subjectConfig.resolver.checkMatch(i.subject, subject)); index.items.splice(idx, 1); @@ -196,20 +195,21 @@ export class Controller> async removePermission>(resourceUrl: string, removedPermission: Permission, subject: SubjectType) { console.log('Controller.removePermission called'); console.log('Arguments:', { resourceUrl, removedPermission, subject }); - const release = await this.acquire(); + const release = await this.acquire() try { - let oldPermissions = await this.getExistingPermissions(resourceUrl, subject); - let newPermissions = oldPermissions.filter((p) => p !== removedPermission); - if (newPermissions.length === oldPermissions.length) { - console.error("Permission not found") - return oldPermissions; - } + // 1. Delete a permission for the subject + console.log("were here", removedPermission, subject) + await this.getSubjectConfig(subject).manager.deletePermissions(resourceUrl, subject, [removedPermission]); - await this.updateItem(resourceUrl, subject, newPermissions) - return newPermissions; - } catch (e) { - throw e; + // 2. Let the manager delete the permission, return the updated version + const webId = getDefaultSession().info.webId!; + const permissions = await this.getSubjectConfig(subject).manager.getTargetPermissionsForUser(webId, subject.selector!.url, resourceUrl); + + return permissions + + } catch (error) { + return [] } finally { release(); } @@ -256,45 +256,26 @@ export class Controller> // Eventually, we would only need to use the webIDManager... const configs: SubjectConfig[] = Object.values(this.subjectConfigs); + console.log(configs) const results = await Promise.all( - configs.map(c => c.manager.getContainerPermissionList(containerUrl)) - ); + configs.filter(c => c.manager.type === "webId").map(c => c.manager.getContainerPermissionList(containerUrl))); return results.flat() } // NOTE: Do we want to force this to only use the index stored in the store? - async getResourcePermissionList(resourceUrl: string) { - console.log('Controller.getResourcePermissionList called'); - console.log('Arguments:', { resourceUrl }); - // Need to put it in a variable because the type declaration vanishes + async getResourcePermissionList(resourceUrl: string): Promise> { const configs: SubjectConfig[] = Object.values(this.subjectConfigs); - const index = await this.index.getCurrent(); - const results = await Promise.allSettled(configs.map(c => c.manager.getRemotePermissions(resourceUrl))) - - let permissionsPerSubject = index.items.filter(i => i.resource === resourceUrl) + console.log(configs) + const results = await Promise.all( + configs.filter(c => c.manager.type === 'webId').map(c => c.manager.getRemotePermissions(resourceUrl)) + ); return { resourceUrl, - canRequestAccess: await this.accessRequest.canRequestAccessToResource(resourceUrl), - permissionsPerSubject: results.reduce[]>((arr, v) => { - if (v.status === "fulfilled") { - v.value.forEach(remotePps => { - const { resolver } = this.getSubjectConfig(remotePps.subject); - const indexItem = arr.find(pps => resolver.checkMatch(remotePps.subject, pps.subject)); - - if (indexItem) { - if (indexItem.isEnabled) { - indexItem.permissions = remotePps.permissions; - } - } else { - arr.push(remotePps) - } - }) - } - return arr; - }, permissionsPerSubject) - } + canRequestAccess: true, // TODO + permissionsPerSubject: results.flat() + }; } isSubjectSupported>(subject: BaseSubject): IController> { diff --git a/controller/src/classes/permissionManager/inrupt/GroupManager.ts b/controller/src/classes/permissionManager/inrupt/GroupManager.ts index 8ad2dea..c3b2e6b 100644 --- a/controller/src/classes/permissionManager/inrupt/GroupManager.ts +++ b/controller/src/classes/permissionManager/inrupt/GroupManager.ts @@ -58,4 +58,6 @@ export class GroupManager = { read: Permission.Read, @@ -98,12 +98,43 @@ export abstract class InruptPermissionManager { + const store: Store = await this.fetchPolicies(assignerId); + const target: TargetSubjects = new PolicyInterpreter().permissionsForOneResource(targetId, store); + + // If there are no private permissions, or no private permissions for the assignee, return the public ones (or nothing if they don't exist) + if (!target.private || !target.private.get(assigneeId)) return Array.from(target.public?.permissions!) ?? [] + + return Array.from(target.private.get(assigneeId)?.permissions!) ?? [] + } + public async getRemotePermissions>(resourceUrl: string): Promise[]> { // Extract our webID const session = getDefaultSession(); const webId = session.info.webId; - console.log("webId", webId) + console.log("webId", webId, resourceUrl) // We must be logged on if (!webId) { @@ -112,13 +143,13 @@ export abstract class InruptPermissionManager[] = []; - targets.forEach(target => { + if (target) { + const subjectPermissions: SubjectPermissions[] = []; // Add the owner information subjectPermissions.push({ subject: { @@ -150,15 +181,40 @@ export abstract class InruptPermissionManager[]> { - return []; + // Extract our webID + const session = getDefaultSession(); + const webId = session.info.webId; + console.log("webId", webId) + + // We must be logged on + if (!webId) { + throw new Error("User not logged in"); + } + + const store = await this.fetchPolicies(webId); + + // Collect target urls + const targetUrls = Array.from(new Set(store.getQuads(null, ODRL('target'), null, null).map(q => q.object.id))); + const resourcePermissions: ResourcePermissions[] = [] + for (const targetUrl of targetUrls) { + console.log(targetUrl) + const perms = await this.getRemotePermissions(targetUrl); + resourcePermissions.push({ + resourceUrl: targetUrl, + canRequestAccess: true, // TODO: based on proper access logic + permissionsPerSubject: perms + }) + } + + return resourcePermissions; } shouldDeleteOnAllRevoked() { return true } diff --git a/controller/src/classes/permissionManager/inrupt/PublicManager.ts b/controller/src/classes/permissionManager/inrupt/PublicManager.ts index 72a0e61..f9dd900 100644 --- a/controller/src/classes/permissionManager/inrupt/PublicManager.ts +++ b/controller/src/classes/permissionManager/inrupt/PublicManager.ts @@ -1,3 +1,4 @@ +import { PolicyEditor } from "@/classes/utils/PolicyEditor"; import { BaseSubject, IndexItem, Permission, ResourcePermissions } from "../../../types"; import { IPermissionManager, SubjectKey } from "../../../types/modules"; import { InruptPermissionManager } from "./InruptPermissionManager"; @@ -6,17 +7,19 @@ export class PublicManager>(resource: string, subject: T[K], permissions: Permission[]): Promise { - console.log("public manager: create permissions") + new PolicyEditor().insertActionRule(resource, permissions) } - async deletePermissions>(resource: string, subject: T[K]) { - console.log("public manager: delete permissions") + async deletePermissions>(resource: string, subject: T[K], permissions: Permission[]) { + new PolicyEditor().deleteActionRule(resource, permissions) + } async editPermissions>(resource: string, item: IndexItem, subject: T[K], permissions: Permission[]) { console.log("public manager: edit permissions") } + type = 'public' } diff --git a/controller/src/classes/permissionManager/inrupt/WebIdManager.ts b/controller/src/classes/permissionManager/inrupt/WebIdManager.ts index f74db24..ae6382d 100644 --- a/controller/src/classes/permissionManager/inrupt/WebIdManager.ts +++ b/controller/src/classes/permissionManager/inrupt/WebIdManager.ts @@ -1,50 +1,23 @@ -import { getDefaultSession } from "@inrupt/solid-client-authn-browser"; -import { BaseSubject, IndexItem, Permission, ResourcePermissions, SubjectPermissions } from "../../../types"; +import { BaseSubject, IndexItem, Permission } from "../../../types"; import { IPermissionManager, SubjectKey } from "../../../types/modules"; import { InruptPermissionManager } from "./InruptPermissionManager"; -import { ODRL, PolicyParser } from "../../utils/PolicyParser"; +import { PolicyEditor } from "../../../classes/utils/PolicyEditor"; // Temporal export class WebIdManager>> extends InruptPermissionManager implements IPermissionManager { - //. NOTE: Currently, it doesn't do any recursive permission setting on containers + // Create an action for this resource and this subject with the given permissions async createPermissions>(resource: string, subject: T[K], permissions: Permission[]): Promise { + new PolicyEditor().insertActionRule(resource, permissions, subject.selector!.url); } - async deletePermissions>(resource: string, subject: T[K]) { + async deletePermissions>(resource: string, subject: T[K], permissions: Permission[]) { + new PolicyEditor().deleteActionRule(resource, permissions, subject.selector!.url) } async editPermissions>(resource: string, item: IndexItem, subject: T[K], permissions: Permission[]) { } - - async getContainerPermissionList(containerUrl: string, resourceToSkip: string[] = []): Promise[]> { - // Extract our webID - const session = getDefaultSession(); - const webId = session.info.webId; - console.log("webId", webId) - - // We must be logged on - if (!webId) { - throw new Error("User not logged in"); - } - - const store = await this.fetchPolicies(webId); - - // Collect target urls - const targetUrls = Array.from(new Set(store.getQuads(null, ODRL('target'), null, null).map(q => q.object.id))); - const resourcePermissions: ResourcePermissions[] = [] - for (const targetUrl of targetUrls) { - console.log(targetUrl) - const perms = await this.getRemotePermissions(targetUrl); - resourcePermissions.push({ - resourceUrl: targetUrl, - canRequestAccess: true, // TODO: based on proper access logic - permissionsPerSubject: perms - }) - } - - return resourcePermissions; - } + type = "webId" } diff --git a/controller/src/classes/utils/PolicyEditor.ts b/controller/src/classes/utils/PolicyEditor.ts new file mode 100644 index 0000000..394b35c --- /dev/null +++ b/controller/src/classes/utils/PolicyEditor.ts @@ -0,0 +1,156 @@ +import { getDefaultSession } from "@inrupt/solid-client-authn-browser"; +import { Permission } from "../../types/"; +import { UMA_URL } from "../permissionManager/inrupt"; +import { ODRL, PolicyParser } from "./PolicyParser"; +import { DataFactory, Writer } from "n3" +const { namedNode } = DataFactory + +export class PolicyEditor { + constructor() { } + + private getRandomString(length: number): string { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + for (let i = 0; i < length; i++) { + const randIndex = Math.floor(Math.random() * chars.length); + result += chars[randIndex]; + } + return result; + } + + + public async insertActionRule(targetId: string, actions: Permission[], assignee: string = ""): Promise { + const webId = getDefaultSession().info.webId! + + const policyId: string = `http://example.org/policy${this.getRandomString(20)}`; + + // We need a proper way to create new rules, probably better server side? + const ruleId = `http://example.org/rule${this.getRandomString(20)}`; + + // Define the new triples in the rule + const actionTriples = actions.map(action => `odrl:action odrl:${action.toLowerCase()} ;`).join("\n"); + const assigneeTriple = assignee + ? `odrl:assignee <${assignee}> ;` + : ""; + + // The response contains the full and updated version of the policy, which we cannot return in this interface + const response = await fetch(UMA_URL(/*`/${encodeURIComponent(policyId)}`*/), { + method: 'POST', + headers: { + 'Authorization': webId, + 'Content-type': 'text/turtle' + // 'Content-type': 'application/sparql-update' + }, + // We need to make sure there's no way to have any injection... + body: `@prefix odrl: . + +<${policyId}> odrl:permission <${ruleId}> . + +<${ruleId}> a odrl:Permission ; + odrl:target <${targetId}> ; + ${actionTriples} + ${assigneeTriple} + odrl:assigner <${webId}> . +` + // PATCH way of doing this + // `PREFIX odrl: + // INSERT { + // <${policyId}> odrl:permission <${ruleId}> . + // <${ruleId}> a odrl:Permission ; + // odrl:assigner <${webId}> ; + // odrl:target <${targetId}> . + // ${actionTriples} + // ${assigneeTriple} + // } + // WHERE {}` + }) + } + + /** + * Funcion that searches every owned rule by the logged on client, finds the target + * of an assigner and deletese the actions on it + */ + public async deleteActionRule(targetId: string, actions: Permission[], assignee: string = ""): Promise { + const session = getDefaultSession(); + const webId = session.info.webId!; + + // 1: Fetch the policy contents + const response = await fetch(UMA_URL(), { + headers: { + Authorization: webId, + Accept: "text/turtle" + } + }); + + if (!response.ok) { + throw new Error(`Failed to fetch policy: ${response.status}`); + } + + const turtleText = await response.text(); + + // 2: Parse into store + const parser = new PolicyParser(); + const store = parser.parseText(turtleText); + + // 3: Find all rules with our target + const targetRules = store.getQuads(null, ODRL("target"), namedNode(targetId), null); + + const policyIds = new Set(); + targetRules.forEach( + // Filter only the targets that have rules with us as assignee, or public if no assignee + target => { + // Search the rule of the target, and then the policy of the rule, only for permission (for now) + console.log("target to be checked: ", target) + const rule = target.subject; + console.log("rule to be checked: ", rule.id) + const matches = store.getQuads(null, ODRL("permission"), rule, null); + if (matches.length === 0) + console.warn("out of bounds rule"); + const policyId = matches[0].subject.id; + + console.log(policyId) + + // Since the policy has this target, and every policy has only one rule with one target, we need to look at the assignee + if (assignee === "") { + // If this rule does not have an assignee and it has an action to be deleted, select it + if (store.getQuads(rule, ODRL("assignee"), null, null).length === 0) { + for (const action of actions) + if (store.getQuads(rule, ODRL("action"), ODRL(action.toLowerCase()), null).length > 0) + policyIds.add(policyId) + } + } else { + // Do the same, with a check if the assignee is correct + if (store.getQuads(rule, ODRL("assignee"), namedNode(assignee), null).length >= 1) { + for (const action of actions) { + console.log(new Writer().quadsToString(store.getQuads(rule, ODRL("action"), ODRL(action.toLowerCase()), null))) + if (store.getQuads(rule, ODRL("action"), ODRL(action.toLowerCase()), null).length > 0) + policyIds.add(policyId) + } + + } + } + } + ) + + + + + // 4: Delete the entire policy if rule matches + for (const policyId of policyIds) { + const deleteResponse = await fetch(UMA_URL(`/${encodeURIComponent(policyId)}`), { + method: "DELETE", + headers: { + Authorization: webId + } + }); + + if (!deleteResponse.ok) { + throw new Error(`Policy deletion failed: ${deleteResponse.status}`); + } + } + } + + + + +} \ No newline at end of file diff --git a/controller/src/classes/utils/PolicyInterpreter.ts b/controller/src/classes/utils/PolicyInterpreter.ts new file mode 100644 index 0000000..30a095c --- /dev/null +++ b/controller/src/classes/utils/PolicyInterpreter.ts @@ -0,0 +1,160 @@ +import { Permission } from "../../types/"; +import { IPolicy, ISpecificTargetInfo, TargetSubjects } from "../../types/modules"; +import { DataFactory, Parser, Store } from "n3"; +import { ODRL } from "./PolicyParser"; + +const { namedNode } = DataFactory; + +export class PolicyInterpreter { + private fromODRL = (odrlString: string) => odrlString.split('/')[6]; + private defaultTarget = (uri: string, subject: string = ""): ISpecificTargetInfo => ({ uri: uri, permissions: new Set(), subject: subject, public: subject === "" }) + + /** + * Extract the quads of one subject, and recursively add whatever their object is referring to + * @param store store to extract subject from + * @param subjectIRI + * @param existing IDs that have already been added to the store + * @returns detailed store of the original subject and all of their children + */ + private extractQuadsRecursive(store: Store, subjectIRI: string, existing: Set = new Set([subjectIRI])): Store { + // Add the direct quads to the store + const result = new Store(); + const subjectQuads = store.getQuads(subjectIRI, null, null, null); + result.addQuads(subjectQuads); + + // If objects are not already added, add their quads and their children + for (const quad of subjectQuads) { + if (!existing.has(quad.object.id)) { + existing.add(quad.object.id); + result.addQuads(this.extractQuadsRecursive(store, quad.object.id, existing).getQuads(null, null, null, null)); + } + } + return result; + } + + /** + * Function that returns the stored Target objects without sanitization + * This function assumes all policies are correct, and only contains information for the logged on client + * + * Currently, it does not check the rule type (permission, prohibition, duty) and it assumes permission + * @param store the owned policies in a store + * @returns the target -> subjects -> permissions relation for all owned targets + */ + public ownedPoliciesToObject = (store: Store, specifiedTarget: string = ""): TargetSubjects[] => { + + // 1. Get every odrl:target . quad, or only the rules targetting the specified target + // Note that multiple rules can refer to the same target, and one rule can refer to multiple targets + const relevantRuleSet: Set = new Set((specifiedTarget === "" + ? store.getQuads(null, ODRL('target'), null, null) + : store.getQuads(null, ODRL('target'), namedNode(specifiedTarget), null)) + .map(quad => quad.subject.id)); + + // 2. Add permission information for every target we find + // Every target ID corresponds with the subjects that each have some permissions etc. + const idToTarget: Map = new Map(); + for (const ruleId of relevantRuleSet) { + + // Get the policy information + // Since a valid policy only has unique ID's, we can just search for ' .' quads on the entire store + const policyIDs: Set = new Set(); + for (const relation of ['permission'/*, 'prohibition', 'duty'*/].map(x => ODRL(x))) + store.getQuads(null, relation, namedNode(ruleId), null).forEach(res => policyIDs.add(res.subject.id)); + if (policyIDs.size !== 1) + console.warn("Corrupted Policy"); + const policyId = [...policyIDs][0]; + + // 2.1 Get the every quad defined by the rule (and their children recursively) + const ruleStore = this.extractQuadsRecursive(store, ruleId); + + // 2.2 List all relevant actions for this rule + const permissions = []; + for (const quad of ruleStore.getQuads(null, ODRL('action'), null, null)) { + // TODO: find a way to categorize all actions as one of the Permission types + const action = this.fromODRL(quad.object.id).toLowerCase(); + console.log('action', action) + + switch (action) { + case "read": + permissions.push(Permission.Read); + break; + + case "write": + permissions.push(Permission.Write); + break; + + case "append": + permissions.push(Permission.Append); + break; + + case "control": + permissions.push(Permission.Control); + break; + + default: + console.warn(`Unrecognized ODRL action: ${action}`); + } + + } + + // Get assigner ID + const assigner = ruleStore.getQuads(null, ODRL('assigner'), null, null)[0].object.id; + if (!assigner) throw new Error("Corrupted Policy"); + + // Get assignee IDs + const subjects: string[] = ruleStore.getQuads(null, ODRL('assignee'), null, null).map(quad => quad.object.id); + + for (const target of ruleStore.getQuads(namedNode(ruleId), ODRL('target'), null, null).map(quad => quad.object.id)) { + // 2.3 Set the target's assigner if not already done + if (!idToTarget.has(target)) idToTarget.set(target, { assigner: assigner, targetUrl: target, policies: new Set(), rules: new Set() }); + idToTarget.get(target)!.policies.add(policyId); + idToTarget.get(target)!.rules.add(ruleId); + + // 2.4 Add the private assignee information for every target in the rule + for (const subject of subjects) { + // If target does not have subjects yet, set a default object + if (!idToTarget.get(target)!.private) + idToTarget.get(target)!.private = new Map(); + + // If subject is new to the target, set its permissions to a new set + if (!idToTarget.get(target)!.private!.has(subject)) + idToTarget.get(target)!.private!.set(subject, { uri: target, subject: subject, public: false, permissions: new Set() }); + + // Add the permissions of this rule to the subject + const targetObject: ISpecificTargetInfo = idToTarget.get(target)!.private!.get(subject)!; + permissions.forEach(p => targetObject.permissions.add(p)); + } + + if (subjects.length === 0) { + // If there is no public permission set, add one + if (!idToTarget.get(target)!.public) + idToTarget.get(target)!.public = { uri: target, public: true, subject: "", permissions: new Set() }; + + // Add the permissions to the set + const publicPermissions: Set = idToTarget.get(target)!.public!.permissions; + permissions.forEach(p => publicPermissions.add(p)); + } + } + } + + // Return the list of target info objects + return Array.from(idToTarget.values()); + } + + // Return the subject -> permissions relation for a target + public permissionsForOneResource(resourceUrl: string, store: Store): TargetSubjects { + const targets = this.ownedPoliciesToObject(store, resourceUrl); + + console.log('targets before', targets) + + // Only return the target we need + const target = targets.filter(t => t.targetUrl === resourceUrl); + + console.log('target', target) + + if (target.length > 1) console.warn("Something went wrong while getting the permissions for", resourceUrl); + // Handle empty subjects + + return target[0]; + } + +} \ No newline at end of file diff --git a/controller/src/classes/utils/PolicyParser.ts b/controller/src/classes/utils/PolicyParser.ts index ee33d2c..7910594 100644 --- a/controller/src/classes/utils/PolicyParser.ts +++ b/controller/src/classes/utils/PolicyParser.ts @@ -1,5 +1,3 @@ -import { Permission } from "../../types/"; -import { IPolicy, ISpecificTargetInfo, TargetSubjects } from "../../types/modules"; import { DataFactory, Parser, Store } from "n3"; const { namedNode } = DataFactory @@ -11,137 +9,9 @@ export class PolicyParser { public constructor() { } - private fromODRL = (odrlString: string) => odrlString.split('/')[6]; - private defaultTarget = (uri: string, subject: string = ""): ISpecificTargetInfo => ({ uri: uri, permissions: new Set(), subject: subject, public: subject === "" }) - public parseText = (text: string): Store => { const parser = new Parser({ format: 'text/turtle' }); const quads = parser.parse(text); return new Store(quads); } - - /** - * Extract the quads of one subject, and recursively add whatever their object is referring to - * @param store store to extract subject from - * @param subjectIRI - * @param existing IDs that have already been added to the store - * @returns detailed store of the original subject and all of their children - */ - private extractQuadsRecursive(store: Store, subjectIRI: string, existing: Set = new Set([subjectIRI])): Store { - // Add the direct quads to the store - const result = new Store(); - const subjectQuads = store.getQuads(subjectIRI, null, null, null); - result.addQuads(subjectQuads); - - // If objects are not already added, add their quads and their children - for (const quad of subjectQuads) { - if (!existing.has(quad.object.id)) { - existing.add(quad.object.id); - result.addQuads(this.extractQuadsRecursive(store, quad.object.id, existing).getQuads(null, null, null, null)); - } - } - return result; - } - - /** - * Function that returns the stored Target objects without sanitization - * This function assumes all policies are correct, and only contains information for the logged on client - * - * Currently, it does not check the rule type (permission, prohibition, duty) and it assumes permission - * @param store the owned policies in a store - * @returns the target -> subjects -> permissions relation for all owned targets - */ - public ownedPoliciesToObject = (store: Store, specifiedTarget: string = ""): TargetSubjects[] => { - - // 1. Get every odrl:target . quad, or only the rules targetting the specified target - // Note that multiple rules can refer to the same target, and one rule can refer to multiple targets - const relevantRuleSet: Set = new Set((specifiedTarget === "" - ? store.getQuads(null, ODRL('target'), null, null) - : store.getQuads(null, ODRL('target'), namedNode(specifiedTarget), null)) - .map(quad => quad.subject.id)); - - // 2. Add permission information for every target we find - // Every target ID corresponds with the subjects that each have some permissions etc. - const idToTarget: Map = new Map(); - for (const ruleId of relevantRuleSet) { - - // 2.1 Get the every quad defined by the rule (and their children recursively) - const ruleStore = this.extractQuadsRecursive(store, ruleId); - - // 2.2 List all relevant actions for this rule - const permissions = []; - for (const quad of ruleStore.getQuads(null, ODRL('action'), null, null)) { - // TODO: find a way to categorize all actions as one of the Permission types - const action = this.fromODRL(quad.object.id).toLowerCase(); - console.log('action', action) - - switch (action) { - case "read": - permissions.push(Permission.Read); - break; - - case "write": - permissions.push(Permission.Write); - break; - - case "append": - permissions.push(Permission.Append); - break; - - case "control": - permissions.push(Permission.Control); - break; - - default: - console.warn(`Unrecognized ODRL action: ${action}`); - } - - } - - // Get assigner ID - const assigner = ruleStore.getQuads(null, ODRL('assigner'), null, null)[0].object.id; - if (!assigner) throw new Error("Corrupted Policy"); - - // Get assignee IDs - const subjects: string[] = ruleStore.getQuads(null, ODRL('assignee'), null, null).map(quad => quad.object.id); - - for (const target of ruleStore.getQuads(null, ODRL('target'), null, null).map(quad => quad.object.id)) { - // 2.3 Set the target's assigner if not already done - if (!idToTarget.get(target)) idToTarget.set(target, { assigner: assigner, targetUrl: target }); - - // 2.4 Add the private assignee information for every target in the rule - for (const subject of subjects) { - // If target does not have subjects yet, set a default object - if (!idToTarget.get(target)!.private) - idToTarget.get(target)!.private = new Map(); - - // If subject is new to the target, set its permissions to a new set - if (!idToTarget.get(target)!.private!.has(subject)) - idToTarget.get(target)!.private!.set(subject, { uri: target, subject: subject, public: false, permissions: new Set() }); - - // Add the permissions of this rule to the subject - const targetObject: ISpecificTargetInfo = idToTarget.get(target)!.private!.get(subject)!; - permissions.forEach(p => targetObject.permissions.add(p)); - } - - if (subjects.length === 0) { - // If there is no public permission set, add one - if (!idToTarget.get(target)!.public) - idToTarget.get(target)!.public = { uri: target, public: true, subject: "", permissions: new Set() }; - - // Add the permissions to the set - const publicPermissions: Set = idToTarget.get(target)!.public!.permissions; - permissions.forEach(p => publicPermissions.add(p)); - } - } - } - - // Return the list of target info objects - return Array.from(idToTarget.values()); - } - - // Return the subject -> permissions relation for a target - public permissionsForOneResource(resourceUrl: string, store: Store): TargetSubjects[] { - return this.ownedPoliciesToObject(store, resourceUrl); - } } \ No newline at end of file diff --git a/controller/src/types/modules.ts b/controller/src/types/modules.ts index e284a37..160d28f 100644 --- a/controller/src/types/modules.ts +++ b/controller/src/types/modules.ts @@ -126,7 +126,7 @@ export interface IPermissionManager>> { createPermissions>(resource: string, subject: T[K], permissions: Permission[]): Promise // Does not update the index file editPermissions>(resource: string, item: IndexItem, subject: T[K], permissions: Permission[]): Promise - deletePermissions>(resource: string, subject: T[K]): Promise + deletePermissions>(resource: string, subject: T[K], permissions: Permission[]): Promise getRemotePermissions>(resourceUrl: string): Promise[]> /** * Retrieve the permissions of the resources in this container. @@ -139,6 +139,8 @@ export interface IPermissionManager>> { * This indicates if the underlying SDK automatically removes the entry from the SDK if all permissions are revoked */ shouldDeleteOnAllRevoked(): boolean + getTargetPermissionsForUser(assignerId: string, assigneeId: string, targetId: string): Promise; + type: string; } // Temporal (?) interface to represent a policy @@ -198,4 +200,10 @@ export interface TargetSubjects { // WebID of the target owner assigner: string; + + // The policies referring to this target + policies: Set; + + // The rules referring to this target + rules: Set; } From 290937bbc6bd6b67392786d58885e028fe0371ac Mon Sep 17 00:00:00 2001 From: lennertdr Date: Mon, 28 Jul 2025 17:31:07 +0200 Subject: [PATCH 05/17] reworked insert and delete --- controller/src/classes/Controller.ts | 43 +------ controller/src/classes/utils/PolicyEditor.ts | 116 +++++++++++-------- 2 files changed, 70 insertions(+), 89 deletions(-) diff --git a/controller/src/classes/Controller.ts b/controller/src/classes/Controller.ts index ac69ebe..949c97b 100644 --- a/controller/src/classes/Controller.ts +++ b/controller/src/classes/Controller.ts @@ -3,7 +3,6 @@ import { BaseSubject, Index, IndexItem, Permission, ResourcePermissions, Resourc import { IAccessRequest, IController, IInboxConstructor, IStore, IStoreConstructor, SubjectConfig, SubjectConfigs, SubjectKey, SubjectType } from "../types/modules"; import { AccessRequest } from "./accessRequests/AccessRequest"; import { InruptAccessRequest } from "./accessRequests/InruptAccessRequest"; -import { WebIdManager } from "./permissionManager/inrupt"; import { Mutex } from "./utils/Mutex"; export class Controller>> extends Mutex implements IController { @@ -57,46 +56,6 @@ export class Controller> private async updateItem>(resourceUrl: string, subject: SubjectType, permissions: Permission[], alwaysKeepItem = false) { console.log('Controller.updateItem called'); console.log('Arguments:', { resourceUrl, subject, permissions, alwaysKeepItem }); - // let item = await this.getItem(resourceUrl, subject); - // const { manager } = this.getSubjectConfig(subject) - - // if (item) { - // await manager.editPermissions(resourceUrl, item, subject, permissions); - // } else { - // await manager.createPermissions(resourceUrl, subject, permissions); - - // item = { - // id: crypto.randomUUID(), - // requestId: crypto.randomUUID(), - // isEnabled: true, - // permissions: permissions, - // resource: resourceUrl, - // subject: subject, - // } - - // const index = await this.index.getCurrent(); - // index.items.push(item); - // } - - // if (!alwaysKeepItem && permissions.length === 0 && manager.shouldDeleteOnAllRevoked()) { - // const index = await this.index.getCurrent(); - // const idx = index.items.findIndex(i => i.id === item.id); - // index.items.splice(idx, 1); - // } else { - // item.permissions = permissions; - - // // The SOLID server OR the SDK does not directly push the update to the acl files for some reason - // // Here we give it some time to save/push the changes - // await new Promise(res => setTimeout(res, 500)); - // // extra check what the ACL currently has stored as info. Will decrease the chance of the index going out of sync with the ACL file - // const remotePermissions = await this.getExistingRemotePermissions(resourceUrl, subject); - // if (remotePermissions !== permissions) { - // console.debug("Permissions in index are out of sync with remote, updating index...", subject); - // item.permissions = remotePermissions; - // } - // } - - // await this.index.saveToRemote(); } AccessRequest(): IAccessRequest { @@ -155,7 +114,7 @@ export class Controller> // 2. Let the manager add the permission, return the updated version const webId = getDefaultSession().info.webId!; - const permissions = await this.getSubjectConfig(subject).manager.getTargetPermissionsForUser(webId, subject.selector!.url, resourceUrl); + const permissions = await this.getSubjectConfig(subject).manager.getTargetPermissionsForUser(webId, subject.selector?.url ?? "", resourceUrl); // if (permissions.indexOf(addedPermission) !== -1) { // console.error("Permission already granted") diff --git a/controller/src/classes/utils/PolicyEditor.ts b/controller/src/classes/utils/PolicyEditor.ts index 394b35c..0a84400 100644 --- a/controller/src/classes/utils/PolicyEditor.ts +++ b/controller/src/classes/utils/PolicyEditor.ts @@ -19,51 +19,57 @@ export class PolicyEditor { } + /** + * Function to insert an action rule for each permission in the provided array. They will be inserted in a new policy, via POST and not PATCH. + */ public async insertActionRule(targetId: string, actions: Permission[], assignee: string = ""): Promise { const webId = getDefaultSession().info.webId! const policyId: string = `http://example.org/policy${this.getRandomString(20)}`; - // We need a proper way to create new rules, probably better server side? - const ruleId = `http://example.org/rule${this.getRandomString(20)}`; + for (const action of actions) { + // We need a proper way to create new rules, probably better server side? + const ruleId = `http://example.org/rule${this.getRandomString(20)}`; - // Define the new triples in the rule - const actionTriples = actions.map(action => `odrl:action odrl:${action.toLowerCase()} ;`).join("\n"); - const assigneeTriple = assignee - ? `odrl:assignee <${assignee}> ;` - : ""; + // Define the new triples in the rule + const actionTriple = `odrl:action odrl:${action.toLowerCase()} ;`; + const assigneeTriple = assignee + ? `odrl:assignee <${assignee}> ;` + : ""; - // The response contains the full and updated version of the policy, which we cannot return in this interface - const response = await fetch(UMA_URL(/*`/${encodeURIComponent(policyId)}`*/), { - method: 'POST', - headers: { - 'Authorization': webId, - 'Content-type': 'text/turtle' - // 'Content-type': 'application/sparql-update' - }, - // We need to make sure there's no way to have any injection... - body: `@prefix odrl: . + // The response contains the full and updated version of the policy, which we cannot return in this interface + const response = await fetch(UMA_URL(/*`/${encodeURIComponent(policyId)}`*/), { + method: 'POST', + headers: { + 'Authorization': webId, + 'Content-type': 'text/turtle' + // 'Content-type': 'application/sparql-update' + }, + // We need to make sure there's no way to have any injection... + body: `@prefix odrl: . -<${policyId}> odrl:permission <${ruleId}> . +<${policyId}> a odrl:Agreement ; + odrl:permission <${ruleId}> . <${ruleId}> a odrl:Permission ; odrl:target <${targetId}> ; - ${actionTriples} + ${actionTriple} ${assigneeTriple} odrl:assigner <${webId}> . ` - // PATCH way of doing this - // `PREFIX odrl: - // INSERT { - // <${policyId}> odrl:permission <${ruleId}> . - // <${ruleId}> a odrl:Permission ; - // odrl:assigner <${webId}> ; - // odrl:target <${targetId}> . - // ${actionTriples} - // ${assigneeTriple} - // } - // WHERE {}` - }) + // PATCH way of doing this + // `PREFIX odrl: + // INSERT { + // <${policyId}> odrl:permission <${ruleId}> . + // <${ruleId}> a odrl:Permission ; + // odrl:assigner <${webId}> ; + // odrl:target <${targetId}> . + // ${actionTriples} + // ${assigneeTriple} + // } + // WHERE {}` + }) + } } /** @@ -110,9 +116,9 @@ export class PolicyEditor { console.log(policyId) - // Since the policy has this target, and every policy has only one rule with one target, we need to look at the assignee + // We now have the policies that have our target, check if our assignee has an action to delete here if (assignee === "") { - // If this rule does not have an assignee and it has an action to be deleted, select it + // If no assignee specified, the rule is public and it has an action to be deleted, select it if (store.getQuads(rule, ODRL("assignee"), null, null).length === 0) { for (const action of actions) if (store.getQuads(rule, ODRL("action"), ODRL(action.toLowerCase()), null).length > 0) @@ -132,25 +138,41 @@ export class PolicyEditor { } ) + // 4: Delete the rule that has the matching target and permission for the matching assignee + for (const policyId of policyIds) { + for (const action of actions) { + const deleteResponse = await fetch(UMA_URL(`/${encodeURIComponent(policyId)}`), { + method: "PATCH", + headers: { + 'Authorization': webId, + 'Content-type': 'application/sparql-update', + }, + body: ` + PREFIX odrl: + + DELETE { + ?rule ?p ?o . + <${policyId}> odrl:permission ?rule . + } + WHERE { + ?rule odrl:action odrl:${action.toLowerCase()} . + ?rule odrl:assigner <${webId}> . + ?rule odrl:target <${targetId}> . + + ${assignee + ? `?rule odrl:assignee <${assignee}> .` + : `FILTER NOT EXISTS { ?rule odrl:assignee ?anyAssignee . }`} + ?policy odrl:permission ?rule . + } + ` + }); - // 4: Delete the entire policy if rule matches - for (const policyId of policyIds) { - const deleteResponse = await fetch(UMA_URL(`/${encodeURIComponent(policyId)}`), { - method: "DELETE", - headers: { - Authorization: webId + if (!deleteResponse.ok) { + throw new Error(`Policy deletion failed: ${deleteResponse.status}`); } - }); - - if (!deleteResponse.ok) { - throw new Error(`Policy deletion failed: ${deleteResponse.status}`); } } } - - - - } \ No newline at end of file From d68e24e3a89c58f746deb35a74779755746d6aca Mon Sep 17 00:00:00 2001 From: lennertdr Date: Tue, 29 Jul 2025 00:38:37 +0200 Subject: [PATCH 06/17] forgot docs --- controller/src/classes/utils/PolicyEditor.ts | 2 +- .../src/classes/utils/PolicyInterpreter.ts | 2 +- uma-loama.md | 59 +++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 uma-loama.md diff --git a/controller/src/classes/utils/PolicyEditor.ts b/controller/src/classes/utils/PolicyEditor.ts index 0a84400..b8d89c4 100644 --- a/controller/src/classes/utils/PolicyEditor.ts +++ b/controller/src/classes/utils/PolicyEditor.ts @@ -74,7 +74,7 @@ export class PolicyEditor { /** * Funcion that searches every owned rule by the logged on client, finds the target - * of an assigner and deletese the actions on it + * of an assigner and deletes the actions on it */ public async deleteActionRule(targetId: string, actions: Permission[], assignee: string = ""): Promise { const session = getDefaultSession(); diff --git a/controller/src/classes/utils/PolicyInterpreter.ts b/controller/src/classes/utils/PolicyInterpreter.ts index 30a095c..7cc17e3 100644 --- a/controller/src/classes/utils/PolicyInterpreter.ts +++ b/controller/src/classes/utils/PolicyInterpreter.ts @@ -36,7 +36,7 @@ export class PolicyInterpreter { * Function that returns the stored Target objects without sanitization * This function assumes all policies are correct, and only contains information for the logged on client * - * Currently, it does not check the rule type (permission, prohibition, duty) and it assumes permission + * Currently, it does not check the rule type (permission, prohibition, duty) and it only takes permission into account * @param store the owned policies in a store * @returns the target -> subjects -> permissions relation for all owned targets */ diff --git a/uma-loama.md b/uma-loama.md new file mode 100644 index 0000000..fd80fa5 --- /dev/null +++ b/uma-loama.md @@ -0,0 +1,59 @@ +# Linked Open Access Management Application with UMA + +This documentation describes the important changes made to transform the loama controller to UMA. + +## Controller Structure + +The controller contains the important, high level operations that loama provides. These operations are the ones to edit (add, delete) permissions, get permission information for each target and add/delete subjects. Other functional parts, like handling requests, are not relevant for these changes. + +To handle different subjects (public and webID), the controller uses preconfigured managers and resolvers. Each manager and resolver handles specific tasks for one subject. Some managers have common tasks, which are grouped in a common manager called `InruptPermissionManager.ts`. + +To also not make the managers too big, they use specific policy related helper classes: `PolicyParser`, `PolicyInterpreter` and `PoliyEditor`. These classes provide the basic functionality for the managers to implement their functions. + +### Policy Helper Classes + +This section handles the implementation of the `PolicyParser`, `PolicyInterpreter` and `PoliyEditor`. + +#### PolicyParser + +The `PolicyParser` is used to convert text into an `N3 store`. It does so by interpreting the text as Turtle, because that is what we receive from the server. The parser has only one method. + +#### PoliyInterpreter + +The `PolicyInterpreter` is the class that extracts the necessary information out of a given store. It contains one helper function, and two main functions: +- `ownedPoliciesToObject`: Get every policy for the logged on client, and group them by target. Return an information object for each target to indicate who has what permission, as documented in the code. +- `permissionsForOneResource`: Use the `ownedPoliciesToObject` with a specified target. This retrieves permission information for one single target. +- `extractQuadsRecursive`: Helper function to get all information about a subject with recursive depth, used in `ownedPoliciesToObject`. + +#### PolicyEditor + +The `PolicyEditor` is used to edit the existing permissions. The main functionality is to insert an atomic rule to add a new permission for a subject to a target, and the function to delete a permission for a subject to a target. + +- `insertActionRule`: Given the target ID, the permissions to add and the specified subject, create a new policy and a new rule for every action to be inserted. The new rule contains the logged on client as the assigner, the new subject as assignee (or none if no subject was specified, this means the subject is public), the target ID as odrl:target and the action to be inserted as odrl:action. + +There are many things to be considered about this function. +1. It should actually not create a new policy for every added rule. +2. It currently generates a random policy and rule ID, but does not check if it already exists. The chances are slim that this happens, but never zero. +3. The current version will always POST. + +- `deleteActionRule`: Given the target ID, the permissions to be deleted and the specified subject, remove the atomic rules where the subject has these permissions for this target. + +The function + +This function also has many things to consider. +1. It first fetches and parses every policy of the logged on client. It then finds every policy where the subject has one of the permissions to delete. After this, it performs a query that tracks the rules to delete from every policy. +This is a lot of work, because the UMA server is very policy-oriented. We first need to extract the policies, to send the PATCH to `/policies/`. An idea could be to also make the server in able to work target-oriented. + +2. When every rule is deleted from a policy, the current implementation can still contain some 'dangling triples'. These are triples that contain information about a policy that does not have any rules anymore. This can be fixed by an extra step. After the deletion of every permission rule, we could GET every policy again, and look if there are any rules left in there. If not, we could DELETE the policy. This is also some work, so we are currently looking for better solutions. + +### Managers + +The main managers are the `InruptPermissionManager`, the `WebIdManager` and the `PublicManager`. More as discussed later + +### Controller + + + +## Impurities + +The common manager, the `InruptPermissionManager.ts`, has one implementation to get all remote permissions for a target, and to get all permissions for a container. This is because it is not efficient to have one lookup for only public permissions, and one for only permissions via webID. Both goals are reached with the same procedure, thus it is done that way From 1ca4ad96023aac35aaad8348446aee80f392928d Mon Sep 17 00:00:00 2001 From: lennertdr Date: Tue, 29 Jul 2025 09:48:15 +0200 Subject: [PATCH 07/17] manager doc --- .../inrupt/InruptPermissionManager.ts | 10 +++++----- .../permissionManager/inrupt/WebIdManager.ts | 2 -- uma-loama.md | 18 +++++++++++++++++- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/controller/src/classes/permissionManager/inrupt/InruptPermissionManager.ts b/controller/src/classes/permissionManager/inrupt/InruptPermissionManager.ts index 2312df3..6c60921 100644 --- a/controller/src/classes/permissionManager/inrupt/InruptPermissionManager.ts +++ b/controller/src/classes/permissionManager/inrupt/InruptPermissionManager.ts @@ -140,12 +140,12 @@ export abstract class InruptPermissionManager>> extends InruptPermissionManager implements IPermissionManager { // Create an action for this resource and this subject with the given permissions diff --git a/uma-loama.md b/uma-loama.md index fd80fa5..504ee4c 100644 --- a/uma-loama.md +++ b/uma-loama.md @@ -46,9 +46,25 @@ This is a lot of work, because the UMA server is very policy-oriented. We first 2. When every rule is deleted from a policy, the current implementation can still contain some 'dangling triples'. These are triples that contain information about a policy that does not have any rules anymore. This can be fixed by an extra step. After the deletion of every permission rule, we could GET every policy again, and look if there are any rules left in there. If not, we could DELETE the policy. This is also some work, so we are currently looking for better solutions. +3. Although it seems quite impossible, things like sparql-injection might need more attention + + ### Managers -The main managers are the `InruptPermissionManager`, the `WebIdManager` and the `PublicManager`. More as discussed later +The main managers are the `InruptPermissionManager`, the `WebIdManager` and the `PublicManager`. They contain the logic to perform the clientside operations on the policies. + +#### InruptPermissionManager +This manager handles the functions that have the same implementation for underlaying managers. The methods in this manager are the following: +- `fetchPolicies`: Retrieve every policy that you own, and turn it into an N3 store. +- `fetchOnePolicy`: Retrieve one specific policy as an N3 store. +- `getContainerPermissionList`: Get every target where you are the assigner of its policy. Since we need every target, independent of the subject, it could be placed here. + +There are also functions here where it would make more sense to move them to their specific subject manager. +- `getRemotePermissions`: Get the permissions for one single target. We do retrieve the information in a way that we get every subject, but we need to handle those subjects in the common managers. This means adding a new subject would require changes in the common manager, which would be cleaner if moved to the separate managers. +- `getTargetPermissionsForUser`: A specific version of getRemotePermissions, where we are only interested in the permissions for one user on one target. This must be moved to the underlaying managers. + +#### Underlaying Managers +Because most of the work is done in the `PolicyEditor` and `PolicyInterpreter`, there is no big implementation in the managers. The `createPermissions` and `deletePermissions` functions are one-liners, and the `editPermissions` is not actually needed in this implementation. ### Controller From 67b4607833864d87d38ee212d9f95f5f7ebb422e Mon Sep 17 00:00:00 2001 From: lennertdr Date: Tue, 29 Jul 2025 10:58:44 +0200 Subject: [PATCH 08/17] full docs --- uma-loama.md | 46 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/uma-loama.md b/uma-loama.md index 504ee4c..39e4f27 100644 --- a/uma-loama.md +++ b/uma-loama.md @@ -12,7 +12,7 @@ To also not make the managers too big, they use specific policy related helper c ### Policy Helper Classes -This section handles the implementation of the `PolicyParser`, `PolicyInterpreter` and `PoliyEditor`. +This section handles the implementation of the `PolicyParser`, `PolicyInterpreter` and `PoliyEditor`. They do not follow the generic type system as used in the rest of the controller-structure. #### PolicyParser @@ -55,21 +55,55 @@ The main managers are the `InruptPermissionManager`, the `WebIdManager` and the #### InruptPermissionManager This manager handles the functions that have the same implementation for underlaying managers. The methods in this manager are the following: -- `fetchPolicies`: Retrieve every policy that you own, and turn it into an N3 store. -- `fetchOnePolicy`: Retrieve one specific policy as an N3 store. +- `fetchPolicies`: Retrieve every policy that you own, and turn it into an N3 store. To keep layers clean, we might want to move this to a policy related helper class, since it would make sense to only make server calls from a centralized place. +- `fetchOnePolicy`: Retrieve one specific policy as an N3 store. This could also be moved. - `getContainerPermissionList`: Get every target where you are the assigner of its policy. Since we need every target, independent of the subject, it could be placed here. +It is to be said that the meaning of this function changed. The old implementation fetched the permissions for one specific container, we fetch it from the logged on user. This makes the argument `containerUrl: string` redundant. The argument `resourceToSkip: string[] = []` has been ignored, since no useful purpose of this was found. There are also functions here where it would make more sense to move them to their specific subject manager. - `getRemotePermissions`: Get the permissions for one single target. We do retrieve the information in a way that we get every subject, but we need to handle those subjects in the common managers. This means adding a new subject would require changes in the common manager, which would be cleaner if moved to the separate managers. -- `getTargetPermissionsForUser`: A specific version of getRemotePermissions, where we are only interested in the permissions for one user on one target. This must be moved to the underlaying managers. +- `getTargetPermissionsForUser`: A specific version of getRemotePermissions, where we are only interested in the permissions for one user on one target. This must be moved to the underlaying managers. This function will probably be removed in the future. #### Underlaying Managers Because most of the work is done in the `PolicyEditor` and `PolicyInterpreter`, there is no big implementation in the managers. The `createPermissions` and `deletePermissions` functions are one-liners, and the `editPermissions` is not actually needed in this implementation. ### Controller +High level logic is implemented in the controller. It uses the managers to delegate the main functionality of the application. The functions that remained unchanged are: +- `getSubjectConfig`: Given a subject, return is its configuration (manager, resolver). +- `getExistingRemotePermissions`: Given a resource and a subject, get the list of permissions of that subject for the resource. +- `AccessRequest`: Returns the IAccessRequest +- `getLabelForSubject`: Uses the resolver of the subject to return its label. +- `isSubjectSupported`: Check if there is a configuration that supports the subject type. +The functions that have a new implementations are the following: +- `addPermission`: The new implementation became very short: + 1. Let the specific manager create the permission for the subject. + 2. Even though the server already returns an updated version, we cannot return this in the current interface. We just fetch the new permissions using `getTargetPermissionsForUser` from the manager, which we might change to the `getExistingRemotePermissions` from the controller. +- `removePermission`: Works exactly the same as `addPermission`, uses the deletePermissions function from the manager. +- `getContainerPermissionList`: This is nothing but a call to the manager's `getContainerPermissionList` function. Its implementation has been reduced a lot, but the current way is not clean. Because we know that we only need one manager to get every target and their permission info, we need to force our way to get only one. To quickly select only one type, an indicator was added to the interface. We now select the manager of this type, and execute the function. Of course, this is not clean. It's just a quick way to make things work, and will be replaced. +- `getResourcePermissionList`: Works exactly the same as `getContainerPermissionList`, it just calls the `getRemotePermissions`. If we were to replace this function from the common manager to its children, this would be cleaner. -## Impurities -The common manager, the `InruptPermissionManager.ts`, has one implementation to get all remote permissions for a target, and to get all permissions for a container. This is because it is not efficient to have one lookup for only public permissions, and one for only permissions via webID. Both goals are reached with the same procedure, thus it is done that way +The functions that are yet to be changed are the following: +- `enablePermissions`/`disablePermissions`: We need an elegant way to implement this. +- `removeSubject`: Remove a subject from all its permissions. This would just be a remove call with the four possible permissions. + +The functions that we do not need in the new implementation are: +- `getExistingPermissions`: Our implementation always gets the data straight from the server. This function was used in the index-context, which has been replaced by the UMA-server. +- `updateItem`: This function was used to set certain permissions for a target. Because our functionality only uses `addPermission` and `removePermission`, no `updateItem` is necessary. +- Index related functions that we no longer need: + - `setPodUrl` + - `unsetPodUrl` + - `getOrCreateIndex` + - `getItem` + + The downside of these functions is the fact that the front end still uses some, but they don't need to anymore. + + +## Impact of UMA +The introduction of the UMA server to this project reduced the controller and manager side significantly. It introduced independent classes to handle the server calls (PolicyParser and -Editor) and to turn policies into the format required by the frontend (PolicyInterpreter). + +Because the most important part of the logic is implemented in those independent classes, the managers and controllers take on a delegating role. The reason why it's still important to have them, is to separate concerns: +- The controller contains top level, generic functionality. They just need to call the right managers based on the relevant subjects. The controller does not have any direct contact to the independent Policy helper classes. +- Managers contain specific functionality. They are in direct contact with the Policy Helper classes. \ No newline at end of file From af371538e2efcf8ba31cecda6fe371bc740f0733 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Tue, 29 Jul 2025 16:10:48 +0200 Subject: [PATCH 09/17] implement add and delete subject --- controller/src/classes/Controller.ts | 148 +++--------------- .../inrupt/InruptPermissionManager.ts | 71 +++------ .../permissionManager/inrupt/PublicManager.ts | 10 +- .../permissionManager/inrupt/WebIdManager.ts | 7 +- .../{PolicyEditor.ts => PolicyService.ts} | 67 ++++++-- .../explorer/SubjectPermissionTable.vue | 6 - loama/src/components/subjectForms/Public.vue | 6 + uma-loama.md | 31 ++-- 8 files changed, 128 insertions(+), 218 deletions(-) rename controller/src/classes/utils/{PolicyEditor.ts => PolicyService.ts} (76%) diff --git a/controller/src/classes/Controller.ts b/controller/src/classes/Controller.ts index 949c97b..b039e39 100644 --- a/controller/src/classes/Controller.ts +++ b/controller/src/classes/Controller.ts @@ -22,8 +22,6 @@ export class Controller> } private getSubjectConfig>(subject: T[K]): SubjectConfig { - console.log('Controller.getSubjectConfig called'); - console.log('Arguments:', { subject }); const subjectConfig = this.subjectConfigs[subject.type]; if (!subjectConfig) { throw new Error(`No config found for subject type ${subject.type}`); @@ -31,81 +29,48 @@ export class Controller> return subjectConfig as SubjectConfig } - private async getExistingRemotePermissions>(resourceUrl: string, subject: T[K]): Promise { - console.log('Controller.getExistingRemotePermissions called'); - console.log('Arguments:', { resourceUrl, subject }); - const subjectConfig = this.getSubjectConfig(subject) - const subjects = await subjectConfig.manager.getRemotePermissions(resourceUrl); - const subjectPermission = subjects.find(entry => subjectConfig.resolver.checkMatch(entry.subject, subject)) - - return [...subjectPermission?.permissions ?? []] - } - - - private async getExistingPermissions>(resourceUrl: string, subject: T[K]): Promise { - console.log('Controller.getExistingPermissions called'); - console.log('Arguments:', { resourceUrl, subject }); - const item = await this.getItem(resourceUrl, subject); - if (item) { - // Making sure the array is not a reference to the one stored in the index - return [...item.permissions] - } - return this.getExistingRemotePermissions(resourceUrl, subject); - } - private async updateItem>(resourceUrl: string, subject: SubjectType, permissions: Permission[], alwaysKeepItem = false) { - console.log('Controller.updateItem called'); - console.log('Arguments:', { resourceUrl, subject, permissions, alwaysKeepItem }); } AccessRequest(): IAccessRequest { - console.log('Controller.AccessRequest called'); - console.log('Arguments:', {}); return this.accessRequest; } async setPodUrl(podUrl: string) { - console.log('Controller.setPodUrl called'); - console.log('Arguments:', { podUrl }); - // this.index.setPodUrl(podUrl); - // this.resources.setPodUrl(podUrl); - // await this.accessRequest.setPodUrl(podUrl) } unsetPodUrl() { - console.log('Controller.unsetPodUrl called'); - console.log('Arguments:', {}); - // this.index.unsetPodUrl(); - // this.resources.unsetPodUrl(); - // this.accessRequest.unsetPodUrl(); } async getOrCreateIndex() { - console.log('Controller.getOrCreateIndex called'); - console.log('Arguments:', {}); - // return this.index.getOrCreate(); return { id: "", items: [] } } getLabelForSubject>(subject: T[K]): string { - console.log('Controller.getLabelForSubject called'); - console.log('Arguments:', { subject }); const { resolver } = this.getSubjectConfig(subject); return resolver.toLabel(subject); } + /** + * Assemble the information for a subject in a resource + * @returns + */ async getItem>(resourceUrl: string, subject: SubjectType): Promise | undefined> { - console.log('Controller.getItem called'); - console.log('Arguments:', { resourceUrl, subject }); - // const { resolver } = this.getSubjectConfig(subject); - - // const index = await this.index.getCurrent() as Index; - // return resolver.getItem(index, resourceUrl, subject.selector) - return {} as IndexItem + const subjectConfig = this.getSubjectConfig(subject) + const subjects = await subjectConfig.manager.getRemotePermissions(resourceUrl); + const subjectPermission = subjects.find(entry => subjectConfig.resolver.checkMatch(entry.subject, subject)) + if (!subjectPermission) return undefined + return { + id: "string", + requestId: "string", + isEnabled: subjectPermission.isEnabled, + permissions: [...subjectPermission.permissions ?? []], + resource: resourceUrl, + subject: subject, + } as IndexItem } async addPermission>(resourceUrl: string, addedPermission: Permission, subject: SubjectType) { - console.log("add permission with: ", { resourceUrl, addedPermission, subject }) const release = await this.acquire(); try { @@ -116,15 +81,7 @@ export class Controller> const webId = getDefaultSession().info.webId!; const permissions = await this.getSubjectConfig(subject).manager.getTargetPermissionsForUser(webId, subject.selector?.url ?? "", resourceUrl); - // if (permissions.indexOf(addedPermission) !== -1) { - // console.error("Permission already granted") - // return permissions; - // } - - // permissions.push(addedPermission) - - // await this.updateItem(resourceUrl, subject, permissions) - return permissions; //Promise[] + return permissions; } catch (e) { throw e; } finally { @@ -133,32 +90,17 @@ export class Controller> } async removeSubject>(resourceUrl: string, subject: SubjectType) { - console.log('Controller.removeSubject called'); - console.log('Arguments:', { resourceUrl, subject }); - await this.updateItem(resourceUrl, subject, []); - const subjectConfig = this.getSubjectConfig(subject); - const index = await this.index.getCurrent() as Index; - - const item = subjectConfig.resolver.getItem(index, resourceUrl, subject.selector); - if (!item) return; - - await subjectConfig.manager.deletePermissions(resourceUrl, subject, []); - - const idx = index.items.findIndex(i => subjectConfig.resolver.checkMatch(i.subject, subject)); - index.items.splice(idx, 1); + const item = await this.getItem(resourceUrl, subject); - await this.index.saveToRemote(); + await subjectConfig.manager.deletePermissions(resourceUrl, subject, item?.permissions ?? []); } async removePermission>(resourceUrl: string, removedPermission: Permission, subject: SubjectType) { - console.log('Controller.removePermission called'); - console.log('Arguments:', { resourceUrl, removedPermission, subject }); const release = await this.acquire() try { // 1. Delete a permission for the subject - console.log("were here", removedPermission, subject) await this.getSubjectConfig(subject).manager.deletePermissions(resourceUrl, subject, [removedPermission]); // 2. Let the manager delete the permission, return the updated version @@ -175,71 +117,29 @@ export class Controller> } async enablePermissions>(resource: string, subject: SubjectType) { - console.log('Controller.enablePermissions called'); - console.log('Arguments:', { resource, subject }); - let item = await this.getItem(resource, subject); - if (!item) { - // This point should never be reached - throw new Error("Item not found to enable permissions from") - } - - const { manager } = this.getSubjectConfig(subject) - await manager.createPermissions(resource, subject, item.permissions); - - item.isEnabled = true; - await this.index.saveToRemote() + // won't fix } async disablePermissions>(resourceUrl: string, subject: SubjectType) { - console.log('Controller.disablePermissions called'); - console.log('Arguments:', { resourceUrl, subject }); - let item = await this.getItem(resourceUrl, subject); - if (!item) { - throw new Error("Item not found to disable permissions from") - } - - const { manager } = this.getSubjectConfig(subject) - await manager.editPermissions(resourceUrl, item, subject, []); - - item.isEnabled = false; - - await this.index.saveToRemote() + // won't fix } async getContainerPermissionList(containerUrl: string): Promise[]> { - console.log('Controller.getContainerPermissionList called'); - console.log('Arguments:', { containerUrl }); - - // Use the subjectConfigs to get permissions for the container - // For each subjectConfig, call its manager.getRemotePermissions for the containerUrl - - // Eventually, we would only need to use the webIDManager... - const configs: SubjectConfig[] = Object.values(this.subjectConfigs); - console.log(configs) - const results = await Promise.all( - configs.filter(c => c.manager.type === "webId").map(c => c.manager.getContainerPermissionList(containerUrl))); - - return results.flat() + return this.getSubjectConfig({ type: "public" } as T[SubjectKey]).manager.getContainerPermissionList(containerUrl); } // NOTE: Do we want to force this to only use the index stored in the store? async getResourcePermissionList(resourceUrl: string): Promise> { - const configs: SubjectConfig[] = Object.values(this.subjectConfigs); - console.log(configs) - const results = await Promise.all( - configs.filter(c => c.manager.type === 'webId').map(c => c.manager.getRemotePermissions(resourceUrl)) - ); + const result = await this.getSubjectConfig({ type: "public" } as T[SubjectKey]).manager.getRemotePermissions(resourceUrl); return { resourceUrl, canRequestAccess: true, // TODO - permissionsPerSubject: results.flat() + permissionsPerSubject: result }; } isSubjectSupported>(subject: BaseSubject): IController> { - console.log('Controller.isSubjectSupported called'); - console.log('Arguments:', { subject }); if (!this.subjectConfigs[subject.type as unknown as keyof T]) { throw new Error(`Subject type ${subject.type} is not supported`); diff --git a/controller/src/classes/permissionManager/inrupt/InruptPermissionManager.ts b/controller/src/classes/permissionManager/inrupt/InruptPermissionManager.ts index 6c60921..5565d3d 100644 --- a/controller/src/classes/permissionManager/inrupt/InruptPermissionManager.ts +++ b/controller/src/classes/permissionManager/inrupt/InruptPermissionManager.ts @@ -2,8 +2,9 @@ import { Access, AccessModes, getSolidDataset, getThingAll } from "@inrupt/solid import { SubjectPermissions, BaseSubject, IndexItem, Permission, ResourcePermissions } from "../../../types"; import { SubjectKey, TargetSubjects } from "../../../types/modules"; import { getDefaultSession } from "@inrupt/solid-client-authn-browser"; -import { ODRL, PolicyParser } from "../../../classes/utils/PolicyParser"; +import { ODRL } from "../../../classes/utils/PolicyParser"; import { PolicyInterpreter } from "../../../classes/utils/PolicyInterpreter"; +import { PolicyService } from "../../../classes/utils/PolicyService"; import { Store } from 'n3'; const ACCESS_MODES_TO_PERMISSION_MAPPING: Record = { @@ -15,8 +16,6 @@ const ACCESS_MODES_TO_PERMISSION_MAPPING: Record `http://localhost:4000/uma/policies${encodedId}` - /** * A permission manager implementation using the inrupt sdk to actually update the ACL * The "Inrupt" prefix is to indicate the usage of the inrupt sdk @@ -79,48 +78,13 @@ export abstract class InruptPermissionManager { - const store: Store = await this.fetchPolicies(assignerId); + const store: Store = await new PolicyService().fetchPolicies(assignerId); const target: TargetSubjects = new PolicyInterpreter().permissionsForOneResource(targetId, store); // If there are no private permissions, or no private permissions for the assignee, return the public ones (or nothing if they don't exist) @@ -142,12 +106,15 @@ export abstract class InruptPermissionManager[] = []; // Add the owner information @@ -162,7 +129,7 @@ export abstract class InruptPermissionManager 0) subjectPermissions.push({ subject: { type: "public", } as unknown as T[K], @@ -172,15 +139,17 @@ export abstract class InruptPermissionManager subjectPermissions.push({ - subject: { - type: "webId", - selector: { url: subject.subject } - } as unknown as T[K], - permissions: Array.from(subject.permissions), - isEnabled: true, // Not yet implemented, there is no odrl equivalent? - targetId: target.targetUrl - })) + if (target.private) target.private.forEach(subject => { + if (subject.permissions.size > 0) subjectPermissions.push({ + subject: { + type: "webId", + selector: { url: subject.subject } + } as unknown as T[K], + permissions: Array.from(subject.permissions), + isEnabled: true, // Not yet implemented, there is no odrl equivalent? + targetId: target.targetUrl + }) + }) return subjectPermissions; } @@ -199,7 +168,7 @@ export abstract class InruptPermissionManager q.object.id))); diff --git a/controller/src/classes/permissionManager/inrupt/PublicManager.ts b/controller/src/classes/permissionManager/inrupt/PublicManager.ts index f9dd900..b389fa3 100644 --- a/controller/src/classes/permissionManager/inrupt/PublicManager.ts +++ b/controller/src/classes/permissionManager/inrupt/PublicManager.ts @@ -1,5 +1,5 @@ -import { PolicyEditor } from "@/classes/utils/PolicyEditor"; -import { BaseSubject, IndexItem, Permission, ResourcePermissions } from "../../../types"; +import { PolicyService } from "../../../classes/utils/PolicyService"; +import { BaseSubject, IndexItem, Permission } from "../../../types"; import { IPermissionManager, SubjectKey } from "../../../types/modules"; import { InruptPermissionManager } from "./InruptPermissionManager"; @@ -7,16 +7,16 @@ export class PublicManager>(resource: string, subject: T[K], permissions: Permission[]): Promise { - new PolicyEditor().insertActionRule(resource, permissions) + await new PolicyService().insertActionRule(resource, permissions) } async deletePermissions>(resource: string, subject: T[K], permissions: Permission[]) { - new PolicyEditor().deleteActionRule(resource, permissions) + await new PolicyService().deleteActionRule(resource, permissions) } async editPermissions>(resource: string, item: IndexItem, subject: T[K], permissions: Permission[]) { - console.log("public manager: edit permissions") + // not needed } type = 'public' diff --git a/controller/src/classes/permissionManager/inrupt/WebIdManager.ts b/controller/src/classes/permissionManager/inrupt/WebIdManager.ts index 8541a71..5a0a0bc 100644 --- a/controller/src/classes/permissionManager/inrupt/WebIdManager.ts +++ b/controller/src/classes/permissionManager/inrupt/WebIdManager.ts @@ -1,20 +1,21 @@ import { BaseSubject, IndexItem, Permission } from "../../../types"; import { IPermissionManager, SubjectKey } from "../../../types/modules"; import { InruptPermissionManager } from "./InruptPermissionManager"; -import { PolicyEditor } from "../../../classes/utils/PolicyEditor"; +import { PolicyService } from "../../utils/PolicyService"; export class WebIdManager>> extends InruptPermissionManager implements IPermissionManager { // Create an action for this resource and this subject with the given permissions async createPermissions>(resource: string, subject: T[K], permissions: Permission[]): Promise { - new PolicyEditor().insertActionRule(resource, permissions, subject.selector!.url); + await new PolicyService().insertActionRule(resource, permissions, subject.selector!.url); } async deletePermissions>(resource: string, subject: T[K], permissions: Permission[]) { - new PolicyEditor().deleteActionRule(resource, permissions, subject.selector!.url) + await new PolicyService().deleteActionRule(resource, permissions, subject.selector!.url) } async editPermissions>(resource: string, item: IndexItem, subject: T[K], permissions: Permission[]) { + // not needed } type = "webId" diff --git a/controller/src/classes/utils/PolicyEditor.ts b/controller/src/classes/utils/PolicyService.ts similarity index 76% rename from controller/src/classes/utils/PolicyEditor.ts rename to controller/src/classes/utils/PolicyService.ts index b8d89c4..af428a2 100644 --- a/controller/src/classes/utils/PolicyEditor.ts +++ b/controller/src/classes/utils/PolicyService.ts @@ -1,11 +1,11 @@ import { getDefaultSession } from "@inrupt/solid-client-authn-browser"; -import { Permission } from "../../types/"; -import { UMA_URL } from "../permissionManager/inrupt"; +import { Permission } from "../../types"; import { ODRL, PolicyParser } from "./PolicyParser"; import { DataFactory, Writer } from "n3" const { namedNode } = DataFactory +export const UMA_URL = (encodedId: string = "") => `http://localhost:4000/uma/policies${encodedId}` -export class PolicyEditor { +export class PolicyService { constructor() { } private getRandomString(length: number): string { @@ -18,6 +18,41 @@ export class PolicyEditor { return result; } + public async fetchPolicies(webId: string) { + + // Get all our policies + const response = await fetch(UMA_URL(), { + headers: { + "Authorization": webId, + "Accept": "text/turtle" + } + }); + + // Extract the target Ids + const turtleText = await response.text(); + console.log("Retrieved Turtle:", turtleText); + + // Use parser to extract an N3 Store + const parser = new PolicyParser(); + return parser.parseText(turtleText); + } + + public async fetchOnePolicy(webId: string, policyId: string) { + // Get all our policies + const response = await fetch(UMA_URL(`/${encodeURIComponent(policyId)}`), { + headers: { + "Authorization": webId, + "Accept": "text/turtle" + } + }); + + const turtleText = await response.text(); + + // Use parser to extract an N3 Store + const parser = new PolicyParser(); + return parser.parseText(turtleText); + } + /** * Function to insert an action rule for each permission in the provided array. They will be inserted in a new policy, via POST and not PATCH. @@ -101,36 +136,36 @@ export class PolicyEditor { // 3: Find all rules with our target const targetRules = store.getQuads(null, ODRL("target"), namedNode(targetId), null); - const policyIds = new Set(); + const policyIds = new Map>(); targetRules.forEach( // Filter only the targets that have rules with us as assignee, or public if no assignee target => { // Search the rule of the target, and then the policy of the rule, only for permission (for now) - console.log("target to be checked: ", target) const rule = target.subject; - console.log("rule to be checked: ", rule.id) const matches = store.getQuads(null, ODRL("permission"), rule, null); if (matches.length === 0) console.warn("out of bounds rule"); const policyId = matches[0].subject.id; - console.log(policyId) - // We now have the policies that have our target, check if our assignee has an action to delete here if (assignee === "") { // If no assignee specified, the rule is public and it has an action to be deleted, select it if (store.getQuads(rule, ODRL("assignee"), null, null).length === 0) { for (const action of actions) - if (store.getQuads(rule, ODRL("action"), ODRL(action.toLowerCase()), null).length > 0) - policyIds.add(policyId) + if (store.getQuads(rule, ODRL("action"), ODRL(action.toLowerCase()), null).length > 0) { + if (!policyIds.has(policyId)) policyIds.set(policyId, new Set()); + policyIds.get(policyId)!.add(action); + } } } else { // Do the same, with a check if the assignee is correct if (store.getQuads(rule, ODRL("assignee"), namedNode(assignee), null).length >= 1) { for (const action of actions) { console.log(new Writer().quadsToString(store.getQuads(rule, ODRL("action"), ODRL(action.toLowerCase()), null))) - if (store.getQuads(rule, ODRL("action"), ODRL(action.toLowerCase()), null).length > 0) - policyIds.add(policyId) + if (store.getQuads(rule, ODRL("action"), ODRL(action.toLowerCase()), null).length > 0) { + if (!policyIds.has(policyId)) policyIds.set(policyId, new Set()); + policyIds.get(policyId)!.add(action); + } } } @@ -138,9 +173,11 @@ export class PolicyEditor { } ) + console.log("The policies we found are...", ...policyIds) + // 4: Delete the rule that has the matching target and permission for the matching assignee - for (const policyId of policyIds) { - for (const action of actions) { + for (const policyId of policyIds.keys()) { + for (const action of policyIds.get(policyId)!) { const deleteResponse = await fetch(UMA_URL(`/${encodeURIComponent(policyId)}`), { method: "PATCH", headers: { @@ -172,6 +209,8 @@ export class PolicyEditor { if (!deleteResponse.ok) { throw new Error(`Policy deletion failed: ${deleteResponse.status}`); } + + console.log(await deleteResponse.text()) } } } diff --git a/loama/src/components/explorer/SubjectPermissionTable.vue b/loama/src/components/explorer/SubjectPermissionTable.vue index ee25eb8..eb24656 100644 --- a/loama/src/components/explorer/SubjectPermissionTable.vue +++ b/loama/src/components/explorer/SubjectPermissionTable.vue @@ -37,12 +37,6 @@ - - -