Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Throw custom error #1190

Merged
merged 7 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading