Skip to content

Commit

Permalink
Entryway-to-pds account deletion flow (#1820)
Browse files Browse the repository at this point in the history
* add admin lexicon for account deletion

* handle entryway-to-pds account deletion flow

* ensure pds before creating report or putting preferences

* tidy

* better guarantee that acct deletion makes it out to pds behind entryway
  • Loading branch information
devinivy authored Nov 7, 2023
1 parent 64d2c01 commit cca9f91
Show file tree
Hide file tree
Showing 16 changed files with 366 additions and 13 deletions.
20 changes: 20 additions & 0 deletions lexicons/com/atproto/admin/deleteAccount.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"lexicon": 1,
"id": "com.atproto.admin.deleteAccount",
"defs": {
"main": {
"type": "procedure",
"description": "Delete a user account as an administrator.",
"input": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["did"],
"properties": {
"did": { "type": "string", "format": "did" }
}
}
}
}
}
}
13 changes: 13 additions & 0 deletions packages/api/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { schemas } from './lexicons'
import { CID } from 'multiformats/cid'
import * as ComAtprotoAdminDefs from './types/com/atproto/admin/defs'
import * as ComAtprotoAdminDeleteAccount from './types/com/atproto/admin/deleteAccount'
import * as ComAtprotoAdminDisableAccountInvites from './types/com/atproto/admin/disableAccountInvites'
import * as ComAtprotoAdminDisableInviteCodes from './types/com/atproto/admin/disableInviteCodes'
import * as ComAtprotoAdminEnableAccountInvites from './types/com/atproto/admin/enableAccountInvites'
Expand Down Expand Up @@ -145,6 +146,7 @@ import * as AppBskyUnspeccedSearchActorsSkeleton from './types/app/bsky/unspecce
import * as AppBskyUnspeccedSearchPostsSkeleton from './types/app/bsky/unspecced/searchPostsSkeleton'

export * as ComAtprotoAdminDefs from './types/com/atproto/admin/defs'
export * as ComAtprotoAdminDeleteAccount from './types/com/atproto/admin/deleteAccount'
export * as ComAtprotoAdminDisableAccountInvites from './types/com/atproto/admin/disableAccountInvites'
export * as ComAtprotoAdminDisableInviteCodes from './types/com/atproto/admin/disableInviteCodes'
export * as ComAtprotoAdminEnableAccountInvites from './types/com/atproto/admin/enableAccountInvites'
Expand Down Expand Up @@ -369,6 +371,17 @@ export class AdminNS {
this._service = service
}

deleteAccount(
data?: ComAtprotoAdminDeleteAccount.InputSchema,
opts?: ComAtprotoAdminDeleteAccount.CallOptions,
): Promise<ComAtprotoAdminDeleteAccount.Response> {
return this._service.xrpc
.call('com.atproto.admin.deleteAccount', opts?.qp, data, opts)
.catch((e) => {
throw ComAtprotoAdminDeleteAccount.toKnownErr(e)
})
}

disableAccountInvites(
data?: ComAtprotoAdminDisableAccountInvites.InputSchema,
opts?: ComAtprotoAdminDisableAccountInvites.CallOptions,
Expand Down
24 changes: 24 additions & 0 deletions packages/api/src/client/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,29 @@ export const schemaDict = {
},
},
},
ComAtprotoAdminDeleteAccount: {
lexicon: 1,
id: 'com.atproto.admin.deleteAccount',
defs: {
main: {
type: 'procedure',
description: 'Delete a user account as an administrator.',
input: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['did'],
properties: {
did: {
type: 'string',
format: 'did',
},
},
},
},
},
},
},
ComAtprotoAdminDisableAccountInvites: {
lexicon: 1,
id: 'com.atproto.admin.disableAccountInvites',
Expand Down Expand Up @@ -7574,6 +7597,7 @@ export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[]
export const lexicons: Lexicons = new Lexicons(schemas)
export const ids = {
ComAtprotoAdminDefs: 'com.atproto.admin.defs',
ComAtprotoAdminDeleteAccount: 'com.atproto.admin.deleteAccount',
ComAtprotoAdminDisableAccountInvites:
'com.atproto.admin.disableAccountInvites',
ComAtprotoAdminDisableInviteCodes: 'com.atproto.admin.disableInviteCodes',
Expand Down
32 changes: 32 additions & 0 deletions packages/api/src/client/types/com/atproto/admin/deleteAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers, XRPCError } from '@atproto/xrpc'
import { ValidationResult, BlobRef } from '@atproto/lexicon'
import { isObj, hasProp } from '../../../../util'
import { lexicons } from '../../../../lexicons'
import { CID } from 'multiformats/cid'

export interface QueryParams {}

export interface InputSchema {
did: string
[k: string]: unknown
}

export interface CallOptions {
headers?: Headers
qp?: QueryParams
encoding: 'application/json'
}

export interface Response {
success: boolean
headers: Headers
}

export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}
12 changes: 12 additions & 0 deletions packages/bsky/src/lexicon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
StreamAuthVerifier,
} from '@atproto/xrpc-server'
import { schemas } from './lexicons'
import * as ComAtprotoAdminDeleteAccount from './types/com/atproto/admin/deleteAccount'
import * as ComAtprotoAdminDisableAccountInvites from './types/com/atproto/admin/disableAccountInvites'
import * as ComAtprotoAdminDisableInviteCodes from './types/com/atproto/admin/disableInviteCodes'
import * as ComAtprotoAdminEnableAccountInvites from './types/com/atproto/admin/enableAccountInvites'
Expand Down Expand Up @@ -195,6 +196,17 @@ export class AdminNS {
this._server = server
}

