From 652c788d18393008d1e3093422b28756bd6f60e9 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Mon, 16 Dec 2024 17:29:00 +0000 Subject: [PATCH 01/12] :sparkles: Allow appeals on takendown account --- .../atproto/server/appealAccountAction.json | 35 +++++++ packages/api/src/client/index.ts | 14 +++ packages/api/src/client/lexicons.ts | 39 ++++++++ .../com/atproto/server/appealAccountAction.ts | 40 ++++++++ packages/bsky/src/lexicon/index.ts | 12 +++ packages/bsky/src/lexicon/lexicons.ts | 39 ++++++++ .../com/atproto/server/appealAccountAction.ts | 52 ++++++++++ packages/ozone/src/lexicon/index.ts | 12 +++ packages/ozone/src/lexicon/lexicons.ts | 39 ++++++++ .../com/atproto/server/appealAccountAction.ts | 52 ++++++++++ packages/pds/src/account-manager/index.ts | 10 +- .../com/atproto/server/appealAccountAction.ts | 79 +++++++++++++++ .../api/com/atproto/server/createSession.ts | 11 ++- .../pds/src/api/com/atproto/server/index.ts | 3 + packages/pds/src/lexicon/index.ts | 12 +++ packages/pds/src/lexicon/lexicons.ts | 39 ++++++++ .../com/atproto/server/appealAccountAction.ts | 52 ++++++++++ packages/pds/src/pipethrough.ts | 2 +- .../__snapshots__/account-appeal.test.ts.snap | 53 ++++++++++ packages/pds/tests/account-appeal.test.ts | 96 +++++++++++++++++++ packages/pds/tests/auth.test.ts | 47 ++++++--- 21 files changed, 716 insertions(+), 22 deletions(-) create mode 100644 lexicons/com/atproto/server/appealAccountAction.json create mode 100644 packages/api/src/client/types/com/atproto/server/appealAccountAction.ts create mode 100644 packages/bsky/src/lexicon/types/com/atproto/server/appealAccountAction.ts create mode 100644 packages/ozone/src/lexicon/types/com/atproto/server/appealAccountAction.ts create mode 100644 packages/pds/src/api/com/atproto/server/appealAccountAction.ts create mode 100644 packages/pds/src/lexicon/types/com/atproto/server/appealAccountAction.ts create mode 100644 packages/pds/tests/__snapshots__/account-appeal.test.ts.snap create mode 100644 packages/pds/tests/account-appeal.test.ts diff --git a/lexicons/com/atproto/server/appealAccountAction.json b/lexicons/com/atproto/server/appealAccountAction.json new file mode 100644 index 00000000000..f2b238813e3 --- /dev/null +++ b/lexicons/com/atproto/server/appealAccountAction.json @@ -0,0 +1,35 @@ +{ + "lexicon": 1, + "id": "com.atproto.server.appealAccountAction", + "defs": { + "main": { + "type": "procedure", + "description": "Appeal an account level moderation action", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["identifier", "password"], + "properties": { + "identifier": { + "type": "string", + "description": "Handle or other identifier supported by the server for the authenticating user." + }, + "password": { "type": "string" }, + "comment": { + "type": "string", + "description": "User's comment to be included with the appeal." + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": {} + } + } + } + } +} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index 9ee329fe593..e8d1a3718e0 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -43,6 +43,7 @@ import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord' import * as ComAtprotoRepoStrongRef from './types/com/atproto/repo/strongRef' import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' import * as ComAtprotoServerActivateAccount from './types/com/atproto/server/activateAccount' +import * as ComAtprotoServerAppealAccountAction from './types/com/atproto/server/appealAccountAction' import * as ComAtprotoServerCheckAccountStatus from './types/com/atproto/server/checkAccountStatus' import * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail' import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' @@ -270,6 +271,7 @@ export * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord' export * as ComAtprotoRepoStrongRef from './types/com/atproto/repo/strongRef' export * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' export * as ComAtprotoServerActivateAccount from './types/com/atproto/server/activateAccount' +export * as ComAtprotoServerAppealAccountAction from './types/com/atproto/server/appealAccountAction' export * as ComAtprotoServerCheckAccountStatus from './types/com/atproto/server/checkAccountStatus' export * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail' export * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' @@ -990,6 +992,18 @@ export class ComAtprotoServerNS { ) } + appealAccountAction( + data?: ComAtprotoServerAppealAccountAction.InputSchema, + opts?: ComAtprotoServerAppealAccountAction.CallOptions, + ): Promise { + return this._client.call( + 'com.atproto.server.appealAccountAction', + opts?.qp, + data, + opts, + ) + } + checkAccountStatus( params?: ComAtprotoServerCheckAccountStatus.QueryParams, opts?: ComAtprotoServerCheckAccountStatus.CallOptions, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 1e616f1b284..330e45d9a42 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -2018,6 +2018,44 @@ export const schemaDict = { }, }, }, + ComAtprotoServerAppealAccountAction: { + lexicon: 1, + id: 'com.atproto.server.appealAccountAction', + defs: { + main: { + type: 'procedure', + description: 'Appeal an account level moderation action', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['identifier', 'password'], + properties: { + identifier: { + type: 'string', + description: + 'Handle or other identifier supported by the server for the authenticating user.', + }, + password: { + type: 'string', + }, + comment: { + type: 'string', + description: "User's comment to be included with the appeal.", + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + properties: {}, + }, + }, + }, + }, + }, ComAtprotoServerCheckAccountStatus: { lexicon: 1, id: 'com.atproto.server.checkAccountStatus', @@ -13552,6 +13590,7 @@ export const ids = { ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', ComAtprotoServerActivateAccount: 'com.atproto.server.activateAccount', + ComAtprotoServerAppealAccountAction: 'com.atproto.server.appealAccountAction', ComAtprotoServerCheckAccountStatus: 'com.atproto.server.checkAccountStatus', ComAtprotoServerConfirmEmail: 'com.atproto.server.confirmEmail', ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount', diff --git a/packages/api/src/client/types/com/atproto/server/appealAccountAction.ts b/packages/api/src/client/types/com/atproto/server/appealAccountAction.ts new file mode 100644 index 00000000000..21db85d1ab5 --- /dev/null +++ b/packages/api/src/client/types/com/atproto/server/appealAccountAction.ts @@ -0,0 +1,40 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { HeadersMap, 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 { + /** Handle or other identifier supported by the server for the authenticating user. */ + identifier: string + password: string + /** User's comment to be included with the appeal. */ + comment?: string + [k: string]: unknown +} + +export interface OutputSchema { + [k: string]: unknown +} + +export interface CallOptions { + signal?: AbortSignal + headers?: HeadersMap + qp?: QueryParams + encoding?: 'application/json' +} + +export interface Response { + success: boolean + headers: HeadersMap + data: OutputSchema +} + +export function toKnownErr(e: any) { + return e +} diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index 7a8a13149ed..cc1f476eea8 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -43,6 +43,7 @@ import * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord' import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' import * as ComAtprotoServerActivateAccount from './types/com/atproto/server/activateAccount' +import * as ComAtprotoServerAppealAccountAction from './types/com/atproto/server/appealAccountAction' import * as ComAtprotoServerCheckAccountStatus from './types/com/atproto/server/checkAccountStatus' import * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail' import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' @@ -670,6 +671,17 @@ export class ComAtprotoServerNS { return this._server.xrpc.method(nsid, cfg) } + appealAccountAction( + cfg: ConfigOf< + AV, + ComAtprotoServerAppealAccountAction.Handler>, + ComAtprotoServerAppealAccountAction.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.appealAccountAction' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + checkAccountStatus( cfg: ConfigOf< AV, diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 140e617917b..80b68170415 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -2018,6 +2018,44 @@ export const schemaDict = { }, }, }, + ComAtprotoServerAppealAccountAction: { + lexicon: 1, + id: 'com.atproto.server.appealAccountAction', + defs: { + main: { + type: 'procedure', + description: 'Appeal an account level moderation action', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['identifier', 'password'], + properties: { + identifier: { + type: 'string', + description: + 'Handle or other identifier supported by the server for the authenticating user.', + }, + password: { + type: 'string', + }, + comment: { + type: 'string', + description: "User's comment to be included with the appeal.", + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + properties: {}, + }, + }, + }, + }, + }, ComAtprotoServerCheckAccountStatus: { lexicon: 1, id: 'com.atproto.server.checkAccountStatus', @@ -10786,6 +10824,7 @@ export const ids = { ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', ComAtprotoServerActivateAccount: 'com.atproto.server.activateAccount', + ComAtprotoServerAppealAccountAction: 'com.atproto.server.appealAccountAction', ComAtprotoServerCheckAccountStatus: 'com.atproto.server.checkAccountStatus', ComAtprotoServerConfirmEmail: 'com.atproto.server.confirmEmail', ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount', diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/appealAccountAction.ts b/packages/bsky/src/lexicon/types/com/atproto/server/appealAccountAction.ts new file mode 100644 index 00000000000..b6a484a09a4 --- /dev/null +++ b/packages/bsky/src/lexicon/types/com/atproto/server/appealAccountAction.ts @@ -0,0 +1,52 @@ +/** + * 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, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + /** Handle or other identifier supported by the server for the authenticating user. */ + identifier: string + password: string + /** User's comment to be included with the appeal. */ + comment?: string + [k: string]: unknown +} + +export interface OutputSchema { + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/ozone/src/lexicon/index.ts b/packages/ozone/src/lexicon/index.ts index b4c27f52018..88d92218009 100644 --- a/packages/ozone/src/lexicon/index.ts +++ b/packages/ozone/src/lexicon/index.ts @@ -43,6 +43,7 @@ import * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord' import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' import * as ComAtprotoServerActivateAccount from './types/com/atproto/server/activateAccount' +import * as ComAtprotoServerAppealAccountAction from './types/com/atproto/server/appealAccountAction' import * as ComAtprotoServerCheckAccountStatus from './types/com/atproto/server/checkAccountStatus' import * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail' import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' @@ -713,6 +714,17 @@ export class ComAtprotoServerNS { return this._server.xrpc.method(nsid, cfg) } + appealAccountAction( + cfg: ConfigOf< + AV, + ComAtprotoServerAppealAccountAction.Handler>, + ComAtprotoServerAppealAccountAction.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.appealAccountAction' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + checkAccountStatus( cfg: ConfigOf< AV, diff --git a/packages/ozone/src/lexicon/lexicons.ts b/packages/ozone/src/lexicon/lexicons.ts index 1e616f1b284..330e45d9a42 100644 --- a/packages/ozone/src/lexicon/lexicons.ts +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -2018,6 +2018,44 @@ export const schemaDict = { }, }, }, + ComAtprotoServerAppealAccountAction: { + lexicon: 1, + id: 'com.atproto.server.appealAccountAction', + defs: { + main: { + type: 'procedure', + description: 'Appeal an account level moderation action', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['identifier', 'password'], + properties: { + identifier: { + type: 'string', + description: + 'Handle or other identifier supported by the server for the authenticating user.', + }, + password: { + type: 'string', + }, + comment: { + type: 'string', + description: "User's comment to be included with the appeal.", + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + properties: {}, + }, + }, + }, + }, + }, ComAtprotoServerCheckAccountStatus: { lexicon: 1, id: 'com.atproto.server.checkAccountStatus', @@ -13552,6 +13590,7 @@ export const ids = { ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', ComAtprotoServerActivateAccount: 'com.atproto.server.activateAccount', + ComAtprotoServerAppealAccountAction: 'com.atproto.server.appealAccountAction', ComAtprotoServerCheckAccountStatus: 'com.atproto.server.checkAccountStatus', ComAtprotoServerConfirmEmail: 'com.atproto.server.confirmEmail', ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount', diff --git a/packages/ozone/src/lexicon/types/com/atproto/server/appealAccountAction.ts b/packages/ozone/src/lexicon/types/com/atproto/server/appealAccountAction.ts new file mode 100644 index 00000000000..b6a484a09a4 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/appealAccountAction.ts @@ -0,0 +1,52 @@ +/** + * 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, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + /** Handle or other identifier supported by the server for the authenticating user. */ + identifier: string + password: string + /** User's comment to be included with the appeal. */ + comment?: string + [k: string]: unknown +} + +export interface OutputSchema { + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/account-manager/index.ts b/packages/pds/src/account-manager/index.ts index 654b45ea94d..5691ed7bbfe 100644 --- a/packages/pds/src/account-manager/index.ts +++ b/packages/pds/src/account-manager/index.ts @@ -295,6 +295,7 @@ export class AccountManager }): Promise<{ user: ActorAccount appPassword: password.AppPassDescript | null + isSoftDeleted: boolean }> { const start = Date.now() try { @@ -326,14 +327,7 @@ export class AccountManager } } - if (softDeleted(user)) { - throw new AuthRequiredError( - 'Account has been taken down', - 'AccountTakedown', - ) - } - - return { user, appPassword } + return { user, appPassword, isSoftDeleted: softDeleted(user) } } finally { // Mitigate timing attacks await wait(350 - (Date.now() - start)) diff --git a/packages/pds/src/api/com/atproto/server/appealAccountAction.ts b/packages/pds/src/api/com/atproto/server/appealAccountAction.ts new file mode 100644 index 00000000000..2d6d6870954 --- /dev/null +++ b/packages/pds/src/api/com/atproto/server/appealAccountAction.ts @@ -0,0 +1,79 @@ +import { DAY, MINUTE } from '@atproto/common' +import { InvalidRequestError } from '@atproto/xrpc-server' +import { ComAtprotoModerationDefs } from '@atproto/api' + +import AppContext from '../../../../context' +import { Server } from '../../../../lexicon' +import { authPassthru, resultPassthru } from '../../../proxy' +import { parseProxyInfo } from '../../../../pipethrough' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.server.appealAccountAction({ + // @TODO: we probably will be fine with a very high rate limit on this endpoint + rateLimit: [ + { + durationMs: DAY, + points: 300, + calcKey: ({ input, req }) => `${input.body.identifier}-${req.ip}`, + }, + { + durationMs: 5 * MINUTE, + points: 30, + calcKey: ({ input, req }) => `${input.body.identifier}-${req.ip}`, + }, + ], + handler: async ({ input, req }) => { + if (ctx.entrywayAgent) { + return resultPassthru( + await ctx.entrywayAgent.com.atproto.server.appealAccountAction( + input.body, + authPassthru(req, true), + ), + ) + } + + const { user, isSoftDeleted } = await ctx.accountManager.login(input.body) + + // If user is not soft deleted, they should not be able to send appeals through this route + if (!isSoftDeleted) { + throw new InvalidRequestError( + 'No account action found', + 'InvalidAppeal', + ) + } + + // If no moderationAgent is configured, we can't route the appeal to the right mod instance + if (!ctx.moderationAgent) { + throw new InvalidRequestError( + 'Moderation service not configured', + 'InvalidAppeal', + ) + } + + // Create a service token just for the purpose of creating the appeal report and send the appeal using that token + const lxm = 'com.atproto.moderation.createReport' + const { did: aud } = await parseProxyInfo(ctx, req, lxm) + const serviceJwt = await ctx.serviceAuthJwt(user.did, aud, lxm) + await ctx.moderationAgent.com.atproto.moderation.createReport( + { + reasonType: ComAtprotoModerationDefs.REASONAPPEAL, + reason: input.body.comment, + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: user.did, + }, + }, + { + headers: { + Authorization: `Bearer ${serviceJwt}`, + }, + }, + ) + + return { + encoding: 'application/json', + body: {}, + } + }, + }) +} diff --git a/packages/pds/src/api/com/atproto/server/createSession.ts b/packages/pds/src/api/com/atproto/server/createSession.ts index 20ca89e6ef3..bc4fa877ded 100644 --- a/packages/pds/src/api/com/atproto/server/createSession.ts +++ b/packages/pds/src/api/com/atproto/server/createSession.ts @@ -1,5 +1,6 @@ import { DAY, MINUTE } from '@atproto/common' import { INVALID_HANDLE } from '@atproto/syntax' +import { AuthRequiredError } from '@atproto/xrpc-server' import { formatAccountStatus } from '../../../../account-manager' import AppContext from '../../../../context' @@ -31,7 +32,15 @@ export default function (server: Server, ctx: AppContext) { ) } - const { user, appPassword } = await ctx.accountManager.login(input.body) + const { user, isSoftDeleted, appPassword } = + await ctx.accountManager.login(input.body) + + if (isSoftDeleted) { + throw new AuthRequiredError( + 'Account has been taken down', + 'AccountTakedown', + ) + } const [{ accessJwt, refreshJwt }, didDoc] = await Promise.all([ ctx.accountManager.createSession(user.did, appPassword), diff --git a/packages/pds/src/api/com/atproto/server/index.ts b/packages/pds/src/api/com/atproto/server/index.ts index 7208f106b17..5d9f64fcf68 100644 --- a/packages/pds/src/api/com/atproto/server/index.ts +++ b/packages/pds/src/api/com/atproto/server/index.ts @@ -35,6 +35,8 @@ import checkAccountStatus from './checkAccountStatus' import activateAccount from './activateAccount' import deactivateAccount from './deactivateAccount' +import appealAccountAction from './appealAccountAction' + export default function (server: Server, ctx: AppContext) { describeServer(server, ctx) createAccount(server, ctx) @@ -61,4 +63,5 @@ export default function (server: Server, ctx: AppContext) { checkAccountStatus(server, ctx) activateAccount(server, ctx) deactivateAccount(server, ctx) + appealAccountAction(server, ctx) } diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index b4c27f52018..88d92218009 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -43,6 +43,7 @@ import * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord' import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' import * as ComAtprotoServerActivateAccount from './types/com/atproto/server/activateAccount' +import * as ComAtprotoServerAppealAccountAction from './types/com/atproto/server/appealAccountAction' import * as ComAtprotoServerCheckAccountStatus from './types/com/atproto/server/checkAccountStatus' import * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail' import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' @@ -713,6 +714,17 @@ export class ComAtprotoServerNS { return this._server.xrpc.method(nsid, cfg) } + appealAccountAction( + cfg: ConfigOf< + AV, + ComAtprotoServerAppealAccountAction.Handler>, + ComAtprotoServerAppealAccountAction.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.appealAccountAction' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + checkAccountStatus( cfg: ConfigOf< AV, diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 1e616f1b284..330e45d9a42 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -2018,6 +2018,44 @@ export const schemaDict = { }, }, }, + ComAtprotoServerAppealAccountAction: { + lexicon: 1, + id: 'com.atproto.server.appealAccountAction', + defs: { + main: { + type: 'procedure', + description: 'Appeal an account level moderation action', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['identifier', 'password'], + properties: { + identifier: { + type: 'string', + description: + 'Handle or other identifier supported by the server for the authenticating user.', + }, + password: { + type: 'string', + }, + comment: { + type: 'string', + description: "User's comment to be included with the appeal.", + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + properties: {}, + }, + }, + }, + }, + }, ComAtprotoServerCheckAccountStatus: { lexicon: 1, id: 'com.atproto.server.checkAccountStatus', @@ -13552,6 +13590,7 @@ export const ids = { ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', ComAtprotoServerActivateAccount: 'com.atproto.server.activateAccount', + ComAtprotoServerAppealAccountAction: 'com.atproto.server.appealAccountAction', ComAtprotoServerCheckAccountStatus: 'com.atproto.server.checkAccountStatus', ComAtprotoServerConfirmEmail: 'com.atproto.server.confirmEmail', ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount', diff --git a/packages/pds/src/lexicon/types/com/atproto/server/appealAccountAction.ts b/packages/pds/src/lexicon/types/com/atproto/server/appealAccountAction.ts new file mode 100644 index 00000000000..b6a484a09a4 --- /dev/null +++ b/packages/pds/src/lexicon/types/com/atproto/server/appealAccountAction.ts @@ -0,0 +1,52 @@ +/** + * 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, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + /** Handle or other identifier supported by the server for the authenticating user. */ + identifier: string + password: string + /** User's comment to be included with the appeal. */ + comment?: string + [k: string]: unknown +} + +export interface OutputSchema { + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/pipethrough.ts b/packages/pds/src/pipethrough.ts index b326d5fba8b..deca77c0a75 100644 --- a/packages/pds/src/pipethrough.ts +++ b/packages/pds/src/pipethrough.ts @@ -207,7 +207,7 @@ export async function pipethrough( // Request setup/formatting // ------------------- -async function parseProxyInfo( +export async function parseProxyInfo( ctx: AppContext, req: express.Request, lxm: string, diff --git a/packages/pds/tests/__snapshots__/account-appeal.test.ts.snap b/packages/pds/tests/__snapshots__/account-appeal.test.ts.snap new file mode 100644 index 00000000000..7ac193ec17e --- /dev/null +++ b/packages/pds/tests/__snapshots__/account-appeal.test.ts.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`account action appeal actor takedown allows appeal request. 1`] = ` +Object { + "appealed": true, + "createdAt": "1970-01-01T00:00:00.000Z", + "hosting": Object { + "$type": "tools.ozone.moderation.defs#accountHosting", + "status": "unknown", + }, + "id": 9, + "lastAppealedAt": "1970-01-01T00:00:00.000Z", + "lastReportedAt": "1970-01-01T00:00:00.000Z", + "reviewState": "tools.ozone.moderation.defs#reviewEscalated", + "subject": Object { + "$type": "com.atproto.admin.defs#repoRef", + "did": "user(0)", + }, + "subjectBlobCids": Array [], + "subjectRepoHandle": "handle.invalid", + "tags": Array [ + "lang:und", + ], + "takendown": false, + "updatedAt": "1970-01-01T00:00:00.000Z", +} +`; + +exports[`auth actor takedown allows appeal request. 1`] = ` +Object { + "appealed": true, + "createdAt": "1970-01-01T00:00:00.000Z", + "hosting": Object { + "$type": "tools.ozone.moderation.defs#accountHosting", + "status": "unknown", + }, + "id": 7, + "lastAppealedAt": "1970-01-01T00:00:00.000Z", + "lastReportedAt": "1970-01-01T00:00:00.000Z", + "reviewState": "tools.ozone.moderation.defs#reviewEscalated", + "subject": Object { + "$type": "com.atproto.admin.defs#repoRef", + "did": "user(0)", + }, + "subjectBlobCids": Array [], + "subjectRepoHandle": "handle.invalid", + "tags": Array [ + "lang:und", + ], + "takendown": false, + "updatedAt": "1970-01-01T00:00:00.000Z", +} +`; diff --git a/packages/pds/tests/account-appeal.test.ts b/packages/pds/tests/account-appeal.test.ts new file mode 100644 index 00000000000..3fc17823e5a --- /dev/null +++ b/packages/pds/tests/account-appeal.test.ts @@ -0,0 +1,96 @@ +import { AtpAgent } from '@atproto/api' +import { SeedClient, TestNetwork } from '@atproto/dev-env' +import { forSnapshot } from './_util' + +describe('account action appeal', () => { + let network: TestNetwork + let agent: AtpAgent + let sc: SeedClient + let moderator: string + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'auth', + }) + sc = network.getSeedClient() + const modAccount = await sc.createAccount('moderator', { + handle: 'testmod.test', + email: 'testmod@test.com', + password: 'testmod-pass', + }) + moderator = modAccount.did + await network.ozone.addModeratorDid(moderator) + + agent = network.pds.getClient() + }) + + afterAll(async () => { + await network.close() + }) + + it('actor takedown allows appeal request.', async () => { + const { data: account } = await agent.com.atproto.server.createAccount({ + handle: 'jeff.test', + email: 'jeff@test.com', + password: 'password', + }) + + // Manually set the account as takendown at the PDS level + await agent.com.atproto.admin.updateSubjectStatus( + { + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: account.did, + }, + takedown: { applied: true }, + }, + { + encoding: 'application/json', + headers: { authorization: network.pds.adminAuth() }, + }, + ) + + // Verify user can not get session token + await expect( + agent.com.atproto.server.createSession({ + identifier: 'jeff.test', + password: 'password', + }), + ).rejects.toThrow('Account has been taken down') + + // send appeal event as the takendown account + await agent.com.atproto.server.appealAccountAction({ + identifier: 'jeff.test', + password: 'password', + comment: 'I want my account back', + }) + + // Verify that the appeal was created + const { data: result } = await agent.tools.ozone.moderation.queryStatuses( + { + subject: account.did, + }, + { headers: sc.getHeaders(moderator) }, + ) + + expect(result.subjectStatuses[0].appealed).toBe(true) + expect(forSnapshot(result.subjectStatuses[0])).toMatchSnapshot() + }) + + it('does not allow appeal from active account.', async () => { + await agent.com.atproto.server.createAccount({ + handle: 'jeff2.test', + email: 'jeff2@test.com', + password: 'password', + }) + + // send appeal event as the takendown account + await expect( + agent.com.atproto.server.appealAccountAction({ + identifier: 'jeff2.test', + password: 'password', + comment: 'I want my account back', + }), + ).rejects.toThrow('No account action found') + }) +}) diff --git a/packages/pds/tests/auth.test.ts b/packages/pds/tests/auth.test.ts index 507f835e036..139825b3b84 100644 --- a/packages/pds/tests/auth.test.ts +++ b/packages/pds/tests/auth.test.ts @@ -1,6 +1,6 @@ import * as jose from 'jose' import { AtpAgent } from '@atproto/api' -import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' +import { TestNetworkNoAppView, SeedClient, TestNetwork } from '@atproto/dev-env' import { createRefreshToken } from '../src/account-manager/helpers/auth' describe('auth', () => { @@ -8,7 +8,7 @@ describe('auth', () => { let agent: AtpAgent beforeAll(async () => { - network = await TestNetworkNoAppView.create({ + network = await TestNetwork.create({ dbPostgresSchema: 'auth', }) agent = network.pds.getClient() @@ -19,11 +19,11 @@ describe('auth', () => { }) const createAccount = async (info) => { - const { data } = await agent.api.com.atproto.server.createAccount(info) + const { data } = await agent.com.atproto.server.createAccount(info) return data } const getSession = async (jwt) => { - const { data } = await agent.api.com.atproto.server.getSession( + const { data } = await agent.com.atproto.server.getSession( {}, { headers: SeedClient.getHeaders(jwt), @@ -32,19 +32,18 @@ describe('auth', () => { return data } const createSession = async (info) => { - const { data } = await agent.api.com.atproto.server.createSession(info) + const { data } = await agent.com.atproto.server.createSession(info) return data } const deleteSession = async (jwt) => { - await agent.api.com.atproto.server.deleteSession(undefined, { + await agent.com.atproto.server.deleteSession(undefined, { headers: SeedClient.getHeaders(jwt), }) } const refreshSession = async (jwt: string) => { - const { data } = await agent.api.com.atproto.server.refreshSession( - undefined, - { headers: SeedClient.getHeaders(jwt) }, - ) + const { data } = await agent.com.atproto.server.refreshSession(undefined, { + headers: SeedClient.getHeaders(jwt), + }) return data } @@ -269,7 +268,7 @@ describe('auth', () => { email: 'iris@test.com', password: 'password', }) - await agent.api.com.atproto.admin.updateSubjectStatus( + await agent.com.atproto.admin.updateSubjectStatus( { subject: { $type: 'com.atproto.admin.defs#repoRef', @@ -295,7 +294,31 @@ describe('auth', () => { email: 'jared@test.com', password: 'password', }) - await agent.api.com.atproto.admin.updateSubjectStatus( + await agent.com.atproto.admin.updateSubjectStatus( + { + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: account.did, + }, + takedown: { applied: true }, + }, + { + encoding: 'application/json', + headers: { authorization: network.pds.adminAuth() }, + }, + ) + await expect(refreshSession(account.refreshJwt)).rejects.toMatchObject({ + error: 'AccountTakedown', + }) + }) + + it('actor takedown disallows refresh session.', async () => { + const account = await createAccount({ + handle: 'jared.test', + email: 'jared@test.com', + password: 'password', + }) + await agent.com.atproto.admin.updateSubjectStatus( { subject: { $type: 'com.atproto.admin.defs#repoRef', From c7df8ebaf33fe1ccd3e194f5b9bcd444ba61efea Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Mon, 16 Dec 2024 18:33:01 +0000 Subject: [PATCH 02/12] :white_check_mark: Update snapshot --- .../__snapshots__/account-appeal.test.ts.snap | 28 +------------------ 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/packages/pds/tests/__snapshots__/account-appeal.test.ts.snap b/packages/pds/tests/__snapshots__/account-appeal.test.ts.snap index 7ac193ec17e..86d2574802b 100644 --- a/packages/pds/tests/__snapshots__/account-appeal.test.ts.snap +++ b/packages/pds/tests/__snapshots__/account-appeal.test.ts.snap @@ -8,33 +8,7 @@ Object { "$type": "tools.ozone.moderation.defs#accountHosting", "status": "unknown", }, - "id": 9, - "lastAppealedAt": "1970-01-01T00:00:00.000Z", - "lastReportedAt": "1970-01-01T00:00:00.000Z", - "reviewState": "tools.ozone.moderation.defs#reviewEscalated", - "subject": Object { - "$type": "com.atproto.admin.defs#repoRef", - "did": "user(0)", - }, - "subjectBlobCids": Array [], - "subjectRepoHandle": "handle.invalid", - "tags": Array [ - "lang:und", - ], - "takendown": false, - "updatedAt": "1970-01-01T00:00:00.000Z", -} -`; - -exports[`auth actor takedown allows appeal request. 1`] = ` -Object { - "appealed": true, - "createdAt": "1970-01-01T00:00:00.000Z", - "hosting": Object { - "$type": "tools.ozone.moderation.defs#accountHosting", - "status": "unknown", - }, - "id": 7, + "id": 1, "lastAppealedAt": "1970-01-01T00:00:00.000Z", "lastReportedAt": "1970-01-01T00:00:00.000Z", "reviewState": "tools.ozone.moderation.defs#reviewEscalated", From ce68d522d48d9af0dc1729fa8275e41f10fddc01 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Mon, 16 Dec 2024 21:32:47 +0000 Subject: [PATCH 03/12] :white_check_mark: Remove duplicate test --- packages/pds/tests/auth.test.ts | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/packages/pds/tests/auth.test.ts b/packages/pds/tests/auth.test.ts index 139825b3b84..4c930f7c097 100644 --- a/packages/pds/tests/auth.test.ts +++ b/packages/pds/tests/auth.test.ts @@ -1,6 +1,6 @@ import * as jose from 'jose' import { AtpAgent } from '@atproto/api' -import { TestNetworkNoAppView, SeedClient, TestNetwork } from '@atproto/dev-env' +import { SeedClient, TestNetworkNoAppView } from '@atproto/dev-env' import { createRefreshToken } from '../src/account-manager/helpers/auth' describe('auth', () => { @@ -8,7 +8,7 @@ describe('auth', () => { let agent: AtpAgent beforeAll(async () => { - network = await TestNetwork.create({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'auth', }) agent = network.pds.getClient() @@ -311,28 +311,4 @@ describe('auth', () => { error: 'AccountTakedown', }) }) - - it('actor takedown disallows refresh session.', async () => { - const account = await createAccount({ - handle: 'jared.test', - email: 'jared@test.com', - password: 'password', - }) - await agent.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: account.did, - }, - takedown: { applied: true }, - }, - { - encoding: 'application/json', - headers: { authorization: network.pds.adminAuth() }, - }, - ) - await expect(refreshSession(account.refreshJwt)).rejects.toMatchObject({ - error: 'AccountTakedown', - }) - }) }) From 77b1e5b7516867ce70e46b66c07fed7997daa3d6 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Tue, 17 Dec 2024 20:08:47 +0000 Subject: [PATCH 04/12] :sparkles: Respond with takendown token from createSession for takendown accounts --- .../com/atproto/server/createSession.json | 3 +- packages/api/src/client/lexicons.ts | 3 + .../types/com/atproto/server/createSession.ts | 1 + packages/bsky/src/lexicon/lexicons.ts | 3 + .../types/com/atproto/server/createSession.ts | 1 + packages/ozone/src/api/report/createReport.ts | 43 ++++++++++++- packages/ozone/src/lexicon/lexicons.ts | 3 + .../types/com/atproto/server/createSession.ts | 1 + packages/pds/src/account-manager/index.ts | 12 +++- .../api/com/atproto/server/createSession.ts | 4 +- packages/pds/src/auth-verifier.ts | 1 + packages/pds/src/lexicon/lexicons.ts | 3 + .../types/com/atproto/server/createSession.ts | 1 + packages/pds/src/pipethrough.ts | 15 ++++- ...t.ts.snap => takedown-appeal.test.ts.snap} | 10 +-- ...appeal.test.ts => takedown-appeal.test.ts} | 64 ++++++++++++++----- 16 files changed, 139 insertions(+), 29 deletions(-) rename packages/pds/tests/__snapshots__/{account-appeal.test.ts.snap => takedown-appeal.test.ts.snap} (71%) rename packages/pds/tests/{account-appeal.test.ts => takedown-appeal.test.ts} (56%) diff --git a/lexicons/com/atproto/server/createSession.json b/lexicons/com/atproto/server/createSession.json index fd0fae38d31..4657001b5c2 100644 --- a/lexicons/com/atproto/server/createSession.json +++ b/lexicons/com/atproto/server/createSession.json @@ -16,7 +16,8 @@ "description": "Handle or other identifier supported by the server for the authenticating user." }, "password": { "type": "string" }, - "authFactorToken": { "type": "string" } + "authFactorToken": { "type": "string" }, + "allowTakendown": { "type": "boolean" } } } }, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 330e45d9a42..6dcb4fa3a3c 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -2450,6 +2450,9 @@ export const schemaDict = { authFactorToken: { type: 'string', }, + allowTakendown: { + type: 'boolean', + }, }, }, }, diff --git a/packages/api/src/client/types/com/atproto/server/createSession.ts b/packages/api/src/client/types/com/atproto/server/createSession.ts index 3ac1194b36d..684b7e4583d 100644 --- a/packages/api/src/client/types/com/atproto/server/createSession.ts +++ b/packages/api/src/client/types/com/atproto/server/createSession.ts @@ -14,6 +14,7 @@ export interface InputSchema { identifier: string password: string authFactorToken?: string + allowTakendown?: boolean [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 80b68170415..3d700df79b7 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -2450,6 +2450,9 @@ export const schemaDict = { authFactorToken: { type: 'string', }, + allowTakendown: { + type: 'boolean', + }, }, }, }, diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts b/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts index 96f7d79d5bc..7bf7ce0ba99 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts @@ -15,6 +15,7 @@ export interface InputSchema { identifier: string password: string authFactorToken?: string + allowTakendown?: boolean [k: string]: unknown } diff --git a/packages/ozone/src/api/report/createReport.ts b/packages/ozone/src/api/report/createReport.ts index dc76b22763a..a3b3ab7e6c9 100644 --- a/packages/ozone/src/api/report/createReport.ts +++ b/packages/ozone/src/api/report/createReport.ts @@ -2,9 +2,13 @@ import { Server } from '../../lexicon' import AppContext from '../../context' import { getReasonType } from '../util' import { subjectFromInput } from '../../mod-service/subject' -import { REASONAPPEAL } from '../../lexicon/types/com/atproto/moderation/defs' +import { + REASONAPPEAL, + ReasonType, +} from '../../lexicon/types/com/atproto/moderation/defs' import { ForbiddenError } from '@atproto/xrpc-server' import { TagService } from '../../tag-service' +import { ModerationService } from '../../mod-service' export default function (server: Server, ctx: AppContext) { server.com.atproto.moderation.createReport({ @@ -21,6 +25,9 @@ export default function (server: Server, ctx: AppContext) { } const db = ctx.db + + await assertValidReporter(ctx.modService(db), reasonType, requester) + const report = await db.transaction(async (dbTxn) => { const moderationTxn = ctx.modService(dbTxn) const { event: reportEvent, subjectStatus } = @@ -50,3 +57,37 @@ export default function (server: Server, ctx: AppContext) { }, }) } + +const assertValidReporter = async ( + modService: ModerationService, + reasonType: ReasonType, + did: string, +) => { + const reporterStatus = await modService.getCurrentStatus({ did }) + + // If we don't have a mod status for the reporter, no need to do further checks + if (!reporterStatus.length) { + return + } + + // For appeals, we just need to make sure that the account does not have pending appeal + if (reasonType === REASONAPPEAL) { + if (reporterStatus[0]?.appealed) { + throw new ForbiddenError( + 'Awaiting decision on previous appeal', + 'AlreadyAppealed', + ) + } + return + } + + // For non appeals, we need to make sure the reporter account is not already in takendown status + // This is necessary because we allow takendown accounts call createReport but that's only meant for appeals + // and we need to make sure takendown accounts don't abuse this endpoint + if (reporterStatus[0]?.takendown) { + throw new ForbiddenError( + 'Report not accepted from takendown account', + 'AccountTakedown', + ) + } +} diff --git a/packages/ozone/src/lexicon/lexicons.ts b/packages/ozone/src/lexicon/lexicons.ts index 330e45d9a42..6dcb4fa3a3c 100644 --- a/packages/ozone/src/lexicon/lexicons.ts +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -2450,6 +2450,9 @@ export const schemaDict = { authFactorToken: { type: 'string', }, + allowTakendown: { + type: 'boolean', + }, }, }, }, diff --git a/packages/ozone/src/lexicon/types/com/atproto/server/createSession.ts b/packages/ozone/src/lexicon/types/com/atproto/server/createSession.ts index 96f7d79d5bc..7bf7ce0ba99 100644 --- a/packages/ozone/src/lexicon/types/com/atproto/server/createSession.ts +++ b/packages/ozone/src/lexicon/types/com/atproto/server/createSession.ts @@ -15,6 +15,7 @@ export interface InputSchema { identifier: string password: string authFactorToken?: string + allowTakendown?: boolean [k: string]: unknown } diff --git a/packages/pds/src/account-manager/index.ts b/packages/pds/src/account-manager/index.ts index 5691ed7bbfe..5970b91c5f2 100644 --- a/packages/pds/src/account-manager/index.ts +++ b/packages/pds/src/account-manager/index.ts @@ -211,15 +211,21 @@ export class AccountManager async createSession( did: string, appPassword: password.AppPassDescript | null, + isSoftDeleted = false, ) { const { accessJwt, refreshJwt } = await auth.createTokens({ did, jwtKey: this.jwtKey, serviceDid: this.serviceDid, - scope: auth.formatScope(appPassword), + scope: isSoftDeleted + ? AuthScope.Takendown + : auth.formatScope(appPassword), }) - const refreshPayload = auth.decodeRefreshToken(refreshJwt) - await auth.storeRefreshToken(this.db, refreshPayload, appPassword) + // For soft deleted accounts don't store refresh token so that it can't be rotated. + if (!isSoftDeleted) { + const refreshPayload = auth.decodeRefreshToken(refreshJwt) + await auth.storeRefreshToken(this.db, refreshPayload, appPassword) + } return { accessJwt, refreshJwt } } diff --git a/packages/pds/src/api/com/atproto/server/createSession.ts b/packages/pds/src/api/com/atproto/server/createSession.ts index bc4fa877ded..3b15cde409f 100644 --- a/packages/pds/src/api/com/atproto/server/createSession.ts +++ b/packages/pds/src/api/com/atproto/server/createSession.ts @@ -35,7 +35,7 @@ export default function (server: Server, ctx: AppContext) { const { user, isSoftDeleted, appPassword } = await ctx.accountManager.login(input.body) - if (isSoftDeleted) { + if (!input.body.allowTakendown && isSoftDeleted) { throw new AuthRequiredError( 'Account has been taken down', 'AccountTakedown', @@ -43,7 +43,7 @@ export default function (server: Server, ctx: AppContext) { } const [{ accessJwt, refreshJwt }, didDoc] = await Promise.all([ - ctx.accountManager.createSession(user.did, appPassword), + ctx.accountManager.createSession(user.did, appPassword, isSoftDeleted), didDocForSession(ctx, user.did), ]) diff --git a/packages/pds/src/auth-verifier.ts b/packages/pds/src/auth-verifier.ts index 162d977b40f..22bfc7d7bd6 100644 --- a/packages/pds/src/auth-verifier.ts +++ b/packages/pds/src/auth-verifier.ts @@ -32,6 +32,7 @@ export enum AuthScope { AppPass = 'com.atproto.appPass', AppPassPrivileged = 'com.atproto.appPassPrivileged', SignupQueued = 'com.atproto.signupQueued', + Takendown = 'com.atproto.takendown', } export type AccessOpts = { diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 330e45d9a42..6dcb4fa3a3c 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -2450,6 +2450,9 @@ export const schemaDict = { authFactorToken: { type: 'string', }, + allowTakendown: { + type: 'boolean', + }, }, }, }, diff --git a/packages/pds/src/lexicon/types/com/atproto/server/createSession.ts b/packages/pds/src/lexicon/types/com/atproto/server/createSession.ts index 96f7d79d5bc..7bf7ce0ba99 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/createSession.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/createSession.ts @@ -15,6 +15,7 @@ export interface InputSchema { identifier: string password: string authFactorToken?: string + allowTakendown?: boolean [k: string]: unknown } diff --git a/packages/pds/src/pipethrough.ts b/packages/pds/src/pipethrough.ts index deca77c0a75..183dc3280c7 100644 --- a/packages/pds/src/pipethrough.ts +++ b/packages/pds/src/pipethrough.ts @@ -23,12 +23,23 @@ import { import AppContext from './context' import { ids } from './lexicon/lexicons' import { httpLogger } from './logger' +import { AuthScope } from './auth-verifier' + +const getStandardAccess = (ctx: AppContext, path: string) => { + const additionalAccess: AuthScope[] = [] + if (path.startsWith(`/xrpc/${ids.ComAtprotoModerationCreateReport}`)) { + additionalAccess.push(AuthScope.Takendown) + } + + return ctx.authVerifier.accessStandard({ + additional: additionalAccess, + }) +} export const proxyHandler = (ctx: AppContext): CatchallHandler => { - const accessStandard = ctx.authVerifier.accessStandard() return async (req, res, next) => { // /!\ Hot path - + const accessStandard = getStandardAccess(ctx, req.originalUrl) try { if ( req.method !== 'GET' && diff --git a/packages/pds/tests/__snapshots__/account-appeal.test.ts.snap b/packages/pds/tests/__snapshots__/takedown-appeal.test.ts.snap similarity index 71% rename from packages/pds/tests/__snapshots__/account-appeal.test.ts.snap rename to packages/pds/tests/__snapshots__/takedown-appeal.test.ts.snap index 86d2574802b..ab542bb4d38 100644 --- a/packages/pds/tests/__snapshots__/account-appeal.test.ts.snap +++ b/packages/pds/tests/__snapshots__/takedown-appeal.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`account action appeal actor takedown allows appeal request. 1`] = ` +exports[`appeal account takedown actor takedown allows appeal request. 1`] = ` Object { "appealed": true, "createdAt": "1970-01-01T00:00:00.000Z", @@ -11,17 +11,19 @@ Object { "id": 1, "lastAppealedAt": "1970-01-01T00:00:00.000Z", "lastReportedAt": "1970-01-01T00:00:00.000Z", + "lastReviewedAt": "1970-01-01T00:00:00.000Z", + "lastReviewedBy": "user(0)", "reviewState": "tools.ozone.moderation.defs#reviewEscalated", "subject": Object { "$type": "com.atproto.admin.defs#repoRef", - "did": "user(0)", + "did": "user(1)", }, "subjectBlobCids": Array [], - "subjectRepoHandle": "handle.invalid", + "subjectRepoHandle": "jeff.test", "tags": Array [ "lang:und", ], - "takendown": false, + "takendown": true, "updatedAt": "1970-01-01T00:00:00.000Z", } `; diff --git a/packages/pds/tests/account-appeal.test.ts b/packages/pds/tests/takedown-appeal.test.ts similarity index 56% rename from packages/pds/tests/account-appeal.test.ts rename to packages/pds/tests/takedown-appeal.test.ts index 3fc17823e5a..9cba9703b2d 100644 --- a/packages/pds/tests/account-appeal.test.ts +++ b/packages/pds/tests/takedown-appeal.test.ts @@ -1,8 +1,8 @@ -import { AtpAgent } from '@atproto/api' +import { AtpAgent, ComAtprotoModerationDefs } from '@atproto/api' import { SeedClient, TestNetwork } from '@atproto/dev-env' import { forSnapshot } from './_util' -describe('account action appeal', () => { +describe('appeal account takedown', () => { let network: TestNetwork let agent: AtpAgent let sc: SeedClient @@ -35,7 +35,16 @@ describe('account action appeal', () => { password: 'password', }) + // Emit a takedown event + await network.ozone.getModClient().performTakedown({ + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: account.did, + }, + }) + // Manually set the account as takendown at the PDS level + // since the takedown event only propagates when the daemon is running await agent.com.atproto.admin.updateSubjectStatus( { subject: { @@ -50,7 +59,7 @@ describe('account action appeal', () => { }, ) - // Verify user can not get session token + // Verify user can not get session token without setting the optional param await expect( agent.com.atproto.server.createSession({ identifier: 'jeff.test', @@ -58,13 +67,26 @@ describe('account action appeal', () => { }), ).rejects.toThrow('Account has been taken down') - // send appeal event as the takendown account - await agent.com.atproto.server.appealAccountAction({ + const { data: auth } = await agent.com.atproto.server.createSession({ identifier: 'jeff.test', password: 'password', - comment: 'I want my account back', + allowTakendown: true, }) + // send appeal event as the takendown account + await agent.com.atproto.moderation.createReport( + { + reasonType: ComAtprotoModerationDefs.REASONAPPEAL, + reason: 'I want my account back', + subject: { $type: 'com.atproto.admin.defs#repoRef', did: account.did }, + }, + { + headers: { + authorization: `Bearer ${auth.accessJwt}`, + }, + }, + ) + // Verify that the appeal was created const { data: result } = await agent.tools.ozone.moderation.queryStatuses( { @@ -77,20 +99,30 @@ describe('account action appeal', () => { expect(forSnapshot(result.subjectStatuses[0])).toMatchSnapshot() }) - it('does not allow appeal from active account.', async () => { - await agent.com.atproto.server.createAccount({ - handle: 'jeff2.test', - email: 'jeff2@test.com', + it('takendown actor is not allowed to create reports.', async () => { + const { data: auth } = await agent.com.atproto.server.createSession({ + identifier: 'jeff.test', password: 'password', + allowTakendown: true, }) // send appeal event as the takendown account await expect( - agent.com.atproto.server.appealAccountAction({ - identifier: 'jeff2.test', - password: 'password', - comment: 'I want my account back', - }), - ).rejects.toThrow('No account action found') + agent.com.atproto.moderation.createReport( + { + reasonType: ComAtprotoModerationDefs.REASONRUDE, + reason: 'reporting others', + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: 'did:plc:test', + }, + }, + { + headers: { + authorization: `Bearer ${auth.accessJwt}`, + }, + }, + ), + ).rejects.toThrow('Report not accepted from takendown account') }) }) From d0e26acf17c22598ad27743720f31a1a93344f86 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Tue, 17 Dec 2024 20:11:37 +0000 Subject: [PATCH 05/12] :broom: cleanup appeal account action stuff --- .../atproto/server/appealAccountAction.json | 35 -------- packages/api/src/client/index.ts | 14 ---- packages/api/src/client/lexicons.ts | 39 --------- .../com/atproto/server/appealAccountAction.ts | 40 ---------- packages/bsky/src/lexicon/index.ts | 12 --- packages/bsky/src/lexicon/lexicons.ts | 39 --------- .../com/atproto/server/appealAccountAction.ts | 52 ------------ packages/ozone/src/lexicon/index.ts | 12 --- packages/ozone/src/lexicon/lexicons.ts | 39 --------- .../com/atproto/server/appealAccountAction.ts | 52 ------------ .../com/atproto/server/appealAccountAction.ts | 79 ------------------- .../pds/src/api/com/atproto/server/index.ts | 3 - packages/pds/src/lexicon/index.ts | 12 --- packages/pds/src/lexicon/lexicons.ts | 39 --------- .../com/atproto/server/appealAccountAction.ts | 52 ------------ 15 files changed, 519 deletions(-) delete mode 100644 lexicons/com/atproto/server/appealAccountAction.json delete mode 100644 packages/api/src/client/types/com/atproto/server/appealAccountAction.ts delete mode 100644 packages/bsky/src/lexicon/types/com/atproto/server/appealAccountAction.ts delete mode 100644 packages/ozone/src/lexicon/types/com/atproto/server/appealAccountAction.ts delete mode 100644 packages/pds/src/api/com/atproto/server/appealAccountAction.ts delete mode 100644 packages/pds/src/lexicon/types/com/atproto/server/appealAccountAction.ts diff --git a/lexicons/com/atproto/server/appealAccountAction.json b/lexicons/com/atproto/server/appealAccountAction.json deleted file mode 100644 index f2b238813e3..00000000000 --- a/lexicons/com/atproto/server/appealAccountAction.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "lexicon": 1, - "id": "com.atproto.server.appealAccountAction", - "defs": { - "main": { - "type": "procedure", - "description": "Appeal an account level moderation action", - "input": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["identifier", "password"], - "properties": { - "identifier": { - "type": "string", - "description": "Handle or other identifier supported by the server for the authenticating user." - }, - "password": { "type": "string" }, - "comment": { - "type": "string", - "description": "User's comment to be included with the appeal." - } - } - } - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "properties": {} - } - } - } - } -} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index e8d1a3718e0..9ee329fe593 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -43,7 +43,6 @@ import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord' import * as ComAtprotoRepoStrongRef from './types/com/atproto/repo/strongRef' import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' import * as ComAtprotoServerActivateAccount from './types/com/atproto/server/activateAccount' -import * as ComAtprotoServerAppealAccountAction from './types/com/atproto/server/appealAccountAction' import * as ComAtprotoServerCheckAccountStatus from './types/com/atproto/server/checkAccountStatus' import * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail' import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' @@ -271,7 +270,6 @@ export * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord' export * as ComAtprotoRepoStrongRef from './types/com/atproto/repo/strongRef' export * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' export * as ComAtprotoServerActivateAccount from './types/com/atproto/server/activateAccount' -export * as ComAtprotoServerAppealAccountAction from './types/com/atproto/server/appealAccountAction' export * as ComAtprotoServerCheckAccountStatus from './types/com/atproto/server/checkAccountStatus' export * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail' export * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' @@ -992,18 +990,6 @@ export class ComAtprotoServerNS { ) } - appealAccountAction( - data?: ComAtprotoServerAppealAccountAction.InputSchema, - opts?: ComAtprotoServerAppealAccountAction.CallOptions, - ): Promise { - return this._client.call( - 'com.atproto.server.appealAccountAction', - opts?.qp, - data, - opts, - ) - } - checkAccountStatus( params?: ComAtprotoServerCheckAccountStatus.QueryParams, opts?: ComAtprotoServerCheckAccountStatus.CallOptions, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 6dcb4fa3a3c..d14bc764d59 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -2018,44 +2018,6 @@ export const schemaDict = { }, }, }, - ComAtprotoServerAppealAccountAction: { - lexicon: 1, - id: 'com.atproto.server.appealAccountAction', - defs: { - main: { - type: 'procedure', - description: 'Appeal an account level moderation action', - input: { - encoding: 'application/json', - schema: { - type: 'object', - required: ['identifier', 'password'], - properties: { - identifier: { - type: 'string', - description: - 'Handle or other identifier supported by the server for the authenticating user.', - }, - password: { - type: 'string', - }, - comment: { - type: 'string', - description: "User's comment to be included with the appeal.", - }, - }, - }, - }, - output: { - encoding: 'application/json', - schema: { - type: 'object', - properties: {}, - }, - }, - }, - }, - }, ComAtprotoServerCheckAccountStatus: { lexicon: 1, id: 'com.atproto.server.checkAccountStatus', @@ -13593,7 +13555,6 @@ export const ids = { ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', ComAtprotoServerActivateAccount: 'com.atproto.server.activateAccount', - ComAtprotoServerAppealAccountAction: 'com.atproto.server.appealAccountAction', ComAtprotoServerCheckAccountStatus: 'com.atproto.server.checkAccountStatus', ComAtprotoServerConfirmEmail: 'com.atproto.server.confirmEmail', ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount', diff --git a/packages/api/src/client/types/com/atproto/server/appealAccountAction.ts b/packages/api/src/client/types/com/atproto/server/appealAccountAction.ts deleted file mode 100644 index 21db85d1ab5..00000000000 --- a/packages/api/src/client/types/com/atproto/server/appealAccountAction.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * GENERATED CODE - DO NOT MODIFY - */ -import { HeadersMap, 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 { - /** Handle or other identifier supported by the server for the authenticating user. */ - identifier: string - password: string - /** User's comment to be included with the appeal. */ - comment?: string - [k: string]: unknown -} - -export interface OutputSchema { - [k: string]: unknown -} - -export interface CallOptions { - signal?: AbortSignal - headers?: HeadersMap - qp?: QueryParams - encoding?: 'application/json' -} - -export interface Response { - success: boolean - headers: HeadersMap - data: OutputSchema -} - -export function toKnownErr(e: any) { - return e -} diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index cc1f476eea8..7a8a13149ed 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -43,7 +43,6 @@ import * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord' import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' import * as ComAtprotoServerActivateAccount from './types/com/atproto/server/activateAccount' -import * as ComAtprotoServerAppealAccountAction from './types/com/atproto/server/appealAccountAction' import * as ComAtprotoServerCheckAccountStatus from './types/com/atproto/server/checkAccountStatus' import * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail' import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' @@ -671,17 +670,6 @@ export class ComAtprotoServerNS { return this._server.xrpc.method(nsid, cfg) } - appealAccountAction( - cfg: ConfigOf< - AV, - ComAtprotoServerAppealAccountAction.Handler>, - ComAtprotoServerAppealAccountAction.HandlerReqCtx> - >, - ) { - const nsid = 'com.atproto.server.appealAccountAction' // @ts-ignore - return this._server.xrpc.method(nsid, cfg) - } - checkAccountStatus( cfg: ConfigOf< AV, diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 3d700df79b7..73d13fe465c 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -2018,44 +2018,6 @@ export const schemaDict = { }, }, }, - ComAtprotoServerAppealAccountAction: { - lexicon: 1, - id: 'com.atproto.server.appealAccountAction', - defs: { - main: { - type: 'procedure', - description: 'Appeal an account level moderation action', - input: { - encoding: 'application/json', - schema: { - type: 'object', - required: ['identifier', 'password'], - properties: { - identifier: { - type: 'string', - description: - 'Handle or other identifier supported by the server for the authenticating user.', - }, - password: { - type: 'string', - }, - comment: { - type: 'string', - description: "User's comment to be included with the appeal.", - }, - }, - }, - }, - output: { - encoding: 'application/json', - schema: { - type: 'object', - properties: {}, - }, - }, - }, - }, - }, ComAtprotoServerCheckAccountStatus: { lexicon: 1, id: 'com.atproto.server.checkAccountStatus', @@ -10827,7 +10789,6 @@ export const ids = { ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', ComAtprotoServerActivateAccount: 'com.atproto.server.activateAccount', - ComAtprotoServerAppealAccountAction: 'com.atproto.server.appealAccountAction', ComAtprotoServerCheckAccountStatus: 'com.atproto.server.checkAccountStatus', ComAtprotoServerConfirmEmail: 'com.atproto.server.confirmEmail', ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount', diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/appealAccountAction.ts b/packages/bsky/src/lexicon/types/com/atproto/server/appealAccountAction.ts deleted file mode 100644 index b6a484a09a4..00000000000 --- a/packages/bsky/src/lexicon/types/com/atproto/server/appealAccountAction.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * 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, HandlerPipeThrough } from '@atproto/xrpc-server' - -export interface QueryParams {} - -export interface InputSchema { - /** Handle or other identifier supported by the server for the authenticating user. */ - identifier: string - password: string - /** User's comment to be included with the appeal. */ - comment?: string - [k: string]: unknown -} - -export interface OutputSchema { - [k: string]: unknown -} - -export interface HandlerInput { - encoding: 'application/json' - body: InputSchema -} - -export interface HandlerSuccess { - encoding: 'application/json' - body: OutputSchema - headers?: { [key: string]: string } -} - -export interface HandlerError { - status: number - message?: string -} - -export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough -export type HandlerReqCtx = { - auth: HA - params: QueryParams - input: HandlerInput - req: express.Request - res: express.Response -} -export type Handler = ( - ctx: HandlerReqCtx, -) => Promise | HandlerOutput diff --git a/packages/ozone/src/lexicon/index.ts b/packages/ozone/src/lexicon/index.ts index 88d92218009..b4c27f52018 100644 --- a/packages/ozone/src/lexicon/index.ts +++ b/packages/ozone/src/lexicon/index.ts @@ -43,7 +43,6 @@ import * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord' import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' import * as ComAtprotoServerActivateAccount from './types/com/atproto/server/activateAccount' -import * as ComAtprotoServerAppealAccountAction from './types/com/atproto/server/appealAccountAction' import * as ComAtprotoServerCheckAccountStatus from './types/com/atproto/server/checkAccountStatus' import * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail' import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' @@ -714,17 +713,6 @@ export class ComAtprotoServerNS { return this._server.xrpc.method(nsid, cfg) } - appealAccountAction( - cfg: ConfigOf< - AV, - ComAtprotoServerAppealAccountAction.Handler>, - ComAtprotoServerAppealAccountAction.HandlerReqCtx> - >, - ) { - const nsid = 'com.atproto.server.appealAccountAction' // @ts-ignore - return this._server.xrpc.method(nsid, cfg) - } - checkAccountStatus( cfg: ConfigOf< AV, diff --git a/packages/ozone/src/lexicon/lexicons.ts b/packages/ozone/src/lexicon/lexicons.ts index 6dcb4fa3a3c..d14bc764d59 100644 --- a/packages/ozone/src/lexicon/lexicons.ts +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -2018,44 +2018,6 @@ export const schemaDict = { }, }, }, - ComAtprotoServerAppealAccountAction: { - lexicon: 1, - id: 'com.atproto.server.appealAccountAction', - defs: { - main: { - type: 'procedure', - description: 'Appeal an account level moderation action', - input: { - encoding: 'application/json', - schema: { - type: 'object', - required: ['identifier', 'password'], - properties: { - identifier: { - type: 'string', - description: - 'Handle or other identifier supported by the server for the authenticating user.', - }, - password: { - type: 'string', - }, - comment: { - type: 'string', - description: "User's comment to be included with the appeal.", - }, - }, - }, - }, - output: { - encoding: 'application/json', - schema: { - type: 'object', - properties: {}, - }, - }, - }, - }, - }, ComAtprotoServerCheckAccountStatus: { lexicon: 1, id: 'com.atproto.server.checkAccountStatus', @@ -13593,7 +13555,6 @@ export const ids = { ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', ComAtprotoServerActivateAccount: 'com.atproto.server.activateAccount', - ComAtprotoServerAppealAccountAction: 'com.atproto.server.appealAccountAction', ComAtprotoServerCheckAccountStatus: 'com.atproto.server.checkAccountStatus', ComAtprotoServerConfirmEmail: 'com.atproto.server.confirmEmail', ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount', diff --git a/packages/ozone/src/lexicon/types/com/atproto/server/appealAccountAction.ts b/packages/ozone/src/lexicon/types/com/atproto/server/appealAccountAction.ts deleted file mode 100644 index b6a484a09a4..00000000000 --- a/packages/ozone/src/lexicon/types/com/atproto/server/appealAccountAction.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * 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, HandlerPipeThrough } from '@atproto/xrpc-server' - -export interface QueryParams {} - -export interface InputSchema { - /** Handle or other identifier supported by the server for the authenticating user. */ - identifier: string - password: string - /** User's comment to be included with the appeal. */ - comment?: string - [k: string]: unknown -} - -export interface OutputSchema { - [k: string]: unknown -} - -export interface HandlerInput { - encoding: 'application/json' - body: InputSchema -} - -export interface HandlerSuccess { - encoding: 'application/json' - body: OutputSchema - headers?: { [key: string]: string } -} - -export interface HandlerError { - status: number - message?: string -} - -export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough -export type HandlerReqCtx = { - auth: HA - params: QueryParams - input: HandlerInput - req: express.Request - res: express.Response -} -export type Handler = ( - ctx: HandlerReqCtx, -) => Promise | HandlerOutput diff --git a/packages/pds/src/api/com/atproto/server/appealAccountAction.ts b/packages/pds/src/api/com/atproto/server/appealAccountAction.ts deleted file mode 100644 index 2d6d6870954..00000000000 --- a/packages/pds/src/api/com/atproto/server/appealAccountAction.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { DAY, MINUTE } from '@atproto/common' -import { InvalidRequestError } from '@atproto/xrpc-server' -import { ComAtprotoModerationDefs } from '@atproto/api' - -import AppContext from '../../../../context' -import { Server } from '../../../../lexicon' -import { authPassthru, resultPassthru } from '../../../proxy' -import { parseProxyInfo } from '../../../../pipethrough' - -export default function (server: Server, ctx: AppContext) { - server.com.atproto.server.appealAccountAction({ - // @TODO: we probably will be fine with a very high rate limit on this endpoint - rateLimit: [ - { - durationMs: DAY, - points: 300, - calcKey: ({ input, req }) => `${input.body.identifier}-${req.ip}`, - }, - { - durationMs: 5 * MINUTE, - points: 30, - calcKey: ({ input, req }) => `${input.body.identifier}-${req.ip}`, - }, - ], - handler: async ({ input, req }) => { - if (ctx.entrywayAgent) { - return resultPassthru( - await ctx.entrywayAgent.com.atproto.server.appealAccountAction( - input.body, - authPassthru(req, true), - ), - ) - } - - const { user, isSoftDeleted } = await ctx.accountManager.login(input.body) - - // If user is not soft deleted, they should not be able to send appeals through this route - if (!isSoftDeleted) { - throw new InvalidRequestError( - 'No account action found', - 'InvalidAppeal', - ) - } - - // If no moderationAgent is configured, we can't route the appeal to the right mod instance - if (!ctx.moderationAgent) { - throw new InvalidRequestError( - 'Moderation service not configured', - 'InvalidAppeal', - ) - } - - // Create a service token just for the purpose of creating the appeal report and send the appeal using that token - const lxm = 'com.atproto.moderation.createReport' - const { did: aud } = await parseProxyInfo(ctx, req, lxm) - const serviceJwt = await ctx.serviceAuthJwt(user.did, aud, lxm) - await ctx.moderationAgent.com.atproto.moderation.createReport( - { - reasonType: ComAtprotoModerationDefs.REASONAPPEAL, - reason: input.body.comment, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: user.did, - }, - }, - { - headers: { - Authorization: `Bearer ${serviceJwt}`, - }, - }, - ) - - return { - encoding: 'application/json', - body: {}, - } - }, - }) -} diff --git a/packages/pds/src/api/com/atproto/server/index.ts b/packages/pds/src/api/com/atproto/server/index.ts index 5d9f64fcf68..7208f106b17 100644 --- a/packages/pds/src/api/com/atproto/server/index.ts +++ b/packages/pds/src/api/com/atproto/server/index.ts @@ -35,8 +35,6 @@ import checkAccountStatus from './checkAccountStatus' import activateAccount from './activateAccount' import deactivateAccount from './deactivateAccount' -import appealAccountAction from './appealAccountAction' - export default function (server: Server, ctx: AppContext) { describeServer(server, ctx) createAccount(server, ctx) @@ -63,5 +61,4 @@ export default function (server: Server, ctx: AppContext) { checkAccountStatus(server, ctx) activateAccount(server, ctx) deactivateAccount(server, ctx) - appealAccountAction(server, ctx) } diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 88d92218009..b4c27f52018 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -43,7 +43,6 @@ import * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord' import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' import * as ComAtprotoServerActivateAccount from './types/com/atproto/server/activateAccount' -import * as ComAtprotoServerAppealAccountAction from './types/com/atproto/server/appealAccountAction' import * as ComAtprotoServerCheckAccountStatus from './types/com/atproto/server/checkAccountStatus' import * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail' import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' @@ -714,17 +713,6 @@ export class ComAtprotoServerNS { return this._server.xrpc.method(nsid, cfg) } - appealAccountAction( - cfg: ConfigOf< - AV, - ComAtprotoServerAppealAccountAction.Handler>, - ComAtprotoServerAppealAccountAction.HandlerReqCtx> - >, - ) { - const nsid = 'com.atproto.server.appealAccountAction' // @ts-ignore - return this._server.xrpc.method(nsid, cfg) - } - checkAccountStatus( cfg: ConfigOf< AV, diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 6dcb4fa3a3c..d14bc764d59 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -2018,44 +2018,6 @@ export const schemaDict = { }, }, }, - ComAtprotoServerAppealAccountAction: { - lexicon: 1, - id: 'com.atproto.server.appealAccountAction', - defs: { - main: { - type: 'procedure', - description: 'Appeal an account level moderation action', - input: { - encoding: 'application/json', - schema: { - type: 'object', - required: ['identifier', 'password'], - properties: { - identifier: { - type: 'string', - description: - 'Handle or other identifier supported by the server for the authenticating user.', - }, - password: { - type: 'string', - }, - comment: { - type: 'string', - description: "User's comment to be included with the appeal.", - }, - }, - }, - }, - output: { - encoding: 'application/json', - schema: { - type: 'object', - properties: {}, - }, - }, - }, - }, - }, ComAtprotoServerCheckAccountStatus: { lexicon: 1, id: 'com.atproto.server.checkAccountStatus', @@ -13593,7 +13555,6 @@ export const ids = { ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', ComAtprotoServerActivateAccount: 'com.atproto.server.activateAccount', - ComAtprotoServerAppealAccountAction: 'com.atproto.server.appealAccountAction', ComAtprotoServerCheckAccountStatus: 'com.atproto.server.checkAccountStatus', ComAtprotoServerConfirmEmail: 'com.atproto.server.confirmEmail', ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount', diff --git a/packages/pds/src/lexicon/types/com/atproto/server/appealAccountAction.ts b/packages/pds/src/lexicon/types/com/atproto/server/appealAccountAction.ts deleted file mode 100644 index b6a484a09a4..00000000000 --- a/packages/pds/src/lexicon/types/com/atproto/server/appealAccountAction.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * 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, HandlerPipeThrough } from '@atproto/xrpc-server' - -export interface QueryParams {} - -export interface InputSchema { - /** Handle or other identifier supported by the server for the authenticating user. */ - identifier: string - password: string - /** User's comment to be included with the appeal. */ - comment?: string - [k: string]: unknown -} - -export interface OutputSchema { - [k: string]: unknown -} - -export interface HandlerInput { - encoding: 'application/json' - body: InputSchema -} - -export interface HandlerSuccess { - encoding: 'application/json' - body: OutputSchema - headers?: { [key: string]: string } -} - -export interface HandlerError { - status: number - message?: string -} - -export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough -export type HandlerReqCtx = { - auth: HA - params: QueryParams - input: HandlerInput - req: express.Request - res: express.Response -} -export type Handler = ( - ctx: HandlerReqCtx, -) => Promise | HandlerOutput From ff21efb344519869c48d5e696fddbae48d21977b Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Wed, 18 Dec 2024 22:30:29 +0000 Subject: [PATCH 06/12] :memo: Add description to new field --- lexicons/com/atproto/server/createSession.json | 5 ++++- packages/api/src/client/lexicons.ts | 2 ++ .../api/src/client/types/com/atproto/server/createSession.ts | 1 + packages/bsky/src/lexicon/lexicons.ts | 2 ++ .../src/lexicon/types/com/atproto/server/createSession.ts | 1 + packages/ozone/src/lexicon/lexicons.ts | 2 ++ .../src/lexicon/types/com/atproto/server/createSession.ts | 1 + packages/pds/src/lexicon/lexicons.ts | 2 ++ .../src/lexicon/types/com/atproto/server/createSession.ts | 1 + 9 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lexicons/com/atproto/server/createSession.json b/lexicons/com/atproto/server/createSession.json index 4657001b5c2..8cde0863aa8 100644 --- a/lexicons/com/atproto/server/createSession.json +++ b/lexicons/com/atproto/server/createSession.json @@ -17,7 +17,10 @@ }, "password": { "type": "string" }, "authFactorToken": { "type": "string" }, - "allowTakendown": { "type": "boolean" } + "allowTakendown": { + "type": "boolean", + "description": "When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned" + } } } }, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index d14bc764d59..76a1e170a30 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -2414,6 +2414,8 @@ export const schemaDict = { }, allowTakendown: { type: 'boolean', + description: + 'When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned', }, }, }, diff --git a/packages/api/src/client/types/com/atproto/server/createSession.ts b/packages/api/src/client/types/com/atproto/server/createSession.ts index 684b7e4583d..5dd724668df 100644 --- a/packages/api/src/client/types/com/atproto/server/createSession.ts +++ b/packages/api/src/client/types/com/atproto/server/createSession.ts @@ -14,6 +14,7 @@ export interface InputSchema { identifier: string password: string authFactorToken?: string + /** When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned */ allowTakendown?: boolean [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 73d13fe465c..ca95ec85547 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -2414,6 +2414,8 @@ export const schemaDict = { }, allowTakendown: { type: 'boolean', + description: + 'When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned', }, }, }, diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts b/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts index 7bf7ce0ba99..4ed0ae70fb1 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts @@ -15,6 +15,7 @@ export interface InputSchema { identifier: string password: string authFactorToken?: string + /** When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned */ allowTakendown?: boolean [k: string]: unknown } diff --git a/packages/ozone/src/lexicon/lexicons.ts b/packages/ozone/src/lexicon/lexicons.ts index d14bc764d59..76a1e170a30 100644 --- a/packages/ozone/src/lexicon/lexicons.ts +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -2414,6 +2414,8 @@ export const schemaDict = { }, allowTakendown: { type: 'boolean', + description: + 'When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned', }, }, }, diff --git a/packages/ozone/src/lexicon/types/com/atproto/server/createSession.ts b/packages/ozone/src/lexicon/types/com/atproto/server/createSession.ts index 7bf7ce0ba99..4ed0ae70fb1 100644 --- a/packages/ozone/src/lexicon/types/com/atproto/server/createSession.ts +++ b/packages/ozone/src/lexicon/types/com/atproto/server/createSession.ts @@ -15,6 +15,7 @@ export interface InputSchema { identifier: string password: string authFactorToken?: string + /** When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned */ allowTakendown?: boolean [k: string]: unknown } diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index d14bc764d59..76a1e170a30 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -2414,6 +2414,8 @@ export const schemaDict = { }, allowTakendown: { type: 'boolean', + description: + 'When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned', }, }, }, diff --git a/packages/pds/src/lexicon/types/com/atproto/server/createSession.ts b/packages/pds/src/lexicon/types/com/atproto/server/createSession.ts index 7bf7ce0ba99..4ed0ae70fb1 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/createSession.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/createSession.ts @@ -15,6 +15,7 @@ export interface InputSchema { identifier: string password: string authFactorToken?: string + /** When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned */ allowTakendown?: boolean [k: string]: unknown } From 927a5e807628a010dcbcc8ae718fb8a9da9f4cf8 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Thu, 19 Dec 2024 01:49:06 +0000 Subject: [PATCH 07/12] :recycle: Refactor authscope formatter and add test for create record with takendown token --- .../pds/src/account-manager/helpers/auth.ts | 6 +++- packages/pds/src/account-manager/index.ts | 4 +-- packages/pds/tests/takedown-appeal.test.ts | 31 ++++++++++++++++++- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/packages/pds/src/account-manager/helpers/auth.ts b/packages/pds/src/account-manager/helpers/auth.ts index 9cd9b67611a..f236aa987e5 100644 --- a/packages/pds/src/account-manager/helpers/auth.ts +++ b/packages/pds/src/account-manager/helpers/auth.ts @@ -208,7 +208,11 @@ export const getRefreshTokenId = () => { return ui8.toString(crypto.randomBytes(32), 'base64') } -export const formatScope = (appPassword: AppPassDescript | null): AuthScope => { +export const formatScope = ( + appPassword: AppPassDescript | null, + isSoftDeleted?: boolean, +): AuthScope => { + if (isSoftDeleted) return AuthScope.Takendown if (!appPassword) return AuthScope.Access return appPassword.privileged ? AuthScope.AppPassPrivileged diff --git a/packages/pds/src/account-manager/index.ts b/packages/pds/src/account-manager/index.ts index 5970b91c5f2..820ae641be7 100644 --- a/packages/pds/src/account-manager/index.ts +++ b/packages/pds/src/account-manager/index.ts @@ -217,9 +217,7 @@ export class AccountManager did, jwtKey: this.jwtKey, serviceDid: this.serviceDid, - scope: isSoftDeleted - ? AuthScope.Takendown - : auth.formatScope(appPassword), + scope: auth.formatScope(appPassword, isSoftDeleted), }) // For soft deleted accounts don't store refresh token so that it can't be rotated. if (!isSoftDeleted) { diff --git a/packages/pds/tests/takedown-appeal.test.ts b/packages/pds/tests/takedown-appeal.test.ts index 9cba9703b2d..363827985c1 100644 --- a/packages/pds/tests/takedown-appeal.test.ts +++ b/packages/pds/tests/takedown-appeal.test.ts @@ -1,6 +1,7 @@ import { AtpAgent, ComAtprotoModerationDefs } from '@atproto/api' import { SeedClient, TestNetwork } from '@atproto/dev-env' import { forSnapshot } from './_util' +import { ids } from '../src/lexicon/lexicons' describe('appeal account takedown', () => { let network: TestNetwork @@ -10,7 +11,7 @@ describe('appeal account takedown', () => { beforeAll(async () => { network = await TestNetwork.create({ - dbPostgresSchema: 'auth', + dbPostgresSchema: 'takedown_appeal', }) sc = network.getSeedClient() const modAccount = await sc.createAccount('moderator', { @@ -125,4 +126,32 @@ describe('appeal account takedown', () => { ), ).rejects.toThrow('Report not accepted from takendown account') }) + it('takendown actor is not allowed to create records.', async () => { + const { data: auth } = await agent.com.atproto.server.createSession({ + identifier: 'jeff.test', + password: 'password', + allowTakendown: true, + }) + + // send appeal event as the takendown account + await expect( + agent.com.atproto.repo.createRecord( + { + repo: auth.did, + collection: ids.AppBskyFeedPost, + // rkey: 'self', + record: { + text: 'test', + createdAt: new Date().toISOString(), + }, + }, + { + headers: { + authorization: `Bearer ${auth.accessJwt}`, + }, + encoding: 'application/json', + }, + ), + ).rejects.toThrow('Bad token scope') + }) }) From 0b84d7a07a4f7860633c14054ca840e491934f96 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Thu, 19 Dec 2024 13:23:32 +0000 Subject: [PATCH 08/12] :white_check_mark: Update snapshot --- packages/pds/tests/__snapshots__/takedown-appeal.test.ts.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/pds/tests/__snapshots__/takedown-appeal.test.ts.snap b/packages/pds/tests/__snapshots__/takedown-appeal.test.ts.snap index ab542bb4d38..a4dfd5d505c 100644 --- a/packages/pds/tests/__snapshots__/takedown-appeal.test.ts.snap +++ b/packages/pds/tests/__snapshots__/takedown-appeal.test.ts.snap @@ -22,6 +22,7 @@ Object { "subjectRepoHandle": "jeff.test", "tags": Array [ "lang:und", + "report:appeal", ], "takendown": true, "updatedAt": "1970-01-01T00:00:00.000Z", From 183c05706d41bb7e64aff6c7584f576416fb7073 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 19 Dec 2024 12:31:10 -0600 Subject: [PATCH 09/12] add createReport route --- packages/pds/src/api/com/atproto/index.ts | 2 ++ .../com/atproto/moderation/createReport.ts | 36 +++++++++++++++++++ .../src/api/com/atproto/moderation/index.ts | 7 ++++ packages/pds/src/pipethrough.ts | 14 +------- 4 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 packages/pds/src/api/com/atproto/moderation/createReport.ts create mode 100644 packages/pds/src/api/com/atproto/moderation/index.ts diff --git a/packages/pds/src/api/com/atproto/index.ts b/packages/pds/src/api/com/atproto/index.ts index 3a218c915c5..c7d4f217f88 100644 --- a/packages/pds/src/api/com/atproto/index.ts +++ b/packages/pds/src/api/com/atproto/index.ts @@ -2,6 +2,7 @@ import AppContext from '../../../context' import { Server } from '../../../lexicon' import admin from './admin' import identity from './identity' +import moderation from './moderation' import repo from './repo' import serverMethods from './server' import sync from './sync' @@ -10,6 +11,7 @@ import temp from './temp' export default function (server: Server, ctx: AppContext) { admin(server, ctx) identity(server, ctx) + moderation(server, ctx) repo(server, ctx) serverMethods(server, ctx) sync(server, ctx) diff --git a/packages/pds/src/api/com/atproto/moderation/createReport.ts b/packages/pds/src/api/com/atproto/moderation/createReport.ts new file mode 100644 index 00000000000..fce5dc3b827 --- /dev/null +++ b/packages/pds/src/api/com/atproto/moderation/createReport.ts @@ -0,0 +1,36 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { parseProxyInfo } from '../../../../pipethrough' +import { ids } from '../../../../lexicon/lexicons' +import { AtpAgent } from '@atproto/api' +import { AuthScope } from '../../../../auth-verifier' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.moderation.createReport({ + auth: ctx.authVerifier.accessStandard({ + additional: [AuthScope.Takendown], + }), + handler: async ({ auth, input, req }) => { + const { url, did: aud } = await parseProxyInfo( + ctx, + req, + ids.ComAtprotoModerationCreateReport, + ) + const agent = new AtpAgent({ service: url }) + const serviceAuth = await ctx.serviceAuthHeaders( + auth.credentials.did, + aud, + ids.ComAtprotoModerationCreateReport, + ) + const res = await agent.com.atproto.moderation.createReport(input.body, { + ...serviceAuth, + encoding: 'application/json', + }) + + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/com/atproto/moderation/index.ts b/packages/pds/src/api/com/atproto/moderation/index.ts new file mode 100644 index 00000000000..d3f181f3316 --- /dev/null +++ b/packages/pds/src/api/com/atproto/moderation/index.ts @@ -0,0 +1,7 @@ +import AppContext from '../../../../context' +import { Server } from '../../../../lexicon' +import createReport from './createReport' + +export default function (server: Server, ctx: AppContext) { + createReport(server, ctx) +} diff --git a/packages/pds/src/pipethrough.ts b/packages/pds/src/pipethrough.ts index 183dc3280c7..d96b33ac8c2 100644 --- a/packages/pds/src/pipethrough.ts +++ b/packages/pds/src/pipethrough.ts @@ -23,23 +23,11 @@ import { import AppContext from './context' import { ids } from './lexicon/lexicons' import { httpLogger } from './logger' -import { AuthScope } from './auth-verifier' - -const getStandardAccess = (ctx: AppContext, path: string) => { - const additionalAccess: AuthScope[] = [] - if (path.startsWith(`/xrpc/${ids.ComAtprotoModerationCreateReport}`)) { - additionalAccess.push(AuthScope.Takendown) - } - - return ctx.authVerifier.accessStandard({ - additional: additionalAccess, - }) -} export const proxyHandler = (ctx: AppContext): CatchallHandler => { + const accessStandard = ctx.authVerifier.accessStandard() return async (req, res, next) => { // /!\ Hot path - const accessStandard = getStandardAccess(ctx, req.originalUrl) try { if ( req.method !== 'GET' && From 6b8aa4c87803ae5a0b42e069f3b4d51ad0902ab8 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 19 Dec 2024 14:07:24 -0600 Subject: [PATCH 10/12] fix scopes for account mgiration --- packages/pds/src/account-manager/index.ts | 7 +++++- .../src/api/app/bsky/actor/getPreferences.ts | 5 ++++- .../identity/requestPlcOperationSignature.ts | 3 ++- .../com/atproto/server/deactivateAccount.ts | 3 ++- .../api/com/atproto/server/getServiceAuth.ts | 15 ++++++++++++- .../atproto/sync/deprecated/getCheckout.ts | 2 +- .../com/atproto/sync/deprecated/getHead.ts | 2 +- .../pds/src/api/com/atproto/sync/getBlob.ts | 5 ++++- .../pds/src/api/com/atproto/sync/getBlocks.ts | 2 +- .../api/com/atproto/sync/getLatestCommit.ts | 2 +- .../pds/src/api/com/atproto/sync/getRecord.ts | 2 +- .../pds/src/api/com/atproto/sync/getRepo.ts | 5 ++++- .../pds/src/api/com/atproto/sync/listBlobs.ts | 5 ++++- packages/pds/src/auth-verifier.ts | 22 ++++++++++--------- 14 files changed, 57 insertions(+), 23 deletions(-) diff --git a/packages/pds/src/account-manager/index.ts b/packages/pds/src/account-manager/index.ts index 820ae641be7..b646e69bf7c 100644 --- a/packages/pds/src/account-manager/index.ts +++ b/packages/pds/src/account-manager/index.ts @@ -318,6 +318,7 @@ export class AccountManager if (!user) { throw new AuthRequiredError('Invalid identifier or password') } + const isSoftDeleted = softDeleted(user) let appPassword: password.AppPassDescript | null = null const validAccountPass = await this.verifyAccountPassword( @@ -325,13 +326,17 @@ export class AccountManager password, ) if (!validAccountPass) { + // takendown/suspended accounts cannot login with app password + if (isSoftDeleted) { + throw new AuthRequiredError('Invalid identifier or password') + } appPassword = await this.verifyAppPassword(user.did, password) if (appPassword === null) { throw new AuthRequiredError('Invalid identifier or password') } } - return { user, appPassword, isSoftDeleted: softDeleted(user) } + return { user, appPassword, isSoftDeleted } } finally { // Mitigate timing attacks await wait(350 - (Date.now() - start)) diff --git a/packages/pds/src/api/app/bsky/actor/getPreferences.ts b/packages/pds/src/api/app/bsky/actor/getPreferences.ts index 310495e3282..761873df473 100644 --- a/packages/pds/src/api/app/bsky/actor/getPreferences.ts +++ b/packages/pds/src/api/app/bsky/actor/getPreferences.ts @@ -1,10 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { AuthScope } from '../../../../auth-verifier' export default function (server: Server, ctx: AppContext) { if (!ctx.cfg.bskyAppView) return server.app.bsky.actor.getPreferences({ - auth: ctx.authVerifier.accessStandard(), + auth: ctx.authVerifier.accessStandard({ + additional: [AuthScope.Takendown], + }), handler: async ({ auth }) => { const requester = auth.credentials.did const preferences = await ctx.actorStore.read(requester, (store) => diff --git a/packages/pds/src/api/com/atproto/identity/requestPlcOperationSignature.ts b/packages/pds/src/api/com/atproto/identity/requestPlcOperationSignature.ts index d611eddc5a0..93be620f7c1 100644 --- a/packages/pds/src/api/com/atproto/identity/requestPlcOperationSignature.ts +++ b/packages/pds/src/api/com/atproto/identity/requestPlcOperationSignature.ts @@ -5,10 +5,11 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import AppContext from '../../../../context' import { Server } from '../../../../lexicon' import { ids } from '../../../../lexicon/lexicons' +import { AuthScope } from '../../../../auth-verifier' export default function (server: Server, ctx: AppContext) { server.com.atproto.identity.requestPlcOperationSignature({ - auth: ctx.authVerifier.accessFull(), + auth: ctx.authVerifier.accessFull({ additional: [AuthScope.Takendown] }), handler: async ({ auth }) => { if (ctx.entrywayAgent) { assert(ctx.cfg.entryway) diff --git a/packages/pds/src/api/com/atproto/server/deactivateAccount.ts b/packages/pds/src/api/com/atproto/server/deactivateAccount.ts index 7076b17a1a7..42b39b7ce84 100644 --- a/packages/pds/src/api/com/atproto/server/deactivateAccount.ts +++ b/packages/pds/src/api/com/atproto/server/deactivateAccount.ts @@ -1,10 +1,11 @@ +import { AuthScope } from '../../../../auth-verifier' import AppContext from '../../../../context' import { Server } from '../../../../lexicon' import { authPassthru } from '../../../proxy' export default function (server: Server, ctx: AppContext) { server.com.atproto.server.deactivateAccount({ - auth: ctx.authVerifier.accessFull(), + auth: ctx.authVerifier.accessFull({ additional: [AuthScope.Takendown] }), handler: async ({ req, auth, input }) => { // in the case of entryway, the full flow is deactivateAccount (PDS) -> deactivateAccount (Entryway) -> updateSubjectStatus(PDS) if (ctx.entrywayAgent) { diff --git a/packages/pds/src/api/com/atproto/server/getServiceAuth.ts b/packages/pds/src/api/com/atproto/server/getServiceAuth.ts index 7a9a2c7a617..5f85ba57cb3 100644 --- a/packages/pds/src/api/com/atproto/server/getServiceAuth.ts +++ b/packages/pds/src/api/com/atproto/server/getServiceAuth.ts @@ -3,14 +3,27 @@ import { HOUR, MINUTE } from '@atproto/common' import AppContext from '../../../../context' import { Server } from '../../../../lexicon' import { PRIVILEGED_METHODS, PROTECTED_METHODS } from '../../../../pipethrough' +import { AuthScope } from '../../../../auth-verifier' +import { ids } from '../../../../lexicon/lexicons' export default function (server: Server, ctx: AppContext) { server.com.atproto.server.getServiceAuth({ - auth: ctx.authVerifier.accessStandard(), + auth: ctx.authVerifier.accessStandard({ + additional: [AuthScope.Takendown], + }), handler: async ({ params, auth }) => { const did = auth.credentials.did const { aud, lxm = null } = params const exp = params.exp ? params.exp * 1000 : undefined + + // Takendown accounts should not be able to generate service auth tokens except for methods necessary for account migration + if ( + auth.credentials.scope === AuthScope.Takendown && + lxm !== ids.ComAtprotoServerCreateAccount + ) { + throw new InvalidRequestError('Bad token scope', 'InvalidToken') + } + if (exp) { const diff = exp - Date.now() if (diff < 0) { diff --git a/packages/pds/src/api/com/atproto/sync/deprecated/getCheckout.ts b/packages/pds/src/api/com/atproto/sync/deprecated/getCheckout.ts index 85f15f84779..0abc358534c 100644 --- a/packages/pds/src/api/com/atproto/sync/deprecated/getCheckout.ts +++ b/packages/pds/src/api/com/atproto/sync/deprecated/getCheckout.ts @@ -5,7 +5,7 @@ import { assertRepoAvailability } from '../util' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getCheckout({ - auth: ctx.authVerifier.optionalAccessOrAdminToken, + auth: ctx.authVerifier.optionalAccessOrAdminToken(), handler: async ({ params, auth }) => { const { did } = params await assertRepoAvailability( diff --git a/packages/pds/src/api/com/atproto/sync/deprecated/getHead.ts b/packages/pds/src/api/com/atproto/sync/deprecated/getHead.ts index 205652ee9ca..9c74e33315d 100644 --- a/packages/pds/src/api/com/atproto/sync/deprecated/getHead.ts +++ b/packages/pds/src/api/com/atproto/sync/deprecated/getHead.ts @@ -5,7 +5,7 @@ import { assertRepoAvailability } from '../util' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getHead({ - auth: ctx.authVerifier.optionalAccessOrAdminToken, + auth: ctx.authVerifier.optionalAccessOrAdminToken(), handler: async ({ params, auth }) => { const { did } = params await assertRepoAvailability( diff --git a/packages/pds/src/api/com/atproto/sync/getBlob.ts b/packages/pds/src/api/com/atproto/sync/getBlob.ts index 7c3290c83a6..8125656072e 100644 --- a/packages/pds/src/api/com/atproto/sync/getBlob.ts +++ b/packages/pds/src/api/com/atproto/sync/getBlob.ts @@ -4,10 +4,13 @@ import AppContext from '../../../../context' import { InvalidRequestError } from '@atproto/xrpc-server' import { BlobNotFoundError } from '@atproto/repo' import { assertRepoAvailability } from './util' +import { AuthScope } from '../../../../auth-verifier' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getBlob({ - auth: ctx.authVerifier.optionalAccessOrAdminToken, + auth: ctx.authVerifier.optionalAccessOrAdminToken({ + additional: [AuthScope.Takendown], + }), handler: async ({ params, res, auth }) => { const { did } = params await assertRepoAvailability( diff --git a/packages/pds/src/api/com/atproto/sync/getBlocks.ts b/packages/pds/src/api/com/atproto/sync/getBlocks.ts index 21d330357ef..dfd10b61dc5 100644 --- a/packages/pds/src/api/com/atproto/sync/getBlocks.ts +++ b/packages/pds/src/api/com/atproto/sync/getBlocks.ts @@ -8,7 +8,7 @@ import { assertRepoAvailability } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getBlocks({ - auth: ctx.authVerifier.optionalAccessOrAdminToken, + auth: ctx.authVerifier.optionalAccessOrAdminToken(), handler: async ({ params, auth }) => { const { did } = params await assertRepoAvailability( diff --git a/packages/pds/src/api/com/atproto/sync/getLatestCommit.ts b/packages/pds/src/api/com/atproto/sync/getLatestCommit.ts index a8809f77e39..65043612c1e 100644 --- a/packages/pds/src/api/com/atproto/sync/getLatestCommit.ts +++ b/packages/pds/src/api/com/atproto/sync/getLatestCommit.ts @@ -5,7 +5,7 @@ import { assertRepoAvailability } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getLatestCommit({ - auth: ctx.authVerifier.optionalAccessOrAdminToken, + auth: ctx.authVerifier.optionalAccessOrAdminToken(), handler: async ({ params, auth }) => { const { did } = params await assertRepoAvailability( diff --git a/packages/pds/src/api/com/atproto/sync/getRecord.ts b/packages/pds/src/api/com/atproto/sync/getRecord.ts index 4ea429e3a43..9cc4adfecd5 100644 --- a/packages/pds/src/api/com/atproto/sync/getRecord.ts +++ b/packages/pds/src/api/com/atproto/sync/getRecord.ts @@ -10,7 +10,7 @@ import { assertRepoAvailability } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getRecord({ - auth: ctx.authVerifier.optionalAccessOrAdminToken, + auth: ctx.authVerifier.optionalAccessOrAdminToken(), handler: async ({ params, auth }) => { const { did, collection, rkey } = params await assertRepoAvailability( diff --git a/packages/pds/src/api/com/atproto/sync/getRepo.ts b/packages/pds/src/api/com/atproto/sync/getRepo.ts index f4a93d483b0..3282220fda0 100644 --- a/packages/pds/src/api/com/atproto/sync/getRepo.ts +++ b/packages/pds/src/api/com/atproto/sync/getRepo.ts @@ -7,10 +7,13 @@ import { SqlRepoReader, } from '../../../../actor-store/repo/sql-repo-reader' import { assertRepoAvailability } from './util' +import { AuthScope } from '../../../../auth-verifier' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getRepo({ - auth: ctx.authVerifier.optionalAccessOrAdminToken, + auth: ctx.authVerifier.optionalAccessOrAdminToken({ + additional: [AuthScope.Takendown], + }), handler: async ({ params, auth }) => { const { did, since } = params await assertRepoAvailability( diff --git a/packages/pds/src/api/com/atproto/sync/listBlobs.ts b/packages/pds/src/api/com/atproto/sync/listBlobs.ts index 0466386731e..97d04644d11 100644 --- a/packages/pds/src/api/com/atproto/sync/listBlobs.ts +++ b/packages/pds/src/api/com/atproto/sync/listBlobs.ts @@ -1,10 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { assertRepoAvailability } from './util' +import { AuthScope } from '../../../../auth-verifier' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.listBlobs({ - auth: ctx.authVerifier.optionalAccessOrAdminToken, + auth: ctx.authVerifier.optionalAccessOrAdminToken({ + additional: [AuthScope.Takendown], + }), handler: async ({ params, auth }) => { const { did, since, limit, cursor } = params await assertRepoAvailability( diff --git a/packages/pds/src/auth-verifier.ts b/packages/pds/src/auth-verifier.ts index 22bfc7d7bd6..53c93c8abd8 100644 --- a/packages/pds/src/auth-verifier.ts +++ b/packages/pds/src/auth-verifier.ts @@ -210,17 +210,19 @@ export class AuthVerifier { return this.validateAdminToken(ctx) } - optionalAccessOrAdminToken = async ( - ctx: ReqCtx, - ): Promise => { - if (isAccessToken(ctx.req)) { - return await this.accessStandard()(ctx) - } else if (isBasicToken(ctx.req)) { - return await this.adminToken(ctx) - } else { - return this.null(ctx) + optionalAccessOrAdminToken = + (opts: Partial = {}) => + async ( + ctx: ReqCtx, + ): Promise => { + if (isAccessToken(ctx.req)) { + return await this.accessStandard(opts)(ctx) + } else if (isBasicToken(ctx.req)) { + return await this.adminToken(ctx) + } else { + return this.null(ctx) + } } - } userServiceAuth = async (ctx: ReqCtx): Promise => { const payload = await this.verifyServiceJwt(ctx, { From df7ae7d059628b76a0de979f3f0f64c4bd29d0a1 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 19 Dec 2024 14:17:46 -0600 Subject: [PATCH 11/12] changeset --- .changeset/kind-meals-grab.md | 5 +++++ .changeset/large-laws-hang.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/kind-meals-grab.md create mode 100644 .changeset/large-laws-hang.md diff --git a/.changeset/kind-meals-grab.md b/.changeset/kind-meals-grab.md new file mode 100644 index 00000000000..8778d27a005 --- /dev/null +++ b/.changeset/kind-meals-grab.md @@ -0,0 +1,5 @@ +--- +"@atproto/pds": patch +--- + +Allow takendown account scope on access tokens. Allow takendown accounts to createReports at discretion of the moderation service diff --git a/.changeset/large-laws-hang.md b/.changeset/large-laws-hang.md new file mode 100644 index 00000000000..daeb865bb76 --- /dev/null +++ b/.changeset/large-laws-hang.md @@ -0,0 +1,5 @@ +--- +"@atproto/api": patch +--- + +Allow createSession to request takendown account scope From 0dfce9bf4dbd7f3daf71965887cf81839124e01c Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 19 Dec 2024 14:18:19 -0600 Subject: [PATCH 12/12] changset --- .changeset/thick-shrimps-warn.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/thick-shrimps-warn.md diff --git a/.changeset/thick-shrimps-warn.md b/.changeset/thick-shrimps-warn.md new file mode 100644 index 00000000000..b8d37a82738 --- /dev/null +++ b/.changeset/thick-shrimps-warn.md @@ -0,0 +1,5 @@ +--- +"@atproto/pds": patch +--- + +Allow takendown accounts to perform account migration