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

Better graphs #252

Merged
merged 3 commits into from
Aug 27, 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
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