diff --git a/assets/loama-extended.drawio.png b/assets/loama-extended.drawio.png new file mode 100644 index 0000000..18f420c Binary files /dev/null and b/assets/loama-extended.drawio.png differ diff --git a/assets/loama-short.drawio.png b/assets/loama-short.drawio.png new file mode 100644 index 0000000..f46f192 Binary files /dev/null and b/assets/loama-short.drawio.png differ diff --git a/controller/src/classes/Controller.ts b/controller/src/classes/Controller.ts index aa3aad1..b039e39 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"; @@ -5,10 +6,10 @@ import { InruptAccessRequest } from "./accessRequests/InruptAccessRequest"; 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) { @@ -28,65 +29,7 @@ export class Controller> return subjectConfig as SubjectConfig } - private async getExistingRemotePermissions>(resourceUrl: string, subject: T[K]): Promise { - 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 { - const item = await this.getItem(resourceUrl, subject); - if (item) { - // Makeing 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) { - 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 { @@ -94,19 +37,13 @@ export class Controller> } async setPodUrl(podUrl: string) { - this.index.setPodUrl(podUrl); - this.resources.setPodUrl(podUrl); - await this.accessRequest.setPodUrl(podUrl) } unsetPodUrl() { - this.index.unsetPodUrl(); - this.resources.unsetPodUrl(); - this.accessRequest.unsetPodUrl(); } async getOrCreateIndex() { - return this.index.getOrCreate(); + return { id: "", items: [] } } getLabelForSubject>(subject: T[K]): string { @@ -114,26 +51,36 @@ export class Controller> return resolver.toLabel(subject); } + /** + * Assemble the information for a subject in a resource + * @returns + */ async getItem>(resourceUrl: string, subject: SubjectType): Promise | undefined> { - const { resolver } = this.getSubjectConfig(subject); - - const index = await this.index.getCurrent() as Index; - return resolver.getItem(index, resourceUrl, subject.selector) + 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) { 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. Create a new permission for the subject + await this.getSubjectConfig(subject).manager.createPermissions(resourceUrl, subject, [addedPermission]) - permissions.push(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); - await this.updateItem(resourceUrl, subject, permissions) return permissions; } catch (e) { throw e; @@ -143,187 +90,53 @@ export class Controller> } async removeSubject>(resourceUrl: string, subject: SubjectType) { - 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) { - 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 + 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(); } } async enablePermissions>(resource: string, subject: SubjectType) { - 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) { - 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[]> { - const configs: SubjectConfig[] = Object.values(this.subjectConfigs); - - 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 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(); - - 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 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) { - // 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(); - const results = await Promise.allSettled(configs.map(c => c.manager.getRemotePermissions(resourceUrl))) - - let permissionsPerSubject = index.items.filter(i => i.resource === resourceUrl) + async getResourcePermissionList(resourceUrl: string): Promise> { + const result = await this.getSubjectConfig({ type: "public" } as T[SubjectKey]).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: result + }; } 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, @@ -12,7 +16,6 @@ const ACCESS_MODES_TO_PERMISSION_MAPPING: Record>(resourceUrl: string): Promise[]> + /** + * Function to specifically get the permission list for an assignee on a certain target + * + * TODO: split in subject + */ + public async getTargetPermissionsForUser(assignerId: string, assigneeId: string, targetId: string): Promise { + 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) + if (!target.private || !target.private.get(assigneeId)) return Array.from(target.public?.permissions! ?? []) + + return Array.from(target.private.get(assigneeId)?.permissions!) ?? [] + } - async getContainerPermissionList(containerUrl: string, resourceToSkip: string[] = []) { + + public async getRemotePermissions>(resourceUrl: string): Promise[]> { + // Extract our webID 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) - } + const webId = session.info.webId; + + // We must be logged on + if (!webId) { + throw new Error("User not logged in"); + } + + // Retrieve our policies + const store = await new PolicyService().fetchPolicies(webId); + + // Get detailed info about the target + const interpreter = new PolicyInterpreter(); + const target: TargetSubjects = interpreter.permissionsForOneResource(resourceUrl, store); + + + if (target) { + const subjectPermissions: SubjectPermissions[] = []; + // Add the owner information + subjectPermissions.push({ + subject: { + type: "webId", + selector: { url: target.assigner } + } as unknown as T[K], + permissions: [Permission.Append, Permission.Control, Permission.Create, Permission.Read, Permission.Write], + isEnabled: true, + targetId: target.targetUrl + }) + + // Add the public information + if (target.public && target.public.permissions.size > 0) subjectPermissions.push({ + subject: { + type: "public", + } as unknown as T[K], + permissions: Array.from(target.public.permissions), + isEnabled: true, // Not yet implemented, there is no odrl equivalent? + targetId: target.targetUrl + }) + + // Add the private subjects + 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 results.reduce[]>((arr, v) => { - if (v.status == "fulfilled") { - arr.push(v.value); - } - return arr; - }, []) + }) + return subjectPermissions; + } + + return []; + } + + + async getContainerPermissionList(containerUrl: string, resourceToSkip: string[] = []): Promise[]> { + // Extract our webID + const session = getDefaultSession(); + const webId = session.info.webId; + + // We must be logged on + if (!webId) { + throw new Error("User not logged in"); + } + + const store = await new PolicyService().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) { + 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 cd8e6b4..b389fa3 100644 --- a/controller/src/classes/permissionManager/inrupt/PublicManager.ts +++ b/controller/src/classes/permissionManager/inrupt/PublicManager.ts @@ -1,49 +1,25 @@ -import { AccessModes, getSolidDataset, getThingAll } from "@inrupt/solid-client"; -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"; -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) + await new PolicyService().insertActionRule(resource, permissions) } - async deletePermissions>(resource: string, subject: T[K]) { - await this.updateACL(resource, subject, {}); + async deletePermissions>(resource: string, subject: T[K], permissions: Permission[]) { + await new PolicyService().deleteActionRule(resource, permissions) + } async editPermissions>(resource: string, item: IndexItem, subject: T[K], permissions: Permission[]) { - const accessModes = this.editPermissionsToAccessModes(item, permissions); - await this.updateACL(resource, subject, accessModes) + // not needed } - 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, - }] - } + type = 'public' + + } diff --git a/controller/src/classes/permissionManager/inrupt/WebIdManager.ts b/controller/src/classes/permissionManager/inrupt/WebIdManager.ts index 2c275ec..5a0a0bc 100644 --- a/controller/src/classes/permissionManager/inrupt/WebIdManager.ts +++ b/controller/src/classes/permissionManager/inrupt/WebIdManager.ts @@ -1,52 +1,22 @@ -import { getDefaultSession } from "@inrupt/solid-client-authn-browser"; import { BaseSubject, IndexItem, Permission } from "../../../types"; import { IPermissionManager, SubjectKey } from "../../../types/modules"; import { InruptPermissionManager } from "./InruptPermissionManager"; -import { getAgentAccessAll, setAgentAccess } from "@inrupt/solid-client/universal"; -import { AccessModes, } from "@inrupt/solid-client"; -import { cacheBustedSessionFetch } from "../../../util"; +import { PolicyService } from "../../utils/PolicyService"; 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 + + // Create an action for this resource and this subject with the given permissions async createPermissions>(resource: string, subject: T[K], permissions: Permission[]): Promise { - const accessModes = this.permissionsToAccessModes(permissions, []); - await this.updateACL(resource, subject, accessModes) + await new PolicyService().insertActionRule(resource, permissions, subject.selector!.url); } - async deletePermissions>(resource: string, subject: T[K]) { - await this.updateACL(resource, subject, {}); + async deletePermissions>(resource: string, subject: T[K], permissions: Permission[]) { + await new PolicyService().deleteActionRule(resource, permissions, subject.selector!.url) } async editPermissions>(resource: string, item: IndexItem, subject: T[K], permissions: Permission[]) { - const accessModes = this.editPermissionsToAccessModes(item, permissions); - await this.updateACL(resource, subject, accessModes) + // not needed } - async getRemotePermissions>(resourceUrl: string) { - const session = getDefaultSession(); - const agentAccess = await getAgentAccessAll(resourceUrl, { fetch: cacheBustedSessionFetch(session) }); - - if (!agentAccess) { - return []; - } - - return Object.entries(agentAccess).map(([url, access]) => ({ - // @ts-expect-error selector is required for webId - subject: { - type: "webId", - selector: { url }, - } as T[K], - permissions: this.AccessModesToPermissions(access), - isEnabled: true, - })) - } + type = "webId" } diff --git a/controller/src/classes/utils/PolicyInterpreter.ts b/controller/src/classes/utils/PolicyInterpreter.ts new file mode 100644 index 0000000..4cee135 --- /dev/null +++ b/controller/src/classes/utils/PolicyInterpreter.ts @@ -0,0 +1,159 @@ +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 only takes permission into account + * @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(); + + 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; + + case "create": + permissions.push(Permission.Create); + 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); + + // Only return the target we need + const target = targets.filter(t => t.targetUrl === resourceUrl); + + 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 new file mode 100644 index 0000000..7910594 --- /dev/null +++ b/controller/src/classes/utils/PolicyParser.ts @@ -0,0 +1,17 @@ +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() { } + + public parseText = (text: string): Store => { + const parser = new Parser({ format: 'text/turtle' }); + const quads = parser.parse(text); + return new Store(quads); + } +} \ No newline at end of file diff --git a/controller/src/classes/utils/PolicyService.ts b/controller/src/classes/utils/PolicyService.ts new file mode 100644 index 0000000..bec43de --- /dev/null +++ b/controller/src/classes/utils/PolicyService.ts @@ -0,0 +1,238 @@ +import { getDefaultSession } from "@inrupt/solid-client-authn-browser"; +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 PolicyService { + 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 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(); + + // 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); + } + + public async postPolicy(webId: string, body: string) { + await fetch(UMA_URL(), { + method: 'POST', + headers: { + 'Authorization': webId, + 'Content-type': 'text/turtle' + // 'Content-type': 'application/sparql-update' + }, + body: body + }) + } + + public async patchPolicy(webId: string, policyId: string, body: string) { + await fetch(UMA_URL(`/${encodeURIComponent(policyId)}`), { + method: 'PATCH', + headers: { + 'Authorization': webId, + 'Content-type': 'application/sparql-update' + }, + body: body + }) + } + + + /** + * 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! + + // Find out if this target already has a policy + const store = (await this.fetchPolicies(webId)); + const ruleIds = store.getQuads(null, ODRL('target'), namedNode(targetId), null).map(quad => quad.subject); + const policyIds = new Set(); + ruleIds.forEach(ruleId => + // We also only take permission into account (for now) + store.getQuads(null, ODRL('permission'), ruleId, null).forEach(quad => + // Add each policyId + policyIds.add(quad.subject.id) + ) + ) + + // Our policyId is either one from the set or a random generator if there are none, this is not verified to be unique, but 20^62 possibilities should work for now + // Since we found this target, it implicitly means that there must exist a policy and thus we will never create a random one... + const policyId: string = policyIds.size > 0 + ? [...policyIds][0] + : `http://example.org/policy${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 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 + // If there already exists a policy for this target, patch this rule into it. Otherwise, just post a new one + const response = policyIds.size > 0 + ? this.patchPolicy(webId, policyId, `PREFIX odrl: +INSERT { + <${policyId}> odrl:permission <${ruleId}> . + <${ruleId}> a odrl:Permission ; + odrl:target <${targetId}> ; + ${actionTriple} + ${assigneeTriple} + odrl:assigner <${webId}> . +} +WHERE {}`) + : this.postPolicy(webId, `@prefix odrl: . + +<${policyId}> a odrl:Agreement ; + odrl:permission <${ruleId}> . + +<${ruleId}> a odrl:Permission ; + odrl:target <${targetId}> ; + ${actionTriple} + ${assigneeTriple} + odrl:assigner <${webId}> . +`) + } + } + + /** + * Funcion that searches every owned rule by the logged on client, finds the target + * of an assigner and deletes 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 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) + const rule = target.subject; + 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; + + // 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) { + if (!policyIds.has(policyId)) policyIds.set(policyId, new Set()); + policyIds.get(policyId)!.add(rule.id); + } + } + } 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) { + 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(rule.id); + } + } + + } + } + } + ) + + // 4: Delete the rule that has the matching target and permission for the matching assignee + for (const policyId of policyIds.keys()) { + for (const ruleId of policyIds.get(policyId)!) { + const deleteResponse = await fetch(UMA_URL(`/${encodeURIComponent(policyId)}`), { + method: "PATCH", + headers: { + 'Authorization': webId, + 'Content-type': 'application/sparql-update', + }, + body: ` + PREFIX odrl: + +DELETE { + <${ruleId}> ?p ?o . + ?policy odrl:permission <${ruleId}> . +} +WHERE { + OPTIONAL { + <${ruleId}> ?p ?o . + } + OPTIONAL { + ?policy odrl:permission <${ruleId}> . + } +}` + }); + + if (!deleteResponse.ok) { + throw new Error(`Policy deletion failed: ${deleteResponse.status}`); + } + } + } + } +} \ No newline at end of file diff --git a/controller/src/types/index.ts b/controller/src/types/index.ts index 7287883..5feb781 100644 --- a/controller/src/types/index.ts +++ b/controller/src/types/index.ts @@ -30,6 +30,7 @@ export enum Permission { Control = "Control", Read = "Read", Write = "Write", + Create = "Create", } export interface BaseSubject { @@ -43,6 +44,7 @@ export interface SubjectPermissions> { subject: T; permissions: Permission[]; isEnabled: boolean; + targetId?: string; } export interface ResourcePermissions> { diff --git a/controller/src/types/modules.ts b/controller/src/types/modules.ts index db9b0f9..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,4 +139,71 @@ 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 +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; +} + +// 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 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; + + // The policies referring to this target + policies: Set; + + // The rules referring to this target + rules: Set; } diff --git a/loama/src/components/explorer/SubjectPermissionTable.vue b/loama/src/components/explorer/SubjectPermissionTable.vue index ee25eb8..ac0aeb0 100644 --- a/loama/src/components/explorer/SubjectPermissionTable.vue +++ b/loama/src/components/explorer/SubjectPermissionTable.vue @@ -37,12 +37,6 @@ - - -