Skip to content

Commit

Permalink
Better graphs (#252)
Browse files Browse the repository at this point in the history
* add simpler graph builder

* better graphs

* turn off graph tests in ci
  • Loading branch information
OR13 authored Aug 27, 2024
1 parent ae786f7 commit 6ce41a2
Show file tree
Hide file tree
Showing 8 changed files with 310 additions and 125 deletions.
50 changes: 25 additions & 25 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,31 +147,31 @@ jobs:
./tests/fixtures/message.json.gcp.cbor \
3073d614f853aaec9a1146872c7bab75495ee678c8864ed3562f8787555c1e22
graph:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Push Graph Fragment
id: push_single_graph
uses: ./
with:
neo4j-uri: ${{ secrets.NEO4J_URI }}
neo4j-user: ${{ secrets.NEO4J_USERNAME }}
neo4j-password: ${{ secrets.NEO4J_PASSWORD }}
transmute: |
graph assist ./tests/fixtures/issuer-claims.json --verbose --credential-type application/vc --graph-type application/gql --push
- name: Push Presentations
id: push_multiple_graphs
uses: ./
with:
neo4j-uri: ${{ secrets.NEO4J_URI }}
neo4j-user: ${{ secrets.NEO4J_USERNAME }}
neo4j-password: ${{ secrets.NEO4J_PASSWORD }}
transmute-client-id: ${{ secrets.CLIENT_ID }}
transmute-client-secret: ${{ secrets.CLIENT_SECRET }}
transmute-api: ${{ secrets.API_BASE_URL }}
transmute: |
graph assist --graph-type application/gql --push
# graph:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - name: Push Graph Fragment
# id: push_single_graph
# uses: ./
# with:
# neo4j-uri: ${{ secrets.NEO4J_URI }}
# neo4j-user: ${{ secrets.NEO4J_USERNAME }}
# neo4j-password: ${{ secrets.NEO4J_PASSWORD }}
# transmute: |
# graph assist ./tests/fixtures/issuer-claims.json --verbose --credential-type application/vc --graph-type application/gql --push
# - name: Push Presentations
# id: push_multiple_graphs
# uses: ./
# with:
# neo4j-uri: ${{ secrets.NEO4J_URI }}
# neo4j-user: ${{ secrets.NEO4J_USERNAME }}
# neo4j-password: ${{ secrets.NEO4J_PASSWORD }}
# transmute-client-id: ${{ secrets.CLIENT_ID }}
# transmute-client-secret: ${{ secrets.CLIENT_SECRET }}
# transmute-api: ${{ secrets.API_BASE_URL }}
# transmute: |
# graph assist --graph-type application/gql --push

jose:
runs-on: ubuntu-latest
Expand Down
13 changes: 11 additions & 2 deletions src/graph/graph/driver.ts
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion src/graph/graph/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const setParam = (
const index = Object.keys(params).length
params[index] = value
const param = '$' + index.toString()
if (moment(value, moment.ISO_8601).isValid()) {
if (typeof value === 'string' && value.includes(':') && moment(value, moment.ISO_8601).isValid()) {
return `datetime(${param})`
}
return param
Expand Down
151 changes: 140 additions & 11 deletions src/graph/graph/jsongraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -186,36 +186,165 @@ const fromPresentation = async (document: any) => {
return graph
}

export type DecodedJwt = {
header: Record<string, any>,
payload: Record<string, any>,
signature: string
}

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<string, any>, signer: any) => {
const nextId = jose.base64url.encode(await signer.sign(new TextEncoder().encode(object.id)))
const [prefix, token] = object.id.split(';')
const contentType = prefix.replace('data:', '')
addLabel(graph.nodes[object.id], contentType)
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<string, any>, 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, type: string) => {
const { header, payload } = decodeToken(token)
const root = `data:${type};${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], type)
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))
break
return annotate(await fromPresentation(tokenToClaimset(document)))
}
case 'application/vc+jwt':
case 'application/vp+jwt':
case 'application/jwt': {
return await fromJwt(document, type)
}
default: {
throw new Error('Cannot compute graph from unsupported content type: ' + type)
}
}
return annotate(graph)
}

export const jsongraph = {
Expand Down
11 changes: 7 additions & 4 deletions src/graph/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const handler = async function ({ positionals, values }: Arguments) {
case 'assist': {
const output = values.output
const graphType = values['graph-type'] || 'application/vnd.jgf+json'
const contentType: any = values['credential-type'] || values['presentation-type']
const contentType: any = values['content-type'] || values['credential-type'] || values['presentation-type']
const verbose = values.verbose || false
const [pathToContent] = positionals
if (verbose) {
Expand All @@ -36,13 +36,16 @@ export const handler = async function ({ positionals, values }: Arguments) {
let allGraphText = ''
const allGraphs = [] as any[]
const api = await getApi()
const { items } = await getPresentations({ sent: true, received: true, api })
let presentations = await getPresentations({ sent: true, received: true, api })
presentations = presentations.items.filter((item) => {
return item.id === 'urn:transmute:presentation:2d05386b-ec60-4f7a-b531-de1d1fd6bfec'
})
const d = await driver()
const session = d.session()
for (const item of items) {
for (const item of presentations) {
try {
const content = encoder.encode(item.content)
graph = await jsongraph.graph(content, 'application/vp-ld+sd-jwt')
graph = await jsongraph.graph(content, 'application/vp+jwt')
allGraphs.push(graph)
const components = await query(graph)
const dangerousQuery = await injection(components)
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/example.jwt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
49 changes: 49 additions & 0 deletions tests/jsonld2cypher.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as core from '@actions/core'

import { facade } from '../src'

let debug: jest.SpiedFunction<typeof core.debug>
let output: jest.SpiedFunction<typeof core.setOutput>
let secret: jest.SpiedFunction<typeof core.setSecret>

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.skip('graph assist with regular jwt', async () => {
await facade(`graph assist ./tests/fixtures/example.jwt \
--content-type application/jwt \
--graph-type application/gql \
--env ./.env \
--verbose --push `)
expect(debug).toHaveBeenCalledTimes(1)
expect(output).toHaveBeenCalledTimes(1)
expect(secret).toHaveBeenCalledTimes(1)
})

it.skip('graph assist with transmute platform presentations', async () => {
await facade(`graph assist \
--graph-type application/gql \
--env ./.env \
--push `)
expect(debug).toHaveBeenCalledTimes(0)
expect(output).toHaveBeenCalledTimes(1)
expect(secret).toHaveBeenCalledTimes(1)
})


it.skip('graph assist with verifiable credential', async () => {
await facade(`graph assist ./tests/fixtures/issuer-claims.json \
--content-type application/vc \
--graph-type application/gql \
--env ./.env \
--verbose --push `)
expect(debug).toHaveBeenCalledTimes(1)
expect(output).toHaveBeenCalledTimes(1)
expect(secret).toHaveBeenCalledTimes(1)
})
Loading

0 comments on commit 6ce41a2

Please sign in to comment.