From 9411be4abb55a8cb3ce599f1afae50b0bed60b74 Mon Sep 17 00:00:00 2001 From: Orie Steele Date: Mon, 26 Aug 2024 18:55:54 -0500 Subject: [PATCH] add simpler graph builder --- src/graph/graph/driver.ts | 13 ++- src/graph/graph/jsongraph.ts | 149 ++++++++++++++++++++++++++++++++--- tests/jsonld2cypher.test.ts | 28 +++++++ 3 files changed, 178 insertions(+), 12 deletions(-) create mode 100644 tests/jsonld2cypher.test.ts diff --git a/src/graph/graph/driver.ts b/src/graph/graph/driver.ts index 865de530..f497ae11 100644 --- a/src/graph/graph/driver.ts +++ b/src/graph/graph/driver.ts @@ -1,11 +1,20 @@ import neo4j from 'neo4j-driver' -import { getInput } from '@actions/core' +import { getInput, setSecret } from '@actions/core' + + +import { env } from '../../action' export const driver = () => { + const password = `${process.env.NEO4J_PASSWORD || getInput("neo4j-password")}` + if (env.github()) { + if (password) { + setSecret(password) + } + } const driver = neo4j.driver( `${process.env.NEO4J_URI || getInput("neo4j-uri")}`, - neo4j.auth.basic(`${process.env.NEO4J_USERNAME || getInput("neo4j-user")}`, `${process.env.NEO4J_PASSWORD || getInput("neo4j-password")}`) + neo4j.auth.basic(`${process.env.NEO4J_USERNAME || getInput("neo4j-user")}`, password) ) return driver } diff --git a/src/graph/graph/jsongraph.ts b/src/graph/graph/jsongraph.ts index 2480ab86..ffd7af8f 100644 --- a/src/graph/graph/jsongraph.ts +++ b/src/graph/graph/jsongraph.ts @@ -2,7 +2,7 @@ // https://github.com/jsongraph/json-graph-specification import * as jose from 'jose' -import { QuadValue, JsonGraph } from '../../types' +import { QuadValue, JsonGraph, JsonGraphNode } from '../../types' import { documentLoader, defaultContext } from './documentLoader' import { annotate } from './annotate' import { canonize } from './canonize' @@ -186,36 +186,165 @@ const fromPresentation = async (document: any) => { return graph } +export type DecodedJwt = { + header: Record, + payload: Record, + signature: string +} + +// json pointer? + +const decodeToken = (token: Uint8Array) => { + const [header, payload, signature] = new TextDecoder().decode(token).split('.') + return { + header: JSON.parse(new TextDecoder().decode(jose.base64url.decode(header))), + payload: JSON.parse(new TextDecoder().decode(jose.base64url.decode(payload))), + signature + } as DecodedJwt +} + +const addLabel = (node: JsonGraphNode, label: string | string[]) => { + if (node === undefined || label === null || label === undefined) { + return + } + if (Array.isArray(label)) { + for (const lab of label) { + addLabel(node, lab) + } + } else { + if (node.labels && !node.labels.includes(label)) { + node.labels.push(label) + } + } +} + +const addEnvelopedCredentialToGraph = async (graph: JsonGraph, id: string, object: Record, signer: any) => { + const nextId = jose.base64url.encode(await signer.sign(new TextEncoder().encode(object.id))) + const [prefix, token] = object.id.split(';') + const { header, payload } = decodeToken(new TextEncoder().encode(token)) + const claimsetId = payload.id || `${nextId}:claims` + addGraphNode({ graph, id: claimsetId }) + await addObjectToGraph(graph, object.id, header, signer) + await addObjectToGraph(graph, claimsetId, payload, signer) + addGraphEdge({ graph, source: object.id, label: 'claims', target: claimsetId }) + + return graph +} + + + +const addArrayToGraph = async (graph: JsonGraph, id: string, array: any[], signer: any, label = 'includes') => { + for (const index in array) { + const item = array[index] + if (Array.isArray(item)) { + const nextId = `${id}:${index}` + addGraphNode({ graph, id: nextId }) + addGraphEdge({ graph, source: id, label, target: nextId }) + await addArrayToGraph(graph, nextId, item, signer) + } else if (typeof item === 'object') { + const nextId = item.id || `${id}:${index}` + addGraphNode({ graph, id: nextId }) + addGraphEdge({ graph, source: id, label, target: nextId }) + await addObjectToGraph(graph, nextId, item, signer) + } else { + if (label !== '@context') { + addLabel(graph.nodes[id], item) + } + } + } +} + +const addObjectToGraph = async (graph: JsonGraph, id: string, object: Record, signer: any) => { + for (const [key, value] of Object.entries(object)) { + if (['id', 'kid'].includes(key)) { + if (value.startsWith("data:")) { + await addEnvelopedCredentialToGraph(graph, id, object, signer) + } else { + addGraphNode({ graph, id: value }) + if (id !== value) { + addGraphEdge({ graph, source: id, label: key, target: value }) + } + } + } else if (['holder', 'issuer',].includes(key)) { + if (typeof value === 'object') { + const nextId = value.id || `${id}:${key}` + addGraphNode({ graph, id: nextId }) + addGraphEdge({ graph, source: id, label: key, target: nextId }) + await addObjectToGraph(graph, nextId, value, signer) + } else { + addGraphNode({ graph, id: value }) + addGraphEdge({ graph, source: value, label: key, target: id }) + } + } else if (['type'].includes(key)) { + addLabel(graph.nodes[id], value) + } else if (Array.isArray(value)) { + await addArrayToGraph(graph, id, value, signer, key) + } else if (typeof value === 'object') { + // handle objects + const nextId = value.id || `${id}:${key}` + addGraphNode({ graph, id: nextId }) + addGraphEdge({ graph, source: id, label: key, target: nextId }) + await addObjectToGraph(graph, nextId, value, signer) + } else { + // simple types + addGraphNodeProperty( + graph, + id, + key, + value + ) + } + } +} + +const fromJwt = async (token: Uint8Array) => { + const { header, payload } = decodeToken(token) + const root = `data:application/jwt;${new TextDecoder().decode(token)}` + const signer = await hmac.signer(new TextEncoder().encode(root)) + const graph = { + nodes: {}, + edges: [] + } + addGraphNode({ graph, id: root }) + addLabel(graph.nodes[root], 'JWT') + const nextId = jose.base64url.encode(await signer.sign(new TextEncoder().encode(root))) + const claimsetId = payload.id || `${nextId}:claims` + addGraphNode({ graph, id: claimsetId }) + await addObjectToGraph(graph, root, header, signer) + addGraphEdge({ graph, source: root, label: 'claims', target: claimsetId }) + await addObjectToGraph(graph, claimsetId, payload, signer) + return graph +} + + const graph = async (document: Uint8Array, type: string) => { - let graph const tokenToClaimset = (token: Uint8Array) => { const [_header, payload, _signature] = new TextDecoder().decode(token).split('.') return JSON.parse(new TextDecoder().decode(jose.base64url.decode(payload))) } switch (type) { case 'application/vc': { - graph = await fromCredential(JSON.parse(new TextDecoder().decode(document))) - break + return annotate(await fromCredential(JSON.parse(new TextDecoder().decode(document)))) } case 'application/vp': { - graph = await fromPresentation(document) - break + return annotate(await fromPresentation(document)) } case 'application/vc-ld+jwt': case 'application/vc-ld+sd-jwt': { - graph = await fromCredential(tokenToClaimset(document)) - break + return annotate(await fromCredential(tokenToClaimset(document))) } case 'application/vp-ld+jwt': case 'application/vp-ld+sd-jwt': { - graph = await fromPresentation(tokenToClaimset(document)) + return annotate(await fromPresentation(tokenToClaimset(document))) break } + case 'application/jwt': { + return await fromJwt(document) + } default: { throw new Error('Cannot compute graph from unsupported content type: ' + type) } } - return annotate(graph) } export const jsongraph = { diff --git a/tests/jsonld2cypher.test.ts b/tests/jsonld2cypher.test.ts new file mode 100644 index 00000000..732e2f16 --- /dev/null +++ b/tests/jsonld2cypher.test.ts @@ -0,0 +1,28 @@ +import * as core from '@actions/core' + +import { facade } from '../src' + +let debug: jest.SpiedFunction +let output: jest.SpiedFunction +let secret: jest.SpiedFunction + +beforeEach(() => { + process.env.GITHUB_ACTION = 'jest-mock' + jest.clearAllMocks() + debug = jest.spyOn(core, 'debug').mockImplementation() + output = jest.spyOn(core, 'setOutput').mockImplementation() + secret = jest.spyOn(core, 'setSecret').mockImplementation() +}) + + +it.only('graph assist with scitt transparent statement', async () => { + await facade(`graph assist ./tests/fixtures/vp.jwt \ +--credential-type application/jwt \ +--graph-type application/gql \ +--env ./.env \ +--verbose --push `) + expect(debug).toHaveBeenCalledTimes(1) + expect(output).toHaveBeenCalledTimes(1) + expect(secret).toHaveBeenCalledTimes(1) +}) +