Skip to content

Commit 986aae5

Browse files
authored
Merge pull request #35 from hyphacoop/protocol-integration
Protocol Integration
2 parents 974f245 + d2a078e commit 986aae5

34 files changed

+16632
-5623
lines changed

.github/workflows/tests.yaml

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
name: Tests
22

3-
on:
4-
push:
5-
branches: [ main, v1 ]
6-
pull_request:
7-
branches: [ main, v1 ]
3+
on: [ push, pull_request ]
84

95
jobs:
106
build:
11-
runs-on: ubuntu-latest
7+
runs-on: ubuntu-22.04
128
defaults:
139
run:
1410
working-directory: v1

v1/@types/go-ipfs.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
declare module 'go-ipfs' {
3+
export function path (): string
4+
}

v1/@types/hyper-sdk.d.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
declare module 'hyper-sdk' {
2+
export class Hyperdrive {
3+
once (evt: string, cb: () => void): void
4+
get url (): string
5+
close (): Promise<void>
6+
}
7+
8+
export type HyperOpts = Partial<{
9+
storage: string | boolean
10+
corestoreOpts: {}
11+
swarmOpts: {}
12+
}>
13+
14+
export class SDK {
15+
getDrive (nameOrKeyOrURL: string): Promise<Hyperdrive>
16+
close (): Promise<void>
17+
}
18+
19+
export function create (opts?: HyperOpts): Promise<SDK>
20+
}

v1/@types/localdrive.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
declare module 'localdrive' {
3+
import MirrorDrive, { DriveLike } from 'mirror-drive'
4+
export default class LocalDrive {
5+
constructor (path: string)
6+
mirror (otherDrive: DriveLike): MirrorDrive
7+
}
8+
}

v1/@types/mirror-drive.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
declare module 'mirror-drive' {
3+
import { Hyperdrive } from 'hyper-sdk'
4+
import LocalDrive from 'localdrive'
5+
export type DriveLike = LocalDrive | Hyperdrive
6+
export default class MirrorDrive {
7+
constructor (drive1: DriveLike, drive2: DriveLike)
8+
get count (): number
9+
done (): Promise<void>
10+
}
11+
}

v1/api/admin.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import { Static, Type } from '@sinclair/typebox'
22
import { StoreI } from '../config/index.js'
33
import { FastifyTypebox } from './index.js'
4-
import { NewAdmin } from './schemas.js'
4+
import { Admin, NewAdmin } from './schemas.js'
55

