From 9d5bc4cf81ba97d8ec500f023ca69611728939c9 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Fri, 10 Nov 2023 17:43:26 +0000 Subject: [PATCH] WIP: use RDFJS API --- package-lock.json | 88 ++++++++++++++++++----- package.json | 5 +- src/common/constants.ts | 62 ++++++++++++++++ src/common/getters.ts | 79 +++++++++++++++------ src/odrl/type/AccessGrant.ts | 133 ++++++++++------------------------- 5 files changed, 229 insertions(+), 138 deletions(-) create mode 100644 src/common/constants.ts diff --git a/package-lock.json b/package-lock.json index fb183d7e0..8077af03f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,12 @@ "license": "MIT", "dependencies": { "@inrupt/solid-client": "^1.25.1", - "@inrupt/solid-client-vc": "^0.7.0", + "@inrupt/solid-client-vc": "file:../solid-client-vc-js", "@inrupt/universal-fetch": "^1.0.1", "@types/rdfjs__dataset": "^1.0.5", "auth-header": "^1.0.0", "base64url": "^3.0.1", + "n3": "^1.17.2", "rdf-namespaces": "^1.9.2" }, "devDependencies": { @@ -26,10 +27,12 @@ "@inrupt/solid-client-authn-browser": "^1.12.2", "@inrupt/solid-client-authn-node": "^1.12.1", "@playwright/test": "^1.34.3", + "@rdfjs/types": "^1.1.0", "@rushstack/eslint-patch": "^1.1.4", "@types/auth-header": "^1.0.2", "@types/dotenv-flow": "^3.1.0", "@types/jest": "^29.2.2", + "@types/n3": "^1.16.4", "@types/uuid": "^9.0.4", "dotenv-flow": "^3.2.0", "eslint": "^8.18.0", @@ -54,6 +57,58 @@ "fsevents": "^2.3.2" } }, + "../solid-client-vc-js": { + "name": "@inrupt/solid-client-vc", + "version": "0.7.2", + "license": "MIT", + "dependencies": { + "@inrupt/solid-client": "^1.25.2", + "@inrupt/universal-fetch": "^1.0.1", + "content-type": "^1.0.5", + "event-emitter-promisify": "^1.1.0", + "jsonld-context-parser": "^2.4.0", + "jsonld-streaming-parser": "^3.3.0", + "jsonld-streaming-serializer": "^2.1.0", + "md5": "^2.3.0", + "n3": "^1.17.0" + }, + "devDependencies": { + "@inrupt/eslint-config-lib": "^2.0.0", + "@inrupt/internal-playwright-helpers": "^2.0.4", + "@inrupt/internal-test-env": "^2.0.4", + "@inrupt/jest-jsdom-polyfills": "^2.0.4", + "@playwright/test": "^1.28.1", + "@rdfjs/dataset": "2.0.1", + "@rdfjs/types": "^1.1.0", + "@rushstack/eslint-patch": "^1.1.4", + "@types/content-type": "^1.1.5", + "@types/dotenv-flow": "^3.1.1", + "@types/jest": "^29.2.2", + "@types/md5": "^2.3.4", + "@types/n3": "^1.16.0", + "@types/node": "^20.1.2", + "@types/rdfjs__dataset": "2.0.5", + "dotenv-flow": "^3.2.0", + "eslint": "^8.18.0", + "jest": "^29.3.0", + "jest-environment-jsdom": "^29.3.0", + "prettier": "^3.0.2", + "rdf-isomorphic": "^1.3.1", + "rollup": "^3.1.0", + "rollup-plugin-typescript2": "^0.35.0", + "ts-jest": "^29.0.3", + "ts-node": "^10.9.1", + "typedoc": "^0.25.1", + "typedoc-plugin-markdown": "^3.15.1", + "typescript": "^5.0.4" + }, + "engines": { + "node": "^16.0.0 || ^18.0.0 || ^20.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -1054,19 +1109,8 @@ } }, "node_modules/@inrupt/solid-client-vc": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@inrupt/solid-client-vc/-/solid-client-vc-0.7.2.tgz", - "integrity": "sha512-eGq3PlHCjDpKDyUpuDZJwmy3rr9rYTRra5OTZY8WSM4iD+KL7acQGtZHceCnJ61naygjodz8keSfAjw54t5OPA==", - "dependencies": { - "@inrupt/solid-client": "^1.25.2", - "@inrupt/universal-fetch": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || ^18.0.0 || ^20.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } + "resolved": "../solid-client-vc-js", + "link": true }, "node_modules/@inrupt/universal-fetch": { "version": "1.0.3", @@ -1948,6 +1992,16 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/n3": { + "version": "1.16.4", + "resolved": "https://registry.npmjs.org/@types/n3/-/n3-1.16.4.tgz", + "integrity": "sha512-6PmHRYCCdjbbBV2UVC/HjtL6/5Orx9ku2CQjuojucuHvNvPmnm6+02B18YGhHfvU25qmX2jPXyYPHsMNkn+w2w==", + "dev": true, + "dependencies": { + "@rdfjs/types": "^1.1.0", + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "20.5.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.0.tgz", @@ -7638,9 +7692,9 @@ "dev": true }, "node_modules/n3": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/n3/-/n3-1.17.1.tgz", - "integrity": "sha512-HlanMWpvN2kcTrFuU3GPObyY7qrVQWy2Hp7l4GSXJlcQapjQMR7OM4kCr788pTQzNIpiHS3JRvyZ2YUcYJ82rA==", + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/n3/-/n3-1.17.2.tgz", + "integrity": "sha512-BxSM52wYFqXrbQQT5WUEzKUn6qpYV+2L4XZLfn3Gblz2kwZ09S+QxC33WNdVEQy2djenFL8SNkrjejEKlvI6+Q==", "dependencies": { "queue-microtask": "^1.1.2", "readable-stream": "^4.0.0" diff --git a/package.json b/package.json index 42e3386a1..5ef4eac16 100644 --- a/package.json +++ b/package.json @@ -109,10 +109,12 @@ "@inrupt/solid-client-authn-browser": "^1.12.2", "@inrupt/solid-client-authn-node": "^1.12.1", "@playwright/test": "^1.34.3", + "@rdfjs/types": "^1.1.0", "@rushstack/eslint-patch": "^1.1.4", "@types/auth-header": "^1.0.2", "@types/dotenv-flow": "^3.1.0", "@types/jest": "^29.2.2", + "@types/n3": "^1.16.4", "@types/uuid": "^9.0.4", "dotenv-flow": "^3.2.0", "eslint": "^8.18.0", @@ -132,11 +134,12 @@ }, "dependencies": { "@inrupt/solid-client": "^1.25.1", - "@inrupt/solid-client-vc": "^0.7.0", + "@inrupt/solid-client-vc": "file:../solid-client-vc-js", "@inrupt/universal-fetch": "^1.0.1", "@types/rdfjs__dataset": "^1.0.5", "auth-header": "^1.0.0", "base64url": "^3.0.1", + "n3": "^1.17.2", "rdf-namespaces": "^1.9.2" }, "engines": { diff --git a/src/common/constants.ts b/src/common/constants.ts new file mode 100644 index 000000000..652302c61 --- /dev/null +++ b/src/common/constants.ts @@ -0,0 +1,62 @@ +import { Term, Quad, NamedNode, BlankNode, Literal } from "@rdfjs/types"; +import { DataFactory } from "n3"; +import { rdf } from "rdf-namespaces"; +const { namedNode } = DataFactory; + +export const cred = "https://www.w3.org/2018/credentials#"; +export const gc = "https://w3id.org/GConsent#"; +export const acl = "http://www.w3.org/ns/auth/acl#"; +export const odrl2 = "http://www.w3.org/ns/odrl/2/"; +export const solidOdrl = "https://www.w3.org/ns/solid/odrl/"; +export const vc = "http://www.w3.org/ns/solid/vc#"; +export const xsd = "http://www.w3.org/2001/XMLSchema#"; + +export const XSD_BOOLEAN = namedNode(`${xsd}boolean`); + +export const SOLID_ACCESS_GRANT = namedNode(`${vc}SolidAccessGrant`); +export const CREDENTIAL_SUBJECT = namedNode(`${cred}credentialSubject`); +export const TYPE = namedNode(rdf.type); +export const PROVIDED_CONSENT = namedNode(`${gc}providedConsent`); +export const HAS_CONSENT = namedNode(`${gc}hasConsent`); +export const IS_PROVIDED_TO = namedNode(`${gc}isProvidedTo`); +export const PERMISSION = namedNode(`${odrl2}permission`); +// Check this +export const CONSTRAINT = namedNode(`${odrl2}constraint`); +export const ACTION = namedNode(`${odrl2}action`); +export const LEFT_OPERAND = namedNode(`${odrl2}leftOperand`); +export const OPERATOR = namedNode(`${odrl2}operator`); +export const RIGHT_OPERAND = namedNode(`${odrl2}rightOperand`); +export const AGREEMENT = namedNode(`${odrl2}Agreement`); +export const PROHIBITION = namedNode(`${odrl2}prohibition`); +export const PROFILE = namedNode(`${odrl2}profile`); +export const ASSIGNER = namedNode(`${odrl2}assigner`); +export const ASSIGNEE = namedNode(`${odrl2}assignee`); +export const TARGET = namedNode(`${odrl2}target`); +export const ACCESS = namedNode(`${solidOdrl}access`); +export const MODE = namedNode(`${acl}mode`); +export const READ = namedNode(`${acl}Read`); +export const WRITE = namedNode(`${acl}Write`); +export const APPEND = namedNode(`${acl}Append`); +export const ISSUANCE_DATE = namedNode(`${cred}issuanceDate`); +export const EXPIRATION_DATE = namedNode(`${cred}expirationDate`); +export const ISSUER = namedNode(`${cred}issuer`); +export const INHERIT = namedNode("urn:uuid:71ab2f68-a68b-4452-b968-dd23e0570227"); + +export function assertTermType(term: Term): NamedNode +export function assertTermType(term: Term, type: 'NamedNode'): NamedNode +export function assertTermType(term: Term, type: 'BlankNode'): BlankNode +export function assertTermType(term: Term, type: 'Literal'): Literal +export function assertTermType(term: Term, type?: Term['termType']): Term +export function assertTermType(term: Term, type: Term['termType'] = 'NamedNode'): Term { + if (term.termType !== type) { + throw new Error(`Expected [${term.value}] to be a ${type}. Found ${term.termType}`); + } + return term; +} +export function getSingleQuad(quads: Quad[]): Quad { + if (quads.length !== 1) { + throw new Error(`Expected exactly one result. Found ${quads.length}`); + } + return quads[0]; +} + diff --git a/src/common/getters.ts b/src/common/getters.ts index 64437acd9..b8634a9f5 100644 --- a/src/common/getters.ts +++ b/src/common/getters.ts @@ -25,6 +25,12 @@ import type { AccessModes } from "../type/AccessModes"; import { resourceAccessToAccessMode } from "../type/AccessModes"; import type { AccessGrantOdrl } from "../odrl"; import { isCredentialAccessGrantOdrl } from "../odrl"; +import { DataFactory } from "n3"; +import { NamedNode, DatasetCore, Quad_Subject, Quad_Predicate, Quad_Object, Term, BlankNode, Literal } from "@rdfjs/types"; +import { CREDENTIAL_SUBJECT, READ, WRITE, APPEND, PERMISSION, ACTION, PROVIDED_CONSENT, MODE, HAS_CONSENT, ISSUANCE_DATE, EXPIRATION_DATE, ISSUER, INHERIT, XSD_BOOLEAN, ASSIGNEE, IS_PROVIDED_TO } from "./constants"; +import { assertTermType, getSingleQuad } from "./constants"; +import { GC_IS_PROVIDED_TO } from "../gConsent/constants"; +const { namedNode, defaultGraph, quad, literal } = DataFactory; /** * Get the resources to which an Access Grant/Request applies. @@ -94,13 +100,22 @@ export function getResourceOwner( export function getRequestor( vc: AccessGrantGConsent | AccessRequestGConsent | AccessGrantOdrl, ): string { + const credentialSubject = getSingleObject(vc, namedNode(vc.id), CREDENTIAL_SUBJECT); if (isCredentialAccessGrantOdrl(vc)) { - return vc.credentialSubject.assignee; + return getSingleObject(vc, credentialSubject, ASSIGNEE).value; } if (isGConsentAccessGrant(vc)) { - return vc.credentialSubject.providedConsent.isProvidedTo; + const providedConsent = getSingleObject(vc, credentialSubject, PROVIDED_CONSENT); + return getSingleObject(vc, providedConsent, IS_PROVIDED_TO).value; } - return vc.credentialSubject.id; + return credentialSubject.value; +} + +function getSingleObject(vc: DatasetCore, subject: Term, predicate: Term): NamedNode; +function getSingleObject(vc: DatasetCore, subject: Term, predicate: Term, type: 'BlankNode'): BlankNode; +function getSingleObject(vc: DatasetCore, subject: Term, predicate: Term, type: 'Literal'): Literal; +function getSingleObject(vc: DatasetCore, subject: Term, predicate: Term, type?: T) { + return assertTermType(getSingleQuad([...vc.match(subject, predicate, null, defaultGraph())]).object, type); } /** @@ -118,18 +133,23 @@ export function getRequestor( export function getAccessModes( vc: AccessGrantGConsent | AccessRequestGConsent | AccessGrantOdrl, ): AccessModes { + const credentialSubject = getSingleObject(vc, namedNode(vc.id), CREDENTIAL_SUBJECT); + if (isCredentialAccessGrantOdrl(vc)) { - const modes = vc.credentialSubject.permission - .map((permission) => permission.action) - .flat(); - return resourceAccessToAccessMode(modes); + const permissions = [...vc.match(credentialSubject, PERMISSION, null, defaultGraph())]; + return { + read: permissions.some((permission) => vc.has(quad(permission.object as Quad_Subject, ACTION, READ, defaultGraph()))), + write: permissions.some((permission) => vc.has(quad(permission.object as Quad_Subject, ACTION, WRITE, defaultGraph()))), + append: permissions.some((permission) => vc.has(quad(permission.object as Quad_Subject, ACTION, APPEND, defaultGraph()))), + }; } - if (isGConsentAccessGrant(vc)) { - return resourceAccessToAccessMode( - vc.credentialSubject.providedConsent.mode, - ); - } - return resourceAccessToAccessMode(vc.credentialSubject.hasConsent.mode); + + const consent = getSingleObject(vc, credentialSubject, isGConsentAccessGrant(vc) ? PROVIDED_CONSENT : HAS_CONSENT); + return { + read: vc.has(quad(consent, MODE, READ, defaultGraph())), + write: vc.has(quad(consent, MODE, WRITE, defaultGraph())), + append: vc.has(quad(consent, MODE, APPEND, defaultGraph())), + }; } /** @@ -168,6 +188,13 @@ export function getTypes( return vc.type; } +function wrapDate(date: Literal) { + if (!date.datatype.equals(literal('http://www.w3.org/2001/XMLSchema#dateTime'))) { + throw new Error(`Expected date to be a dateTime; recieved [${date.datatype.value}]`); + } + return new Date(date.value); +} + /** * Get the issuance date of an Access Grant/Request. * @@ -183,7 +210,7 @@ export function getTypes( export function getIssuanceDate( vc: AccessGrantGConsent | AccessRequestGConsent | AccessGrantOdrl, ): Date { - return new Date(vc.issuanceDate); + return wrapDate(getSingleObject(vc, namedNode(vc.id), ISSUANCE_DATE, 'Literal')); } /** @@ -201,9 +228,17 @@ export function getIssuanceDate( export function getExpirationDate( vc: AccessGrantGConsent | AccessRequestGConsent | AccessGrantOdrl, ): Date | undefined { - return typeof vc.expirationDate === "string" - ? new Date(vc.expirationDate) - : undefined; + const expirationDate = [...vc.match(namedNode(vc.id), EXPIRATION_DATE, null, defaultGraph())]; + + if (expirationDate.length > 1) { + throw new Error(`Expected at most one expiration date. Found ${expirationDate.length}`); + } + + if (expirationDate.length === 1) { + return wrapDate(assertTermType(expirationDate[0].object, 'Literal')); + } + + return undefined; } /** @@ -221,7 +256,7 @@ export function getExpirationDate( export function getIssuer( vc: AccessGrantGConsent | AccessRequestGConsent | AccessGrantOdrl, ): string { - return vc.issuer; + return getSingleObject(vc, namedNode(vc.id), ISSUER).value; } /** @@ -242,11 +277,9 @@ export function getInherit( if (isCredentialAccessGrantOdrl(vc)) { return true; } - if (isGConsentAccessGrant(vc)) { - // Inherit defaults to true. - return vc.credentialSubject.providedConsent.inherit ?? true; - } - return vc.credentialSubject.hasConsent.inherit ?? true; + const credentialSubject = getSingleObject(vc, namedNode(vc.id), CREDENTIAL_SUBJECT); + const consent = getSingleObject(vc, credentialSubject, isGConsentAccessGrant(vc) ? PROVIDED_CONSENT : HAS_CONSENT); + return !vc.has(quad(consent, INHERIT, literal('false', XSD_BOOLEAN), defaultGraph())); } /** diff --git a/src/odrl/type/AccessGrant.ts b/src/odrl/type/AccessGrant.ts index b48abadd4..054b4d1a3 100644 --- a/src/odrl/type/AccessGrant.ts +++ b/src/odrl/type/AccessGrant.ts @@ -21,7 +21,11 @@ import type { Iri, VerifiableCredential } from "@inrupt/solid-client-vc"; import type { ResourceAccessMode } from "../../type/ResourceAccessMode"; -import { RESOURCE_ACCESS_MODE } from "../../type/ResourceAccessMode"; +import { DataFactory } from "n3"; +import { ACCESS, ACTION, AGREEMENT, APPEND, ASSIGNEE, ASSIGNER, CONSTRAINT, CREDENTIAL_SUBJECT, LEFT_OPERAND, OPERATOR, PERMISSION, PROFILE, PROHIBITION, READ, RIGHT_OPERAND, SOLID_ACCESS_GRANT, TARGET, TYPE, WRITE, assertTermType, getSingleQuad } from "../../common/constants"; +import { defaultGraph } from "@rdfjs/dataset"; +import { BlankNode, DatasetCore, Quad } from "@rdfjs/types"; +const { namedNode, quad, literal } = DataFactory; const ODRL_ACCESS = "https://www.w3.org/ns/solid/odrl/access"; @@ -63,54 +67,31 @@ export type AccessGrantOdrl = VerifiableCredential & { credentialSubject: CredentialSubjectOdrl; }; -function isOdrlConstraint(constraint: unknown): constraint is OdrlConstraint { - if ( - typeof (constraint as OdrlConstraint).leftOperand !== "string" || - !ODRL_CONSTRAINTS.includes((constraint as OdrlConstraint).leftOperand) - ) { - return false; - } - if ( - typeof (constraint as OdrlConstraint).operator !== "string" || - !ODRL_OPERATORS.includes((constraint as OdrlConstraint).operator) - ) { - return false; - } - if (typeof (constraint as OdrlConstraint).rightOperand !== "string") { - return false; - } - return true; +function _isOdrlConstraint(vc: VerifiableCredential, constraint: BlankNode): boolean { + const leftOperand = [...vc.match(constraint, LEFT_OPERAND, null, defaultGraph())]; + const rightOperand = [...vc.match(constraint, RIGHT_OPERAND, null, defaultGraph())]; + const operator = [...vc.match(constraint, OPERATOR, null, defaultGraph())]; + + return operator.length === 1 && operator[0].equals(literal('eq')) + && [leftOperand, rightOperand].every(operand => operand.length === 1 && (operator[0].equals(literal('application')) || operator[0].equals(literal('purpose')))); } -function isOdrlPermission(permission: unknown): permission is OdrlPermission { - if (typeof (permission as OdrlPermission).target !== "string") { - return false; - } - if ( - !Array.isArray((permission as OdrlPermission).action) || - (permission as OdrlPermission).action.some( - (mode) => !RESOURCE_ACCESS_MODE.has(mode), - ) - ) { - return false; +function every(store: DatasetCore, cb: (store: Quad) => boolean) { + for (const q of store) { + if (!cb(q)) + return false; } + return true; +} - // permission.constraint is either undefined or a constraint array. - if ( - typeof (permission as OdrlPermission).constraint !== "undefined" && - !Array.isArray((permission as OdrlPermission).constraint) - ) { - return false; - } - if ( - (permission as OdrlPermission).constraint?.some( - (constraint) => !isOdrlConstraint(constraint), - ) - ) { - return false; - } +function _isOdrlPermission(vc: VerifiableCredential, permission: BlankNode) { + const target = [...vc.match(permission, TARGET, null, defaultGraph())]; + const actions = [...vc.match(permission, ACTION, null, defaultGraph())]; - return true; + return target.length === 1 && target[0].object.termType === 'NamedNode' + && actions.length > 0 && actions.every(action => [READ, WRITE, APPEND].some(a => a.equals(action.object))) + && every(vc.match(permission, CONSTRAINT, null, defaultGraph()), (constraint) => constraint.object.termType === "BlankNode" && _isOdrlConstraint(vc, constraint.object)); + } /** @@ -145,57 +126,15 @@ function isOdrlPermission(permission: unknown): permission is OdrlPermission { export function isCredentialAccessGrantOdrl( vc: VerifiableCredential, ): vc is AccessGrantOdrl { - if (!vc.type.includes("SolidAccessGrant")) { - return false; - } - if ( - typeof vc.credentialSubject.type !== "string" || - vc.credentialSubject.type !== "Agreement" - ) { - return false; - } - if ( - typeof vc.credentialSubject.profile !== "string" || - vc.credentialSubject.profile !== ODRL_ACCESS - ) { - return false; - } - if (typeof vc.credentialSubject.assigner !== "string") { - return false; - } - - if (typeof vc.credentialSubject.assignee !== "string") { - return false; - } - - if ( - !Array.isArray( - (vc.credentialSubject as CredentialSubjectOdrl).permission, - ) || - (vc.credentialSubject as CredentialSubjectOdrl).permission.some( - (permission) => !isOdrlPermission(permission), - ) - ) { - return false; - } - - // vc.credentialSubject.prohibition is either undefined or an array of prohibitions. - if ( - !Array.isArray( - (vc.credentialSubject as CredentialSubjectOdrl).prohibition, - ) && - typeof (vc.credentialSubject as CredentialSubjectOdrl).prohibition !== - "undefined" - ) { - return false; - } - - if ( - (vc.credentialSubject as CredentialSubjectOdrl).prohibition?.some( - (permission) => !isOdrlPermission(permission), - ) - ) { - return false; - } - return true; + const credentialSubject = assertTermType(getSingleQuad([...vc.match(namedNode(vc.id), CREDENTIAL_SUBJECT, null, defaultGraph())]).object, 'NamedNode'); + const assigner = [...vc.match(credentialSubject, ASSIGNER, null, defaultGraph())]; + const assignee = [...vc.match(credentialSubject, ASSIGNEE, null, defaultGraph())]; + + return vc.has(quad(namedNode(vc.id), TYPE, SOLID_ACCESS_GRANT)) + && vc.has(quad(credentialSubject, TYPE, AGREEMENT)) + && vc.has(quad(credentialSubject, PROFILE, ACCESS)) + && assigner.length === 1 && assigner[0].object.termType === 'NamedNode' + && assignee.length === 1 && assignee[0].object.termType === 'NamedNode' + && every(vc.match(credentialSubject, PERMISSION, null, defaultGraph()), (permission) => permission.object.termType === "BlankNode" && _isOdrlPermission(vc, permission.object)) + && every(vc.match(credentialSubject, PROHIBITION, null, defaultGraph()), (prohibition) => prohibition.object.termType === "BlankNode" && _isOdrlPermission(vc, prohibition.object)); }