From 6d308b857ba2a514ee3c75ebdef7225e298ed7d7 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Fri, 20 Dec 2024 19:52:20 +0000 Subject: [PATCH] :sparkles: Allow appeals on takendown account (#3251) * :sparkles: Allow appeals on takendown account * :white_check_mark: Update snapshot * :white_check_mark: Remove duplicate test * :sparkles: Respond with takendown token from createSession for takendown accounts * :broom: cleanup appeal account action stuff * :memo: Add description to new field * :recycle: Refactor authscope formatter and add test for create record with takendown token * :white_check_mark: Update snapshot * add createReport route * changeset --------- Co-authored-by: dholms --- .changeset/kind-meals-grab.md | 5 + .changeset/large-laws-hang.md | 5 + .../com/atproto/server/createSession.json | 6 +- packages/api/src/client/lexicons.ts | 5 + .../types/com/atproto/server/createSession.ts | 2 + packages/bsky/src/lexicon/lexicons.ts | 5 + .../types/com/atproto/server/createSession.ts | 2 + packages/ozone/src/api/report/createReport.ts | 43 ++++- packages/ozone/src/lexicon/lexicons.ts | 5 + .../types/com/atproto/server/createSession.ts | 2 + .../pds/src/account-manager/helpers/auth.ts | 6 +- packages/pds/src/account-manager/index.ts | 20 +-- packages/pds/src/api/com/atproto/index.ts | 2 + .../com/atproto/moderation/createReport.ts | 36 ++++ .../src/api/com/atproto/moderation/index.ts | 7 + .../api/com/atproto/server/createSession.ts | 13 +- packages/pds/src/auth-verifier.ts | 1 + packages/pds/src/lexicon/lexicons.ts | 5 + .../types/com/atproto/server/createSession.ts | 2 + packages/pds/src/pipethrough.ts | 3 +- .../takedown-appeal.test.ts.snap | 30 ++++ packages/pds/tests/auth.test.ts | 21 ++- packages/pds/tests/takedown-appeal.test.ts | 157 ++++++++++++++++++ 23 files changed, 354 insertions(+), 29 deletions(-) create mode 100644 .changeset/kind-meals-grab.md create mode 100644 .changeset/large-laws-hang.md create mode 100644 packages/pds/src/api/com/atproto/moderation/createReport.ts create mode 100644 packages/pds/src/api/com/atproto/moderation/index.ts create mode 100644 packages/pds/tests/__snapshots__/takedown-appeal.test.ts.snap create mode 100644 packages/pds/tests/takedown-appeal.test.ts 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 diff --git a/lexicons/com/atproto/server/createSession.json b/lexicons/com/atproto/server/createSession.json index fd0fae38d31..8cde0863aa8 100644 --- a/lexicons/com/atproto/server/createSession.json +++ b/lexicons/com/atproto/server/createSession.json @@ -16,7 +16,11 @@ "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", + "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 0e21313d62b..1d0efff5611 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -2412,6 +2412,11 @@ export const schemaDict = { authFactorToken: { type: 'string', }, + 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 3ac1194b36d..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,8 @@ 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 c24ef7394f2..28191074e8c 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -2412,6 +2412,11 @@ export const schemaDict = { authFactorToken: { type: 'string', }, + 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 96f7d79d5bc..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,8 @@ 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/api/report/createReport.ts b/packages/ozone/src/api/report/createReport.ts index 80f8eedfea2..ae40cf0b5b5 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' import { getTagForReport } from '../../tag-service/util' export default function (server: Server, ctx: AppContext) { @@ -22,6 +26,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 } = @@ -51,3 +58,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 0e21313d62b..1d0efff5611 100644 --- a/packages/ozone/src/lexicon/lexicons.ts +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -2412,6 +2412,11 @@ export const schemaDict = { authFactorToken: { type: 'string', }, + 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 96f7d79d5bc..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,8 @@ 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/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 654b45ea94d..820ae641be7 100644 --- a/packages/pds/src/account-manager/index.ts +++ b/packages/pds/src/account-manager/index.ts @@ -211,15 +211,19 @@ 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: auth.formatScope(appPassword, isSoftDeleted), }) - 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 } } @@ -295,6 +299,7 @@ export class AccountManager }): Promise<{ user: ActorAccount appPassword: password.AppPassDescript | null + isSoftDeleted: boolean }> { const start = Date.now() try { @@ -326,14 +331,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/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/api/com/atproto/server/createSession.ts b/packages/pds/src/api/com/atproto/server/createSession.ts index 20ca89e6ef3..3b15cde409f 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,10 +32,18 @@ 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 (!input.body.allowTakendown && isSoftDeleted) { + throw new AuthRequiredError( + 'Account has been taken down', + 'AccountTakedown', + ) + } 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 0e21313d62b..1d0efff5611 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -2412,6 +2412,11 @@ export const schemaDict = { authFactorToken: { type: 'string', }, + 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 96f7d79d5bc..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,8 @@ 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/pipethrough.ts b/packages/pds/src/pipethrough.ts index b326d5fba8b..d96b33ac8c2 100644 --- a/packages/pds/src/pipethrough.ts +++ b/packages/pds/src/pipethrough.ts @@ -28,7 +28,6 @@ export const proxyHandler = (ctx: AppContext): CatchallHandler => { const accessStandard = ctx.authVerifier.accessStandard() return async (req, res, next) => { // /!\ Hot path - try { if ( req.method !== 'GET' && @@ -207,7 +206,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__/takedown-appeal.test.ts.snap b/packages/pds/tests/__snapshots__/takedown-appeal.test.ts.snap new file mode 100644 index 00000000000..a4dfd5d505c --- /dev/null +++ b/packages/pds/tests/__snapshots__/takedown-appeal.test.ts.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`appeal account takedown 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": 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(1)", + }, + "subjectBlobCids": Array [], + "subjectRepoHandle": "jeff.test", + "tags": Array [ + "lang:und", + "report:appeal", + ], + "takendown": true, + "updatedAt": "1970-01-01T00:00:00.000Z", +} +`; diff --git a/packages/pds/tests/auth.test.ts b/packages/pds/tests/auth.test.ts index 507f835e036..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 } from '@atproto/dev-env' +import { SeedClient, TestNetworkNoAppView } from '@atproto/dev-env' import { createRefreshToken } from '../src/account-manager/helpers/auth' describe('auth', () => { @@ -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,7 @@ 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', diff --git a/packages/pds/tests/takedown-appeal.test.ts b/packages/pds/tests/takedown-appeal.test.ts new file mode 100644 index 00000000000..363827985c1 --- /dev/null +++ b/packages/pds/tests/takedown-appeal.test.ts @@ -0,0 +1,157 @@ +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 + let agent: AtpAgent + let sc: SeedClient + let moderator: string + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'takedown_appeal', + }) + 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', + }) + + // 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: { + $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 without setting the optional param + await expect( + agent.com.atproto.server.createSession({ + identifier: 'jeff.test', + password: 'password', + }), + ).rejects.toThrow('Account has been taken down') + + const { data: auth } = await agent.com.atproto.server.createSession({ + identifier: 'jeff.test', + password: 'password', + 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( + { + subject: account.did, + }, + { headers: sc.getHeaders(moderator) }, + ) + + expect(result.subjectStatuses[0].appealed).toBe(true) + expect(forSnapshot(result.subjectStatuses[0])).toMatchSnapshot() + }) + + 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.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') + }) + 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') + }) +})