diff --git a/.env.example b/.env.example index daa1be1..f4050f4 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,7 @@ CRED_STATUS_SERVICE=github CRED_STATUS_DID_SEED=z1AackbUm8U69ohKnihoRRFkXcXJd4Ra1PkAboQ2ZRy1ngB PORT=4008 # default port is 4008 ENABLE_ACCESS_LOGGING=true +ENABLE_HTTPS_FOR_DEV=false ERROR_LOG_FILE=logs/error.log ALL_LOG_FILE=logs/all.log CONSOLE_LOG_LEVEL=silly # default is silly, i.e. log everything - see the README for allowed levels diff --git a/README.md b/README.md index 35a1dd2..6bbaf0f 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ IMPORTANT NOTE ABOUT VERSIONING: If you are using a Docker Hub image of this rep - [DID Registries](#did-registries) - [Usage](#usage) - [Allocate Status Position](#allocate-status-position) - - [Revoke](#revoke) + - [Revocation and suspension](#revocation-and-suspension) - [Versioning](#versioning) - [Logging](#logging) - [Log Levels](#log-levels) @@ -49,6 +49,7 @@ This service provides support for managing credential status in a variety of Git | `CRED_STATUS_DID_SEED` | seed used to deterministically generate DID | string | yes | | `PORT` | HTTP port on which to run the express app | number | no (default: `4008`) | | `ENABLE_ACCESS_LOGGING` | whether to enable access logging (see [Logging](#logging)) | boolean | no (default: `true`) | +| `ENABLE_HTTPS_FOR_DEV` | whether to enable HTTPS in a development instance of the app | boolean | no (default: `true`) | | `ERROR_LOG_FILE` | log file for all errors (see [Logging](#logging)) | string | no | | `ALL_LOG_FILE` | log file for everything (see [Logging](#logging)) | string | no | | `CONSOLE_LOG_LEVEL` | console log level (see [Logging](#logging)) | `error` \| `warn`\| `info` \| `http` \| `verbose` \| `debug` \| `silly` | no (default: `silly`) | @@ -89,7 +90,7 @@ curl --location 'http://localhost:4008/credentials/status/allocate' \ --header 'Content-Type: application/json' \ --data-raw '{ "@context": [ - "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/ns/credentials/v2", "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json" ], "id": "urn:uuid:2fe53dc9-b2ec-4939-9b2c-0d00f6663b6c", @@ -107,7 +108,7 @@ curl --location 'http://localhost:4008/credentials/status/allocate' \ "url": "https://dcconsortium.org", "image": "https://user-images.githubusercontent.com/752326/230469660-8f80d264-eccf-4edd-8e50-ea634d407778.png" }, - "issuanceDate": "2023-08-02T17:43:32.903Z", + "validFrom": "2023-08-02T17:43:32.903Z", "credentialSubject": { "type": [ "AchievementSubject" @@ -139,10 +140,8 @@ This should return the same credential but with an allocated status. It should l ``` { "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json", - "https://w3id.org/vc/status-list/2021/v1", - "https://w3id.org/security/suites/ed25519-2020/v1" + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json" ], "id": "urn:uuid:2fe53dc9-b2ec-4939-9b2c-0d00f6663b6c", "type": [ @@ -159,7 +158,7 @@ This should return the same credential but with an allocated status. It should l "url": "https://dcconsortium.org", "image": "https://user-images.githubusercontent.com/752326/230469660-8f80d264-eccf-4edd-8e50-ea634d407778.png" }, - "issuanceDate": "2023-08-02T17:43:32.903Z", + "validFrom": "2023-08-02T17:43:32.903Z", "credentialSubject": { "type": [ "AchievementSubject" @@ -185,7 +184,7 @@ This should return the same credential but with an allocated status. It should l }, "credentialStatus": { "id": "https://jchartrand.github.io/status-test-three/DKSPRCX9WB#5", - "type": "StatusList2021Entry", + "type": "BitstringStatusListEntry", "statusPurpose": "revocation", "statusListIndex": 5, "statusListCredential": "https://jchartrand.github.io/status-test-three/DKSPRCX9WB" @@ -197,12 +196,12 @@ Now, your next step would be to sign this Verifiable Credential. You could pass NOTE: CURL can get a bit clunky if you want to experiment more (e.g., by changing what goes into the VC before signing), so you might consider trying [Postman](https://www.postman.com/downloads) which makes it easier to construct and send HTTP calls. -### Revoke +### Revocation and suspension -Revocation is fully explained in the Bitstring Status List specification and our implemenations thereof, but effectively, it amounts to POSTing an object to the revocation endpoint, like so: +Revocation and suspension are fully explained in the [Bitstring Status List](https://www.w3.org/TR/vc-bitstring-status-list/) specification and our implemenations thereof, but effectively, it amounts to POSTing an object to the revocation endpoint, like so: ``` -{credentialId: '23kdr', credentialStatus: [{type: 'StatusList2021Credential', status: 'revoked'}]} +{credentialId: '23kdr', credentialStatus: [{type: 'BitstringStatusListCredential', status: 'revoked'}]} ``` Fundamentally, you are just posting up the ID of the credential. diff --git a/server-dev-only.cert b/server-dev-only.cert new file mode 100644 index 0000000..2aa4034 --- /dev/null +++ b/server-dev-only.cert @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDcjCCAloCCQC5dkGYdTI6MjANBgkqhkiG9w0BAQsFADB7MQswCQYDVQQGEwJD +QTEQMA4GA1UECAwHT05UQVJJTzERMA8GA1UEBwwIQU5DQVNURVIxDDAKBgNVBAoM +A0RDQzESMBAGA1UEAwwJbG9jYWxob3N0MSUwIwYJKoZIhvcNAQkBFhZqYy5jaGFy +dHJhbmRAZ21haWwuY29tMB4XDTIzMDEzMTE1NDcwMFoXDTIzMDMwMjE1NDcwMFow +ezELMAkGA1UEBhMCQ0ExEDAOBgNVBAgMB09OVEFSSU8xETAPBgNVBAcMCEFOQ0FT +VEVSMQwwCgYDVQQKDANEQ0MxEjAQBgNVBAMMCWxvY2FsaG9zdDElMCMGCSqGSIb3 +DQEJARYWamMuY2hhcnRyYW5kQGdtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBALswlM6FgU6gPGIFhO0ohyRSJePtDpSLR0wnjyZobVzZjtd8 +5HqbawRfE1zAPAyVmPZ1cUMPKc8owiF1PGdMP/q7Dsk9adUKkEipAHni84fOYgFk ++FxQwV58kjz5zqrCCnYQlLTTk2NtS/wTKl+5k3yXGrYlL41f7vMiWrEFTbvujWAz +Qa0GbcXdckRM3xt8HJb0iMAvOwAMi+gCDPXe3hqwu5AyzaN9tC3JwHLdVBwWtsGv +IlM49SYDnvOmEGxZDTLemhPCypkFWE/A+0VSDjMxs7BgtGfYxcV2grU7lE3bJOxT +fhzCtqtxaHkGSfr0MY+OplEED0foyRchzCyq+8sCAwEAATANBgkqhkiG9w0BAQsF +AAOCAQEAof2Yu6M74yyX8HIampxWE+NIHdRVoYksUYqfjIwEsdWkMzTvcLve5mQn +1PhlYk/5X8aFEMMwMrSZb5EFqV+fOktRymtrUDyYYdopAyct6OWutu4cih47wRdL +qd4dDale4OYJemD7m4VgXa7j0zcJfby8jqZPM+N3iNwLCDGWaGj6CsZDr0BR6xh0 +YyhyZBVCseTLfZGyxmCTHnYg4/cGBoKm/I4CIpXmuLbDMv1n/+xlK9oVn84UvqkY +oydsBrg4gsxHVNMBK8/ftZzDOF5ZVHHNHx0Z37oRE18Al4+1vFzlzPzpKCG9U3Ss +u2NnWz9AjicouE3ikIDINMFctaywEw== +-----END CERTIFICATE----- diff --git a/server-dev-only.key b/server-dev-only.key new file mode 100644 index 0000000..e2922d2 --- /dev/null +++ b/server-dev-only.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC7MJTOhYFOoDxi +BYTtKIckUiXj7Q6Ui0dMJ48maG1c2Y7XfOR6m2sEXxNcwDwMlZj2dXFDDynPKMIh +dTxnTD/6uw7JPWnVCpBIqQB54vOHzmIBZPhcUMFefJI8+c6qwgp2EJS005NjbUv8 +EypfuZN8lxq2JS+NX+7zIlqxBU277o1gM0GtBm3F3XJETN8bfByW9IjALzsADIvo +Agz13t4asLuQMs2jfbQtycBy3VQcFrbBryJTOPUmA57zphBsWQ0y3poTwsqZBVhP +wPtFUg4zMbOwYLRn2MXFdoK1O5RN2yTsU34cwrarcWh5Bkn69DGPjqZRBA9H6MkX +IcwsqvvLAgMBAAECggEBAKhF2d91gHJP9Tggwgf34NSzzEADAJJkSimZfkQGqBlJ +sfDg4vuc7y000tEUNmcRrDoSBUlFPk5t02YEX9J0ZydcNMSPIq5TGrVWx4jKjiXN +T6j1PZavOcVYspWB81jpqkHMUgHkGKDOxfnKuNLonj7oDykicIbkcIe8oE51+BUx +p4AX4yT4rZIzpQi6rRWAiaiWnKA5WZd75JqwvjTNXsrpsiHwHPLUtDJzzknHAK5q +7wPcSDQIgpqop3ScxI+T+x0jZbjGoy6q5R70u5IByYdm6eqYnqT37ly4qDiZwgAh +pVL/KaPLnhQAWYHFcBs9ZhrmuqYMBIJNRzJ9roWxoyECgYEA9J2bT+Z4y+1qQIf4 +hlvluaQ9VXguYysk3JWTx9MfNOQ/Xi/fJTZgY21stGY9uyT8xuhK7QIYalyHRvYH +Wf48Aa85Jfo/Uj59QjCnWdwVNhOxRiR4Gn2piZ0Jkbt5TZqBdRaddqUyJYcPOni9 +eaXaQOIaVgUEvBRIVX5X9+ZTwYkCgYEAw+bK7VBGRWQGZA++UjHjaBRSNbp3vY5m +b/b5LWClLHdQ+GAVFxayRLfzb22zbXQpRQgFDeLZO5NWbQ0hvHf3XjoRtYEuvJQA +niOlXWvJbQpj5A3beRVW9nk8Plw5TyX7ONcA+62qyYLf+o+LoL+OF+NtoeyzoI48 +egdmInDoIbMCgYBPecmVay9CKpAECWlw1fjMmRUoaNTBeaoPVTXfjbOs7p+8DVe1 +8nXcuBfCgRl7bWgHhD+bw7uFCy0UnCkFTznV3kV2FlluckkmMUKeSohFup41SPIQ +wVVNFc2fIMcntJRtI3zjqIajdL844zPEi2NfA1dFLXo9VWWvCU6xh48c6QKBgQCb +VqOswA2osmL67xzsUlDPU+XIYt7V+VezCrSVTeBLvSUAfjfbAg1DvlWTEvrHHOgo +q+5OD4ZP3koW2OXaa4pENmmaciAcOoOu4fcbd//Vrfp9eJuCjPBTKtkhXcG3yBdH +//zwlMorVdLC/RYr1hkXSijB0E6zTnYYEUvknYEETwKBgGQljAqFvIGpWTij8b6k +pwt0xymlOJjRvjTFyQMIQuJLhBG/ydQJhGQ5zvHQVY7S7CFM2QyheP25eqxETTXp +N5BQ854zBX4nqIe7NrY++ldwQg5FQgtR+8ZBiJQwHc5eo74D8acMu54oPbyig+nD +7L+JYn0WFx31GgoeLSmegA8x +-----END PRIVATE KEY----- diff --git a/server.js b/server.js index c88c242..dc2a5d7 100644 --- a/server.js +++ b/server.js @@ -1,12 +1,26 @@ -import { build } from './src/app.js' -import { getConfig, setConfig } from './src/config.js'; +import { build } from './src/app.js'; +import { getConfig } from './src/config.js'; import http from 'http'; +import https from 'https'; +import logger from './src/utils/logger.js'; const run = async () => { - await setConfig() - const { port } = getConfig(); + const { port, enableHttpsForDev } = getConfig(); const app = await build(); - http.createServer(app).listen(port, () => console.log(`Server running on port ${port}`)); + + if (enableHttpsForDev) { + https + .createServer( + { + key: fs.readFileSync('server-dev-only.key'), + cert: fs.readFileSync('server-dev-only.cert') + }, + app + ).listen(port, () => logger.info(`Server running on port ${port} with https`)); + } else { + http + .createServer(app).listen(port, () => logger.info(`Server running on port ${port} with http`)); + } }; run(); diff --git a/src/allocateStatus.js b/src/allocateStatus.js deleted file mode 100644 index 60609a9..0000000 --- a/src/allocateStatus.js +++ /dev/null @@ -1,11 +0,0 @@ -import status from './status.js'; - -const allocateStatus = async (verifiableCredential) => { - const statusManager = await status.getStatusManager() - const result = verifiableCredential.credentialStatus ? - verifiableCredential : - await statusManager.allocateStatus(verifiableCredential); - return result; -}; - -export default allocateStatus; diff --git a/src/app.js b/src/app.js index 5499e8e..72e6982 100644 --- a/src/app.js +++ b/src/app.js @@ -1,8 +1,6 @@ import express from 'express'; import cors from 'cors'; import status from './status.js'; -import revoke from './revoke.js' -import allocateStatus from './allocateStatus.js' import accessLogger from './middleware/accessLogger.js'; import errorHandler from './middleware/errorHandler.js'; import errorLogger from './middleware/errorLogger.js'; @@ -23,26 +21,6 @@ export async function build(opts = {}) { res.send({ message: 'status-service-git server status: ok.' }); }); - // Get status credential - app.get('/:statusCredentialId', async function (req, res, next) { - const statusCredentialId = req.params.statusCredentialId; - try { - const statusCredential = await status.getStatusCredential(statusCredentialId); - if (!statusCredential) { - next({ - message: `Unable to find Status Credential with ID "${statusCredentialId}".`, - code: 404 - }); - } - return res.status(200).json(statusCredential); - } catch (error) { - next({ - message: error.message, - code: error.code - }); - } - }); - // Allocate status app.post('/credentials/status/allocate', async function (req, res, next) { @@ -50,55 +28,87 @@ export async function build(opts = {}) { const vc = req.body; if (!vc || !Object.keys(vc).length) { next({ - message: 'A verifiable credential must be provided in the body', + message: 'A Verifiable Credential must be provided in the body.', code: 400 }); } - const vcWithStatus = await allocateStatus(vc); + const vcWithStatus = await status.allocateSupportedStatuses(vc); return res.json(vcWithStatus); } catch (e) { // We catch the async errors and pass them to the error handler. - if (!e.message) {e.message = "Error when allocating status position."} + if (!e.message) { + e.message = 'Unable to allocate status position.' + } // Note that if e contains a code property, the following spread of e will // (correctly) overwrite the 500 - next({code: 500, ...e}); + next({ code: 500, ...e }); } }); // Update status // The body will look like: - // {credentialId: '23kdr', credentialStatus: [{type: 'StatusList2021Credential', status: 'revoked'}]} + // {credentialId: '23kdr', credentialStatus: [{type: 'BitstringStatusListCredential', status: 'revoked'}]} app.post('/credentials/status', async function (req, res, next) { try { const updateRequest = req.body; if (!updateRequest || !updateRequest.credentialId || !updateRequest.credentialStatus) { next({ - message: 'A status update request must be provided in the body', + message: 'A status update request must be provided in the body.', code: 400 }); } const { credentialId, credentialStatus } = updateRequest; - const status = credentialStatus[0].status; + const statusId = credentialStatus[0].status; const statusType = credentialStatus[0].type; - if (statusType !== 'StatusList2021Credential') { + if (statusType !== 'BitstringStatusListCredential') { next({ - message: 'StatusList2021Credential is the only supported status type.', + message: 'BitstringStatusListCredential is the only supported status type.', code: 400 }); } - const statusResponse = await revoke(credentialId, status); - return res.status(statusResponse.code).send(statusResponse); + const updateStatusResponse = await status.updateStatus(credentialId, statusId); + return res.status(updateStatusResponse.code).json(updateStatusResponse); } catch (e) { // We catch the async errors and pass them to the error handler. - if (!e.message) {e.message = "Error updating credential status position."} + if (!e.message) { + e.message = 'Error updating credential status position.' + } // Note that if e contains a code property, the following spread of e will // (correctly) overwrite the 500 - next({code: 500, ...e}); + next({ code: 500, ...e }); } }); + // Get credential info + app.get('/credentials/:credentialId', async function (req, res, next) { + const credentialId = req.params.credentialId; + try { + const credentialInfo = await status.getCredentialInfo(credentialId); + return res.status(200).json(credentialInfo); + } catch (error) { + next({ + message: error.message, + code: error.code + }); + } + }); + + // Get status credential + app.get('/:statusCredentialId', async function (req, res, next) { + const statusCredentialId = req.params.statusCredentialId; + try { + const statusCredential = await status.getStatusCredential(statusCredentialId); + return res.status(200).json(statusCredential); + } catch (error) { + next({ + message: error.message, + code: error.code + }); + } + }); + // Attach the error handling middleware calls, in the order that they should run app.use(errorLogger); app.use(errorHandler); diff --git a/src/app.test.js b/src/app.test.js index dbaf8a2..4b196a0 100644 --- a/src/app.test.js +++ b/src/app.test.js @@ -1,13 +1,20 @@ import { expect } from 'chai'; import sinon from 'sinon'; import request from 'supertest'; -import { getUnsignedVC, getUnsignedVCWithStatus, getValidStatusUpdateBody, getInvalidStatusUpdateBody } from './test-fixtures/fixtures.js'; +import { + validCredentialId, + invalidCredentialId, + invalidCredentialIdErrorMessage, + getUnsignedVC, + getUnsignedVCWithStatus, + getValidStatusUpdateBody, + getInvalidStatusUpdateBody +} from './test-fixtures/fixtures.js'; import status from './status.js'; import { build } from './app.js'; const allocateEndpoint = '/credentials/status/allocate'; const updateEndpoint = '/credentials/status'; -const missingCredIdErrorMessage = 'Unable to find credential with given ID'; const emptyStatusManagerStub = {}; describe('api', () => { @@ -20,20 +27,20 @@ describe('api', () => { .get('/'); expect(response.header['content-type']).to.have.string('json'); - expect(response.status).to.eql(200); - expect(response.body.message).to.eql('status-service-git server status: ok.'); + expect(response.status).to.equal(200); + expect(response.body.message).to.equal('status-service-git server status: ok.'); }); }); - describe('GET /unknown', () => { + describe('GET /unknown/path', () => { it('unknown endpoint returns 404', async () => { await status.initializeStatusManager(emptyStatusManagerStub) const app = await build(); const response = await request(app) - .get('/unknown'); + .get('/unknown/path'); - expect(response.status).to.eql(404); + expect(response.status).to.equal(404); }, 10000); }); @@ -46,13 +53,13 @@ describe('api', () => { .post(allocateEndpoint); expect(response.header['content-type']).to.have.string('json'); - expect(response.status).to.eql(400); + expect(response.status).to.equal(400); }); it('returns updated credential', async () => { const unsignedVCWithStatus = getUnsignedVCWithStatus(); - const allocateStatus = sinon.fake.returns(unsignedVCWithStatus); - const statusManagerStub = { allocateStatus }; + const allocateSupportedStatuses = sinon.fake.returns(unsignedVCWithStatus); + const statusManagerStub = { allocateSupportedStatuses }; await status.initializeStatusManager(statusManagerStub); const app = await build(); @@ -61,13 +68,13 @@ describe('api', () => { .send(getUnsignedVC()); expect(response.header['content-type']).to.have.string('json'); - expect(response.status).to.eql(200); + expect(response.status).to.equal(200); expect(response.body).to.eql(unsignedVCWithStatus); }); it('returns unchanged credential when status already set ', async () => { - const allocateStatus = sinon.fake.returns(getUnsignedVCWithStatus()); - const statusManagerStub = { allocateStatus }; + const allocateSupportedStatuses = sinon.fake.returns(getUnsignedVCWithStatus()); + const statusManagerStub = { allocateSupportedStatuses }; await status.initializeStatusManager(statusManagerStub); const app = await build(); @@ -76,7 +83,7 @@ describe('api', () => { .send(getUnsignedVCWithStatus()); expect(response.header['content-type']).to.have.string('json'); - expect(response.status).to.eql(200); + expect(response.status).to.equal(200); expect(response.body).to.eql(getUnsignedVCWithStatus()); }); }); @@ -90,39 +97,78 @@ describe('api', () => { .post(updateEndpoint); expect(response.header['content-type']).to.have.string('json'); - expect(response.status).to.eql(400); + expect(response.status).to.equal(400); }); - it('returns update from status manager', async () => { - const updateStatus = sinon.fake.returns({ code: 200, message: 'Credential status successfully updated.' }); - const statusManagerStub = { updateStatus }; + it('returns update from revoked credential', async () => { + const revokeCredential = sinon.fake.returns({ + code: 200, + message: 'Credential status successfully updated.' + }); + const statusManagerStub = { revokeCredential }; await status.initializeStatusManager(statusManagerStub); const app = await build(); const response = await request(app) .post(updateEndpoint) - .send(getValidStatusUpdateBody()); + .send(getValidStatusUpdateBody(validCredentialId, 'revoked')); expect(response.header['content-type']).to.have.string('json'); - expect(response.status).to.eql(200); - expect(response.body.message).to.eql('Credential status successfully updated.'); + expect(response.status).to.equal(200); + expect(response.body.message).to.equal('Credential successfully revoked.'); }); - it('returns 404 for unknown cred id', async () => { - // const allocateStatus = sinon.fake.returns(getUnsignedVCWithStatus()) - const updateStatus = sinon.fake.rejects(missingCredIdErrorMessage); - const statusManagerStub = { updateStatus }; + it('returns update from suspended credential', async () => { + const suspendCredential = sinon.fake.returns({ + code: 200, + message: 'Credential successfully suspended.' + }); + const statusManagerStub = { suspendCredential }; await status.initializeStatusManager(statusManagerStub); const app = await build(); const response = await request(app) .post(updateEndpoint) - .send(getInvalidStatusUpdateBody()); + .send(getValidStatusUpdateBody(validCredentialId, 'suspended')); expect(response.header['content-type']).to.have.string('json'); - expect(response.status).to.eql(404); - console.log(response.body.message); - expect(response.body.message).to.contain('An error occurred in status-service-git: Credential ID not found.'); + expect(response.status).to.equal(200); + expect(response.body.message).to.equal('Credential successfully suspended.'); + }); + + it('returns update from unsuspended credential', async () => { + const unsuspendCredential = sinon.fake.returns({ + code: 200, + message: 'Credential successfully unsuspended.' + }); + const statusManagerStub = { unsuspendCredential }; + await status.initializeStatusManager(statusManagerStub); + const app = await build(); + + const response = await request(app) + .post(updateEndpoint) + .send(getValidStatusUpdateBody(validCredentialId, 'unsuspended')); + + expect(response.header['content-type']).to.have.string('json'); + expect(response.status).to.equal(200); + expect(response.body.message).to.equal('Credential successfully unsuspended.'); + }); + + it('returns 404 for unknown credential id', async () => { + const missingCredentialError = new Error(invalidCredentialIdErrorMessage); + missingCredentialError.code = 404; + const revokeCredential = sinon.fake.rejects(missingCredentialError); + const statusManagerStub = { revokeCredential }; + await status.initializeStatusManager(statusManagerStub); + const app = await build(); + + const response = await request(app) + .post(updateEndpoint) + .send(getInvalidStatusUpdateBody(invalidCredentialId, 'revoked')); + + expect(response.header['content-type']).to.have.string('json'); + expect(response.status).to.equal(404); + expect(response.body.message).to.contain('Unable to find credential with ID'); }); }); }); diff --git a/src/config.js b/src/config.js index bc34fc1..7c98b48 100644 --- a/src/config.js +++ b/src/config.js @@ -9,9 +9,8 @@ export function setConfig() { } function getBooleanValue(value) { + value = value?.toLocaleLowerCase(); if ( - value === true || - value === 1 || value === 'true' || value === '1' || value === 'yes' || @@ -19,8 +18,6 @@ function getBooleanValue(value) { ) { return true; } else if ( - value === false || - value === 0 || value === 'false' || value === '0' || value === 'no' || @@ -31,22 +28,21 @@ function getBooleanValue(value) { return true; } -function getGeneralEnvs() { - const env = process.env; +function getGeneralEnvs(env) { return { port: env.PORT ? parseInt(env.PORT) : defaultPort, credStatusService: env.CRED_STATUS_SERVICE, credStatusDidSeed: env.CRED_STATUS_DID_SEED, - consoleLogLevel: env.CONSOLE_LOG_LEVEL?.toLocaleLowerCase() || defaultConsoleLogLevel, - logLevel: env.LOG_LEVEL?.toLocaleLowerCase() || defaultLogLevel, + consoleLogLevel: env.CONSOLE_LOG_LEVEL?.toLocaleLowerCase() ?? defaultConsoleLogLevel, + logLevel: env.LOG_LEVEL?.toLocaleLowerCase() ?? defaultLogLevel, enableAccessLogging: getBooleanValue(env.ENABLE_ACCESS_LOGGING), + enableHttpsForDev: getBooleanValue(env.ENABLE_HTTPS_FOR_DEV), errorLogFile: env.ERROR_LOG_FILE, allLogFile: env.ALL_LOG_FILE }; } -function getGitHubEnvs() { - const env = process.env; +function getGitHubEnvs(env) { return { credStatusAccessToken: env.CRED_STATUS_ACCESS_TOKEN, credStatusRepoName: env.CRED_STATUS_REPO_NAME, @@ -55,9 +51,8 @@ function getGitHubEnvs() { }; } -function getGitLabEnvs() { - const env = process.env; - const gitHubEnvs = getGitHubEnvs(); +function getGitLabEnvs(env) { + const gitHubEnvs = getGitHubEnvs(env); return { ...gitHubEnvs, credStatusRepoId: env.CRED_STATUS_REPO_ID, @@ -66,19 +61,19 @@ function getGitLabEnvs() { } function parseConfig() { - const env = process.env + const env = process.env; let serviceSpecificEnvs; switch (env.CRED_STATUS_SERVICE) { case 'github': - serviceSpecificEnvs = getGitHubEnvs(); + serviceSpecificEnvs = getGitHubEnvs(env); break; case 'gitlab': - serviceSpecificEnvs = getGitLabEnvs(); + serviceSpecificEnvs = getGitLabEnvs(env); break; default: - throw new Error('Encountered unsupported credential status service'); + throw new Error(`Encountered unsupported credential status service: ${env.CRED_STATUS_SERVICE}`); } - const generalEnvs = getGeneralEnvs(); + const generalEnvs = getGeneralEnvs(env); const config = Object.freeze({ ...generalEnvs, ...serviceSpecificEnvs diff --git a/src/middleware/errorHandler.js b/src/middleware/errorHandler.js index eff9be1..f8ca929 100644 --- a/src/middleware/errorHandler.js +++ b/src/middleware/errorHandler.js @@ -6,7 +6,7 @@ const errorHandler = (error, request, response, next) => { // for more detail const code = error.code | 500; - const message = `An error occurred in status-service-git: ${error.message || 'unknown error.'} See the logs for full details. If you are using docker compose, view the logs with 'docker compose logs', and just the status service logs with: 'docker compose logs status-service-git'`; + const message = `An error occurred in status-service-git: ${error.message ?? 'unknown error.'} See the logs for full details. If you are using docker compose, view the logs with 'docker compose logs', and just the status service logs with: 'docker compose logs status-service-git'`; const errorResponse = {code, message}; response.header('Content-Type', 'application/json'); return response.status(error.code).json(errorResponse); diff --git a/src/revoke.js b/src/revoke.js deleted file mode 100644 index b9e7433..0000000 --- a/src/revoke.js +++ /dev/null @@ -1,20 +0,0 @@ -import status from './status.js'; -import StatusException from './StatusException.js'; - -const revoke = async (credentialId, credentialStatus) => { - try { - const statusManager = await status.getStatusManager(); - const statusCredential = await statusManager.updateStatus({ - credentialId, - credentialStatus - }); - return { code: 200, message: 'Credential status successfully updated.', statusCredential } - } catch (e) { - if (e.message.includes('Unable to find credential with given ID')) { - throw new StatusException(404, 'Credential ID not found.', e); - } - throw new StatusException(400, 'Bad Request', e); - } -} - -export default revoke; diff --git a/src/status.js b/src/status.js index a02fa94..0d030e5 100644 --- a/src/status.js +++ b/src/status.js @@ -71,7 +71,7 @@ async function initializeStatusManager(statusManager) { STATUS_LIST_MANAGER = await createGitLabStatusManager(); break; default: - throw new Error('Encountered unsupported credential status service'); + throw new Error(`Encountered unsupported credential status service: ${credStatusService}`); } } @@ -80,9 +80,54 @@ async function getStatusManager() { return STATUS_LIST_MANAGER; } +async function allocateSupportedStatuses(verifiableCredential) { + const statusManager = await getStatusManager(); + const result = verifiableCredential.credentialStatus ? + verifiableCredential : + await statusManager.allocateSupportedStatuses(verifiableCredential); + return result; +} + +async function updateStatus(credentialId, credentialStatus) { + const statusManager = await getStatusManager(); + try { + switch (credentialStatus) { + case 'revoked': + await statusManager.revokeCredential(credentialId); + return { code: 200, message: 'Credential successfully revoked.' }; + case 'suspended': + await statusManager.suspendCredential(credentialId); + return { code: 200, message: 'Credential successfully suspended.' }; + case 'unsuspended': + await statusManager.unsuspendCredential(credentialId); + return { code: 200, message: 'Credential successfully unsuspended.' }; + default: + return { code: 400, message: `Unsupported credential status: "${credentialStatus}"` }; + } + } catch (error) { + return { + code: error.code ?? 500, + message: error.message ?? + `Unable to apply status "${credentialStatus}" to credential with ID "${credentialId}".` + }; + } +} + +async function getCredentialInfo(credentialId) { + const statusManager = await getStatusManager(); + return statusManager.getCredentialInfo(credentialId); +} + async function getStatusCredential(statusCredentialId) { const statusManager = await getStatusManager(); return statusManager.getStatusCredential(statusCredentialId); } -export default { initializeStatusManager, getStatusManager, getStatusCredential }; +export default { + initializeStatusManager, + getStatusManager, + allocateSupportedStatuses, + updateStatus, + getCredentialInfo, + getStatusCredential +}; diff --git a/src/test-fixtures/.env.testing b/src/test-fixtures/.env.testing index 2d72c57..198fca7 100644 --- a/src/test-fixtures/.env.testing +++ b/src/test-fixtures/.env.testing @@ -1,6 +1,7 @@ # PORT=4008 #default port is 4008 # the CRED_STATUS_* values are used to instantiate the status list manager +CRED_STATUS_SERVICE=github CRED_STATUS_REPO_OWNER=jchartrand CRED_STATUS_REPO_NAME=status-test-three CRED_STATUS_META_REPO_NAME=status-test-meta-three diff --git a/src/test-fixtures/fixtures.js b/src/test-fixtures/fixtures.js index ff4aa13..dd3ea01 100644 --- a/src/test-fixtures/fixtures.js +++ b/src/test-fixtures/fixtures.js @@ -1,33 +1,53 @@ import testVC from './testVC.js'; - // "credentialStatus": - const credentialStatus = { - "id": "https://digitalcredentials.github.io/credential-status-jc-test/XA5AAK1PV4#16", - "type": "StatusList2021Entry", - "statusPurpose": "revocation", - "statusListIndex": 16, - "statusListCredential": "https://digitalcredentials.github.io/credential-status-jc-test/XA5AAK1PV4" -} - -const statusUpdateBody = { "credentialId": "urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1", "credentialStatus": [{ "type": "StatusList2021Credential", "status": "revoked" }] } - -const getUnsignedVC = () => JSON.parse(JSON.stringify(testVC)) - -const getValidStatusUpdateBody = () => JSON.parse(JSON.stringify(statusUpdateBody)) - -const getInvalidStatusUpdateBody = () => { - const updateBody = getValidStatusUpdateBody() - updateBody.credentialId = 'kj09ij' - return updateBody -} - -const getCredentialStatus = () => JSON.parse(JSON.stringify(credentialStatus)) +const validCredentialId = 'urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1'; +const invalidCredentialId = 'kj09ij'; +const invalidCredentialIdErrorMessage = 'An error occurred in status-service-db: ' + + `Unable to find credential with ID ${invalidCredentialId}`; + +// "credentialStatus": +const credentialStatus = { + "id": "https://digitalcredentials.github.io/credential-status-jc-test/XA5AAK1PV4#16", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListIndex": 16, + "statusListCredential": "https://digitalcredentials.github.io/credential-status-jc-test/XA5AAK1PV4" +}; + +const statusUpdateBody = { + "credentialId": "urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1", + "credentialStatus": [{ "type": "BitstringStatusListCredential", "status": "revoked" }] +}; + +const getUnsignedVC = () => JSON.parse(JSON.stringify(testVC)); + +const getValidStatusUpdateBody = (credentialId, status) => { + statusUpdateBody.credentialId = credentialId; + statusUpdateBody.credentialStatus[0].status = status; + return JSON.parse(JSON.stringify(statusUpdateBody)); +}; + +const getInvalidStatusUpdateBody = (credentialId, status) => { + const updateBody = getValidStatusUpdateBody(credentialId, status); + updateBody.credentialId = credentialId; + return updateBody; +}; + +const getCredentialStatus = () => JSON.parse(JSON.stringify(credentialStatus)); const getUnsignedVCWithStatus = () => { const unsignedVCWithStatus = getUnsignedVC(); unsignedVCWithStatus.credentialStatus = getCredentialStatus(); - return unsignedVCWithStatus -} - - -export { getUnsignedVC, getCredentialStatus, getUnsignedVCWithStatus, getValidStatusUpdateBody, getInvalidStatusUpdateBody} + return unsignedVCWithStatus; +}; + +export { + validCredentialId, + invalidCredentialId, + invalidCredentialIdErrorMessage, + getUnsignedVC, + getCredentialStatus, + getUnsignedVCWithStatus, + getValidStatusUpdateBody, + getInvalidStatusUpdateBody +}; diff --git a/src/test-fixtures/testVC.js b/src/test-fixtures/testVC.js index ed5b09f..add91e2 100644 --- a/src/test-fixtures/testVC.js +++ b/src/test-fixtures/testVC.js @@ -1,8 +1,7 @@ export default { "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://purl.imsglobal.org/spec/ob/v3p0/context.json", - "https://w3id.org/vc/status-list/2021/v1" + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context.json" ], "id": "urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1", "type": [ @@ -20,7 +19,7 @@ export default { "type": "Image" } }, - "issuanceDate": "2020-01-01T00:00:00Z", + "validFrom": "2020-01-01T00:00:00Z", "name": "A Simply Wonderful Course", "credentialSubject": { "type": "AchievementSubject", @@ -34,4 +33,5 @@ export default { "name": "Introduction to Wonderfullness" } } - } \ No newline at end of file + } + \ No newline at end of file diff --git a/src/utils/logger.js b/src/utils/logger.js index 586a3d7..5bb4cb4 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -26,7 +26,7 @@ const level = () => { if (logLevel) { return logLevel; } else { - const env = process.env.NODE_ENV || 'development'; + const env = process.env.NODE_ENV ?? 'development'; const isDevelopment = env === 'development'; return isDevelopment ? 'silly' : 'warn'; }