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

Appview v2 use dataplane for identity lookups #2095

Merged
merged 4 commits into from
Jan 26, 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
29 changes: 29 additions & 0 deletions packages/bsky/proto/bsky.proto
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,31 @@ message GetLatestRevResponse {
string rev = 1;
}


message GetIdentityByDidRequest {
string did = 1;
}
message GetIdentityByDidResponse {
string did = 1;
string handle = 2;
bytes keys = 3;
bytes services = 4;
google.protobuf.Timestamp updated = 5;
}

message GetIdentityByHandleRequest {
string handle = 1;
}
message GetIdentityByHandleResponse {
string handle = 1;
string did = 2;
bytes keys = 3;
bytes services = 4;
google.protobuf.Timestamp updated = 5;
}



//
// Moderation
//
Expand Down Expand Up @@ -1003,6 +1028,10 @@ service Service {
rpc GetRecordTakedown(GetRecordTakedownRequest) returns (GetRecordTakedownResponse);
rpc GetActorTakedown(GetActorTakedownRequest) returns (GetActorTakedownResponse);

// Identity
rpc GetIdentityByDid(GetIdentityByDidRequest) returns (GetIdentityByDidResponse);
rpc GetIdentityByHandle(GetIdentityByHandleRequest) returns (GetIdentityByHandleResponse);

// Ping
rpc Ping(PingRequest) returns (PingResponse);

Expand Down
29 changes: 16 additions & 13 deletions packages/bsky/src/api/app/bsky/feed/getFeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,6 @@ import {
serverTimingHeader,
} from '@atproto/xrpc-server'
import { ResponseType, XRPCError } from '@atproto/xrpc'
import {
DidDocument,
PoorlyFormattedDidDocumentError,
getFeedGen,
} from '@atproto/identity'
import { AtpAgent, AppBskyFeedGetFeedSkeleton } from '@atproto/api'
import { noUndefinedVals } from '@atproto/common'
import { QueryParams as GetFeedParams } from '../../../../lexicon/types/app/bsky/feed/getFeed'
Expand All @@ -26,6 +21,13 @@ import {
createPipeline,
} from '../../../../pipeline'
import { FeedItem } from '../../../../hydration/feed'
import { GetIdentityByDidResponse } from '../../../../proto/bsky_pb'
import {
Code,
getServiceEndpoint,
isDataplaneError,
unpackIdentityServices,
} from '../../../../data-plane'

export default function (server: Server, ctx: AppContext) {
const getFeed = createPipeline(
Expand Down Expand Up @@ -157,20 +159,21 @@ const skeletonFromFeedGen = async (
throw new InvalidRequestError('could not find feed')
}

let resolved: DidDocument | null
let identity: GetIdentityByDidResponse
try {
resolved = await ctx.idResolver.did.resolve(feedDid)
identity = await ctx.dataplane.getIdentityByDid({ did: feedDid })
} catch (err) {
if (err instanceof PoorlyFormattedDidDocumentError) {
throw new InvalidRequestError(`invalid did document: ${feedDid}`)
if (isDataplaneError(err, Code.NotFound)) {
throw new InvalidRequestError(`could not resolve identity: ${feedDid}`)
}
throw err
}
if (!resolved) {
throw new InvalidRequestError(`could not resolve did document: ${feedDid}`)
}

const fgEndpoint = getFeedGen(resolved)
const services = unpackIdentityServices(identity.services)
const fgEndpoint = getServiceEndpoint(services, {
id: 'bsky_fg',
type: 'BskyFeedGenerator',
})
if (!fgEndpoint) {
throw new InvalidRequestError(
`invalid feed generator service details in did document: ${feedDid}`,
Expand Down
33 changes: 18 additions & 15 deletions packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { InvalidRequestError } from '@atproto/xrpc-server'
import {
DidDocument,
PoorlyFormattedDidDocumentError,
getFeedGen,
} from '@atproto/identity'
import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
import { GetIdentityByDidResponse } from '../../../../proto/bsky_pb'
import {
Code,
getServiceEndpoint,
isDataplaneError,
unpackIdentityServices,
} from '../../../../data-plane'

export default function (server: Server, ctx: AppContext) {
server.app.bsky.feed.getFeedGenerator({
Expand All @@ -21,22 +23,23 @@ export default function (server: Server, ctx: AppContext) {
}

const feedDid = feedInfo.record.did
let resolved: DidDocument | null
let identity: GetIdentityByDidResponse
try {
resolved = await ctx.idResolver.did.resolve(feedDid)
identity = await ctx.dataplane.getIdentityByDid({ did: feedDid })
} catch (err) {
if (err instanceof PoorlyFormattedDidDocumentError) {
throw new InvalidRequestError(`invalid did document: ${feedDid}`)
if (isDataplaneError(err, Code.NotFound)) {
throw new InvalidRequestError(
`could not resolve identity: ${feedDid}`,
)
}
throw err
}
if (!resolved) {
throw new InvalidRequestError(
`could not resolve did document: ${feedDid}`,
)
}

const fgEndpoint = getFeedGen(resolved)
const services = unpackIdentityServices(identity.services)
const fgEndpoint = getServiceEndpoint(services, {
id: 'bsky_fg',
type: 'BskyFeedGenerator',
})
if (!fgEndpoint) {
throw new InvalidRequestError(
`invalid feed generator service details in did document: ${feedDid}`,
Expand Down
25 changes: 23 additions & 2 deletions packages/bsky/src/api/blob-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import { DidNotFoundError } from '@atproto/identity'
import AppContext from '../context'
import { httpLogger as log } from '../logger'
import { retryHttp } from '../util/retry'
import {
Code,
getServiceEndpoint,
isDataplaneError,
unpackIdentityServices,
} from '../data-plane'

// Resolve and verify blob from its origin host

Expand Down Expand Up @@ -77,10 +83,25 @@ export const createRouter = (ctx: AppContext): express.Router => {
export async function resolveBlob(ctx: AppContext, did: string, cid: CID) {
const cidStr = cid.toString()

const [{ pds }, { takenDown }] = await Promise.all([
ctx.idResolver.did.resolveAtprotoData(did),
const [identity, { takenDown }] = await Promise.all([
ctx.dataplane.getIdentityByDid({ did }).catch((err) => {
if (isDataplaneError(err, Code.NotFound)) {
return undefined
}
throw err
}),
ctx.dataplane.getBlobTakedown({ did, cid: cid.toString() }),
])
const services = identity && unpackIdentityServices(identity.services)
const pds =
services &&
getServiceEndpoint(services, {
id: 'atproto_pds',
type: 'AtprotoPersonalDataServer',
})
if (!pds) {
throw createError(404, 'Origin not found')
}
if (takenDown) {
throw createError(404, 'Blob not found')
}
Expand Down
29 changes: 25 additions & 4 deletions packages/bsky/src/auth-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@ import {
AuthRequiredError,
verifyJwt as verifyServiceJwt,
} from '@atproto/xrpc-server'
import { IdResolver } from '@atproto/identity'
import * as ui8 from 'uint8arrays'
import express from 'express'
import {
Code,
DataPlaneClient,
getKeyAsDidKey,
isDataplaneError,
unpackIdentityKeys,
} from './data-plane'
import { GetIdentityByDidResponse } from './proto/bsky_pb'

type ReqCtx = {
req: express.Request
Expand Down Expand Up @@ -63,7 +70,7 @@ export class AuthVerifier {
public ownDid: string
public adminDid: string

constructor(public idResolver: IdResolver, opts: AuthVerifierOpts) {
constructor(public dataplane: DataPlaneClient, opts: AuthVerifierOpts) {
this._adminPass = opts.adminPass
this._moderatorPass = opts.moderatorPass
this._triagePass = opts.triagePass
Expand Down Expand Up @@ -191,12 +198,26 @@ export class AuthVerifier {
) {
const getSigningKey = async (
did: string,
forceRefresh: boolean,
_forceRefresh: boolean, // @TODO consider propagating to dataplane
): Promise<string> => {
if (opts.iss !== null && !opts.iss.includes(did)) {
throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss')
}
return this.idResolver.did.resolveAtprotoKey(did, forceRefresh)
let identity: GetIdentityByDidResponse
try {
identity = await this.dataplane.getIdentityByDid({ did })
} catch (err) {
if (isDataplaneError(err, Code.NotFound)) {
throw new AuthRequiredError('identity unknown')
}
throw err
}
const keys = unpackIdentityKeys(identity.keys)
const didKey = getKeyAsDidKey(keys, { id: 'atproto' })
if (!didKey) {
throw new AuthRequiredError('missing or bad key')
}
return didKey
}

const jwtStr = bearerTokenFromReq(reqCtx.req)
Expand Down
7 changes: 1 addition & 6 deletions packages/bsky/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as plc from '@did-plc/lib'
import { DidCache, IdResolver } from '@atproto/identity'
import { IdResolver } from '@atproto/identity'
import AtpAgent from '@atproto/api'
import { Keypair } from '@atproto/crypto'
import { createServiceJwt } from '@atproto/xrpc-server'
Expand All @@ -22,7 +22,6 @@ export class AppContext {
views: Views
signingKey: Keypair
idResolver: IdResolver
didCache?: DidCache
bsyncClient: BsyncClient
courierClient: CourierClient
algos: MountedAlgos
Expand Down Expand Up @@ -62,10 +61,6 @@ export class AppContext {
return this.opts.idResolver
}

get didCache(): DidCache | undefined {
return this.opts.didCache
}

get bsyncClient(): BsyncClient {
return this.opts.bsyncClient
}
Expand Down
57 changes: 56 additions & 1 deletion packages/bsky/src/data-plane/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from 'node:assert'
import { randomInt } from 'node:crypto'
import { Service } from '../proto/bsky_connect'
import * as ui8 from 'uint8arrays'
import {
Code,
ConnectError,
Expand All @@ -9,6 +9,8 @@ import {
makeAnyClient,
} from '@connectrpc/connect'
import { createConnectTransport } from '@connectrpc/connect-node'
import { getDidKeyFromMultibase } from '@atproto/identity'
import { Service } from '../proto/bsky_connect'

export type DataPlaneClient = PromiseClient<typeof Service>
type HttpVersion = '1.1' | '2'
Expand Down Expand Up @@ -69,3 +71,56 @@ const randomElement = <T>(arr: T[]): T | undefined => {
if (arr.length === 0) return
return arr[randomInt(arr.length)]
}

export const unpackIdentityServices = (servicesBytes: Uint8Array) => {
const servicesStr = ui8.toString(servicesBytes, 'utf8')
if (!servicesStr) return {}
return JSON.parse(servicesStr) as UnpackedServices
}

export const unpackIdentityKeys = (keysBytes: Uint8Array) => {
const keysStr = ui8.toString(keysBytes, 'utf8')
if (!keysStr) return {}
return JSON.parse(keysStr) as UnpackedKeys
}

export const getServiceEndpoint = (
services: UnpackedServices,
opts: { id: string; type: string },
) => {
const endpoint =
services[opts.id] &&
services[opts.id].Type === opts.type &&
validateUrl(services[opts.id].URL)
return endpoint || undefined
}

export const getKeyAsDidKey = (keys: UnpackedKeys, opts: { id: string }) => {
const key =
keys[opts.id] &&
getDidKeyFromMultibase({
type: keys[opts.id].Type,
publicKeyMultibase: keys[opts.id].PublicKeyMultibase,
})
return key || undefined
}

type UnpackedServices = Record<string, { Type: string; URL: string }>

type UnpackedKeys = Record<string, { Type: string; PublicKeyMultibase: string }>

const validateUrl = (urlStr: string): string | undefined => {
let url
try {
url = new URL(urlStr)
} catch {
return undefined
}
if (!['http:', 'https:'].includes(url.protocol)) {
return undefined
} else if (!url.hostname) {
return undefined
} else {
return urlStr
}
}
Loading
Loading