deleteAccount<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,
ComAtprotoAdminDeleteAccount.Handler<ExtractAuth<AV>>,
ComAtprotoAdminDeleteAccount.HandlerReqCtx<ExtractAuth<AV>>
>,
) {
const nsid = 'com.atproto.admin.deleteAccount' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}

disableAccountInvites<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,
Expand Down
24 changes: 24 additions & 0 deletions packages/bsky/src/lexicon/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,29 @@ export const schemaDict = {
},
},
},
ComAtprotoAdminDeleteAccount: {
lexicon: 1,
id: 'com.atproto.admin.deleteAccount',
defs: {
main: {
type: 'procedure',
description: 'Delete a user account as an administrator.',
input: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['did'],
properties: {
did: {
type: 'string',
format: 'did',
},
},
},
},
},
},
},
ComAtprotoAdminDisableAccountInvites: {
lexicon: 1,
id: 'com.atproto.admin.disableAccountInvites',
Expand Down Expand Up @@ -7574,6 +7597,7 @@ export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[]
export const lexicons: Lexicons = new Lexicons(schemas)
export const ids = {
ComAtprotoAdminDefs: 'com.atproto.admin.defs',
ComAtprotoAdminDeleteAccount: 'com.atproto.admin.deleteAccount',
ComAtprotoAdminDisableAccountInvites:
'com.atproto.admin.disableAccountInvites',
ComAtprotoAdminDisableInviteCodes: 'com.atproto.admin.disableInviteCodes',
Expand Down
38 changes: 38 additions & 0 deletions packages/bsky/src/lexicon/types/com/atproto/admin/deleteAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import express from 'express'
import { ValidationResult, BlobRef } from '@atproto/lexicon'
import { lexicons } from '../../../../lexicons'
import { isObj, hasProp } from '../../../../util'
import { CID } from 'multiformats/cid'
import { HandlerAuth } from '@atproto/xrpc-server'

export interface QueryParams {}

export interface InputSchema {
did: string
[k: string]: unknown
}

export interface HandlerInput {
encoding: 'application/json'
body: InputSchema
}

export interface HandlerError {
status: number
message?: string
}

export type HandlerOutput = HandlerError | void
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
auth: HA
params: QueryParams
input: HandlerInput
req: express.Request
res: express.Response
}
export type Handler<HA extends HandlerAuth = never> = (
ctx: HandlerReqCtx<HA>,
) => Promise<HandlerOutput> | HandlerOutput
4 changes: 3 additions & 1 deletion packages/pds/src/api/app/bsky/actor/putPreferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
import { UserPreference } from '../../../../services/account'
import { InvalidRequestError } from '@atproto/xrpc-server'
import { authPassthru, proxy } from '../../../proxy'
import { authPassthru, ensureThisPds, proxy } from '../../../proxy'

export default function (server: Server, ctx: AppContext) {
server.app.bsky.actor.putPreferences({
Expand All @@ -22,6 +22,8 @@ export default function (server: Server, ctx: AppContext) {
return proxied
}

ensureThisPds(ctx, auth.credentials.pdsDid)

const { preferences } = input.body
const requester = auth.credentials.did
const { services, db } = ctx
Expand Down
3 changes: 3 additions & 0 deletions packages/pds/src/api/com/atproto/moderation/createReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
import {
authPassthru,
ensureThisPds,
proxy,
proxyAppView,
resultPassthru,
Expand All @@ -26,6 +27,8 @@ export default function (server: Server, ctx: AppContext) {
return proxied
}

ensureThisPds(ctx, auth.credentials.pdsDid)

const requester = auth.credentials.did
const { data: result } = await proxyAppView(ctx, async (agent) =>
agent.com.atproto.moderation.createReport(input.body, {
Expand Down
65 changes: 53 additions & 12 deletions packages/pds/src/api/com/atproto/server/deleteAccount.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { AuthRequiredError } from '@atproto/xrpc-server'
import { MINUTE } from '@atproto/common'
import {
AuthRequiredError,
InvalidRequestError,
UpstreamFailureError,
} from '@atproto/xrpc-server'
import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
import { MINUTE } from '@atproto/common'
import { isThisPds } from '../../../proxy'
import { retryHttp } from '../../../../util/retry'

const REASON_ACCT_DELETION = 'account_deletion'

// @TODO negotiate account deletions between pds and entryway
export default function (server: Server, ctx: AppContext) {
server.com.atproto.server.deleteAccount({
rateLimit: {
Expand All @@ -14,19 +19,24 @@ export default function (server: Server, ctx: AppContext) {
},
handler: async ({ input, req }) => {
const { did, password, token } = input.body
const validPass = await ctx.services
.account(ctx.db)
.verifyAccountPassword(did, password)
const accountService = ctx.services.account(ctx.db)
const validPass = await accountService.verifyAccountPassword(
did,
password,
)
if (!validPass) {
throw new AuthRequiredError('Invalid did or password')
}

await ctx.services
.account(ctx.db)
.assertValidToken(did, 'delete_account', token)
const account = await accountService.getAccount(did, true)
if (!account) {
throw new InvalidRequestError('account not found', 'AccountNotFound')
}

await accountService.assertValidToken(did, 'delete_account', token)

await ctx.db.transaction(async (dbTxn) => {
const accountService = ctx.services.account(dbTxn)
const accountTxn = ctx.services.account(dbTxn)
const moderationTxn = ctx.services.moderation(dbTxn)
const currState = await moderationTxn.getRepoTakedownState(did)
// Do not disturb an existing takedown, continue with account deletion
Expand All @@ -36,15 +46,46 @@ export default function (server: Server, ctx: AppContext) {
ref: REASON_ACCT_DELETION,
})
}
await accountService.deleteEmailToken(did, 'delete_account')
await accountTxn.deleteEmailToken(did, 'delete_account')
})

const { pdsDid } = account
if (ctx.cfg.service.isEntryway && pdsDid && !isThisPds(ctx, pdsDid)) {
try {
const pds = await accountService.getPds(pdsDid, { cached: true })
if (!pds) {
throw new UpstreamFailureError('unknown pds')
}
// both entryway and pds behind it need to clean-up account state, then pds sequences tombstone.
// the long flow is: pds(server.deleteAccount) -> entryway(server.deleteAccount) -> pds(admin.deleteAccount)
const agent = ctx.pdsAgents.get(pds.host)
await retryHttp(() =>
agent.com.atproto.admin.deleteAccount(
{ did },
{
encoding: 'application/json',
headers: ctx.authVerifier.createAdminRoleHeaders(),
},
),
)
} catch (err) {
req.log.error(
{ did, pdsDid, err },
'account deletion failed on pds behind entryway',
)
}
}

ctx.backgroundQueue.add(async (db) => {
// in the background perform the hard account deletion work
try {
// In the background perform the hard account deletion work
await ctx.services.record(db).deleteForActor(did)
await ctx.services.repo(db).deleteRepo(did)
await ctx.services.account(db).deleteAccount(did)
if (!ctx.cfg.service.isEntryway || isThisPds(ctx, pdsDid)) {
// if this is the user's pds sequence the tombstone, otherwise taken care of by their pds behind the entryway.
await ctx.services.account(db).sequenceTombstone(did)
}
} catch (err) {
req.log.error({ did, err }, 'account deletion failed')
}
Expand Down
12 changes: 12 additions & 0 deletions packages/pds/src/lexicon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
StreamAuthVerifier,
} from '@atproto/xrpc-server'
import { schemas } from './lexicons'
import * as ComAtprotoAdminDeleteAccount from './types/com/atproto/admin/deleteAccount'
import * as ComAtprotoAdminDisableAccountInvites from './types/com/atproto/admin/disableAccountInvites'
import * as ComAtprotoAdminDisableInviteCodes from './types/com/atproto/admin/disableInviteCodes'
import * as ComAtprotoAdminEnableAccountInvites from './types/com/atproto/admin/enableAccountInvites'
Expand Down Expand Up @@ -195,6 +196,17 @@ export class AdminNS {
this._server = server
}

deleteAccount<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,
ComAtprotoAdminDeleteAccount.Handler<ExtractAuth<AV>>,
ComAtprotoAdminDeleteAccount.HandlerReqCtx<ExtractAuth<AV>>
>,
) {
const nsid = 'com.atproto.admin.deleteAccount' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}

disableAccountInvites<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,
Expand Down
Loading

0 comments on commit cca9f91

Please sign in to comment.