diff --git a/src/common/constants.ts b/src/common/constants.ts index 1de70d09c..0fb1ae50b 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -18,7 +18,6 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -import type { Term, Quad, NamedNode, BlankNode, Literal } from "@rdfjs/types"; import { DataFactory } from "n3"; import { rdf, acl as _acl } from "rdf-namespaces"; @@ -49,34 +48,13 @@ export const acl = { mode: namedNode(_acl.mode), }; -export const ISSUANCE_DATE = namedNode(`${CRED}issuanceDate`); -export const EXPIRATION_DATE = namedNode(`${CRED}expirationDate`); -export const ISSUER = namedNode(`${CRED}issuer`); +export const cred = { + issuanceDate: namedNode(`${CRED}issuanceDate`), + expirationDate: namedNode(`${CRED}expirationDate`), + issuer: namedNode(`${CRED}issuer`), + credentialSubject: namedNode(`${CRED}credentialSubject`), +}; -export const CREDENTIAL_SUBJECT = namedNode(`${CRED}credentialSubject`); 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.test.ts b/src/common/getters.test.ts index cb242b373..20ae27ec4 100644 --- a/src/common/getters.test.ts +++ b/src/common/getters.test.ts @@ -158,12 +158,16 @@ describe("getTypes", () => { describe("gConsent data model", () => { it("gets the gConsent access grant types", async () => { const gConsentGrant = await mockGConsentGrant(); - expect(getTypes(gConsentGrant)).toBe(gConsentGrant.type); + for (const type of gConsentGrant.type) { + expect(getTypes(gConsentGrant)).toContainEqual(type); + } }); it("gets the gConsent access request id", async () => { const gConsentRequest = await mockGConsentRequest(); - expect(getTypes(gConsentRequest)).toBe(gConsentRequest.type); + for (const type of gConsentRequest.type) { + expect(getTypes(gConsentRequest)).toContainEqual(type); + } }); }); }); diff --git a/src/common/getters.ts b/src/common/getters.ts index 2c72d1876..ac482ef63 100644 --- a/src/common/getters.ts +++ b/src/common/getters.ts @@ -29,18 +29,7 @@ import { DataFactory } from "n3"; import type { AccessGrantGConsent } from "../gConsent/type/AccessGrant"; import type { AccessRequestGConsent } from "../gConsent/type/AccessRequest"; import type { AccessModes } from "../type/AccessModes"; -import { - CREDENTIAL_SUBJECT, - EXPIRATION_DATE, - INHERIT, - ISSUANCE_DATE, - ISSUER, - XSD_BOOLEAN, - assertTermType, - getSingleQuad, - gc, - acl, -} from "./constants"; +import { INHERIT, XSD_BOOLEAN, gc, acl, cred, TYPE } from "./constants"; import { GC_CONSENT_STATUS_DENIED, GC_CONSENT_STATUS_EXPLICITLY_GIVEN, @@ -88,34 +77,32 @@ export function isGConsentAccessGrant( vc: AccessGrantGConsent | AccessRequestGConsent, ): boolean { const credentialSubject = getCredentialSubject(vc); - try { - const providedConsent = getSingleObject( - vc, - credentialSubject, - gc.providedConsent, - "BlankNode", - ); - return ( - (vc.has( - quad( - providedConsent, - namedNode(GC_HAS_STATUS), - namedNode(GC_CONSENT_STATUS_DENIED), - ), - ) || - vc.has( - quad( - providedConsent, - namedNode(GC_HAS_STATUS), - namedNode(GC_CONSENT_STATUS_EXPLICITLY_GIVEN), - ), - )) && - // Because of the getSingleObject the try / catch is also needed to wrap this as well as getting the provided consent - !!getSingleObject(vc, providedConsent, gc.isProvidedTo) - ); - } catch (e) { - return false; - } + const providedConsent = getSingleObject( + vc, + credentialSubject, + gc.providedConsent, + undefined, + false, + ); + + if (!providedConsent) return false; + + const gcStatus = getSingleObject( + vc, + credentialSubject, + namedNode(GC_HAS_STATUS), + undefined, + false, + ); + + return ( + gcStatus !== undefined && + [GC_CONSENT_STATUS_DENIED, GC_CONSENT_STATUS_EXPLICITLY_GIVEN].includes( + gcStatus?.value, + ) && + getSingleObject(vc, providedConsent, gc.isProvidedTo, undefined, false) + ?.termType === "NamedNode" + ); } /** @@ -137,24 +124,26 @@ export function getResourceOwner( export function getResourceOwner( vc: AccessGrantGConsent | AccessRequestGConsent, ): string | undefined { - try { - const credentialSubject = getCredentialSubject(vc); - if (isGConsentAccessGrant(vc)) { - return credentialSubject.value; - } - return getSingleObject( - vc, - // We should probably allow this to be Blank or Named Node - getSingleObject(vc, credentialSubject, gc.hasConsent, "BlankNode"), - gc.isConsentForDataSubject, - ).value; - } catch (e) { - return undefined; + const credentialSubject = getCredentialSubject(vc); + if (isGConsentAccessGrant(vc)) { + return credentialSubject.value; } + return getSingleObject( + vc, + getSingleObject(vc, credentialSubject, gc.hasConsent), + gc.isConsentForDataSubject, + "NamedNode", + false, + )?.value; } function getCredentialSubject(vc: AccessGrantGConsent | AccessRequestGConsent) { - return getSingleObject(vc, namedNode(getId(vc)), CREDENTIAL_SUBJECT); + return getSingleObject( + vc, + namedNode(getId(vc)), + cred.credentialSubject, + "NamedNode", + ); } function getConsent(vc: AccessGrantGConsent | AccessRequestGConsent) { @@ -193,26 +182,40 @@ export function getRequestor( vc: AccessGrantGConsent | AccessRequestGConsent, ): string { const credentialSubject = getCredentialSubject(vc); - try { - const providedConsent = getSingleObject( - vc, - credentialSubject, - gc.providedConsent, - "BlankNode", - ); - return getSingleObject(vc, providedConsent, gc.isProvidedTo).value; - } catch (e) { - // noop - } + const providedConsent = getSingleObject( + vc, + credentialSubject, + gc.providedConsent, + undefined, + false, + ); - return credentialSubject.value; + if (!providedConsent) return credentialSubject.value; + + return getSingleObject(vc, providedConsent, gc.isProvidedTo, "NamedNode") + .value; } function getSingleObject( vc: DatasetCore, subject: Term, predicate: Term, + type: "NamedNode", ): NamedNode; +function getSingleObject( + vc: DatasetCore, + subject: Term, + predicate: Term, + type: "NamedNode", + required: false, +): NamedNode | undefined; +function getSingleObject( + vc: DatasetCore, + subject: Term, + predicate: Term, + type: "Literal", + required: false, +): Literal | undefined; function getSingleObject( vc: DatasetCore, subject: Term, @@ -225,17 +228,46 @@ function getSingleObject( predicate: Term, type: "Literal", ): Literal; +function getSingleObject( + vc: DatasetCore, + subject: Term, + predicate: Term, +): NamedNode | BlankNode; +function getSingleObject( + vc: DatasetCore, + subject: Term, + predicate: Term, + type: undefined, + required: false, +): NamedNode | BlankNode | undefined; function getSingleObject( vc: DatasetCore, subject: Term, predicate: Term, type?: Term["termType"], -) { - return assertTermType( - getSingleQuad([...vc.match(subject, predicate, null, defaultGraph())]) - .object, - type, - ); + required = true, +): Term | undefined { + const results = [...vc.match(subject, predicate, null, defaultGraph())]; + + if (results.length === 0 && !required) { + return undefined; + } + + if (results.length !== 1) { + throw new Error(`Expected exactly one result. Found ${results.length}.`); + } + + const [{ object }] = results; + const expectedTypes = type ? [type] : ["NamedNode", "BlankNode"]; + if (!expectedTypes.includes(object.termType)) { + throw new Error( + `Expected [${object.value}] to be a ${expectedTypes.join( + " or ", + )}. Found [${object.termType}]`, + ); + } + + return object; } /** @@ -292,9 +324,34 @@ export function getId(vc: AccessGrantGConsent | AccessRequestGConsent): string { export function getTypes( vc: AccessGrantGConsent | AccessRequestGConsent, ): string[] { - return vc.type; + const results = [ + ...vc.match(namedNode(getId(vc)), TYPE, undefined, defaultGraph()), + ].map((res) => res.object); + + const types: string[] = []; + + for (const result of results) { + if (result.termType !== "NamedNode") { + throw new Error( + `Expected every type to be a Named Node, but found [${result.value}] with term type [${result.termType}]`, + ); + } + types.push(result.value); + if (result.value in shorthand) { + types.push(shorthand[result.value as keyof typeof shorthand]); + } + } + + return types; } +const shorthand = { + "http://www.w3.org/ns/solid/vc#SolidAccessRequest": "SolidAccessRequest", + "http://www.w3.org/ns/solid/vc#SolidAccessGrant": "SolidAccessGrant", + "https://www.w3.org/2018/credentials#VerifiableCredential": + "VerifiableCredential", +}; + function wrapDate(date: Literal) { if ( !date.datatype.equals( @@ -324,7 +381,7 @@ export function getIssuanceDate( vc: AccessGrantGConsent | AccessRequestGConsent, ): Date { return wrapDate( - getSingleObject(vc, namedNode(getId(vc)), ISSUANCE_DATE, "Literal"), + getSingleObject(vc, namedNode(getId(vc)), cred.issuanceDate, "Literal"), ); } @@ -343,21 +400,14 @@ export function getIssuanceDate( export function getExpirationDate( vc: AccessGrantGConsent | AccessRequestGConsent, ): Date | undefined { - const expirationDate = [ - ...vc.match(namedNode(getId(vc)), 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; + const expirationDate = getSingleObject( + vc, + namedNode(getId(vc)), + cred.expirationDate, + "Literal", + false, + ); + return expirationDate && wrapDate(expirationDate); } /** @@ -375,7 +425,8 @@ export function getExpirationDate( export function getIssuer( vc: AccessGrantGConsent | AccessRequestGConsent, ): string { - return getSingleObject(vc, namedNode(getId(vc)), ISSUER).value; + return getSingleObject(vc, namedNode(getId(vc)), cred.issuer, "NamedNode") + .value; } /** diff --git a/src/gConsent/constants.ts b/src/gConsent/constants.ts index 465d98b56..a42748c08 100644 --- a/src/gConsent/constants.ts +++ b/src/gConsent/constants.ts @@ -19,11 +19,6 @@ // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -export const AS_ACTOR = "https://www.w3.org/ns/activitystreams#actor"; -export const AS_ANNOUNCE = "https://www.w3.org/ns/activitystreams#Announce"; -export const AS_OBJECT = "https://www.w3.org/ns/activitystreams#object"; -export const AS_SUMMARY = "https://www.w3.org/ns/activitystreams#summary"; - export const GC_CONSENT = "https://w3id.org/GConsent#Consent"; export const GC_FOR_PERSONAL_DATA = "https://w3id.org/GConsent#forPersonalData"; export const GC_FOR_PURPOSE = "https://w3id.org/GConsent#forPurpose"; @@ -91,18 +86,9 @@ export const instanciateEssAccessGrantContext = ( instanciateContextVcEssTemplate(essVcDomain), ] as const; -export const WELL_KNOWN_SOLID = ".well-known/solid"; - -export const INRUPT_CONSENT_SERVICE_LEGACY = - "http://inrupt.com/ns/ess#consentIssuer"; - -export const SOLID_CONSENT_SERVICE = - "http://www.w3.org/ns/solid/terms#accessIssuer"; - export const CREDENTIAL_TYPE_ACCESS_REQUEST = "SolidAccessRequest"; export const CREDENTIAL_TYPE_ACCESS_GRANT = "SolidAccessGrant"; export const CREDENTIAL_TYPE_ACCESS_DENIAL = "SolidAccessDenial"; -export const CREDENTIAL_TYPE_LEGACY_CONSENT_REQUEST = "SolidConsentRequest"; export const CREDENTIAL_TYPE_BASE = "VerifiableCredential"; export const PRESENTATION_TYPE_BASE = "VerifiablePresentation"; @@ -111,7 +97,6 @@ export const ACCESS_CREDENTIAL_TYPE = new Set([ CREDENTIAL_TYPE_ACCESS_GRANT, CREDENTIAL_TYPE_ACCESS_DENIAL, "vc:SolidAccessDenial", - CREDENTIAL_TYPE_LEGACY_CONSENT_REQUEST, ]); export const ACCESS_GRANT_STATUS = Object.freeze(