Skip to content

Commit

Permalink
Throw custom error (#1190)
Browse files Browse the repository at this point in the history
* Add dependency

* Throw dedicated error from library

* Move AccessGrantError in its own module

* Use AccessGrantError and UmaError

* Lint

---------

Co-authored-by: Pete Edwards <[email protected]>
  • Loading branch information
NSeydoux and edwardsph authored Dec 13, 2024
1 parent 5cbc45a commit c9536f2
Show file tree
Hide file tree
Showing 24 changed files with 134 additions and 59 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
},
"dependencies": {
"@inrupt/solid-client": "^2.0.0",
"@inrupt/solid-client-errors": "^0.0.2",
"@inrupt/solid-client-vc": "^1.1.2",
"@types/rdfjs__dataset": "^2.0.7",
"auth-header": "^1.0.0",
Expand Down
27 changes: 27 additions & 0 deletions src/common/errors/AccessGrantError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// Copyright Inrupt Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
// Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// 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 { InruptClientError } from "@inrupt/solid-client-errors";

/**
* Superclass of errors thrown by the @inrupt/solid-client-access-grants library.
*/
export class AccessGrantError extends InruptClientError {}
27 changes: 27 additions & 0 deletions src/common/errors/UmaError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// Copyright Inrupt Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
// Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// 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 { InruptClientError } from "@inrupt/solid-client-errors";

/**
* Superclass of errors thrown in the context of using an Access Grant to get access to a resource.
*/
export class UmaError extends InruptClientError {}
34 changes: 17 additions & 17 deletions src/common/getters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { DataFactory } from "n3";
import type { AccessGrantGConsent } from "../gConsent/type/AccessGrant";
import type { AccessModes } from "../type/AccessModes";
import { INHERIT, TYPE, XSD_BOOLEAN, acl, gc, ldp } from "./constants";
import { AccessGrantError } from "./errors/AccessGrantError";

const { namedNode, defaultGraph, quad, literal } = DataFactory;

Expand Down Expand Up @@ -101,13 +102,15 @@ export function getSingleObject(
}

if (results.length !== 1) {
throw new Error(`Expected exactly one result. Found ${results.length}.`);
throw new AccessGrantError(
`Expected exactly one result. Found ${results.length}.`,
);
}

const [{ object }] = results;
const expectedTypes = type ? [type] : ["NamedNode", "BlankNode"];
if (!expectedTypes.includes(object.termType)) {
throw new Error(
throw new AccessGrantError(
`Expected [${object.value}] to be a ${expectedTypes.join(
" or ",
)}. Found [${object.termType}]`,
Expand All @@ -134,13 +137,13 @@ export function getConsent(vc: DatasetWithId) {
...vc.match(credentialSubject, gc.hasConsent, null, defaultGraph()),
];
if (consents.length !== 1) {
throw new Error(
throw new AccessGrantError(
`Expected exactly 1 consent value. Found ${consents.length}.`,
);
}
const [{ object }] = consents;
if (object.termType !== "BlankNode" && object.termType !== "NamedNode") {
throw new Error(
throw new AccessGrantError(
`Expected consent to be a Named Node or Blank Node, instead got [${object.termType}].`,
);
}
Expand Down Expand Up @@ -169,7 +172,7 @@ export function getResources(vc: DatasetWithId): string[] {
defaultGraph(),
)) {
if (object.termType !== "NamedNode") {
throw new Error(
throw new AccessGrantError(
`Expected resource to be a Named Node. Instead got [${object.value}] with term type [${object.termType}]`,
);
}
Expand Down Expand Up @@ -202,7 +205,7 @@ export function getPurposes(vc: DatasetWithId): string[] {
defaultGraph(),
)) {
if (object.termType !== "NamedNode") {
throw new Error(
throw new AccessGrantError(
`Expected purpose to be Named Node. Instead got [${object.value}] with term type [${object.termType}]`,
);
}
Expand Down Expand Up @@ -319,12 +322,12 @@ export function getRequestor(vc: DatasetWithId): string {
}

if (candidateResults.length > 1) {
throw new Error(
throw new AccessGrantError(
`Too many requestors found. Expected one, found ${candidateResults}`,
);
}

throw new Error(`No requestor found.`);
throw new AccessGrantError(`No requestor found.`);
}

/**
Expand Down Expand Up @@ -400,7 +403,7 @@ export function getTypes(vc: DatasetWithId): string[] {

for (const result of results) {
if (result.termType !== "NamedNode") {
throw new Error(
throw new AccessGrantError(
`Expected every type to be a Named Node, but found [${result.value}] with term type [${result.termType}]`,
);
}
Expand Down Expand Up @@ -454,7 +457,7 @@ function deserializeFields<T>(
)
.map((q) => {
if (q.object.termType !== "Literal") {
throw new Error(
throw new AccessGrantError(
`Expected value object for predicate ${field.href} to be a literal, found ${q.object.termType}.`,
);
}
Expand All @@ -463,8 +466,7 @@ function deserializeFields<T>(
.map((object) => {
const result = deserializer(object);
if (result === undefined) {
// FIXME use inrupt error library
throw new Error(
throw new AccessGrantError(
`Failed to deserialize value ${object.value} for predicate ${field.href} as type ${type}.`,
);
}
Expand All @@ -480,8 +482,7 @@ function deserializeField<T>(
): T | undefined {
const result = deserializeFields(vc, field, deserializer, type);
if (result.length > 1) {
// FIXME use inrupt error library
throw new Error(
throw new AccessGrantError(
`Expected one value for predicate ${field.href}, found many: ${result}`,
);
}
Expand Down Expand Up @@ -673,7 +674,7 @@ function castLiteral(lit: Literal): unknown {
if (lit.datatype.equals(xmlSchemaTypes.string)) {
return lit.value;
}
throw new Error(`Unsupported literal type ${lit.datatype.value}`);
throw new AccessGrantError(`Unsupported literal type ${lit.datatype.value}`);
}

const WELL_KNOWN_FIELDS = [
Expand Down Expand Up @@ -740,8 +741,7 @@ export function getCustomFields(
// There is a single key in the current object.
const curKey = Object.keys(cur)[0];
if (acc[curKey] !== undefined) {
// FIXME use inrupt error
throw new Error(
throw new AccessGrantError(
`Expected single values for custom fields, found multiple for ${curKey}`,
);
}
Expand Down
5 changes: 3 additions & 2 deletions src/common/verify/isValidAccessGrant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
import { getBaseAccess } from "../../gConsent/util/getBaseAccessVerifiableCredential";
import { getSessionFetch } from "../util/getSessionFetch";
import { toVcDataset } from "../util/toVcDataset";
import { AccessGrantError } from "../errors/AccessGrantError";

/**
* Makes a request to the access server to verify the validity of a given Verifiable Credential.
Expand All @@ -52,7 +53,7 @@ async function isValidAccessGrant(
const validVc = await toVcDataset(vc, options);

if (validVc === undefined) {
throw new Error(
throw new AccessGrantError(
`Invalid argument: expected either a VC URL or a RDFJS DatasetCore, received ${vc}`,
);
}
Expand All @@ -66,7 +67,7 @@ async function isValidAccessGrant(
.verifierService;

if (verifierEndpoint === undefined) {
throw new Error(
throw new AccessGrantError(
`The VC service provider ${getIssuer(
vcObject,
)} does not advertize for a verifier service in its .well-known/vc-configuration document`,
Expand Down
11 changes: 6 additions & 5 deletions src/fetch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
CONTEXT_ESS_DEFAULT,
PRESENTATION_TYPE_BASE,
} from "../gConsent/constants";
import { UmaError } from "../common/errors/UmaError";

const WWW_AUTH_HEADER = "www-authenticate";
const VC_CLAIM_TOKEN_TYPE = "https://www.w3.org/TR/vc-data-model/#json-ld";
Expand Down Expand Up @@ -75,7 +76,7 @@ export async function getUmaConfiguration(
const configurationUrl = new URL(UMA_CONFIG_PATH, authIri).href;
const response = await fetch(configurationUrl);
return response.json().catch((e) => {
throw new Error(
throw new UmaError(
`Parsing the UMA configuration found at ${configurationUrl} failed with the following error: ${e.toString()}`,
);
});
Expand Down Expand Up @@ -178,18 +179,18 @@ export async function fetchWithVc(
const wwwAuthentication = headers.get(WWW_AUTH_HEADER);

if (!wwwAuthentication) {
throw new Error(NO_WWW_AUTH_HEADER_ERROR);
throw new UmaError(NO_WWW_AUTH_HEADER_ERROR);
}

const authTicket = parseUMAAuthTicket(wwwAuthentication);
const authIri = parseUMAAuthIri(wwwAuthentication);

if (!authTicket) {
throw new Error(NO_WWW_AUTH_HEADER_UMA_TICKET_ERROR);
throw new UmaError(NO_WWW_AUTH_HEADER_UMA_TICKET_ERROR);
}

if (!authIri) {
throw new Error(NO_WWW_AUTH_HEADER_UMA_IRI_ERROR);
throw new UmaError(NO_WWW_AUTH_HEADER_UMA_IRI_ERROR);
}

const umaConfiguration = await getUmaConfiguration(authIri);
Expand All @@ -203,7 +204,7 @@ export async function fetchWithVc(
);

if (!accessToken) {
throw new Error(NO_ACCESS_TOKEN_RETURNED);
throw new UmaError(NO_ACCESS_TOKEN_RETURNED);
}

return boundFetch(accessToken);
Expand Down
9 changes: 5 additions & 4 deletions src/gConsent/discover/getAccessApiEndpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import type { UrlString } from "@inrupt/solid-client";
import { parse } from "auth-header";
import type { AccessBaseOptions } from "../type/AccessBaseOptions";
import { AccessGrantError } from "../../common/errors/AccessGrantError";

async function getAccessEndpointForResource(
resource: UrlString,
Expand All @@ -30,14 +31,14 @@ async function getAccessEndpointForResource(
// authorization server.
const response = await fetch(resource);
if (!response.headers.has("WWW-Authenticate")) {
throw new Error(
throw new AccessGrantError(
`Expected a 401 error with a WWW-Authenticate header, got a [${response.status}: ${response.statusText}] response lacking the WWW-Authenticate header.`,
);
}
const authHeader = response.headers.get("WWW-Authenticate") as string;
const authHeaderToken = parse(authHeader);
if (authHeaderToken.scheme !== "UMA") {
throw new Error(
throw new AccessGrantError(
`Unsupported authorization scheme: [${authHeaderToken.scheme}]`,
);
}
Expand All @@ -49,7 +50,7 @@ async function getAccessEndpointForResource(
const rawDiscoveryDocument = await fetch(wellKnownIri.href);
const discoveryDocument = await rawDiscoveryDocument.json();
if (typeof discoveryDocument.verifiable_credential_issuer !== "string") {
throw new Error(
throw new AccessGrantError(
`No access issuer listed for property [verifiable_credential_issuer] in [${JSON.stringify(
discoveryDocument,
)}]`,
Expand All @@ -76,7 +77,7 @@ async function getAccessApiEndpoint(
try {
return await getAccessEndpointForResource(resource.toString());
} catch (e: unknown) {
throw new Error(
throw new AccessGrantError(
`Couldn't figure out the Access Grant issuer from the resources: ${e}`,
);
}
Expand Down
7 changes: 5 additions & 2 deletions src/gConsent/discover/getAccessManagementUi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
import { getSessionFetch } from "../../common/util/getSessionFetch";
import type { AccessBaseOptions } from "../type/AccessBaseOptions";
import { PIM_STORAGE, PREFERRED_CONSENT_MANAGEMENT_UI } from "../constants";
import { AccessGrantError } from "../../common/errors/AccessGrantError";

interface AccessManagementUiFromProfile {
accessEndpoint?: UrlString;
Expand All @@ -48,12 +49,14 @@ async function getAccessManagementUiFromProfile(
fetch: options.fetch,
});
} catch (e) {
throw new Error(`Cannot get the Access Management UI for ${webId}: ${e}.`);
throw new AccessGrantError(
`Cannot get the Access Management UI for ${webId}: ${e}.`,
);
}

const profile = getThing(webIdDocument, webId);
if (profile === null) {
throw new Error(
throw new AccessGrantError(
`Cannot get the Access Management UI for ${webId}: the WebID cannot be dereferenced.`,
);
}
Expand Down
5 changes: 3 additions & 2 deletions src/gConsent/discover/redirectToAccessManagementUi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import type { FetchOptions } from "../../type/FetchOptions";
import type { RedirectOptions } from "../../type/RedirectOptions";
import { getResources } from "../../common";
import { toVcDataset } from "../../common/util/toVcDataset";
import { AccessGrantError } from "../../common/errors/AccessGrantError";

export const REQUEST_VC_URL_PARAM_NAME = "requestVcUrl";
export const REDIRECT_URL_PARAM_NAME = "redirectUrl";
Expand Down Expand Up @@ -104,7 +105,7 @@ export async function redirectToAccessManagementUi(
const validVc = await toVcDataset(accessRequestVc, options);

if (validVc === undefined) {
throw new Error(
throw new AccessGrantError(
`Invalid argument: expected either a VC URL or a RDFJS DatasetCore, received ${accessRequestVc}`,
);
}
Expand All @@ -123,7 +124,7 @@ export async function redirectToAccessManagementUi(
});

if (accessManagementUi === undefined) {
throw new Error(
throw new AccessGrantError(
`Cannot discover access management UI URL for [${resourceUrl}]${
options.resourceOwner ? `, neither from [${options.resourceOwner}]` : ""
}`,
Expand Down
3 changes: 2 additions & 1 deletion src/gConsent/guard/isGConsentAttributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { RESOURCE_ACCESS_MODE } from "../../type/ResourceAccessMode";
import { isUnknownObject } from "./isUnknownObject";
import type { GConsentStatus } from "../type/GConsentStatus";
import { acl, gc } from "../../common/constants";
import { AccessGrantError } from "../../common/errors/AccessGrantError";

const { defaultGraph } = DataFactory;

Expand Down Expand Up @@ -104,7 +105,7 @@ export function isRdfjsGConsentAttributes(
);

if (forPersonalData.size === 0) {
throw new Error("No Personal Data specified for Access Grant");
throw new AccessGrantError("No Personal Data specified for Access Grant");
}

for (const { object } of forPersonalData) {
Expand Down
Loading

0 comments on commit c9536f2

Please sign in to comment.