66
export const adminRoutes = (store: StoreI) => async (server: FastifyTypebox): Promise<void> => {
77
server.post<{
88
Body: Static<typeof NewAdmin>
9-
Reply: string // id of the admin
9+
Reply: Static<typeof Admin>
1010
}>('/admin', {
1111
schema: {
1212
body: NewAdmin,
13+
response: {
14+
200: Admin
15+
},
1316
description: 'Add a new admin.',
1417
tags: ['admin'],
1518
security: [{ jwt: [] }]

v1/api/auth.test.ts

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
1-
import test from 'ava'
1+
import anyTest, { TestFn } from 'ava'
22
import { CAPABILITIES, makeJWTToken } from '../authorization/jwt.js'
3-
import apiBuilder from './index.js'
3+
import { spawnTestServer } from '../fixtures/spawnServer.js'
4+
import { FastifyTypebox } from './index.js'
5+
6+
const test = anyTest as TestFn<{ server: FastifyTypebox }>
7+
8+
test.beforeEach(async t => {
9+
t.context.server = await spawnTestServer()
10+
})
11+
12+
test.afterEach.always(async t => {
13+
await t.context.server?.close()
14+
})
415

516
test('no auth header should 401', async t => {
6-
const server = await apiBuilder({ useMemoryBackedDB: true })
7-
const responseAdmin = await server.inject({
17+
const responseAdmin = await t.context.server.inject({
818
method: 'POST',
919
url: '/v1/admin',
1020
payload: {
1121
name: 'test_admin'
1222
}
1323
})
1424
t.is(responseAdmin.statusCode, 401, 'returns a status code of 401')
15-
const responsePublisher = await server.inject({
25+
const responsePublisher = await t.context.server.inject({
1626
method: 'POST',
1727
url: '/v1/publisher',
1828
payload: {
@@ -23,10 +33,9 @@ test('no auth header should 401', async t => {
2333
})
2434

2535
test('token refresh with non-refresh token should fail', async t => {
26-
const server = await apiBuilder({ useMemoryBackedDB: true })
27-
const tokenAdmin = server.jwt.sign(makeJWTToken({ isAdmin: true, isRefresh: false }))
28-
const tokenPublisher = server.jwt.sign(makeJWTToken({ isAdmin: false, isRefresh: false }))
29-
const adminResponse = await server.inject({
36+
const tokenAdmin = t.context.server.jwt.sign(makeJWTToken({ isAdmin: true, isRefresh: false }))
37+
const tokenPublisher = t.context.server.jwt.sign(makeJWTToken({ isAdmin: false, isRefresh: false }))
38+
const adminResponse = await t.context.server.inject({
3039
method: 'POST',
3140
url: '/v1/auth/exchange',
3241
headers: {
@@ -37,7 +46,7 @@ test('token refresh with non-refresh token should fail', async t => {
3746
}
3847
})
3948
t.is(adminResponse.statusCode, 401, 'refreshing a non-refresh admin token returns a status code of 401')
40-
const publisherResponse = await server.inject({
49+
const publisherResponse = await t.context.server.inject({
4150
method: 'POST',
4251
url: '/v1/auth/exchange',
4352
headers: {
@@ -51,40 +60,34 @@ test('token refresh with non-refresh token should fail', async t => {
5160
})
5261

5362
test('revocation works', async t => {
54-
const server = await apiBuilder({ useMemoryBackedDB: true })
5563
const token = makeJWTToken({ isAdmin: true, isRefresh: false })
56-
const signedToken = server.jwt.sign(token)
64+
const signedToken = t.context.server.jwt.sign(token)
5765
const tokenToBeRevoked = makeJWTToken({ isAdmin: true, isRefresh: false })
58-
const signedTokenToBeRevoked = server.jwt.sign(tokenToBeRevoked)
59-
const revokeResponse = await server.inject({
66+
const signedTokenToBeRevoked = t.context.server.jwt.sign(tokenToBeRevoked)
67+
const revokeResponse = await t.context.server.inject({
6068
method: 'DELETE',
6169
url: `/v1/auth/revoke/${tokenToBeRevoked.tokenId}`,
6270
headers: {
6371
authorization: `Bearer ${signedToken}`
64-
},
65-
payload: {
66-
capabilities: [CAPABILITIES.PUBLISHER, CAPABILITIES.REFRESH]
6772
}
6873
})
6974
t.is(revokeResponse.statusCode, 200, 'revocation of another token works (should return 200)')
7075

71-
const failingRevokeResponse = await server.inject({
76+
const failingRevokeResponse = await t.context.server.inject({
7277
method: 'DELETE',
7378
url: `/v1/auth/revoke/${token.tokenId}`,
7479
headers: {
7580
authorization: `Bearer ${signedTokenToBeRevoked}`
76-
},
77-
payload: {
78-
capabilities: [CAPABILITIES.PUBLISHER, CAPABILITIES.REFRESH]
7981
}
8082
})
8183
t.is(failingRevokeResponse.statusCode, 401, 'trying to revoke the original token using the revoked token should no longer work')
8284
})
8385

84-
test('requesting new token with superset of permissions (publisher -> admin) should fail', async t => {
85-
const server = await apiBuilder({ useMemoryBackedDB: true })
86-
const token = server.jwt.sign(makeJWTToken({ isAdmin: false, isRefresh: true }))
87-
const response = await server.inject({
86+
test('revoking a tokens removes all tokens derived from that token', async t => {
87+
// first token
88+
const tokenBody = makeJWTToken({ isAdmin: true, isRefresh: true })
89+
const token = t.context.server.jwt.sign(tokenBody)
90+
const response = await t.context.server.inject({
8891
method: 'POST',
8992
url: '/v1/auth/exchange',
9093
headers: {
@@ -94,29 +97,49 @@ test('requesting new token with superset of permissions (publisher -> admin) sho
9497
capabilities: [CAPABILITIES.ADMIN, CAPABILITIES.PUBLISHER, CAPABILITIES.REFRESH]
9598
}
9699
})
97-
t.is(response.statusCode, 401)
100+
t.is(response.statusCode, 200, 'publisher refreshing their own token works')
101+
102+
const refreshedToken = response.body
103+
const revokeResponse = await t.context.server.inject({
104+
method: 'DELETE',
105+
url: `/v1/auth/revoke/${tokenBody.tokenId}`,
106+
headers: {
107+
authorization: `Bearer ${refreshedToken}`
108+
}
109+
})
110+
t.is(revokeResponse.statusCode, 200, 'revoking original token should work')
111+
112+
const newPublisherResponse = await t.context.server.inject({
113+
method: 'POST',
114+
url: '/v1/publisher',
115+
headers: {
116+
authorization: `Bearer ${refreshedToken}`
117+
},
118+
payload: {
119+
name: 'malicious new publisher'
120+
}
121+
})
122+
t.is(newPublisherResponse.statusCode, 401, 'operations with token derived from original token should also fail')
98123
})
99124

100-
test('requesting new token with subset of permissions (admin -> publisher) should work', async t => {
101-
const server = await apiBuilder({ useMemoryBackedDB: true })
102-
const token = server.jwt.sign(makeJWTToken({ isAdmin: true, isRefresh: true }))
103-
const response = await server.inject({
125+
test('requesting new token with superset of permissions (publisher -> admin) should fail', async t => {
126+
const token = t.context.server.jwt.sign(makeJWTToken({ isAdmin: false, isRefresh: true }))
127+
const response = await t.context.server.inject({
104128
method: 'POST',
105129
url: '/v1/auth/exchange',
106130
headers: {
107131
authorization: `Bearer ${token}`
108132
},
109133
payload: {
110-
capabilities: [CAPABILITIES.PUBLISHER, CAPABILITIES.REFRESH]
134+
capabilities: [CAPABILITIES.ADMIN, CAPABILITIES.PUBLISHER, CAPABILITIES.REFRESH]
111135
}
112136
})
113-
t.is(response.statusCode, 200)
137+
t.is(response.statusCode, 401)
114138
})
115139

116-
test('trying to create new refresh tokens as a publisher should fail', async t => {
117-
const server = await apiBuilder({ useMemoryBackedDB: true })
118-
const token = server.jwt.sign(makeJWTToken({ isAdmin: false, isRefresh: true }))
119-
const response = await server.inject({
140+
test('requesting new token with subset of permissions (admin -> publisher) should work', async t => {
141+
const token = t.context.server.jwt.sign(makeJWTToken({ isAdmin: true, isRefresh: true }))
142+
const response = await t.context.server.inject({
120143
method: 'POST',
121144
url: '/v1/auth/exchange',
122145
headers: {
@@ -126,5 +149,5 @@ test('trying to create new refresh tokens as a publisher should fail', async t =
126149
capabilities: [CAPABILITIES.PUBLISHER, CAPABILITIES.REFRESH]
127150
}
128151
})
129-
t.is(response.statusCode, 401)
152+
t.is(response.statusCode, 200)
130153
})

v1/api/auth.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Type, Static } from '@sinclair/typebox'
2-
import { CAPABILITIES, getExpiry, subset, NewJWTPayload } from '../authorization/jwt.js'
2+
import { getExpiry, subset, NewJWTPayload } from '../authorization/jwt.js'
33
import { StoreI } from '../config/index.js'
44
import { FastifyTypebox } from './index.js'
55

@@ -23,9 +23,6 @@ export const authRoutes = (store: StoreI) => async (server: FastifyTypebox): Pro
2323
if (!subset(request.body.capabilities, token.capabilities)) {
2424
return await reply.status(401).send('Requested more permissions than original token has')
2525
}
26-
if (!token.capabilities.includes(CAPABILITIES.ADMIN) && request.body.capabilities.includes(CAPABILITIES.REFRESH)) {
27-
return await reply.status(401).send("Can't create new refresh tokens if you are not an admin. Please contact an administrator")
28-
}
2926
const newToken = {
3027
...token,
3128
issuedTo: request.body.issuedTo ?? token.issuedTo,

0 commit comments

Comments
 (0)