From 0ed69e191b4bdf68bd83920f4e35b68ef203030f Mon Sep 17 00:00:00 2001 From: Kayode Ezike Date: Fri, 15 Mar 2024 20:29:32 -0400 Subject: [PATCH] implements isValidCredentialId; throws error from getUserCredentialRecordById for untracked ids; implements getCredentialInfo as alias for getUserCredentialRecordById; renames allocateAllStatuses to allocateSupportedStatuses --- src/credential-status-manager-base.ts | 47 +++++++++++-------- src/helpers.ts | 11 +++++ .../credential-status-manager-mongodb.spec.ts | 4 +- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/src/credential-status-manager-base.ts b/src/credential-status-manager-base.ts index 4ae1f2b..6818606 100644 --- a/src/credential-status-manager-base.ts +++ b/src/credential-status-manager-base.ts @@ -14,9 +14,11 @@ import { } from './errors.js'; import { DidMethod, + MAX_ID_LENGTH, getCredentialSubjectObject, getDateString, getSigningMaterial, + isValidCredentialId, signCredential, validateCredential } from './helpers.js'; @@ -350,6 +352,13 @@ export abstract class BaseCredentialStatusManager { // Note: This assumes that uuid will never generate an ID that // conflicts with an ID that has already been tracked in the event log credentialCopy.id = this.generateUserCredentialId(); + } else { + if(!isValidCredentialId(credentialCopy.id)) { + throw new BadRequestError({ + message: 'The ID must be a URL, UUID, or DID ' + + `that is no more than ${MAX_ID_LENGTH} characters in length.` + }); + } } // validate credential before attaching status @@ -365,7 +374,10 @@ export abstract class BaseCredentialStatusManager { // only search for credential if it was passed with an ID if (credentialContainsId) { // retrieve record for credential with given ID - const credentialRecord = await this.getUserCredentialRecordById(credentialCopy.id, options); + let credentialRecord; + try { + credentialRecord = await this.getUserCredentialRecordById(credentialCopy.id, options); + } catch (error) {} // do not allocate new entry if ID is already being tracked if (credentialRecord) { @@ -583,7 +595,7 @@ export abstract class BaseCredentialStatusManager { } // allocates all supported statuses - async allocateAllStatuses(credential: VerifiableCredential): Promise { + async allocateSupportedStatuses(credential: VerifiableCredential): Promise { return this.allocateStatus({ credential, statusPurposes: SUPPORTED_STATUS_PURPOSES }); } @@ -595,13 +607,6 @@ export abstract class BaseCredentialStatusManager { // retrieve record for credential with given ID const oldCredentialRecord = await this.getUserCredentialRecordById(credentialId, options); - // unable to find credential with given ID - if (!oldCredentialRecord) { - throw new NotFoundError({ - message: `Unable to find credential with ID "${credentialId}".` - }); - } - // retrieve relevant credential info const { statusInfo, ...oldCredentialRecordRest } = oldCredentialRecord; @@ -766,14 +771,6 @@ export abstract class BaseCredentialStatusManager { async getStatus(credentialId: string, options?: DatabaseConnectionOptions): Promise { // retrieve user credential record const record = await this.getUserCredentialRecordById(credentialId, options); - - // unable to find credential with given ID - if (!record) { - throw new NotFoundError({ - message: `Unable to find credential with ID "${credentialId}".` - }); - } - return record.statusInfo; } @@ -1269,19 +1266,29 @@ export abstract class BaseCredentialStatusManager { } // retrieves user credential record by ID - async getUserCredentialRecordById(userCredentialId: string, options?: DatabaseConnectionOptions): Promise { + async getUserCredentialRecordById(userCredentialId: string, options?: DatabaseConnectionOptions): Promise { let record; try { record = await this.getRecordById(this.userCredentialTableName, userCredentialId, options); + if (!record) { + throw new NotFoundError({ + message: `Unable to find credential with ID "${userCredentialId}".` + }); + } } catch (error: any) { if (error instanceof CustomError) { throw error; } throw new InternalServerError({ - message: `Unable to get user credential with ID "${userCredentialId}": ${error.message}` + message: `Unable to get info for credential with ID "${userCredentialId}": ${error.message}` }); } - return record as UserCredentialRecord | null; + return record as UserCredentialRecord; + } + + // alias for getUserCredentialRecordById + async getCredentialInfo(userCredentialId: string, options?: DatabaseConnectionOptions): Promise { + return this.getUserCredentialRecordById(userCredentialId, options); } // retrieves config record by ID diff --git a/src/helpers.ts b/src/helpers.ts index fd22198..69926b7 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,6 +1,7 @@ /*! * Copyright (c) 2024 Digital Credentials Consortium. All rights reserved. */ +import * as uuid from 'uuid'; import * as vc1Context from 'credentials-context'; import * as vc2Context from '@digitalbazaar/credentials-v2-context'; import * as vcBitstringStatusListContext from '@digitalbazaar/vc-bitstring-status-list-context'; @@ -28,6 +29,9 @@ const didKeyDriver = DidKey.driver(); // Document loader const documentLoader = securityLoader().build(); +// Max length for IDs +export const MAX_ID_LENGTH = 64; + // DID method used to sign credentials export enum DidMethod { Key = 'key', @@ -192,6 +196,13 @@ function extractId(objectOrString: any): string { return objectOrString.id; } +// determines if credential ID is valid +export function isValidCredentialId(credentialId: string): boolean { + const isValidFormat = URL.canParse(credentialId) || uuid.validate(credentialId); + const isValidLength = credentialId.length <= MAX_ID_LENGTH; + return isValidFormat && isValidLength; +} + // retrieves current timestamp export function getDateString(): string { return (new Date()).toISOString(); diff --git a/test/credential-status-manager-mongodb.spec.ts b/test/credential-status-manager-mongodb.spec.ts index 4884ca3..05ed035 100644 --- a/test/credential-status-manager-mongodb.spec.ts +++ b/test/credential-status-manager-mongodb.spec.ts @@ -121,12 +121,12 @@ describe('MongoDB Credential Status Manager', () => { expect(databaseState.valid).to.be.true; }); - it('tests allocateRevocationStatus and getUserCredentialRecordById', async () => { + it('tests allocateRevocationStatus and getCredentialInfo', async () => { // allocate status for credential const credentialWithStatus = await statusManager.allocateRevocationStatus(unsignedCredential1) as any; // check user credential info - const credentialRecord = await statusManager.getUserCredentialRecordById(credentialWithStatus.id); + const credentialRecord = await statusManager.getCredentialInfo(credentialWithStatus.id); checkUserCredentialInfo(credentialWithStatus.id, credentialRecord, 1, true); // check if database has valid configuration