diff --git a/.github/workflows/build-and-push-bsky-ghcr.yaml b/.github/workflows/build-and-push-bsky-ghcr.yaml index cfa76dc5a46..5d22cd9a389 100644 --- a/.github/workflows/build-and-push-bsky-ghcr.yaml +++ b/.github/workflows/build-and-push-bsky-ghcr.yaml @@ -3,7 +3,6 @@ on: push: branches: - main - - pds-proxy-headers env: REGISTRY: ghcr.io USERNAME: ${{ github.actor }} diff --git a/.github/workflows/build-and-push-ozone-aws.yaml b/.github/workflows/build-and-push-ozone-aws.yaml index 34c461a0b42..53f95c5b731 100644 --- a/.github/workflows/build-and-push-ozone-aws.yaml +++ b/.github/workflows/build-and-push-ozone-aws.yaml @@ -3,7 +3,6 @@ on: push: branches: - main - - pds-proxy-headers env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }} diff --git a/.github/workflows/build-and-push-pds-ghcr.yaml b/.github/workflows/build-and-push-pds-ghcr.yaml index 988e6b02f84..b11230ab531 100644 --- a/.github/workflows/build-and-push-pds-ghcr.yaml +++ b/.github/workflows/build-and-push-pds-ghcr.yaml @@ -3,7 +3,6 @@ on: push: branches: - main - - pds-proxy-headers env: REGISTRY: ghcr.io USERNAME: ${{ github.actor }} diff --git a/packages/dev-env/src/bsky.ts b/packages/dev-env/src/bsky.ts index 18a3f57b907..81d2caf6775 100644 --- a/packages/dev-env/src/bsky.ts +++ b/packages/dev-env/src/bsky.ts @@ -5,7 +5,7 @@ import { AtpAgent } from '@atproto/api' import { Secp256k1Keypair } from '@atproto/crypto' import { Client as PlcClient } from '@did-plc/lib' import { BskyConfig } from './types' -import { ADMIN_PASSWORD, MOD_PASSWORD, TRIAGE_PASSWORD } from './const' +import { ADMIN_PASSWORD } from './const' import { BackgroundQueue } from '@atproto/bsky/src/data-plane/server/background' export class TestBsky { @@ -64,7 +64,7 @@ export class TestBsky { modServiceDid: cfg.modServiceDid ?? 'did:example:invalidMod', labelsFromIssuerDids: ['did:example:labeler'], // this did is also used as the labeler in seeds ...cfg, - adminPasswords: [ADMIN_PASSWORD, MOD_PASSWORD, TRIAGE_PASSWORD], + adminPasswords: [ADMIN_PASSWORD], }) // Separate migration db in case migration changes some connection state that we need in the tests, e.g. "alter database ... set ..." diff --git a/packages/dev-env/src/const.ts b/packages/dev-env/src/const.ts index 50a6dae52d9..afa11ed4aad 100644 --- a/packages/dev-env/src/const.ts +++ b/packages/dev-env/src/const.ts @@ -1,4 +1,2 @@ export const ADMIN_PASSWORD = 'admin-pass' -export const MOD_PASSWORD = 'mod-pass' -export const TRIAGE_PASSWORD = 'triage-pass' export const JWT_SECRET = 'jwt-secret' diff --git a/packages/dev-env/src/index.ts b/packages/dev-env/src/index.ts index fe11f2275c9..d3b458c55eb 100644 --- a/packages/dev-env/src/index.ts +++ b/packages/dev-env/src/index.ts @@ -4,7 +4,9 @@ export * from './network' export * from './network-no-appview' export * from './pds' export * from './plc' +export * from './ozone' export * from './feed-gen' export * from './seed' +export * from './moderator-client' export * from './types' export * from './util' diff --git a/packages/dev-env/src/mock/index.ts b/packages/dev-env/src/mock/index.ts index 73319ad9c5b..ed222a55927 100644 --- a/packages/dev-env/src/mock/index.ts +++ b/packages/dev-env/src/mock/index.ts @@ -93,6 +93,29 @@ export async function generateMockSetup(env: TestNetwork) { ) } + // Create moderator accounts + const triageRes = + await clients.loggedout.api.com.atproto.server.createAccount({ + email: 'triage@test.com', + handle: 'triage.test', + password: 'triage-pass', + }) + env.ozone.addAdminDid(triageRes.data.did) + const modRes = await clients.loggedout.api.com.atproto.server.createAccount({ + email: 'mod@test.com', + handle: 'mod.test', + password: 'mod-pass', + }) + env.ozone.addAdminDid(modRes.data.did) + const adminRes = await clients.loggedout.api.com.atproto.server.createAccount( + { + email: 'admin-mod@test.com', + handle: 'admin-mod.test', + password: 'admin-mod-pass', + }, + ) + env.ozone.addAdminDid(adminRes.data.did) + // Report one user const reporter = picka(users) await reporter.agent.api.com.atproto.moderation.createReport({ diff --git a/packages/dev-env/src/moderator-client.ts b/packages/dev-env/src/moderator-client.ts new file mode 100644 index 00000000000..cd21e6c1bd6 --- /dev/null +++ b/packages/dev-env/src/moderator-client.ts @@ -0,0 +1,138 @@ +import AtpAgent from '@atproto/api' +import { InputSchema as TakeActionInput } from '@atproto/api/src/client/types/com/atproto/admin/emitModerationEvent' +import { QueryParams as QueryStatusesParams } from '@atproto/api/src/client/types/com/atproto/admin/queryModerationStatuses' +import { QueryParams as QueryEventsParams } from '@atproto/api/src/client/types/com/atproto/admin/queryModerationEvents' +import { TestOzone } from './ozone' + +type ModLevel = 'admin' | 'moderator' | 'triage' + +export class ModeratorClient { + agent: AtpAgent + constructor(public ozone: TestOzone) { + this.agent = ozone.getClient() + } + + async getEvent(id: number, role?: ModLevel) { + const result = await this.agent.api.com.atproto.admin.getModerationEvent( + { id }, + { + headers: await this.ozone.modHeaders(role), + }, + ) + return result.data + } + + async queryModerationStatuses(input: QueryStatusesParams, role?: ModLevel) { + const result = + await this.agent.api.com.atproto.admin.queryModerationStatuses(input, { + headers: await this.ozone.modHeaders(role), + }) + return result.data + } + + async queryModerationEvents(input: QueryEventsParams, role?: ModLevel) { + const result = await this.agent.api.com.atproto.admin.queryModerationEvents( + input, + { + headers: await this.ozone.modHeaders(role), + }, + ) + return result.data + } + + async emitModerationEvent( + opts: { + event: TakeActionInput['event'] + subject: TakeActionInput['subject'] + subjectBlobCids?: TakeActionInput['subjectBlobCids'] + reason?: string + createdBy?: string + meta?: TakeActionInput['meta'] + }, + role?: ModLevel, + ) { + const { + event, + subject, + subjectBlobCids, + reason = 'X', + createdBy = 'did:example:admin', + } = opts + const result = await this.agent.api.com.atproto.admin.emitModerationEvent( + { event, subject, subjectBlobCids, createdBy, reason }, + { + encoding: 'application/json', + headers: await this.ozone.modHeaders(role), + }, + ) + return result.data + } + + async reverseModerationAction( + opts: { + id: number + subject: TakeActionInput['subject'] + reason?: string + createdBy?: string + }, + role?: ModLevel, + ) { + const { subject, reason = 'X', createdBy = 'did:example:admin' } = opts + const result = await this.agent.api.com.atproto.admin.emitModerationEvent( + { + subject, + event: { + $type: 'com.atproto.admin.defs#modEventReverseTakedown', + comment: reason, + }, + createdBy, + }, + { + encoding: 'application/json', + headers: await this.ozone.modHeaders(role), + }, + ) + return result.data + } + + async performTakedown( + opts: { + subject: TakeActionInput['subject'] + subjectBlobCids?: TakeActionInput['subjectBlobCids'] + durationInHours?: number + reason?: string + }, + role?: ModLevel, + ) { + const { durationInHours, ...rest } = opts + return this.emitModerationEvent( + { + event: { + $type: 'com.atproto.admin.defs#modEventTakedown', + durationInHours, + }, + ...rest, + }, + role, + ) + } + + async performReverseTakedown( + opts: { + subject: TakeActionInput['subject'] + subjectBlobCids?: TakeActionInput['subjectBlobCids'] + reason?: string + }, + role?: ModLevel, + ) { + return this.emitModerationEvent( + { + event: { + $type: 'com.atproto.admin.defs#modEventReverseTakedown', + }, + ...opts, + }, + role, + ) + } +} diff --git a/packages/dev-env/src/network.ts b/packages/dev-env/src/network.ts index 06fbd780060..1e890a59722 100644 --- a/packages/dev-env/src/network.ts +++ b/packages/dev-env/src/network.ts @@ -105,6 +105,7 @@ export class TestNetwork extends TestNetworkNoAppView { await this.pds.processAll() await this.processFullSubscription(timeout) await this.bsky.sub.background.processAll() + await this.ozone.processAll() } async serviceHeaders(did: string, aud?: string) { diff --git a/packages/dev-env/src/ozone.ts b/packages/dev-env/src/ozone.ts index d06e45eba13..90733932ec3 100644 --- a/packages/dev-env/src/ozone.ts +++ b/packages/dev-env/src/ozone.ts @@ -1,11 +1,14 @@ import getPort from 'get-port' import * as ui8 from 'uint8arrays' +import * as plc from '@did-plc/lib' import * as ozone from '@atproto/ozone' import { AtpAgent } from '@atproto/api' +import { createServiceJwt } from '@atproto/xrpc-server' import { Keypair, Secp256k1Keypair } from '@atproto/crypto' -import * as plc from '@did-plc/lib' -import { OzoneConfig } from './types' -import { ADMIN_PASSWORD, MOD_PASSWORD, TRIAGE_PASSWORD } from './const' +import { DidAndKey, OzoneConfig } from './types' +import { ADMIN_PASSWORD } from './const' +import { createDidAndKey } from './util' +import { ModeratorClient } from './moderator-client' export class TestOzone { constructor( @@ -13,6 +16,9 @@ export class TestOzone { public port: number, public server: ozone.OzoneService, public daemon: ozone.OzoneDaemon, + public adminAccnt: DidAndKey, + public moderatorAccnt: DidAndKey, + public triageAccnt: DidAndKey, ) {} static async create(config: OzoneConfig): Promise { @@ -24,6 +30,24 @@ export class TestOzone { serverDid = await createOzoneDid(config.plcUrl, serviceKeypair) } + const admin = await createDidAndKey({ + plcUrl: config.plcUrl, + handle: 'admin.ozone', + pds: 'https://pds.invalid', + }) + + const moderator = await createDidAndKey({ + plcUrl: config.plcUrl, + handle: 'moderator.ozone', + pds: 'https://pds.invalid', + }) + + const triage = await createDidAndKey({ + plcUrl: config.plcUrl, + handle: 'triage.ozone', + pds: 'https://pds.invalid', + }) + const port = config.port || (await getPort()) const url = `http://localhost:${port}` @@ -37,11 +61,13 @@ export class TestOzone { signingKeyHex, ...config, adminPassword: ADMIN_PASSWORD, - moderatorPassword: MOD_PASSWORD, - triagePassword: TRIAGE_PASSWORD, - adminDids: [], - moderatorDids: [], - triageDids: [], + adminDids: [...(config.adminDids ?? []), admin.did], + moderatorDids: [ + ...(config.moderatorDids ?? []), + config.appviewDid, + moderator.did, + ], + triageDids: [...(config.triageDids ?? []), triage.did], } // Separate migration db in case migration changes some connection state that we need in the tests, e.g. "alter database ... set ..." @@ -70,7 +96,7 @@ export class TestOzone { // don't do event reversal in dev-env await daemon.ctx.eventReverser.destroy() - return new TestOzone(url, port, server, daemon) + return new TestOzone(url, port, server, daemon, admin, moderator, triage) } get ctx(): ozone.AppContext { @@ -81,23 +107,35 @@ export class TestOzone { return new AtpAgent({ service: this.url }) } - adminAuth(role: 'admin' | 'moderator' | 'triage' = 'admin'): string { - const password = - role === 'triage' - ? TRIAGE_PASSWORD - : role === 'moderator' - ? MOD_PASSWORD - : ADMIN_PASSWORD - return ( - 'Basic ' + - ui8.toString(ui8.fromString(`admin:${password}`, 'utf8'), 'base64pad') - ) + getModClient() { + return new ModeratorClient(this) } - adminAuthHeaders(role?: 'admin' | 'moderator' | 'triage') { - return { - authorization: this.adminAuth(role), - } + addAdminDid(did: string) { + this.ctx.cfg.access.admins.push(did) + } + + addModeratorDid(did: string) { + this.ctx.cfg.access.moderators.push(did) + } + + addTriageDid(did: string) { + this.ctx.cfg.access.triage.push(did) + } + + async modHeaders(role: 'admin' | 'moderator' | 'triage' = 'moderator') { + const account = + role === 'admin' + ? this.adminAccnt + : role === 'moderator' + ? this.moderatorAccnt + : this.triageAccnt + const jwt = await createServiceJwt({ + iss: account.did, + aud: this.ctx.cfg.service.did, + keypair: account.key, + }) + return { authorization: `Bearer ${jwt}` } } async processAll() { diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index cd73c286960..d1b3cbbc330 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -8,12 +8,7 @@ import { createSecretKeyObject } from '@atproto/pds/src/auth-verifier' import { Secp256k1Keypair, randomStr } from '@atproto/crypto' import { AtpAgent } from '@atproto/api' import { PdsConfig } from './types' -import { - ADMIN_PASSWORD, - JWT_SECRET, - MOD_PASSWORD, - TRIAGE_PASSWORD, -} from './const' +import { ADMIN_PASSWORD, JWT_SECRET } from './const' export class TestPds { constructor( @@ -41,8 +36,6 @@ export class TestPds { blobstoreDiskLocation: blobstoreLoc, recoveryDidKey: recoveryKey, adminPassword: ADMIN_PASSWORD, - moderatorPassword: MOD_PASSWORD, - triagePassword: TRIAGE_PASSWORD, jwtSecret: JWT_SECRET, serviceHandleDomains: ['.test'], bskyAppViewUrl: 'https://appview.invalid', @@ -74,22 +67,19 @@ export class TestPds { return agent } - adminAuth(role: 'admin' | 'moderator' | 'triage' = 'admin'): string { - const password = - role === 'triage' - ? TRIAGE_PASSWORD - : role === 'moderator' - ? MOD_PASSWORD - : ADMIN_PASSWORD + adminAuth(): string { return ( 'Basic ' + - ui8.toString(ui8.fromString(`admin:${password}`, 'utf8'), 'base64pad') + ui8.toString( + ui8.fromString(`admin:${ADMIN_PASSWORD}`, 'utf8'), + 'base64pad', + ) ) } - adminAuthHeaders(role?: 'admin' | 'moderator' | 'triage') { + adminAuthHeaders() { return { - authorization: this.adminAuth(role), + authorization: this.adminAuth(), } } diff --git a/packages/dev-env/src/seed/client.ts b/packages/dev-env/src/seed/client.ts index 984115731dd..1e5a9ae10d4 100644 --- a/packages/dev-env/src/seed/client.ts +++ b/packages/dev-env/src/seed/client.ts @@ -2,7 +2,6 @@ import fs from 'fs/promises' import { CID } from 'multiformats/cid' import AtpAgent from '@atproto/api' import { Main as Facet } from '@atproto/api/src/client/types/app/bsky/richtext/facet' -import { InputSchema as TakeActionInput } from '@atproto/api/src/client/types/com/atproto/admin/emitModerationEvent' import { InputSchema as CreateReportInput } from '@atproto/api/src/client/types/com/atproto/moderation/createReport' import { Record as PostRecord } from '@atproto/api/src/client/types/app/bsky/feed/post' import { Record as LikeRecord } from '@atproto/api/src/client/types/app/bsky/feed/like' @@ -423,53 +422,6 @@ export class SeedClient< delete foundList.items[subject] } - async emitModerationEvent(opts: { - event: TakeActionInput['event'] - subject: TakeActionInput['subject'] - reason?: string - createdBy?: string - meta?: TakeActionInput['meta'] - }) { - const { - event, - subject, - reason = 'X', - createdBy = 'did:example:admin', - } = opts - const result = await this.agent.api.com.atproto.admin.emitModerationEvent( - { event, subject, createdBy, reason }, - { - encoding: 'application/json', - headers: this.adminAuthHeaders(), - }, - ) - return result.data - } - - async reverseModerationAction(opts: { - id: number - subject: TakeActionInput['subject'] - reason?: string - createdBy?: string - }) { - const { subject, reason = 'X', createdBy = 'did:example:admin' } = opts - const result = await this.agent.api.com.atproto.admin.emitModerationEvent( - { - subject, - event: { - $type: 'com.atproto.admin.defs#modEventReverseTakedown', - comment: reason, - }, - createdBy, - }, - { - encoding: 'application/json', - headers: this.adminAuthHeaders(), - }, - ) - return result.data - } - async createReport(opts: { reasonType: CreateReportInput['reasonType'] subject: CreateReportInput['subject'] @@ -487,10 +439,6 @@ export class SeedClient< return result.data } - adminAuthHeaders() { - return this.network.pds.adminAuthHeaders() - } - getHeaders(did: string) { return SeedClient.getHeaders(this.accounts[did].accessJwt) } diff --git a/packages/dev-env/src/types.ts b/packages/dev-env/src/types.ts index 83ce4475e10..417b2e3a6bd 100644 --- a/packages/dev-env/src/types.ts +++ b/packages/dev-env/src/types.ts @@ -2,7 +2,7 @@ import * as pds from '@atproto/pds' import * as bsky from '@atproto/bsky' import * as bsync from '@atproto/bsync' import * as ozone from '@atproto/ozone' -import { ExportableKeypair } from '@atproto/crypto' +import { ExportableKeypair, Keypair } from '@atproto/crypto' export type PlcConfig = { port?: number @@ -31,6 +31,7 @@ export type BsyncConfig = Partial & { export type OzoneConfig = Partial & { plcUrl: string appviewUrl: string + appviewDid: string dbPostgresUrl: string migration?: string signingKey?: ExportableKeypair @@ -45,3 +46,8 @@ export type TestServerParams = { bsky: Partial ozone: Partial } + +export type DidAndKey = { + did: string + key: Keypair +} diff --git a/packages/dev-env/src/util.ts b/packages/dev-env/src/util.ts index 679ca89c7a8..3c7276ef330 100644 --- a/packages/dev-env/src/util.ts +++ b/packages/dev-env/src/util.ts @@ -1,7 +1,10 @@ import axios from 'axios' +import * as plc from '@did-plc/lib' import { IdResolver } from '@atproto/identity' +import { Secp256k1Keypair } from '@atproto/crypto' import { TestPds } from './pds' import { TestBsky } from './bsky' +import { DidAndKey } from './types' export const mockNetworkUtilities = (pds: TestPds, bsky?: TestBsky) => { mockResolvers(pds.ctx.idResolver, pds) @@ -67,3 +70,23 @@ export const uniqueLockId = () => { usedLockIds.add(lockId) return lockId } + +export const createDidAndKey = async (opts: { + plcUrl: string + handle: string + pds: string +}): Promise => { + const { plcUrl, handle, pds } = opts + const key = await Secp256k1Keypair.create({ exportable: true }) + const did = await new plc.Client(plcUrl).createDid({ + signingKey: key.did(), + rotationKeys: [key.did()], + handle, + pds, + signer: key, + }) + return { + key, + did, + } +} diff --git a/packages/ozone/package.json b/packages/ozone/package.json index da3abaec419..4f512688785 100644 --- a/packages/ozone/package.json +++ b/packages/ozone/package.json @@ -65,6 +65,7 @@ "@types/express-serve-static-core": "^4.17.36", "@types/pg": "^8.6.6", "@types/qs": "^6.9.7", - "axios": "^0.27.2" + "axios": "^0.27.2", + "nodemailer": "^6.8.0" } } diff --git a/packages/ozone/src/api/admin/createCommunicationTemplate.ts b/packages/ozone/src/api/admin/createCommunicationTemplate.ts index f05db2d71f2..33053aa863f 100644 --- a/packages/ozone/src/api/admin/createCommunicationTemplate.ts +++ b/packages/ozone/src/api/admin/createCommunicationTemplate.ts @@ -4,7 +4,7 @@ import AppContext from '../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.createCommunicationTemplate({ - auth: ctx.authVerifier.modOrRole, + auth: ctx.authVerifier.modOrAdminToken, handler: async ({ input, auth }) => { const access = auth.credentials const db = ctx.db diff --git a/packages/ozone/src/api/admin/deleteCommunicationTemplate.ts b/packages/ozone/src/api/admin/deleteCommunicationTemplate.ts index b70028e710d..671a8628700 100644 --- a/packages/ozone/src/api/admin/deleteCommunicationTemplate.ts +++ b/packages/ozone/src/api/admin/deleteCommunicationTemplate.ts @@ -4,7 +4,7 @@ import AppContext from '../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.deleteCommunicationTemplate({ - auth: ctx.authVerifier.modOrRole, + auth: ctx.authVerifier.modOrAdminToken, handler: async ({ input, auth }) => { const access = auth.credentials const db = ctx.db diff --git a/packages/ozone/src/api/admin/emitModerationEvent.ts b/packages/ozone/src/api/admin/emitModerationEvent.ts index f1bdbab3462..f2885e67e4c 100644 --- a/packages/ozone/src/api/admin/emitModerationEvent.ts +++ b/packages/ozone/src/api/admin/emitModerationEvent.ts @@ -13,12 +13,16 @@ import { retryHttp } from '../../util' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.emitModerationEvent({ - auth: ctx.authVerifier.modOrRole, + auth: ctx.authVerifier.modOrAdminToken, handler: async ({ input, auth }) => { const access = auth.credentials + const createdBy = + auth.credentials.type === 'moderator' + ? auth.credentials.iss + : input.body.createdBy const db = ctx.db const moderationService = ctx.modService(db) - const { createdBy, event } = input.body + const { event } = input.body const isTakedownEvent = isModEventTakedown(event) const isReverseTakedownEvent = isModEventReverseTakedown(event) const isLabelEvent = isModEventLabel(event) diff --git a/packages/ozone/src/api/admin/getModerationEvent.ts b/packages/ozone/src/api/admin/getModerationEvent.ts index fc6b433789e..b29d49af4cc 100644 --- a/packages/ozone/src/api/admin/getModerationEvent.ts +++ b/packages/ozone/src/api/admin/getModerationEvent.ts @@ -3,7 +3,7 @@ import AppContext from '../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationEvent({ - auth: ctx.authVerifier.modOrRole, + auth: ctx.authVerifier.modOrAdminToken, handler: async ({ params }) => { const { id } = params const db = ctx.db diff --git a/packages/ozone/src/api/admin/getRecord.ts b/packages/ozone/src/api/admin/getRecord.ts index 061fc87a0d6..e59b8522638 100644 --- a/packages/ozone/src/api/admin/getRecord.ts +++ b/packages/ozone/src/api/admin/getRecord.ts @@ -6,7 +6,7 @@ import { AtUri } from '@atproto/syntax' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getRecord({ - auth: ctx.authVerifier.modOrRole, + auth: ctx.authVerifier.modOrAdminToken, handler: async ({ params, auth }) => { const db = ctx.db diff --git a/packages/ozone/src/api/admin/getRepo.ts b/packages/ozone/src/api/admin/getRepo.ts index bd0c03c13c6..cb8d85127b8 100644 --- a/packages/ozone/src/api/admin/getRepo.ts +++ b/packages/ozone/src/api/admin/getRepo.ts @@ -5,7 +5,7 @@ import { addAccountInfoToRepoViewDetail, getPdsAccountInfo } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getRepo({ - auth: ctx.authVerifier.modOrRole, + auth: ctx.authVerifier.modOrAdminToken, handler: async ({ params, auth }) => { const { did } = params const db = ctx.db diff --git a/packages/ozone/src/api/admin/listCommunicationTemplates.ts b/packages/ozone/src/api/admin/listCommunicationTemplates.ts index d8a88947895..41b1a364c46 100644 --- a/packages/ozone/src/api/admin/listCommunicationTemplates.ts +++ b/packages/ozone/src/api/admin/listCommunicationTemplates.ts @@ -4,7 +4,7 @@ import AppContext from '../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.listCommunicationTemplates({ - auth: ctx.authVerifier.modOrRole, + auth: ctx.authVerifier.modOrAdminToken, handler: async ({ auth }) => { const access = auth.credentials const db = ctx.db diff --git a/packages/ozone/src/api/admin/queryModerationEvents.ts b/packages/ozone/src/api/admin/queryModerationEvents.ts index 959ee2dcd37..ebcda59ef38 100644 --- a/packages/ozone/src/api/admin/queryModerationEvents.ts +++ b/packages/ozone/src/api/admin/queryModerationEvents.ts @@ -4,7 +4,7 @@ import { getEventType } from '../moderation/util' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.queryModerationEvents({ - auth: ctx.authVerifier.modOrRole, + auth: ctx.authVerifier.modOrAdminToken, handler: async ({ params }) => { const { subject, diff --git a/packages/ozone/src/api/admin/queryModerationStatuses.ts b/packages/ozone/src/api/admin/queryModerationStatuses.ts index 2c4e0d0dd10..a484ae8aedc 100644 --- a/packages/ozone/src/api/admin/queryModerationStatuses.ts +++ b/packages/ozone/src/api/admin/queryModerationStatuses.ts @@ -4,7 +4,7 @@ import { getReviewState } from '../moderation/util' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.queryModerationStatuses({ - auth: ctx.authVerifier.modOrRole, + auth: ctx.authVerifier.modOrAdminToken, handler: async ({ params }) => { const { subject, diff --git a/packages/ozone/src/api/admin/searchRepos.ts b/packages/ozone/src/api/admin/searchRepos.ts index 6026a5ccdc9..ce6e1cfa3a6 100644 --- a/packages/ozone/src/api/admin/searchRepos.ts +++ b/packages/ozone/src/api/admin/searchRepos.ts @@ -4,7 +4,7 @@ import { mapDefined } from '@atproto/common' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.searchRepos({ - auth: ctx.authVerifier.modOrRole, + auth: ctx.authVerifier.modOrAdminToken, handler: async ({ params }) => { const modService = ctx.modService(ctx.db) diff --git a/packages/ozone/src/api/admin/updateCommunicationTemplate.ts b/packages/ozone/src/api/admin/updateCommunicationTemplate.ts index 1b1b124e7f9..fde11111390 100644 --- a/packages/ozone/src/api/admin/updateCommunicationTemplate.ts +++ b/packages/ozone/src/api/admin/updateCommunicationTemplate.ts @@ -4,7 +4,7 @@ import AppContext from '../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.updateCommunicationTemplate({ - auth: ctx.authVerifier.modOrRole, + auth: ctx.authVerifier.modOrAdminToken, handler: async ({ input, auth }) => { const access = auth.credentials const db = ctx.db diff --git a/packages/ozone/src/api/admin/util.ts b/packages/ozone/src/api/admin/util.ts index b4df0664327..004f6eca908 100644 --- a/packages/ozone/src/api/admin/util.ts +++ b/packages/ozone/src/api/admin/util.ts @@ -16,7 +16,7 @@ export const getPdsAccountInfo = async ( try { const res = await agent.api.com.atproto.admin.getAccountInfo({ did }, auth) return res.data - } catch (err) { + } catch { return null } } diff --git a/packages/ozone/src/api/moderation/createReport.ts b/packages/ozone/src/api/moderation/createReport.ts index 3abf5080e7d..5e9769ff33a 100644 --- a/packages/ozone/src/api/moderation/createReport.ts +++ b/packages/ozone/src/api/moderation/createReport.ts @@ -8,8 +8,7 @@ import { ModerationLangService } from '../../mod-service/lang' export default function (server: Server, ctx: AppContext) { server.com.atproto.moderation.createReport({ - // @TODO anonymous reports w/ optional auth are a temporary measure - auth: ctx.authVerifier.standardOptionalOrRole, + auth: ctx.authVerifier.standard, handler: async ({ input, auth }) => { const requester = 'iss' in auth.credentials ? auth.credentials.iss : ctx.cfg.service.did diff --git a/packages/ozone/src/api/proxied.ts b/packages/ozone/src/api/proxied.ts index a2d5040989e..f1d0d748ea7 100644 --- a/packages/ozone/src/api/proxied.ts +++ b/packages/ozone/src/api/proxied.ts @@ -3,7 +3,7 @@ import AppContext from '../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.getProfile({ - auth: ctx.authVerifier.modOrRole, + auth: ctx.authVerifier.moderator, handler: async (request) => { const res = await ctx.appviewAgent.api.app.bsky.actor.getProfile( request.params, @@ -17,7 +17,7 @@ export default function (server: Server, ctx: AppContext) { }) server.app.bsky.actor.getProfiles({ - auth: ctx.authVerifier.modOrRole, + auth: ctx.authVerifier.moderator, handler: async (request) => { const res = await ctx.appviewAgent.api.app.bsky.actor.getProfiles( request.params, @@ -31,7 +31,7 @@ export default function (server: Server, ctx: AppContext) { }) server.app.bsky.feed.getAuthorFeed({ - auth: ctx.authVerifier.modOrRole, + auth: ctx.authVerifier.moderator, handler: async (request) => { const res = await ctx.appviewAgent.api.app.bsky.feed.getAuthorFeed( request.params, @@ -45,7 +45,7 @@ export default function (server: Server, ctx: AppContext) { }) server.app.bsky.feed.getPostThread({ - auth: ctx.authVerifier.modOrRole, + auth: ctx.authVerifier.moderator, handler: async (request) => { const res = await ctx.appviewAgent.api.app.bsky.feed.getPostThread( request.params, @@ -59,7 +59,7 @@ export default function (server: Server, ctx: AppContext) { }) server.app.bsky.feed.getFeedGenerator({ - auth: ctx.authVerifier.modOrRole, + auth: ctx.authVerifier.moderator, handler: async (request) => { const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( request.params, @@ -73,7 +73,7 @@ export default function (server: Server, ctx: AppContext) { }) server.app.bsky.graph.getFollows({ - auth: ctx.authVerifier.modOrRole, + auth: ctx.authVerifier.moderator, handler: async (request) => { const res = await ctx.appviewAgent.api.app.bsky.graph.getFollows( request.params, @@ -87,7 +87,7 @@ export default function (server: Server, ctx: AppContext) { }) server.app.bsky.graph.getFollowers({ - auth: ctx.authVerifier.modOrRole, + auth: ctx.authVerifier.moderator, handler: async (request) => { const res = await ctx.appviewAgent.api.app.bsky.graph.getFollowers( request.params, @@ -101,7 +101,7 @@ export default function (server: Server, ctx: AppContext) { }) server.app.bsky.graph.getList({ - auth: ctx.authVerifier.modOrRole, + auth: ctx.authVerifier.moderator, handler: async (request) => { const res = await ctx.appviewAgent.api.app.bsky.graph.getList( request.params, diff --git a/packages/ozone/src/api/temp/fetchLabels.ts b/packages/ozone/src/api/temp/fetchLabels.ts index b2fbfbb846a..d1a3f5e8e26 100644 --- a/packages/ozone/src/api/temp/fetchLabels.ts +++ b/packages/ozone/src/api/temp/fetchLabels.ts @@ -8,7 +8,7 @@ import { export default function (server: Server, ctx: AppContext) { server.com.atproto.temp.fetchLabels({ - auth: ctx.authVerifier.standardOptionalOrRole, + auth: ctx.authVerifier.standardOptionalOrAdminToken, handler: async ({ auth, params }) => { const { limit } = params const since = diff --git a/packages/ozone/src/auth-verifier.ts b/packages/ozone/src/auth-verifier.ts index 48ca241e6ef..2eec84cb683 100644 --- a/packages/ozone/src/auth-verifier.ts +++ b/packages/ozone/src/auth-verifier.ts @@ -7,11 +7,11 @@ type ReqCtx = { req: express.Request } -type RoleOutput = { +type AdminTokenOutput = { credentials: { - type: 'role' - isAdmin: boolean - isModerator: boolean + type: 'admin_token' + isAdmin: true + isModerator: true isTriage: true } } @@ -51,8 +51,6 @@ export type AuthVerifierOpts = { moderators: string[] triage: string[] adminPassword: string - moderatorPassword: string - triagePassword: string } export class AuthVerifier { @@ -61,8 +59,6 @@ export class AuthVerifier { moderators: string[] triage: string[] private adminPassword: string - private moderatorPassword: string - private triagePassword: string constructor(public idResolver: IdResolver, opts: AuthVerifierOpts) { this.serviceDid = opts.serviceDid @@ -70,15 +66,15 @@ export class AuthVerifier { this.moderators = opts.moderators this.triage = opts.triage this.adminPassword = opts.adminPassword - this.moderatorPassword = opts.moderatorPassword - this.triagePassword = opts.triagePassword } - modOrRole = async (reqCtx: ReqCtx): Promise => { - if (isBearerToken(reqCtx.req)) { - return this.moderator(reqCtx) + modOrAdminToken = async ( + reqCtx: ReqCtx, + ): Promise => { + if (isBasicToken(reqCtx.req)) { + return this.adminToken(reqCtx) } else { - return this.role(reqCtx) + return this.moderator(reqCtx) } } @@ -138,36 +134,30 @@ export class AuthVerifier { return this.nullCreds() } - standardOptionalOrRole = async ( + standardOptionalOrAdminToken = async ( reqCtx: ReqCtx, - ): Promise => { + ): Promise => { if (isBearerToken(reqCtx.req)) { return this.standard(reqCtx) } else if (isBasicToken(reqCtx.req)) { - return this.role(reqCtx) + return this.adminToken(reqCtx) } else { return this.nullCreds() } } - role = async (reqCtx: ReqCtx): Promise => { + adminToken = async (reqCtx: ReqCtx): Promise => { const parsed = parseBasicAuth(reqCtx.req.headers.authorization ?? '') const { username, password } = parsed ?? {} - if (username !== 'admin') { - throw new AuthRequiredError() - } - const isAdmin = password === this.adminPassword - const isModerator = isAdmin || password === this.moderatorPassword - const isTriage = isModerator || password === this.triagePassword - if (!isTriage) { + if (username !== 'admin' || password !== this.adminPassword) { throw new AuthRequiredError() } return { credentials: { - type: 'role', - isAdmin, - isModerator, - isTriage, + type: 'admin_token', + isAdmin: true, + isModerator: true, + isTriage: true, }, } } diff --git a/packages/ozone/src/config/env.ts b/packages/ozone/src/config/env.ts index 554a0f38ba9..e8e1399ae99 100644 --- a/packages/ozone/src/config/env.ts +++ b/packages/ozone/src/config/env.ts @@ -23,8 +23,6 @@ export const readEnv = (): OzoneEnvironment => { moderatorDids: envList('OZONE_MODERATOR_DIDS'), triageDids: envList('OZONE_TRIAGE_DIDS'), adminPassword: envStr('OZONE_ADMIN_PASSWORD'), - moderatorPassword: envStr('OZONE_MODERATOR_PASSWORD'), - triagePassword: envStr('OZONE_TRIAGE_PASSWORD'), signingKeyHex: envStr('OZONE_SIGNING_KEY_HEX'), } } @@ -51,7 +49,5 @@ export type OzoneEnvironment = { moderatorDids: string[] triageDids: string[] adminPassword?: string - moderatorPassword?: string - triagePassword?: string signingKeyHex?: string } diff --git a/packages/ozone/src/config/secrets.ts b/packages/ozone/src/config/secrets.ts index 22593bcec26..659e4506a03 100644 --- a/packages/ozone/src/config/secrets.ts +++ b/packages/ozone/src/config/secrets.ts @@ -3,21 +3,15 @@ import { OzoneEnvironment } from './env' export const envToSecrets = (env: OzoneEnvironment): OzoneSecrets => { assert(env.adminPassword) - assert(env.moderatorPassword) - assert(env.triagePassword) assert(env.signingKeyHex) return { adminPassword: env.adminPassword, - moderatorPassword: env.moderatorPassword, - triagePassword: env.triagePassword, signingKeyHex: env.signingKeyHex, } } export type OzoneSecrets = { adminPassword: string - moderatorPassword: string - triagePassword: string signingKeyHex: string } diff --git a/packages/ozone/src/context.ts b/packages/ozone/src/context.ts index 5205e54f848..699a3f65335 100644 --- a/packages/ozone/src/context.ts +++ b/packages/ozone/src/context.ts @@ -92,8 +92,6 @@ export class AppContext { moderators: cfg.access.moderators, triage: cfg.access.triage, adminPassword: secrets.adminPassword, - moderatorPassword: secrets.moderatorPassword, - triagePassword: secrets.triagePassword, }) return new AppContext( diff --git a/packages/ozone/src/daemon/context.ts b/packages/ozone/src/daemon/context.ts index 3ed0596c2ed..7afca1bf721 100644 --- a/packages/ozone/src/daemon/context.ts +++ b/packages/ozone/src/daemon/context.ts @@ -35,7 +35,7 @@ export class DaemonContext { const appviewAgent = new AtpAgent({ service: cfg.appview.url }) const createAuthHeaders = (aud: string) => createServiceAuthHeaders({ - iss: cfg.service.did, + iss: `${cfg.service.did}#atproto_labeler`, aud, keypair: signingKey, }) diff --git a/packages/ozone/tests/__snapshots__/get-record.test.ts.snap b/packages/ozone/tests/__snapshots__/get-record.test.ts.snap index decfb8f4ba4..23090b631b3 100644 --- a/packages/ozone/tests/__snapshots__/get-record.test.ts.snap +++ b/packages/ozone/tests/__snapshots__/get-record.test.ts.snap @@ -11,7 +11,7 @@ Object { "cid": "cids(0)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(1)", + "src": "user(2)", "uri": "record(0)", "val": "!unspecced-takedown", }, @@ -30,7 +30,7 @@ Object { "id": 1, "lastReportedAt": "1970-01-01T00:00:00.000Z", "lastReviewedAt": "1970-01-01T00:00:00.000Z", - "lastReviewedBy": "did:example:admin", + "lastReviewedBy": "user(1)", "reviewState": "com.atproto.admin.defs#reviewClosed", "subject": Object { "$type": "com.atproto.repo.strongRef", @@ -108,7 +108,7 @@ Object { "cid": "cids(0)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(1)", + "src": "user(2)", "uri": "record(0)", "val": "!unspecced-takedown", }, @@ -127,7 +127,7 @@ Object { "id": 1, "lastReportedAt": "1970-01-01T00:00:00.000Z", "lastReviewedAt": "1970-01-01T00:00:00.000Z", - "lastReviewedBy": "did:example:admin", + "lastReviewedBy": "user(1)", "reviewState": "com.atproto.admin.defs#reviewClosed", "subject": Object { "$type": "com.atproto.repo.strongRef", diff --git a/packages/ozone/tests/__snapshots__/get-repo.test.ts.snap b/packages/ozone/tests/__snapshots__/get-repo.test.ts.snap index 67404b88362..2ae1b8b2a48 100644 --- a/packages/ozone/tests/__snapshots__/get-repo.test.ts.snap +++ b/packages/ozone/tests/__snapshots__/get-repo.test.ts.snap @@ -12,7 +12,7 @@ Object { Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(1)", + "src": "user(2)", "uri": "user(0)", "val": "!unspecced-takedown", }, @@ -23,7 +23,7 @@ Object { "id": 1, "lastReportedAt": "1970-01-01T00:00:00.000Z", "lastReviewedAt": "1970-01-01T00:00:00.000Z", - "lastReviewedBy": "did:example:admin", + "lastReviewedBy": "user(1)", "reviewState": "com.atproto.admin.defs#reviewClosed", "subject": Object { "$type": "com.atproto.admin.defs#repoRef", diff --git a/packages/ozone/tests/communication-templates.test.ts b/packages/ozone/tests/communication-templates.test.ts index 4ddcb45abb4..253ba41fa71 100644 --- a/packages/ozone/tests/communication-templates.test.ts +++ b/packages/ozone/tests/communication-templates.test.ts @@ -31,7 +31,7 @@ describe('communication-templates', () => { await agent.api.com.atproto.admin.listCommunicationTemplates( {}, { - headers: network.ozone.adminAuthHeaders('moderator'), + headers: await network.ozone.modHeaders('moderator'), }, ) return data.communicationTemplates @@ -44,7 +44,7 @@ describe('communication-templates', () => { { ...templateOne, createdBy: sc.dids.bob }, { encoding: 'application/json', - headers: network.ozone.adminAuthHeaders('moderator'), + headers: await network.ozone.modHeaders('moderator'), }, ) await expect(moderatorReq).rejects.toThrow( @@ -55,7 +55,7 @@ describe('communication-templates', () => { { ...templateOne, createdBy: sc.dids.bob }, { encoding: 'application/json', - headers: network.ozone.adminAuthHeaders('admin'), + headers: await network.ozone.modHeaders('admin'), }, ) @@ -79,7 +79,7 @@ describe('communication-templates', () => { { ...templateTwo, createdBy: sc.dids.bob }, { encoding: 'application/json', - headers: network.ozone.adminAuthHeaders('admin'), + headers: await network.ozone.modHeaders('admin'), }, ) @@ -95,7 +95,7 @@ describe('communication-templates', () => { { id: '1', updatedBy: sc.dids.bob, name: '1 Test template' }, { encoding: 'application/json', - headers: network.ozone.adminAuthHeaders('admin'), + headers: await network.ozone.modHeaders('admin'), }, ) @@ -109,7 +109,7 @@ describe('communication-templates', () => { { id: '1' }, { encoding: 'application/json', - headers: network.ozone.adminAuthHeaders('moderator'), + headers: await network.ozone.modHeaders('moderator'), }, ) @@ -121,7 +121,7 @@ describe('communication-templates', () => { { id: '1' }, { encoding: 'application/json', - headers: network.ozone.adminAuthHeaders('admin'), + headers: await network.ozone.modHeaders('admin'), }, ) const list = await listTemplates() diff --git a/packages/ozone/tests/get-record.test.ts b/packages/ozone/tests/get-record.test.ts index 735f4725f11..9334f10e6d8 100644 --- a/packages/ozone/tests/get-record.test.ts +++ b/packages/ozone/tests/get-record.test.ts @@ -1,4 +1,10 @@ -import { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env' +import { + SeedClient, + TestNetwork, + basicSeed, + TestOzone, + ModeratorClient, +} from '@atproto/dev-env' import AtpAgent from '@atproto/api' import { AtUri } from '@atproto/syntax' import { @@ -9,15 +15,19 @@ import { forSnapshot } from './_util' describe('admin get record view', () => { let network: TestNetwork + let ozone: TestOzone let agent: AtpAgent let sc: SeedClient + let modClient: ModeratorClient beforeAll(async () => { network = await TestNetwork.create({ dbPostgresSchema: 'ozone_admin_get_record', }) - agent = network.pds.getClient() + ozone = network.ozone + agent = ozone.getClient() sc = network.getSeedClient() + modClient = ozone.getModClient() await basicSeed(sc) await network.processAll() }) @@ -46,7 +56,7 @@ describe('admin get record view', () => { cid: sc.posts[sc.dids.alice][0].ref.cidStr, }, }) - await sc.emitModerationEvent({ + await modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', @@ -59,7 +69,7 @@ describe('admin get record view', () => { it('gets a record by uri, even when taken down.', async () => { const result = await agent.api.com.atproto.admin.getRecord( { uri: sc.posts[sc.dids.alice][0].ref.uriStr }, - { headers: network.pds.adminAuthHeaders() }, + { headers: await ozone.modHeaders() }, ) expect(forSnapshot(result.data)).toMatchSnapshot() }) @@ -70,7 +80,7 @@ describe('admin get record view', () => { uri: sc.posts[sc.dids.alice][0].ref.uriStr, cid: sc.posts[sc.dids.alice][0].ref.cidStr, }, - { headers: network.pds.adminAuthHeaders() }, + { headers: await ozone.modHeaders() }, ) expect(forSnapshot(result.data)).toMatchSnapshot() }) @@ -84,7 +94,7 @@ describe('admin get record view', () => { 'badrkey', ).toString(), }, - { headers: network.pds.adminAuthHeaders() }, + { headers: await ozone.modHeaders() }, ) await expect(promise).rejects.toThrow('Record not found') }) @@ -95,7 +105,7 @@ describe('admin get record view', () => { uri: sc.posts[sc.dids.alice][0].ref.uriStr, cid: sc.posts[sc.dids.alice][1].ref.cidStr, // Mismatching cid }, - { headers: network.pds.adminAuthHeaders() }, + { headers: await ozone.modHeaders() }, ) await expect(promise).rejects.toThrow('Record not found') }) diff --git a/packages/ozone/tests/get-repo.test.ts b/packages/ozone/tests/get-repo.test.ts index 1e0491465f5..45ca3f37190 100644 --- a/packages/ozone/tests/get-repo.test.ts +++ b/packages/ozone/tests/get-repo.test.ts @@ -1,4 +1,10 @@ -import { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env' +import { + SeedClient, + TestNetwork, + TestOzone, + basicSeed, + ModeratorClient, +} from '@atproto/dev-env' import AtpAgent from '@atproto/api' import { REASONOTHER, @@ -8,15 +14,21 @@ import { forSnapshot } from './_util' describe('admin get repo view', () => { let network: TestNetwork + let ozone: TestOzone let agent: AtpAgent + let pdsAgent: AtpAgent let sc: SeedClient + let modClient: ModeratorClient beforeAll(async () => { network = await TestNetwork.create({ dbPostgresSchema: 'ozone_admin_get_repo', }) - agent = network.pds.getClient() + ozone = network.ozone + agent = ozone.getClient() + pdsAgent = network.pds.getClient() sc = network.getSeedClient() + modClient = ozone.getModClient() await basicSeed(sc) await network.processAll() }) @@ -26,7 +38,7 @@ describe('admin get repo view', () => { }) beforeAll(async () => { - await sc.emitModerationEvent({ + await modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventAcknowledge' }, subject: { $type: 'com.atproto.admin.defs#repoRef', @@ -50,7 +62,7 @@ describe('admin get repo view', () => { did: sc.dids.alice, }, }) - await sc.emitModerationEvent({ + await modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', @@ -62,7 +74,7 @@ describe('admin get repo view', () => { it('gets a repo by did, even when taken down.', async () => { const result = await agent.api.com.atproto.admin.getRepo( { did: sc.dids.alice }, - { headers: network.pds.adminAuthHeaders() }, + { headers: await ozone.modHeaders() }, ) expect(forSnapshot(result.data)).toMatchSnapshot() }) @@ -70,15 +82,15 @@ describe('admin get repo view', () => { it('does not include account emails for triage mods.', async () => { const { data: admin } = await agent.api.com.atproto.admin.getRepo( { did: sc.dids.bob }, - { headers: network.pds.adminAuthHeaders() }, + { headers: await ozone.modHeaders() }, ) const { data: moderator } = await agent.api.com.atproto.admin.getRepo( { did: sc.dids.bob }, - { headers: network.pds.adminAuthHeaders('moderator') }, + { headers: await ozone.modHeaders('moderator') }, ) const { data: triage } = await agent.api.com.atproto.admin.getRepo( { did: sc.dids.bob }, - { headers: network.pds.adminAuthHeaders('triage') }, + { headers: await ozone.modHeaders('triage') }, ) expect(admin.email).toEqual('bob@test.com') expect(moderator.email).toEqual('bob@test.com') @@ -90,7 +102,7 @@ describe('admin get repo view', () => { const { data: beforeEmailVerification } = await agent.api.com.atproto.admin.getRepo( { did: sc.dids.bob }, - { headers: network.pds.adminAuthHeaders() }, + { headers: await ozone.modHeaders() }, ) expect(beforeEmailVerification.emailConfirmedAt).toBeUndefined() @@ -101,7 +113,7 @@ describe('admin get repo view', () => { sc.dids.bob, 'confirm_email', ) - await agent.api.com.atproto.server.confirmEmail( + await pdsAgent.api.com.atproto.server.confirmEmail( { email: bobsAccount.email, token: verificationToken }, { encoding: 'application/json', @@ -112,7 +124,7 @@ describe('admin get repo view', () => { const { data: afterEmailVerification } = await agent.api.com.atproto.admin.getRepo( { did: sc.dids.bob }, - { headers: network.pds.adminAuthHeaders() }, + { headers: await ozone.modHeaders() }, ) expect(afterEmailVerification.emailConfirmedAt).toBeTruthy() @@ -124,7 +136,7 @@ describe('admin get repo view', () => { it('fails when repo does not exist.', async () => { const promise = agent.api.com.atproto.admin.getRepo( { did: 'did:plc:doesnotexist' }, - { headers: network.pds.adminAuthHeaders() }, + { headers: await ozone.modHeaders() }, ) await expect(promise).rejects.toThrow('Repo not found') }) diff --git a/packages/ozone/tests/moderation-appeals.test.ts b/packages/ozone/tests/moderation-appeals.test.ts index 31dae61f9ef..5476c94c99a 100644 --- a/packages/ozone/tests/moderation-appeals.test.ts +++ b/packages/ozone/tests/moderation-appeals.test.ts @@ -1,9 +1,10 @@ -import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' -import AtpAgent, { - ComAtprotoAdminDefs, - ComAtprotoAdminEmitModerationEvent, - ComAtprotoAdminQueryModerationStatuses, -} from '@atproto/api' +import { + TestNetwork, + SeedClient, + basicSeed, + ModeratorClient, +} from '@atproto/dev-env' +import { ComAtprotoAdminDefs } from '@atproto/api' import { REASONMISLEADING, REASONSPAM, @@ -17,33 +18,15 @@ import { REVIEWESCALATED } from '../src/lexicon/types/com/atproto/admin/defs' describe('moderation-appeals', () => { let network: TestNetwork - let agent: AtpAgent - let pdsAgent: AtpAgent let sc: SeedClient - - const emitModerationEvent = async ( - eventData: ComAtprotoAdminEmitModerationEvent.InputSchema, - ) => { - return pdsAgent.api.com.atproto.admin.emitModerationEvent(eventData, { - encoding: 'application/json', - headers: network.ozone.adminAuthHeaders('moderator'), - }) - } - - const queryModerationStatuses = ( - statusQuery: ComAtprotoAdminQueryModerationStatuses.QueryParams, - ) => - agent.api.com.atproto.admin.queryModerationStatuses(statusQuery, { - headers: network.ozone.adminAuthHeaders('moderator'), - }) + let modClient: ModeratorClient beforeAll(async () => { network = await TestNetwork.create({ dbPostgresSchema: 'ozone_moderation_appeals', }) - agent = network.ozone.getClient() - pdsAgent = network.pds.getClient() sc = network.getSeedClient() + modClient = network.ozone.getModClient() await basicSeed(sc) await network.processAll() }) @@ -57,12 +40,12 @@ describe('moderation-appeals', () => { status: string, appealed: boolean | undefined, ): Promise => { - const { data } = await queryModerationStatuses({ + const res = await modClient.queryModerationStatuses({ subject, }) - expect(data.subjectStatuses[0]?.reviewState).toEqual(status) - expect(data.subjectStatuses[0]?.appealed).toEqual(appealed) - return data.subjectStatuses[0] + expect(res.subjectStatuses[0]?.reviewState).toEqual(status) + expect(res.subjectStatuses[0]?.appealed).toEqual(appealed) + return res.subjectStatuses[0] } describe('appeals from users', () => { @@ -83,13 +66,12 @@ describe('moderation-appeals', () => { it('only changes subject status if original author of the content or a moderator is appealing', async () => { // Create a report by alice - await emitModerationEvent({ + await modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventReport', reportType: REASONMISLEADING, }, subject: getBobsPostSubject(), - createdBy: sc.dids.alice, }) await assertBobsPostStatus(REVIEWOPEN, undefined) @@ -108,13 +90,12 @@ describe('moderation-appeals', () => { await assertBobsPostStatus(REVIEWOPEN, undefined) // Emit report event as moderator - await emitModerationEvent({ + await modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventReport', reportType: REASONAPPEAL, }, subject: getBobsPostSubject(), - createdBy: sc.dids.alice, }) // Verify that appeal status changed when appeal report was emitted by moderator @@ -151,23 +132,21 @@ describe('moderation-appeals', () => { }) it('allows multiple appeals and updates last appealed timestamp', async () => { // Resolve appeal with acknowledge - await emitModerationEvent({ + await modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventResolveAppeal', }, subject: getBobsPostSubject(), - createdBy: sc.dids.carol, }) const previousStatus = await assertBobsPostStatus(REVIEWESCALATED, false) - await emitModerationEvent({ + await modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventReport', reportType: REASONAPPEAL, }, subject: getBobsPostSubject(), - createdBy: sc.dids.bob, }) // Verify that even after the appeal event by bob for his post, the appeal status is true again with new timestamp @@ -186,32 +165,29 @@ describe('moderation-appeals', () => { }) it('appeal status is maintained while review state changes based on incoming events', async () => { // Bob reports alice's post - await emitModerationEvent({ + await modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventReport', reportType: REASONMISLEADING, }, subject: getAlicesPostSubject(), - createdBy: sc.dids.bob, }) // Moderator acknowledges the report, assume a label was applied too - await emitModerationEvent({ + await modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventAcknowledge', }, subject: getAlicesPostSubject(), - createdBy: sc.dids.carol, }) // Alice appeals the report - await emitModerationEvent({ + await modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventReport', reportType: REASONAPPEAL, }, subject: getAlicesPostSubject(), - createdBy: sc.dids.alice, }) await assertSubjectStatus( @@ -221,13 +197,12 @@ describe('moderation-appeals', () => { ) // Bob reports it again - await emitModerationEvent({ + await modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventReport', reportType: REASONSPAM, }, subject: getAlicesPostSubject(), - createdBy: sc.dids.bob, }) // Assert that the status is still REVIEWESCALATED, as report events are meant to do @@ -238,12 +213,11 @@ describe('moderation-appeals', () => { ) // Emit an escalation event - await emitModerationEvent({ + await modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventEscalate', }, subject: getAlicesPostSubject(), - createdBy: sc.dids.carol, }) await assertSubjectStatus( @@ -253,25 +227,23 @@ describe('moderation-appeals', () => { ) // Emit an acknowledge event - await emitModerationEvent({ + await modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventAcknowledge', }, subject: getAlicesPostSubject(), - createdBy: sc.dids.carol, }) // Assert that status moved on to reviewClosed while appealed status is still true await assertSubjectStatus(getAlicesPostSubject().uri, REVIEWCLOSED, true) // Emit a resolveAppeal event - await emitModerationEvent({ + await modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventResolveAppeal', comment: 'lgtm', }, subject: getAlicesPostSubject(), - createdBy: sc.dids.carol, }) // Assert that status stayed the same while appealed status is still true diff --git a/packages/ozone/tests/moderation-events.test.ts b/packages/ozone/tests/moderation-events.test.ts index 626d5214896..a6e597d0031 100644 --- a/packages/ozone/tests/moderation-events.test.ts +++ b/packages/ozone/tests/moderation-events.test.ts @@ -1,11 +1,13 @@ import assert from 'node:assert' import EventEmitter, { once } from 'node:events' +import { + TestNetwork, + SeedClient, + basicSeed, + ModeratorClient, +} from '@atproto/dev-env' +import { ComAtprotoAdminDefs } from '@atproto/api' import Mail from 'nodemailer/lib/mailer' -import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' -import AtpAgent, { - ComAtprotoAdminDefs, - ComAtprotoAdminEmitModerationEvent, -} from '@atproto/api' import { forSnapshot } from './_util' import { REASONAPPEAL, @@ -15,23 +17,8 @@ import { describe('moderation-events', () => { let network: TestNetwork - let agent: AtpAgent - let pdsAgent: AtpAgent let sc: SeedClient - - const emitModerationEvent = async ( - eventData: ComAtprotoAdminEmitModerationEvent.InputSchema, - ) => { - return pdsAgent.api.com.atproto.admin.emitModerationEvent(eventData, { - encoding: 'application/json', - headers: network.ozone.adminAuthHeaders('moderator'), - }) - } - - const queryModerationEvents = (eventQuery) => - agent.api.com.atproto.admin.queryModerationEvents(eventQuery, { - headers: network.ozone.adminAuthHeaders('moderator'), - }) + let modClient: ModeratorClient const seedEvents = async () => { const bobsAccount = { @@ -54,25 +41,19 @@ describe('moderation-events', () => { } for (let i = 0; i < 4; i++) { - await emitModerationEvent({ - event: { - $type: 'com.atproto.admin.defs#modEventReport', - reportType: i % 2 ? REASONSPAM : REASONMISLEADING, - comment: 'X', - }, + await sc.createReport({ + reasonType: i % 2 ? REASONSPAM : REASONMISLEADING, + reason: 'X', // Report bob's account by alice and vice versa subject: i % 2 ? bobsAccount : alicesAccount, - createdBy: i % 2 ? sc.dids.alice : sc.dids.bob, + reportedBy: i % 2 ? sc.dids.alice : sc.dids.bob, }) - await emitModerationEvent({ - event: { - $type: 'com.atproto.admin.defs#modEventReport', - reportType: REASONSPAM, - comment: 'X', - }, + await sc.createReport({ + reasonType: REASONSPAM, + reason: 'X', // Report bob's post by alice and vice versa subject: i % 2 ? bobsPost : alicesPost, - createdBy: i % 2 ? sc.dids.alice : sc.dids.bob, + reportedBy: i % 2 ? sc.dids.alice : sc.dids.bob, }) } } @@ -81,9 +62,8 @@ describe('moderation-events', () => { network = await TestNetwork.create({ dbPostgresSchema: 'ozone_moderation_events', }) - agent = network.ozone.getClient() - pdsAgent = network.pds.getClient() sc = network.getSeedClient() + modClient = network.ozone.getModClient() await basicSeed(sc) await network.processAll() await seedEvents() @@ -96,16 +76,16 @@ describe('moderation-events', () => { describe('query events', () => { it('returns all events for record or repo', async () => { const [bobsEvents, alicesPostEvents] = await Promise.all([ - queryModerationEvents({ + modClient.queryModerationEvents({ subject: sc.dids.bob, }), - queryModerationEvents({ + modClient.queryModerationEvents({ subject: sc.posts[sc.dids.alice][0].ref.uriStr, }), ]) - expect(forSnapshot(bobsEvents.data.events)).toMatchSnapshot() - expect(forSnapshot(alicesPostEvents.data.events)).toMatchSnapshot() + expect(forSnapshot(bobsEvents.events)).toMatchSnapshot() + expect(forSnapshot(alicesPostEvents.events)).toMatchSnapshot() }) it('filters events by types', async () => { @@ -114,66 +94,64 @@ describe('moderation-events', () => { did: sc.dids.alice, } await Promise.all([ - emitModerationEvent({ + modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventComment', comment: 'X', }, subject: alicesAccount, - createdBy: 'did:plc:moderator', }), - emitModerationEvent({ + modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventEscalate', comment: 'X', }, subject: alicesAccount, - createdBy: 'did:plc:moderator', }), ]) const [allEvents, reportEvents] = await Promise.all([ - queryModerationEvents({ + modClient.queryModerationEvents({ subject: sc.dids.alice, }), - queryModerationEvents({ + modClient.queryModerationEvents({ subject: sc.dids.alice, types: ['com.atproto.admin.defs#modEventReport'], }), ]) - expect(allEvents.data.events.length).toBeGreaterThan( - reportEvents.data.events.length, + expect(allEvents.events.length).toBeGreaterThan( + reportEvents.events.length, ) expect( - [...new Set(reportEvents.data.events.map((e) => e.event.$type))].length, + [...new Set(reportEvents.events.map((e) => e.event.$type))].length, ).toEqual(1) expect( - [...new Set(allEvents.data.events.map((e) => e.event.$type))].length, + [...new Set(allEvents.events.map((e) => e.event.$type))].length, ).toEqual(4) }) it('returns events for all content by user', async () => { const [forAccount, forPost] = await Promise.all([ - queryModerationEvents({ + modClient.queryModerationEvents({ subject: sc.dids.bob, includeAllUserRecords: true, }), - queryModerationEvents({ + modClient.queryModerationEvents({ subject: sc.posts[sc.dids.bob][0].ref.uriStr, includeAllUserRecords: true, }), ]) - expect(forAccount.data.events.length).toEqual(forPost.data.events.length) + expect(forAccount.events.length).toEqual(forPost.events.length) // Save events are returned from both requests - expect(forPost.data.events.map(({ id }) => id).sort()).toEqual( - forAccount.data.events.map(({ id }) => id).sort(), + expect(forPost.events.map(({ id }) => id).sort()).toEqual( + forAccount.events.map(({ id }) => id).sort(), ) }) it('returns paginated list of events with cursor', async () => { - const allEvents = await queryModerationEvents({ + const allEvents = await modClient.queryModerationEvents({ subject: sc.dids.bob, includeAllUserRecords: true, }) @@ -186,15 +164,15 @@ describe('moderation-events', () => { let count = 0 do { // get 1 event at a time and check we get all events - const { data } = await queryModerationEvents({ + const res = await modClient.queryModerationEvents({ limit: 1, subject: sc.dids.bob, includeAllUserRecords: true, cursor: defaultCursor, sortDirection, }) - events.push(...data.events) - defaultCursor = data.cursor + events.push(...res.events) + defaultCursor = res.cursor count++ // The count is a circuit breaker to prevent infinite loop in case of failing test } while (defaultCursor && count < 10) @@ -205,9 +183,9 @@ describe('moderation-events', () => { const defaultEvents = await getPaginatedEvents() const reversedEvents = await getPaginatedEvents('asc') - expect(allEvents.data.events.length).toEqual(6) - expect(defaultEvents.length).toEqual(allEvents.data.events.length) - expect(reversedEvents.length).toEqual(allEvents.data.events.length) + expect(allEvents.events.length).toEqual(6) + expect(defaultEvents.length).toEqual(allEvents.events.length) + expect(reversedEvents.length).toEqual(allEvents.events.length) // First event in the reversed list is the last item in the default list expect(reversedEvents[0].id).toEqual( defaultEvents[defaultEvents.length - 1].id, @@ -216,40 +194,40 @@ describe('moderation-events', () => { it('returns report events matching reportType filters', async () => { const [spamEvents, misleadingEvents] = await Promise.all([ - queryModerationEvents({ + modClient.queryModerationEvents({ reportTypes: [REASONSPAM], }), - queryModerationEvents({ + modClient.queryModerationEvents({ reportTypes: [REASONMISLEADING, REASONAPPEAL], }), ]) - expect(misleadingEvents.data.events.length).toEqual(2) - expect(spamEvents.data.events.length).toEqual(6) + expect(misleadingEvents.events.length).toEqual(2) + expect(spamEvents.events.length).toEqual(6) }) it('returns events matching keyword in comment', async () => { const [eventsWithX, eventsWithTest, eventsWithComment] = await Promise.all([ - queryModerationEvents({ + modClient.queryModerationEvents({ comment: 'X', }), - queryModerationEvents({ + modClient.queryModerationEvents({ comment: 'test', }), - queryModerationEvents({ + modClient.queryModerationEvents({ hasComment: true, }), ]) - expect(eventsWithX.data.events.length).toEqual(10) - expect(eventsWithTest.data.events.length).toEqual(0) - expect(eventsWithComment.data.events.length).toEqual(10) + expect(eventsWithX.events.length).toEqual(10) + expect(eventsWithTest.events.length).toEqual(0) + expect(eventsWithComment.events.length).toEqual(10) }) it('returns events matching filter params for labels', async () => { const [negatedLabelEvent, createdLabelEvent] = await Promise.all([ - emitModerationEvent({ + modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventLabel', comment: 'X', @@ -261,9 +239,8 @@ describe('moderation-events', () => { $type: 'com.atproto.admin.defs#repoRef', did: sc.dids.alice, }, - createdBy: sc.dids.bob, }), - emitModerationEvent({ + modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventLabel', comment: 'X', @@ -275,36 +252,31 @@ describe('moderation-events', () => { $type: 'com.atproto.admin.defs#repoRef', did: sc.dids.bob, }, - createdBy: sc.dids.alice, }), ]) const [withTwoLabels, withoutTwoLabels, withOneLabel, withoutOneLabel] = await Promise.all([ - queryModerationEvents({ + modClient.queryModerationEvents({ addedLabels: ['L1', 'L3'], }), - queryModerationEvents({ + modClient.queryModerationEvents({ removedLabels: ['L1', 'L2'], }), - queryModerationEvents({ + modClient.queryModerationEvents({ addedLabels: ['L1'], }), - queryModerationEvents({ + modClient.queryModerationEvents({ removedLabels: ['L2'], }), ]) // Verify that when querying for events where 2 different labels were added // events where all of the labels from the list was added are returned - expect(withTwoLabels.data.events.length).toEqual(0) - expect(negatedLabelEvent.data.id).toEqual( - withoutTwoLabels.data.events[0].id, - ) + expect(withTwoLabels.events.length).toEqual(0) + expect(negatedLabelEvent.id).toEqual(withoutTwoLabels.events[0].id) - expect(createdLabelEvent.data.id).toEqual(withOneLabel.data.events[0].id) - expect(negatedLabelEvent.data.id).toEqual( - withoutOneLabel.data.events[0].id, - ) + expect(createdLabelEvent.id).toEqual(withOneLabel.events[0].id) + expect(negatedLabelEvent.id).toEqual(withoutOneLabel.events[0].id) }) it('returns events matching filter params for tags', async () => { const tagEvent = async ({ @@ -314,7 +286,7 @@ describe('moderation-events', () => { add: string[] remove: string[] }) => - emitModerationEvent({ + modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventTag', comment: 'X', @@ -325,43 +297,35 @@ describe('moderation-events', () => { $type: 'com.atproto.admin.defs#repoRef', did: sc.dids.carol, }, - createdBy: sc.dids.bob, }) const addEvent = await tagEvent({ add: ['L1', 'L2'], remove: [] }) const addAndRemoveEvent = await tagEvent({ add: ['L3'], remove: ['L2'] }) const [addFinder, addAndRemoveFinder, _removeFinder] = await Promise.all([ - queryModerationEvents({ + modClient.queryModerationEvents({ addedTags: ['L1'], }), - queryModerationEvents({ + modClient.queryModerationEvents({ addedTags: ['L3'], removedTags: ['L2'], }), - queryModerationEvents({ + modClient.queryModerationEvents({ removedTags: ['L2'], }), ]) - expect(addFinder.data.events.length).toEqual(1) - expect(addEvent.data.id).toEqual(addFinder.data.events[0].id) + expect(addFinder.events.length).toEqual(1) + expect(addEvent.id).toEqual(addFinder.events[0].id) - expect(addAndRemoveEvent.data.id).toEqual( - addAndRemoveFinder.data.events[0].id, - ) - expect(addAndRemoveEvent.data.id).toEqual( - addAndRemoveFinder.data.events[0].id, - ) - expect(addAndRemoveEvent.data.event.add).toEqual(['L3']) - expect(addAndRemoveEvent.data.event.remove).toEqual(['L2']) + expect(addAndRemoveEvent.id).toEqual(addAndRemoveFinder.events[0].id) + expect(addAndRemoveEvent.id).toEqual(addAndRemoveFinder.events[0].id) + expect(addAndRemoveEvent.event.add).toEqual(['L3']) + expect(addAndRemoveEvent.event.remove).toEqual(['L2']) }) }) describe('get event', () => { it('gets an event by specific id', async () => { - const { data } = await pdsAgent.api.com.atproto.admin.getModerationEvent( - { id: 1 }, - { headers: network.ozone.adminAuthHeaders('moderator') }, - ) + const data = await modClient.getEvent(1) expect(forSnapshot(data)).toMatchSnapshot() }) }) @@ -370,7 +334,7 @@ describe('moderation-events', () => { it('are tracked on takedown event', async () => { const post = sc.posts[sc.dids.carol][0] assert(post.images.length > 1) - await emitModerationEvent({ + await modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventTakedown', }, @@ -380,18 +344,13 @@ describe('moderation-events', () => { cid: post.ref.cidStr, }, subjectBlobCids: [post.images[0].image.ref.toString()], - createdBy: sc.dids.alice, }) - const { data: result } = - await pdsAgent.api.com.atproto.admin.queryModerationEvents( - { - subject: post.ref.uriStr, - types: ['com.atproto.admin.defs#modEventTakedown'], - }, - { headers: network.ozone.adminAuthHeaders('moderator') }, - ) + const result = await modClient.queryModerationEvents({ + subject: post.ref.uriStr, + types: ['com.atproto.admin.defs#modEventTakedown'], + }) expect(result.events[0]).toMatchObject({ - createdBy: sc.dids.alice, + createdBy: network.ozone.moderatorAccnt.did, event: { $type: 'com.atproto.admin.defs#modEventTakedown', }, @@ -401,7 +360,7 @@ describe('moderation-events', () => { it("are tracked on reverse-takedown event even if they aren't specified", async () => { const post = sc.posts[sc.dids.carol][0] - await emitModerationEvent({ + await modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown', }, @@ -410,15 +369,12 @@ describe('moderation-events', () => { uri: post.ref.uriStr, cid: post.ref.cidStr, }, - createdBy: sc.dids.alice, }) - const { data: result } = - await pdsAgent.api.com.atproto.admin.queryModerationEvents( - { subject: post.ref.uriStr }, - { headers: network.ozone.adminAuthHeaders('moderator') }, - ) + const result = await modClient.queryModerationEvents({ + subject: post.ref.uriStr, + }) expect(result.events[0]).toMatchObject({ - createdBy: sc.dids.alice, + createdBy: network.ozone.moderatorAccnt.did, event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown', }, @@ -452,7 +408,7 @@ describe('moderation-events', () => { it('sends email via pds.', async () => { const mail = await getMailFrom( - emitModerationEvent({ + modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventEmail', comment: 'Reaching out to Alice', @@ -463,7 +419,6 @@ describe('moderation-events', () => { $type: 'com.atproto.admin.defs#repoRef', did: sc.dids.alice, }, - createdBy: sc.dids.bob, }), ) expect(mail).toEqual({ diff --git a/packages/ozone/tests/moderation-status-tags.test.ts b/packages/ozone/tests/moderation-status-tags.test.ts index eedac64ca39..163d6abb66c 100644 --- a/packages/ozone/tests/moderation-status-tags.test.ts +++ b/packages/ozone/tests/moderation-status-tags.test.ts @@ -1,32 +1,22 @@ -import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' -import AtpAgent from '@atproto/api' +import { + TestNetwork, + SeedClient, + basicSeed, + ModeratorClient, +} from '@atproto/dev-env' import { REASONSPAM } from '../src/lexicon/types/com/atproto/moderation/defs' describe('moderation-status-tags', () => { let network: TestNetwork - let agent: AtpAgent - let pdsAgent: AtpAgent let sc: SeedClient - - const emitModerationEvent = async (eventData) => { - return pdsAgent.api.com.atproto.admin.emitModerationEvent(eventData, { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('moderator'), - }) - } - - const queryModerationStatuses = (statusQuery) => - agent.api.com.atproto.admin.queryModerationStatuses(statusQuery, { - headers: network.bsky.adminAuthHeaders('moderator'), - }) + let modClient: ModeratorClient beforeAll(async () => { network = await TestNetwork.create({ dbPostgresSchema: 'ozone_moderation_status_tags', }) - agent = network.ozone.getClient() - pdsAgent = network.pds.getClient() sc = network.getSeedClient() + modClient = network.ozone.getModClient() await basicSeed(sc) await network.processAll() }) @@ -41,25 +31,21 @@ describe('moderation-status-tags', () => { $type: 'com.atproto.admin.defs#repoRef', did: sc.dids.bob, } - await emitModerationEvent({ + await sc.createReport({ + reasonType: REASONSPAM, + reason: 'X', subject: bobsAccount, - event: { - $type: 'com.atproto.admin.defs#modEventReport', - comment: 'X', - reportType: REASONSPAM, - }, - createdBy: sc.dids.alice, + reportedBy: sc.dids.alice, }) - await emitModerationEvent({ + await modClient.emitModerationEvent({ subject: bobsAccount, event: { $type: 'com.atproto.admin.defs#modEventTag', add: ['interaction-churn'], remove: [], }, - createdBy: sc.dids.alice, }) - const { data: statusAfterInteractionTag } = await queryModerationStatuses( + const statusAfterInteractionTag = await modClient.queryModerationStatuses( { subject: bobsAccount.did, }, @@ -68,16 +54,15 @@ describe('moderation-status-tags', () => { 'interaction-churn', ) - await emitModerationEvent({ + await modClient.emitModerationEvent({ subject: bobsAccount, event: { $type: 'com.atproto.admin.defs#modEventTag', remove: ['interaction-churn'], add: ['follow-churn'], }, - createdBy: sc.dids.alice, }) - const { data: statusAfterFollowTag } = await queryModerationStatuses({ + const statusAfterFollowTag = await modClient.queryModerationStatuses({ subject: bobsAccount.did, }) diff --git a/packages/ozone/tests/moderation-statuses.test.ts b/packages/ozone/tests/moderation-statuses.test.ts index d1f47c880fc..1c2848d9404 100644 --- a/packages/ozone/tests/moderation-statuses.test.ts +++ b/packages/ozone/tests/moderation-statuses.test.ts @@ -1,6 +1,11 @@ import assert from 'node:assert' -import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' -import AtpAgent, { +import { + TestNetwork, + SeedClient, + basicSeed, + ModeratorClient, +} from '@atproto/dev-env' +import { ComAtprotoAdminDefs, ComAtprotoAdminQueryModerationStatuses, } from '@atproto/api' @@ -16,21 +21,8 @@ import { describe('moderation-statuses', () => { let network: TestNetwork - let agent: AtpAgent - let pdsAgent: AtpAgent let sc: SeedClient - - const emitModerationEvent = async (eventData) => { - return pdsAgent.api.com.atproto.admin.emitModerationEvent(eventData, { - encoding: 'application/json', - headers: network.ozone.adminAuthHeaders('moderator'), - }) - } - - const queryModerationStatuses = (statusQuery) => - agent.api.com.atproto.admin.queryModerationStatuses(statusQuery, { - headers: network.ozone.adminAuthHeaders('moderator'), - }) + let modClient: ModeratorClient const seedEvents = async () => { const bobsAccount = { @@ -53,25 +45,19 @@ describe('moderation-statuses', () => { } for (let i = 0; i < 4; i++) { - await emitModerationEvent({ - event: { - $type: 'com.atproto.admin.defs#modEventReport', - reportType: i % 2 ? REASONSPAM : REASONMISLEADING, - comment: 'X', - }, + await sc.createReport({ + reasonType: i % 2 ? REASONSPAM : REASONMISLEADING, + reason: 'X', // Report bob's account by alice and vice versa subject: i % 2 ? bobsAccount : carlasAccount, - createdBy: i % 2 ? sc.dids.alice : sc.dids.bob, + reportedBy: i % 2 ? sc.dids.alice : sc.dids.bob, }) - await emitModerationEvent({ - event: { - $type: 'com.atproto.admin.defs#modEventReport', - reportType: REASONSPAM, - comment: 'X', - }, + await sc.createReport({ + reasonType: REASONSPAM, + reason: 'X', // Report bob's post by alice and vice versa subject: i % 2 ? bobsPost : alicesPost, - createdBy: i % 2 ? sc.dids.alice : sc.dids.bob, + reportedBy: i % 2 ? sc.dids.alice : sc.dids.bob, }) } } @@ -80,9 +66,8 @@ describe('moderation-statuses', () => { network = await TestNetwork.create({ dbPostgresSchema: 'ozone_moderation_statuses', }) - agent = network.ozone.getClient() - pdsAgent = network.pds.getClient() sc = network.getSeedClient() + modClient = network.ozone.getModClient() await basicSeed(sc) await network.processAll() await seedEvents() @@ -94,26 +79,26 @@ describe('moderation-statuses', () => { describe('query statuses', () => { it('returns statuses for subjects that received moderation events', async () => { - const response = await queryModerationStatuses({}) + const response = await modClient.queryModerationStatuses({}) - expect(forSnapshot(response.data.subjectStatuses)).toMatchSnapshot() + expect(forSnapshot(response.subjectStatuses)).toMatchSnapshot() }) it('returns statuses filtered by subject language', async () => { - const klingonQueue = await queryModerationStatuses({ + const klingonQueue = await modClient.queryModerationStatuses({ tags: ['lang:i'], }) - expect(forSnapshot(klingonQueue.data.subjectStatuses)).toMatchSnapshot() + expect(forSnapshot(klingonQueue.subjectStatuses)).toMatchSnapshot() - const nonKlingonQueue = await queryModerationStatuses({ + const nonKlingonQueue = await modClient.queryModerationStatuses({ excludeTags: ['lang:i'], }) // Verify that the klingon tagged subject is not returned when excluding klingon - expect( - nonKlingonQueue.data.subjectStatuses.map((s) => s.id), - ).not.toContain(klingonQueue.data.subjectStatuses[0].id) + expect(nonKlingonQueue.subjectStatuses.map((s) => s.id)).not.toContain( + klingonQueue.subjectStatuses[0].id, + ) }) it('returns paginated statuses', async () => { @@ -125,13 +110,13 @@ describe('moderation-statuses', () => { const statuses: ComAtprotoAdminDefs.SubjectStatusView[] = [] let count = 0 do { - const results = await queryModerationStatuses({ + const results = await modClient.queryModerationStatuses({ limit: 1, cursor, ...params, }) - cursor = results.data.cursor - statuses.push(...results.data.subjectStatuses) + cursor = results.cursor + statuses.push(...results.subjectStatuses) count++ // The count is just a brake-check to prevent infinite loop } while (cursor && count < 10) @@ -143,13 +128,12 @@ describe('moderation-statuses', () => { expect(list[0].id).toEqual(7) expect(list[list.length - 1].id).toEqual(1) - await emitModerationEvent({ + await modClient.emitModerationEvent({ subject: list[1].subject, event: { $type: 'com.atproto.admin.defs#modEventAcknowledge', comment: 'X', }, - createdBy: sc.dids.bob, }) const listReviewedFirst = await getPaginatedStatuses({ @@ -176,7 +160,7 @@ describe('moderation-statuses', () => { cid: sc.posts[sc.dids.alice][0].ref.cidStr, } const getBobsAccountStatus = async () => { - const { data } = await queryModerationStatuses({ + const data = await modClient.queryModerationStatuses({ subject: bobsAccount.did, }) @@ -186,7 +170,7 @@ describe('moderation-statuses', () => { const bobsAccountStatusBeforeTag = await getBobsAccountStatus() await Promise.all([ - emitModerationEvent({ + modClient.emitModerationEvent({ subject: bobsAccount, event: { $type: 'com.atproto.admin.defs#modEventTag', @@ -196,7 +180,7 @@ describe('moderation-statuses', () => { }, createdBy: sc.dids.alice, }), - emitModerationEvent({ + modClient.emitModerationEvent({ subject: bobsAccount, event: { $type: 'com.atproto.admin.defs#modEventComment', @@ -213,7 +197,7 @@ describe('moderation-statuses', () => { // Since alice's post didn't have a reviewState it is set to reviewNone on first non-impactful event const getAlicesPostStatus = async () => { - const { data } = await queryModerationStatuses({ + const data = await modClient.queryModerationStatuses({ subject: alicesPost.uri, }) @@ -223,7 +207,7 @@ describe('moderation-statuses', () => { const alicesPostStatusBeforeTag = await getAlicesPostStatus() expect(alicesPostStatusBeforeTag).toBeUndefined() - await emitModerationEvent({ + await modClient.emitModerationEvent({ subject: alicesPost, event: { $type: 'com.atproto.admin.defs#modEventComment', @@ -234,7 +218,7 @@ describe('moderation-statuses', () => { const alicesPostStatusAfterTag = await getAlicesPostStatus() expect(alicesPostStatusAfterTag.reviewState).toEqual(REVIEWNONE) - await emitModerationEvent({ + await modClient.emitModerationEvent({ subject: alicesPost, event: { $type: 'com.atproto.admin.defs#modEventReport', @@ -252,7 +236,7 @@ describe('moderation-statuses', () => { it('are tracked on takendown subject', async () => { const post = sc.posts[sc.dids.carol][0] assert(post.images.length > 1) - await emitModerationEvent({ + await modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventTakedown', }, @@ -264,11 +248,9 @@ describe('moderation-statuses', () => { subjectBlobCids: [post.images[0].image.ref.toString()], createdBy: sc.dids.alice, }) - const { data: result } = - await pdsAgent.api.com.atproto.admin.queryModerationStatuses( - { subject: post.ref.uriStr }, - { headers: network.ozone.adminAuthHeaders('moderator') }, - ) + const result = await modClient.queryModerationStatuses({ + subject: post.ref.uriStr, + }) expect(result.subjectStatuses.length).toBe(1) expect(result.subjectStatuses[0]).toMatchObject({ takendown: true, @@ -278,7 +260,7 @@ describe('moderation-statuses', () => { it('are tracked on reverse-takendown subject based on previous status', async () => { const post = sc.posts[sc.dids.carol][0] - await emitModerationEvent({ + await modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown', }, @@ -287,13 +269,10 @@ describe('moderation-statuses', () => { uri: post.ref.uriStr, cid: post.ref.cidStr, }, - createdBy: sc.dids.alice, }) - const { data: result } = - await pdsAgent.api.com.atproto.admin.queryModerationStatuses( - { subject: post.ref.uriStr }, - { headers: network.ozone.adminAuthHeaders('moderator') }, - ) + const result = await modClient.queryModerationStatuses({ + subject: post.ref.uriStr, + }) expect(result.subjectStatuses.length).toBe(1) expect(result.subjectStatuses[0]).toMatchObject({ takendown: false, diff --git a/packages/ozone/tests/moderation.test.ts b/packages/ozone/tests/moderation.test.ts index 323fa63eb30..4c736a8ea9e 100644 --- a/packages/ozone/tests/moderation.test.ts +++ b/packages/ozone/tests/moderation.test.ts @@ -4,12 +4,9 @@ import { RecordRef, SeedClient, basicSeed, + ModeratorClient, } from '@atproto/dev-env' -import AtpAgent, { - ComAtprotoAdminEmitModerationEvent, - ComAtprotoAdminQueryModerationStatuses, - ComAtprotoModerationCreateReport, -} from '@atproto/api' +import AtpAgent, { ComAtprotoAdminEmitModerationEvent } from '@atproto/api' import { AtUri } from '@atproto/syntax' import { forSnapshot } from './_util' import { @@ -19,7 +16,6 @@ import { } from '../src/lexicon/types/com/atproto/moderation/defs' import { ModEventLabel, - ModEventTakedown, REVIEWCLOSED, REVIEWESCALATED, } from '../src/lexicon/types/com/atproto/admin/defs' @@ -31,16 +27,6 @@ import { UNSPECCED_TAKEDOWN_LABEL, } from '../src/mod-service/types' -type BaseCreateReportParams = - | { account: string } - | { content: { uri: string; cid: string } } -type CreateReportParams = BaseCreateReportParams & { - author: string -} & Omit - -type TakedownParams = BaseCreateReportParams & - Omit - describe('moderation', () => { let network: TestNetwork let ozone: TestOzone @@ -49,102 +35,18 @@ describe('moderation', () => { let bskyAgent: AtpAgent let pdsAgent: AtpAgent let sc: SeedClient + let modClient: ModeratorClient - const createReport = async (params: CreateReportParams) => { - const { author, ...rest } = params - return agent.api.com.atproto.moderation.createReport( - { - // Set default type to spam - reasonType: REASONSPAM, - ...rest, - subject: - 'account' in params - ? { - $type: 'com.atproto.admin.defs#repoRef', - did: params.account, - } - : { - $type: 'com.atproto.repo.strongRef', - uri: params.content.uri, - cid: params.content.cid, - }, - }, - { - headers: await network.serviceHeaders( - author, - network.ozone.ctx.cfg.service.did, - ), - encoding: 'application/json', - }, - ) - } - - const performTakedown = async ({ - durationInHours, - ...rest - }: TakedownParams & Pick) => - agent.api.com.atproto.admin.emitModerationEvent( - { - event: { - $type: 'com.atproto.admin.defs#modEventTakedown', - durationInHours, - }, - subject: - 'account' in rest - ? { - $type: 'com.atproto.admin.defs#repoRef', - did: rest.account, - } - : { - $type: 'com.atproto.repo.strongRef', - uri: rest.content.uri, - cid: rest.content.cid, - }, - createdBy: 'did:example:admin', - ...rest, - }, - { - encoding: 'application/json', - headers: ozone.adminAuthHeaders(), - }, - ) - - const performReverseTakedown = async (params: TakedownParams) => - agent.api.com.atproto.admin.emitModerationEvent( - { - event: { - $type: 'com.atproto.admin.defs#modEventReverseTakedown', - }, - subject: - 'account' in params - ? { - $type: 'com.atproto.admin.defs#repoRef', - did: params.account, - } - : { - $type: 'com.atproto.repo.strongRef', - uri: params.content.uri, - cid: params.content.cid, - }, - createdBy: 'did:example:admin', - ...params, - }, - { - encoding: 'application/json', - headers: ozone.adminAuthHeaders(), - }, - ) - - const getStatuses = async ( - params: ComAtprotoAdminQueryModerationStatuses.QueryParams, - ) => { - const { data } = await agent.api.com.atproto.admin.queryModerationStatuses( - params, - { headers: ozone.adminAuthHeaders() }, - ) + const repoSubject = (did: string) => ({ + $type: 'com.atproto.admin.defs#repoRef', + did, + }) - return data - } + const recordSubject = (ref: RecordRef) => ({ + $type: 'com.atproto.repo.strongRef', + uri: ref.uriStr, + cid: ref.cidStr, + }) const getLabel = async (uri: string, val: string, neg = false) => { return ozone.ctx.db.db @@ -170,6 +72,7 @@ describe('moderation', () => { bskyAgent = network.bsky.getClient() pdsAgent = network.pds.getClient() sc = network.getSeedClient() + modClient = network.ozone.getModClient() await basicSeed(sc) await network.processAll() }) @@ -180,25 +83,25 @@ describe('moderation', () => { describe('reporting', () => { it('creates reports of a repo.', async () => { - const { data: reportA } = await createReport({ + const reportA = await sc.createReport({ reasonType: REASONSPAM, - account: sc.dids.bob, - author: sc.dids.alice, + subject: repoSubject(sc.dids.bob), + reportedBy: sc.dids.alice, }) - const { data: reportB } = await createReport({ + const reportB = await sc.createReport({ reasonType: REASONOTHER, reason: 'impersonation', - account: sc.dids.bob, - author: sc.dids.carol, + subject: repoSubject(sc.dids.bob), + reportedBy: sc.dids.carol, }) expect(forSnapshot([reportA, reportB])).toMatchSnapshot() }) it("allows reporting a repo that doesn't exist.", async () => { - const promise = createReport({ + const promise = sc.createReport({ reasonType: REASONSPAM, - account: 'did:plc:unknown', - author: sc.dids.alice, + subject: repoSubject('did:plc:unknown'), + reportedBy: sc.dids.alice, }) await expect(promise).resolves.toBeDefined() }) @@ -206,24 +109,16 @@ describe('moderation', () => { it('creates reports of a record.', async () => { const postA = sc.posts[sc.dids.bob][0].ref const postB = sc.posts[sc.dids.bob][1].ref - const { data: reportA } = await createReport({ - author: sc.dids.alice, + const reportA = await sc.createReport({ + reportedBy: sc.dids.alice, reasonType: REASONSPAM, - content: { - $type: 'com.atproto.repo.strongRef', - uri: postA.uriStr, - cid: postA.cidStr, - }, + subject: recordSubject(postA), }) - const { data: reportB } = await createReport({ + const reportB = await sc.createReport({ reasonType: REASONOTHER, reason: 'defamation', - content: { - $type: 'com.atproto.repo.strongRef', - uri: postB.uriStr, - cid: postB.cidStr, - }, - author: sc.dids.carol, + subject: recordSubject(postB), + reportedBy: sc.dids.carol, }) expect(forSnapshot([reportA, reportB])).toMatchSnapshot() }) @@ -234,26 +129,26 @@ describe('moderation', () => { const postUriBad = new AtUri(postA.uriStr) postUriBad.rkey = 'badrkey' - const promiseA = createReport({ + const promiseA = sc.createReport({ reasonType: REASONSPAM, - content: { + subject: { $type: 'com.atproto.repo.strongRef', uri: postUriBad.toString(), cid: postA.cidStr, }, - author: sc.dids.alice, + reportedBy: sc.dids.alice, }) await expect(promiseA).resolves.toBeDefined() - const promiseB = createReport({ + const promiseB = sc.createReport({ reasonType: REASONOTHER, reason: 'defamation', - content: { + subject: { $type: 'com.atproto.repo.strongRef', uri: postB.uri.toString(), cid: postA.cidStr, // bad cid }, - author: sc.dids.carol, + reportedBy: sc.dids.carol, }) await expect(promiseB).resolves.toBeDefined() }) @@ -264,29 +159,27 @@ describe('moderation', () => { const post = sc.posts[sc.dids.bob][1].ref await Promise.all([ - createReport({ + sc.createReport({ reasonType: REASONSPAM, - account: sc.dids.bob, - author: sc.dids.alice, + subject: repoSubject(sc.dids.bob), + reportedBy: sc.dids.alice, }), - createReport({ + sc.createReport({ reasonType: REASONOTHER, reason: 'defamation', - content: { - uri: post.uri.toString(), - cid: post.cid.toString(), - }, - author: sc.dids.carol, + subject: recordSubject(post), + reportedBy: sc.dids.carol, }), ]) - await performTakedown({ - account: sc.dids.bob, + await modClient.performTakedown({ + subject: repoSubject(sc.dids.bob), }) - const moderationStatusOnBobsAccount = await getStatuses({ - subject: sc.dids.bob, - }) + const moderationStatusOnBobsAccount = + await modClient.queryModerationStatuses({ + subject: sc.dids.bob, + }) // Validate that subject status is set to review closed and takendown flag is on expect(moderationStatusOnBobsAccount.subjectStatuses[0]).toMatchObject({ @@ -299,8 +192,8 @@ describe('moderation', () => { }) // Cleanup - await performReverseTakedown({ - account: sc.dids.bob, + await modClient.performReverseTakedown({ + subject: repoSubject(sc.dids.bob), }) }) @@ -311,7 +204,7 @@ describe('moderation', () => { uri: alicesPostRef.uri.toString(), cid: alicesPostRef.cid.toString(), } - await agent.api.com.atproto.admin.emitModerationEvent( + await modClient.emitModerationEvent( { event: { $type: 'com.atproto.admin.defs#modEventEscalate', @@ -320,13 +213,10 @@ describe('moderation', () => { subject: alicesPostSubject, createdBy: 'did:example:admin', }, - { - encoding: 'application/json', - headers: ozone.adminAuthHeaders('triage'), - }, + 'triage', ) - const alicesPostStatus = await getStatuses({ + const alicesPostStatus = await modClient.queryModerationStatuses({ subject: alicesPostRef.uri.toString(), }) @@ -344,7 +234,7 @@ describe('moderation', () => { uri: alicesPostRef.uri.toString(), cid: alicesPostRef.cid.toString(), } - await agent.api.com.atproto.admin.emitModerationEvent( + await modClient.emitModerationEvent( { event: { $type: 'com.atproto.admin.defs#modEventComment', @@ -354,13 +244,10 @@ describe('moderation', () => { subject: alicesPostSubject, createdBy: 'did:example:admin', }, - { - encoding: 'application/json', - headers: ozone.adminAuthHeaders('triage'), - }, + 'triage', ) - const alicesPostStatus = await getStatuses({ + const alicesPostStatus = await modClient.queryModerationStatuses({ subject: alicesPostRef.uri.toString(), }) @@ -383,17 +270,11 @@ describe('moderation', () => { }, createdBy: 'did:example:admin', } - return agent.api.com.atproto.admin.emitModerationEvent( - { - event, - ...baseAction, - ...overwrites, - }, - { - encoding: 'application/json', - headers: ozone.adminAuthHeaders(), - }, - ) + return modClient.emitModerationEvent({ + event, + ...baseAction, + ...overwrites, + }) } // Validate that subject status is marked as escalated await emitModEvent({ @@ -407,9 +288,10 @@ describe('moderation', () => { await emitModEvent({ $type: 'com.atproto.admin.defs#modEventEscalate', }) - const alicesPostStatusAfterEscalation = await getStatuses({ - subject: alicesPostRef.uriStr, - }) + const alicesPostStatusAfterEscalation = + await modClient.queryModerationStatuses({ + subject: alicesPostRef.uriStr, + }) expect( alicesPostStatusAfterEscalation.subjectStatuses[0].reviewState, ).toEqual(REVIEWESCALATED) @@ -425,9 +307,10 @@ describe('moderation', () => { $type: 'com.atproto.admin.defs#modEventTakedown', }) - const alicesPostStatusAfterTakedown = await getStatuses({ - subject: alicesPostRef.uriStr, - }) + const alicesPostStatusAfterTakedown = + await modClient.queryModerationStatuses({ + subject: alicesPostRef.uriStr, + }) expect(alicesPostStatusAfterTakedown.subjectStatuses[0]).toMatchObject({ reviewState: REVIEWCLOSED, takendown: true, @@ -436,9 +319,10 @@ describe('moderation', () => { await emitModEvent({ $type: 'com.atproto.admin.defs#modEventReverseTakedown', }) - const alicesPostStatusAfterRevert = await getStatuses({ - subject: alicesPostRef.uriStr, - }) + const alicesPostStatusAfterRevert = + await modClient.queryModerationStatuses({ + subject: alicesPostRef.uriStr, + }) // Validate that after reverting, the status of the subject is reverted to the last status changing event expect(alicesPostStatusAfterRevert.subjectStatuses[0]).toMatchObject({ reviewState: REVIEWCLOSED, @@ -598,7 +482,7 @@ describe('moderation', () => { }) it('does not allow triage moderators to label.', async () => { - const attemptLabel = agent.api.com.atproto.admin.emitModerationEvent( + const attemptLabel = modClient.emitModerationEvent( { event: { $type: 'com.atproto.admin.defs#modEventLabel', @@ -612,10 +496,7 @@ describe('moderation', () => { did: sc.dids.bob, }, }, - { - encoding: 'application/json', - headers: network.ozone.adminAuthHeaders('triage'), - }, + 'triage', ) await expect(attemptLabel).rejects.toThrow( 'Must be a full moderator to label content', @@ -623,29 +504,29 @@ describe('moderation', () => { }) it('does not allow take down event on takendown post or reverse takedown on available post.', async () => { - await performTakedown({ - account: sc.dids.bob, + await modClient.performTakedown({ + subject: repoSubject(sc.dids.bob), }) await expect( - performTakedown({ - account: sc.dids.bob, + modClient.performTakedown({ + subject: repoSubject(sc.dids.bob), }), ).rejects.toThrow('Subject is already taken down') // Cleanup - await performReverseTakedown({ - account: sc.dids.bob, + await modClient.performReverseTakedown({ + subject: repoSubject(sc.dids.bob), }) await expect( - performReverseTakedown({ - account: sc.dids.bob, + modClient.performReverseTakedown({ + subject: repoSubject(sc.dids.bob), }), ).rejects.toThrow('Subject is not taken down') }) it('fans out repo takedowns', async () => { - await performTakedown({ - account: sc.dids.bob, + await modClient.performTakedown({ + subject: repoSubject(sc.dids.bob), }) await ozone.processAll() @@ -672,7 +553,9 @@ describe('moderation', () => { expect(takedownLabel1).toBeDefined() // cleanup - await performReverseTakedown({ account: sc.dids.bob }) + await modClient.performReverseTakedown({ + subject: repoSubject(sc.dids.bob), + }) await ozone.processAll() const pdsRes2 = await pdsAgent.api.com.atproto.admin.getSubjectStatus( @@ -699,11 +582,10 @@ describe('moderation', () => { }) it('fans out record takedowns', async () => { - const post = sc.posts[sc.dids.bob][0] - const uri = post.ref.uriStr - const cid = post.ref.cidStr - await performTakedown({ - content: { uri, cid }, + const post = sc.posts[sc.dids.bob][0].ref + const uri = post.uriStr + await modClient.performTakedown({ + subject: recordSubject(post), }) await ozone.processAll() @@ -723,7 +605,7 @@ describe('moderation', () => { expect(takedownLabel1).toBeDefined() // cleanup - await performReverseTakedown({ content: { uri, cid } }) + await modClient.performReverseTakedown({ subject: recordSubject(post) }) await ozone.processAll() const pdsRes2 = await pdsAgent.api.com.atproto.admin.getSubjectStatus( @@ -742,7 +624,7 @@ describe('moderation', () => { }) it('allows full moderators to takedown.', async () => { - await agent.api.com.atproto.admin.emitModerationEvent( + await modClient.emitModerationEvent( { event: { $type: 'com.atproto.admin.defs#modEventTakedown', @@ -753,10 +635,7 @@ describe('moderation', () => { did: sc.dids.bob, }, }, - { - encoding: 'application/json', - headers: network.ozone.adminAuthHeaders('moderator'), - }, + 'moderator', ) // cleanup await reverse({ @@ -768,47 +647,42 @@ describe('moderation', () => { }) it('does not allow non-full moderators to takedown.', async () => { - const attemptTakedownTriage = - agent.api.com.atproto.admin.emitModerationEvent( - { - event: { - $type: 'com.atproto.admin.defs#modEventTakedown', - }, - createdBy: 'did:example:moderator', - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.bob, - }, + const attemptTakedownTriage = modClient.emitModerationEvent( + { + event: { + $type: 'com.atproto.admin.defs#modEventTakedown', }, - { - encoding: 'application/json', - headers: network.ozone.adminAuthHeaders('triage'), + createdBy: 'did:example:moderator', + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, }, - ) + }, + 'triage', + ) await expect(attemptTakedownTriage).rejects.toThrow( 'Must be a full moderator to take this type of action', ) }) it('automatically reverses actions marked with duration', async () => { - await createReport({ + await sc.createReport({ reasonType: REASONSPAM, - account: sc.dids.bob, - author: sc.dids.alice, + subject: repoSubject(sc.dids.bob), + reportedBy: sc.dids.alice, }) - const { data: action } = await performTakedown({ - account: sc.dids.bob, + const action = await modClient.performTakedown({ + subject: repoSubject(sc.dids.bob), // Use negative value to set the expiry time in the past so that the action is automatically reversed // right away without having to wait n number of hours for a successful assertion durationInHours: -1, }) await ozone.processAll() - const { data: statusesAfterTakedown } = - await agent.api.com.atproto.admin.queryModerationStatuses( - { subject: sc.dids.bob }, - { headers: network.ozone.adminAuthHeaders('moderator') }, - ) + const statusesAfterTakedown = await modClient.queryModerationStatuses( + { subject: sc.dids.bob }, + 'moderator', + ) expect(statusesAfterTakedown.subjectStatuses[0]).toMatchObject({ takendown: true, @@ -822,14 +696,11 @@ describe('moderation', () => { await reverser.findAndRevertDueActions() await ozone.processAll() - const [{ data: eventList }, { data: statuses }] = await Promise.all([ - agent.api.com.atproto.admin.queryModerationEvents( + const [eventList, statuses] = await Promise.all([ + modClient.queryModerationEvents({ subject: sc.dids.bob }, 'moderator'), + modClient.queryModerationStatuses( { subject: sc.dids.bob }, - { headers: network.ozone.adminAuthHeaders('moderator') }, - ), - agent.api.com.atproto.admin.queryModerationStatuses( - { subject: sc.dids.bob }, - { headers: network.ozone.adminAuthHeaders('moderator') }, + 'moderator', ), ]) @@ -873,22 +744,16 @@ describe('moderation', () => { }, ) { const { createLabelVals, negateLabelVals } = opts - const result = await agent.api.com.atproto.admin.emitModerationEvent( - { - event: { - $type: 'com.atproto.admin.defs#modEventLabel', - createLabelVals, - negateLabelVals, - }, - createdBy: 'did:example:admin', - reason: 'Y', - ...opts, - }, - { - encoding: 'application/json', - headers: network.ozone.adminAuthHeaders(), + const result = await modClient.emitModerationEvent({ + event: { + $type: 'com.atproto.admin.defs#modEventLabel', + createLabelVals, + negateLabelVals, }, - ) + createdBy: 'did:example:admin', + reason: 'Y', + ...opts, + }) return result.data } @@ -897,26 +762,20 @@ describe('moderation', () => { subject: ComAtprotoAdminEmitModerationEvent.InputSchema['subject'] }, ) { - await agent.api.com.atproto.admin.emitModerationEvent( - { - event: { - $type: 'com.atproto.admin.defs#modEventReverseTakedown', - }, - createdBy: 'did:example:admin', - reason: 'Y', - ...opts, - }, - { - encoding: 'application/json', - headers: network.ozone.adminAuthHeaders(), + await modClient.emitModerationEvent({ + event: { + $type: 'com.atproto.admin.defs#modEventReverseTakedown', }, - ) + createdBy: 'did:example:admin', + reason: 'Y', + ...opts, + }) } async function getRecordLabels(uri: string) { const result = await agent.api.com.atproto.admin.getRecord( { uri }, - { headers: network.ozone.adminAuthHeaders() }, + { headers: await network.ozone.modHeaders() }, ) const labels = result.data.labels ?? [] return labels.map((l) => l.val) @@ -925,7 +784,7 @@ describe('moderation', () => { async function getRepoLabels(did: string) { const result = await agent.api.com.atproto.admin.getRepo( { did }, - { headers: network.ozone.adminAuthHeaders() }, + { headers: await network.ozone.modHeaders() }, ) const labels = result.data.labels ?? [] return labels.map((l) => l.val) @@ -951,18 +810,15 @@ describe('moderation', () => { await fetch(imageUri) const cached = await fetch(imageUri) expect(cached.headers.get('x-cache')).toEqual('hit') - await performTakedown({ - content: { - uri: post.ref.uriStr, - cid: post.ref.cidStr, - }, + await modClient.performTakedown({ + subject: recordSubject(post.ref), subjectBlobCids: [blob.image.ref.toString()], }) await ozone.processAll() }) it('sets blobCids in moderation status', async () => { - const { subjectStatuses } = await getStatuses({ + const { subjectStatuses } = await modClient.queryModerationStatuses({ subject: post.ref.uriStr, }) @@ -1020,11 +876,8 @@ describe('moderation', () => { }) it('restores blob when action is reversed.', async () => { - await performReverseTakedown({ - content: { - uri: post.ref.uriStr, - cid: post.ref.cidStr, - }, + await modClient.performReverseTakedown({ + subject: recordSubject(post.ref), subjectBlobCids: [blob.image.ref.toString()], }) diff --git a/packages/ozone/tests/repo-search.test.ts b/packages/ozone/tests/repo-search.test.ts index 1704e934206..8b999b261b8 100644 --- a/packages/ozone/tests/repo-search.test.ts +++ b/packages/ozone/tests/repo-search.test.ts @@ -1,4 +1,9 @@ -import { SeedClient, TestNetwork, usersBulkSeed } from '@atproto/dev-env' +import { + ModeratorClient, + SeedClient, + TestNetwork, + usersBulkSeed, +} from '@atproto/dev-env' import AtpAgent from '@atproto/api' import { paginateAll } from './_util' @@ -6,16 +11,18 @@ describe('admin repo search view', () => { let network: TestNetwork let agent: AtpAgent let sc: SeedClient + let modClient: ModeratorClient let headers: { [s: string]: string } beforeAll(async () => { network = await TestNetwork.create({ dbPostgresSchema: 'ozone_admin_repo_search', }) - agent = network.pds.getClient() + agent = network.ozone.getClient() sc = network.getSeedClient() + modClient = network.ozone.getModClient() await usersBulkSeed(sc) - headers = network.pds.adminAuthHeaders() + headers = await network.ozone.modHeaders() await network.processAll() }) @@ -24,7 +31,7 @@ describe('admin repo search view', () => { }) beforeAll(async () => { - await sc.emitModerationEvent({ + await modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', diff --git a/packages/pds/src/api/com/atproto/admin/deleteAccount.ts b/packages/pds/src/api/com/atproto/admin/deleteAccount.ts index 420fbaecb23..290c556da9a 100644 --- a/packages/pds/src/api/com/atproto/admin/deleteAccount.ts +++ b/packages/pds/src/api/com/atproto/admin/deleteAccount.ts @@ -1,14 +1,10 @@ -import { AuthRequiredError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.deleteAccount({ - auth: ctx.authVerifier.role, - handler: async ({ input, auth }) => { - if (!auth.credentials.admin) { - throw new AuthRequiredError('Must be an admin to delete an account') - } + auth: ctx.authVerifier.adminToken, + handler: async ({ input }) => { const { did } = input.body await ctx.actorStore.destroy(did) await ctx.accountManager.deleteAccount(did) diff --git a/packages/pds/src/api/com/atproto/admin/disableAccountInvites.ts b/packages/pds/src/api/com/atproto/admin/disableAccountInvites.ts index d4a73db71b8..f22cb5a64ab 100644 --- a/packages/pds/src/api/com/atproto/admin/disableAccountInvites.ts +++ b/packages/pds/src/api/com/atproto/admin/disableAccountInvites.ts @@ -1,19 +1,16 @@ -import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' +import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.disableAccountInvites({ - auth: ctx.authVerifier.role, - handler: async ({ input, auth }) => { + auth: ctx.authVerifier.moderator, + handler: async ({ input }) => { if (ctx.cfg.entryway) { throw new InvalidRequestError( 'Account invites are managed by the entryway service', ) } - if (!auth.credentials.moderator) { - throw new AuthRequiredError('Insufficient privileges') - } const { account } = input.body await ctx.accountManager.setAccountInvitesDisabled(account, true) }, diff --git a/packages/pds/src/api/com/atproto/admin/disableInviteCodes.ts b/packages/pds/src/api/com/atproto/admin/disableInviteCodes.ts index f48a7765a2c..5be085450c5 100644 --- a/packages/pds/src/api/com/atproto/admin/disableInviteCodes.ts +++ b/packages/pds/src/api/com/atproto/admin/disableInviteCodes.ts @@ -1,19 +1,16 @@ -import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' +import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.disableInviteCodes({ - auth: ctx.authVerifier.role, - handler: async ({ input, auth }) => { + auth: ctx.authVerifier.moderator, + handler: async ({ input }) => { if (ctx.cfg.entryway) { throw new InvalidRequestError( 'Account invites are managed by the entryway service', ) } - if (!auth.credentials.moderator) { - throw new AuthRequiredError('Insufficient privileges') - } const { codes = [], accounts = [] } = input.body if (accounts.includes('admin')) { throw new InvalidRequestError('cannot disable admin invite codes') diff --git a/packages/pds/src/api/com/atproto/admin/enableAccountInvites.ts b/packages/pds/src/api/com/atproto/admin/enableAccountInvites.ts index 7d809e114d8..d65bd781194 100644 --- a/packages/pds/src/api/com/atproto/admin/enableAccountInvites.ts +++ b/packages/pds/src/api/com/atproto/admin/enableAccountInvites.ts @@ -1,19 +1,16 @@ -import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' +import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.enableAccountInvites({ - auth: ctx.authVerifier.role, - handler: async ({ input, auth }) => { + auth: ctx.authVerifier.moderator, + handler: async ({ input }) => { if (ctx.cfg.entryway) { throw new InvalidRequestError( 'Account invites are managed by the entryway service', ) } - if (!auth.credentials.moderator) { - throw new AuthRequiredError('Insufficient privileges') - } const { account } = input.body await ctx.accountManager.setAccountInvitesDisabled(account, false) }, diff --git a/packages/pds/src/api/com/atproto/admin/getAccountInfo.ts b/packages/pds/src/api/com/atproto/admin/getAccountInfo.ts index 7269bc0dfa9..6210768ab6e 100644 --- a/packages/pds/src/api/com/atproto/admin/getAccountInfo.ts +++ b/packages/pds/src/api/com/atproto/admin/getAccountInfo.ts @@ -5,7 +5,7 @@ import { INVALID_HANDLE } from '@atproto/syntax' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getAccountInfo({ - auth: ctx.authVerifier.roleOrModService, + auth: ctx.authVerifier.moderator, handler: async ({ params }) => { const [account, invites, invitedBy] = await Promise.all([ ctx.accountManager.getAccount(params.did, { diff --git a/packages/pds/src/api/com/atproto/admin/getInviteCodes.ts b/packages/pds/src/api/com/atproto/admin/getInviteCodes.ts index 7647dc08813..0cfe5f5a809 100644 --- a/packages/pds/src/api/com/atproto/admin/getInviteCodes.ts +++ b/packages/pds/src/api/com/atproto/admin/getInviteCodes.ts @@ -11,7 +11,7 @@ import { selectInviteCodesQb } from '../../../../account-manager/helpers/invite' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getInviteCodes({ - auth: ctx.authVerifier.role, + auth: ctx.authVerifier.moderator, handler: async ({ params }) => { if (ctx.cfg.entryway) { throw new InvalidRequestError( diff --git a/packages/pds/src/api/com/atproto/admin/getSubjectStatus.ts b/packages/pds/src/api/com/atproto/admin/getSubjectStatus.ts index 505e3dde4e9..f2070060220 100644 --- a/packages/pds/src/api/com/atproto/admin/getSubjectStatus.ts +++ b/packages/pds/src/api/com/atproto/admin/getSubjectStatus.ts @@ -7,7 +7,7 @@ import { OutputSchema } from '../../../../lexicon/types/com/atproto/admin/getSub export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getSubjectStatus({ - auth: ctx.authVerifier.roleOrModService, + auth: ctx.authVerifier.moderator, handler: async ({ params }) => { const { did, uri, blob } = params let body: OutputSchema | null = null diff --git a/packages/pds/src/api/com/atproto/admin/sendEmail.ts b/packages/pds/src/api/com/atproto/admin/sendEmail.ts index 6e30159c204..67b1755fedc 100644 --- a/packages/pds/src/api/com/atproto/admin/sendEmail.ts +++ b/packages/pds/src/api/com/atproto/admin/sendEmail.ts @@ -1,17 +1,13 @@ import assert from 'node:assert' -import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' +import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { resultPassthru } from '../../../proxy' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.sendEmail({ - auth: ctx.authVerifier.roleOrModService, - handler: async ({ input, auth }) => { - if (auth.credentials.type === 'role' && !auth.credentials.moderator) { - throw new AuthRequiredError('Insufficient privileges') - } - + auth: ctx.authVerifier.moderator, + handler: async ({ input }) => { const { content, recipientDid, diff --git a/packages/pds/src/api/com/atproto/admin/updateAccountEmail.ts b/packages/pds/src/api/com/atproto/admin/updateAccountEmail.ts index c2f266d716d..cf1aefa007b 100644 --- a/packages/pds/src/api/com/atproto/admin/updateAccountEmail.ts +++ b/packages/pds/src/api/com/atproto/admin/updateAccountEmail.ts @@ -1,15 +1,12 @@ -import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' +import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { authPassthru } from '../../../proxy' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.updateAccountEmail({ - auth: ctx.authVerifier.role, - handler: async ({ input, auth, req }) => { - if (!auth.credentials.admin) { - throw new AuthRequiredError('Insufficient privileges') - } + auth: ctx.authVerifier.adminToken, + handler: async ({ input, req }) => { const account = await ctx.accountManager.getAccount(input.body.account, { includeDeactivated: true, includeTakenDown: true, diff --git a/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts b/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts index 1d4e1189c83..298c236f7e6 100644 --- a/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts +++ b/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts @@ -1,4 +1,4 @@ -import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' +import { InvalidRequestError } from '@atproto/xrpc-server' import { normalizeAndValidateHandle } from '../../../../handle' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' @@ -6,12 +6,8 @@ import { httpLogger } from '../../../../logger' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.updateAccountHandle({ - auth: ctx.authVerifier.role, - handler: async ({ input, auth }) => { - if (!auth.credentials.admin) { - throw new AuthRequiredError('Insufficient privileges') - } - + auth: ctx.authVerifier.adminToken, + handler: async ({ input }) => { const { did } = input.body const handle = await normalizeAndValidateHandle({ ctx, diff --git a/packages/pds/src/api/com/atproto/admin/updateAccountPassword.ts b/packages/pds/src/api/com/atproto/admin/updateAccountPassword.ts index 294286c3873..f553678313a 100644 --- a/packages/pds/src/api/com/atproto/admin/updateAccountPassword.ts +++ b/packages/pds/src/api/com/atproto/admin/updateAccountPassword.ts @@ -1,18 +1,11 @@ -import { AuthRequiredError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { authPassthru } from '../../../proxy' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.updateAccountPassword({ - auth: ctx.authVerifier.role, - handler: async ({ input, auth, req }) => { - if (!auth.credentials.admin) { - throw new AuthRequiredError( - 'Must be an admin to update an account password', - ) - } - + auth: ctx.authVerifier.adminToken, + handler: async ({ input, req }) => { if (ctx.entrywayAgent) { await ctx.entrywayAgent.com.atproto.admin.updateAccountPassword( input.body, diff --git a/packages/pds/src/api/com/atproto/admin/updateSubjectStatus.ts b/packages/pds/src/api/com/atproto/admin/updateSubjectStatus.ts index 018d447d6eb..708dfdb6e20 100644 --- a/packages/pds/src/api/com/atproto/admin/updateSubjectStatus.ts +++ b/packages/pds/src/api/com/atproto/admin/updateSubjectStatus.ts @@ -7,19 +7,12 @@ import { isRepoBlobRef, } from '../../../../lexicon/types/com/atproto/admin/defs' import { isMain as isStrongRef } from '../../../../lexicon/types/com/atproto/repo/strongRef' -import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' +import { InvalidRequestError } from '@atproto/xrpc-server' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.updateSubjectStatus({ - auth: ctx.authVerifier.roleOrModService, - handler: async ({ input, auth }) => { - // if less than moderator access then cannot perform a takedown - if (auth.credentials.type === 'role' && !auth.credentials.moderator) { - throw new AuthRequiredError( - 'Must be a full moderator to update subject state', - ) - } - + auth: ctx.authVerifier.moderator, + handler: async ({ input }) => { const { subject, takedown } = input.body if (takedown) { if (isRepoRef(subject)) { diff --git a/packages/pds/src/api/com/atproto/server/createInviteCode.ts b/packages/pds/src/api/com/atproto/server/createInviteCode.ts index 6c00074e31d..49fe2f6cd0b 100644 --- a/packages/pds/src/api/com/atproto/server/createInviteCode.ts +++ b/packages/pds/src/api/com/atproto/server/createInviteCode.ts @@ -1,15 +1,12 @@ -import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' +import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { genInvCode } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.server.createInviteCode({ - auth: ctx.authVerifier.role, - handler: async ({ input, auth }) => { - if (!auth.credentials.admin) { - throw new AuthRequiredError('Insufficient privileges') - } + auth: ctx.authVerifier.adminToken, + handler: async ({ input }) => { if (ctx.cfg.entryway) { throw new InvalidRequestError( 'Account invites are managed by the entryway service', diff --git a/packages/pds/src/api/com/atproto/server/createInviteCodes.ts b/packages/pds/src/api/com/atproto/server/createInviteCodes.ts index 30d0b83d772..8f15d55db0d 100644 --- a/packages/pds/src/api/com/atproto/server/createInviteCodes.ts +++ b/packages/pds/src/api/com/atproto/server/createInviteCodes.ts @@ -1,4 +1,4 @@ -import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' +import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { genInvCodes } from './util' @@ -6,11 +6,8 @@ import { AccountCodes } from '../../../../lexicon/types/com/atproto/server/creat export default function (server: Server, ctx: AppContext) { server.com.atproto.server.createInviteCodes({ - auth: ctx.authVerifier.role, - handler: async ({ input, auth }) => { - if (!auth.credentials.admin) { - throw new AuthRequiredError('Insufficient privileges') - } + auth: ctx.authVerifier.adminToken, + handler: async ({ input }) => { if (ctx.cfg.entryway) { throw new InvalidRequestError( 'Account invites are managed by the entryway service', 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 cb73d23eebe..4e1ff662dcf 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 { getCarStream } from '../getRepo' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getCheckout({ - auth: ctx.authVerifier.optionalAccessOrRole, + auth: ctx.authVerifier.optionalAccessOrAdminToken, handler: async ({ params, auth }) => { const { did } = params // takedown check for anyone other than an admin or the user 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 45be946539a..dc84e7dc6eb 100644 --- a/packages/pds/src/api/com/atproto/sync/deprecated/getHead.ts +++ b/packages/pds/src/api/com/atproto/sync/deprecated/getHead.ts @@ -4,7 +4,7 @@ import AppContext from '../../../../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getHead({ - auth: ctx.authVerifier.optionalAccessOrRole, + auth: ctx.authVerifier.optionalAccessOrAdminToken, handler: async ({ params, auth }) => { const { did } = params // takedown check for anyone other than an admin or the user diff --git a/packages/pds/src/api/com/atproto/sync/getBlob.ts b/packages/pds/src/api/com/atproto/sync/getBlob.ts index 3b3d1d2f65a..40be1602296 100644 --- a/packages/pds/src/api/com/atproto/sync/getBlob.ts +++ b/packages/pds/src/api/com/atproto/sync/getBlob.ts @@ -6,7 +6,7 @@ import { BlobNotFoundError } from '@atproto/repo' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getBlob({ - auth: ctx.authVerifier.optionalAccessOrRole, + auth: ctx.authVerifier.optionalAccessOrAdminToken, handler: async ({ params, res, auth }) => { if (!ctx.authVerifier.isUserOrAdmin(auth, params.did)) { const available = await ctx.accountManager.isRepoAvailable(params.did) diff --git a/packages/pds/src/api/com/atproto/sync/getBlocks.ts b/packages/pds/src/api/com/atproto/sync/getBlocks.ts index cd0d00356e0..f6576105020 100644 --- a/packages/pds/src/api/com/atproto/sync/getBlocks.ts +++ b/packages/pds/src/api/com/atproto/sync/getBlocks.ts @@ -7,7 +7,7 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getBlocks({ - auth: ctx.authVerifier.optionalAccessOrRole, + auth: ctx.authVerifier.optionalAccessOrAdminToken, handler: async ({ params, auth }) => { const { did } = params // takedown check for anyone other than an admin or the user diff --git a/packages/pds/src/api/com/atproto/sync/getLatestCommit.ts b/packages/pds/src/api/com/atproto/sync/getLatestCommit.ts index 9a48681c21d..91af9f796f4 100644 --- a/packages/pds/src/api/com/atproto/sync/getLatestCommit.ts +++ b/packages/pds/src/api/com/atproto/sync/getLatestCommit.ts @@ -4,7 +4,7 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getLatestCommit({ - auth: ctx.authVerifier.optionalAccessOrRole, + auth: ctx.authVerifier.optionalAccessOrAdminToken, handler: async ({ params, auth }) => { const { did } = params // takedown check for anyone other than an admin or the user diff --git a/packages/pds/src/api/com/atproto/sync/getRecord.ts b/packages/pds/src/api/com/atproto/sync/getRecord.ts index 7d3641af0ff..019e1b5d939 100644 --- a/packages/pds/src/api/com/atproto/sync/getRecord.ts +++ b/packages/pds/src/api/com/atproto/sync/getRecord.ts @@ -9,7 +9,7 @@ import { SqlRepoReader } from '../../../../actor-store/repo/sql-repo-reader' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getRecord({ - auth: ctx.authVerifier.optionalAccessOrRole, + auth: ctx.authVerifier.optionalAccessOrAdminToken, handler: async ({ params, auth }) => { const { did, collection, rkey } = params // takedown check for anyone other than an admin or the user diff --git a/packages/pds/src/api/com/atproto/sync/getRepo.ts b/packages/pds/src/api/com/atproto/sync/getRepo.ts index ae07715d134..880dfb6a132 100644 --- a/packages/pds/src/api/com/atproto/sync/getRepo.ts +++ b/packages/pds/src/api/com/atproto/sync/getRepo.ts @@ -9,7 +9,7 @@ import { export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getRepo({ - auth: ctx.authVerifier.optionalAccessOrRole, + auth: ctx.authVerifier.optionalAccessOrAdminToken, handler: async ({ params, auth }) => { const { did, since } = params // takedown check for anyone other than an admin or the user diff --git a/packages/pds/src/api/com/atproto/sync/listBlobs.ts b/packages/pds/src/api/com/atproto/sync/listBlobs.ts index e71a2425bca..cafd9ef95bd 100644 --- a/packages/pds/src/api/com/atproto/sync/listBlobs.ts +++ b/packages/pds/src/api/com/atproto/sync/listBlobs.ts @@ -4,7 +4,7 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.listBlobs({ - auth: ctx.authVerifier.optionalAccessOrRole, + auth: ctx.authVerifier.optionalAccessOrAdminToken, handler: async ({ params, auth }) => { const { did, since, limit, cursor } = params // takedown check for anyone other than an admin or the user diff --git a/packages/pds/src/auth-verifier.ts b/packages/pds/src/auth-verifier.ts index e12e0e8acfe..826ee99456b 100644 --- a/packages/pds/src/auth-verifier.ts +++ b/packages/pds/src/auth-verifier.ts @@ -36,12 +36,9 @@ type NullOutput = { credentials: null } -type RoleOutput = { +type AdminTokenOutput = { credentials: { - type: 'role' - admin: boolean - moderator: boolean - triage: boolean + type: 'admin_token' } } @@ -93,8 +90,6 @@ type ValidatedBearer = { export type AuthVerifierOpts = { jwtKey: KeyObject adminPass: string - moderatorPass: string - triagePass: string dids: { pds: string entryway?: string @@ -105,8 +100,6 @@ export type AuthVerifierOpts = { export class AuthVerifier { private _jwtKey: KeyObject private _adminPass: string - private _moderatorPass: string - private _triagePass: string public dids: AuthVerifierOpts['dids'] constructor( @@ -116,8 +109,6 @@ export class AuthVerifier { ) { this._jwtKey = opts.jwtKey this._adminPass = opts.adminPass - this._moderatorPass = opts.moderatorPass - this._triagePass = opts.triagePass this.dids = opts.dids } @@ -187,46 +178,27 @@ export class AuthVerifier { } } - role = (ctx: ReqCtx): RoleOutput => { - const creds = this.parseRoleCreds(ctx.req) - if (creds.status !== RoleStatus.Valid) { + adminToken = (ctx: ReqCtx): AdminTokenOutput => { + const parsed = parseBasicAuth(ctx.req.headers.authorization || '') + if (!parsed) { throw new AuthRequiredError() } - return { - credentials: { - ...creds, - type: 'role', - }, - } - } - - accessOrRole = async (ctx: ReqCtx): Promise => { - if (isBearerToken(ctx.req)) { - return this.access(ctx) - } else { - return this.role(ctx) + const { username, password } = parsed + if (username !== 'admin' || password !== this._adminPass) { + throw new AuthRequiredError() } + return { credentials: { type: 'admin_token' } } } - optionalAccessOrRole = async ( + optionalAccessOrAdminToken = async ( ctx: ReqCtx, - ): Promise => { + ): Promise => { if (isBearerToken(ctx.req)) { return await this.access(ctx) + } else if (isBasicToken(ctx.req)) { + return await this.adminToken(ctx) } else { - const creds = this.parseRoleCreds(ctx.req) - if (creds.status === RoleStatus.Valid) { - return { - credentials: { - ...creds, - type: 'role', - }, - } - } else if (creds.status === RoleStatus.Missing) { - return { credentials: null } - } else { - throw new AuthRequiredError() - } + return this.null() } } @@ -250,7 +222,7 @@ export class AuthVerifier { if (isBearerToken(reqCtx.req)) { return await this.userDidAuth(reqCtx) } else { - return { credentials: null } + return this.null() } } @@ -280,13 +252,13 @@ export class AuthVerifier { } } - roleOrModService = async ( + moderator = async ( reqCtx: ReqCtx, - ): Promise => { + ): Promise => { if (isBearerToken(reqCtx.req)) { return this.modService(reqCtx) } else { - return this.role(reqCtx) + return this.adminToken(reqCtx) } } @@ -379,36 +351,23 @@ export class AuthVerifier { return { iss: payload.iss, aud: payload.aud } } - parseRoleCreds(req: express.Request) { - const parsed = parseBasicAuth(req.headers.authorization || '') - const { Missing, Valid, Invalid } = RoleStatus - if (!parsed) { - return { status: Missing, admin: false, moderator: false, triage: false } - } - const { username, password } = parsed - if (username === 'admin' && password === this._adminPass) { - return { status: Valid, admin: true, moderator: true, triage: true } - } - if (username === 'admin' && password === this._moderatorPass) { - return { status: Valid, admin: false, moderator: true, triage: true } - } - if (username === 'admin' && password === this._triagePass) { - return { status: Valid, admin: false, moderator: false, triage: true } + null(): NullOutput { + return { + credentials: null, } - return { status: Invalid, admin: false, moderator: false, triage: false } } isUserOrAdmin( - auth: AccessOutput | RoleOutput | NullOutput, + auth: AccessOutput | AdminTokenOutput | NullOutput, did: string, ): boolean { if (!auth.credentials) { return false - } - if ('did' in auth.credentials) { + } else if (auth.credentials.type === 'admin_token') { + return true + } else { return auth.credentials.did === did } - return auth.credentials.admin } } @@ -422,6 +381,10 @@ const isBearerToken = (req: express.Request): boolean => { return req.headers.authorization?.startsWith(BEARER) ?? false } +const isBasicToken = (req: express.Request): boolean => { + return req.headers.authorization?.startsWith(BASIC) ?? false +} + const bearerTokenFromReq = (req: express.Request) => { const header = req.headers.authorization || '' if (!header.startsWith(BEARER)) return null diff --git a/packages/pds/src/config/env.ts b/packages/pds/src/config/env.ts index a334c0c51d3..b8be4ab64c2 100644 --- a/packages/pds/src/config/env.ts +++ b/packages/pds/src/config/env.ts @@ -97,8 +97,6 @@ export const readEnv = (): ServerEnvironment => { // secrets jwtSecret: envStr('PDS_JWT_SECRET'), adminPassword: envStr('PDS_ADMIN_PASSWORD'), - moderatorPassword: envStr('PDS_MODERATOR_PASSWORD'), - triagePassword: envStr('PDS_TRIAGE_PASSWORD'), // kms plcRotationKeyKmsKeyId: envStr('PDS_PLC_ROTATION_KEY_KMS_KEY_ID'), @@ -203,8 +201,6 @@ export type ServerEnvironment = { // secrets jwtSecret?: string adminPassword?: string - moderatorPassword?: string - triagePassword?: string // keys plcRotationKeyKmsKeyId?: string diff --git a/packages/pds/src/config/secrets.ts b/packages/pds/src/config/secrets.ts index 8e18cd830f7..eb0f33d182c 100644 --- a/packages/pds/src/config/secrets.ts +++ b/packages/pds/src/config/secrets.ts @@ -29,9 +29,6 @@ export const envToSecrets = (env: ServerEnvironment): ServerSecrets => { return { jwtSecret: env.jwtSecret, adminPassword: env.adminPassword, - moderatorPassword: env.moderatorPassword ?? env.adminPassword, - triagePassword: - env.triagePassword ?? env.moderatorPassword ?? env.adminPassword, plcRotationKey, } } @@ -39,8 +36,6 @@ export const envToSecrets = (env: ServerEnvironment): ServerSecrets => { export type ServerSecrets = { jwtSecret: string adminPassword: string - moderatorPassword: string - triagePassword: string plcRotationKey: SigningKeyKms | SigningKeyMemory } diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index f2e3834e23d..0df8eb0f6b2 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -194,8 +194,6 @@ export class AppContext { const authVerifier = new AuthVerifier(accountManager, idResolver, { jwtKey, // @TODO support multiple keys? adminPass: secrets.adminPassword, - moderatorPass: secrets.moderatorPassword, - triagePass: secrets.triagePassword, dids: { pds: cfg.service.did, entryway: cfg.entryway?.did, diff --git a/packages/pds/tests/account-deletion.test.ts b/packages/pds/tests/account-deletion.test.ts index cfcd5bf4c76..2365e935c69 100644 --- a/packages/pds/tests/account-deletion.test.ts +++ b/packages/pds/tests/account-deletion.test.ts @@ -220,34 +220,23 @@ describe('account deletion', () => { }) await expect(tryUnauthed).rejects.toThrow('Authentication Required') - const tryAsModerator = agent.api.com.atproto.admin.deleteAccount( - { did: ferris.did }, - { - headers: network.pds.adminAuthHeaders('moderator'), - encoding: 'application/json', - }, - ) - await expect(tryAsModerator).rejects.toThrow( - 'Must be an admin to delete an account', - ) - const { data: acct } = await agent.api.com.atproto.admin.getAccountInfo( { did: ferris.did }, - { headers: network.pds.adminAuthHeaders('admin') }, + { headers: network.pds.adminAuthHeaders() }, ) expect(acct.did).toBe(ferris.did) await agent.api.com.atproto.admin.deleteAccount( { did: ferris.did }, { - headers: network.pds.adminAuthHeaders('admin'), + headers: network.pds.adminAuthHeaders(), encoding: 'application/json', }, ) const tryGetAccountInfo = agent.api.com.atproto.admin.getAccountInfo( { did: ferris.did }, - { headers: network.pds.adminAuthHeaders('admin') }, + { headers: network.pds.adminAuthHeaders() }, ) await expect(tryGetAccountInfo).rejects.toThrow('Account not found') }) diff --git a/packages/pds/tests/account.test.ts b/packages/pds/tests/account.test.ts index 743248814f8..2a887f1b2cb 100644 --- a/packages/pds/tests/account.test.ts +++ b/packages/pds/tests/account.test.ts @@ -261,31 +261,6 @@ describe('account', () => { expect(accnt2?.email).toBe(email) }) - it('disallows non-admin moderators to perform email updates', async () => { - const attemptUpdateMod = agent.api.com.atproto.admin.updateAccountEmail( - { - account: handle, - email: 'new@email.com', - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders('moderator'), - }, - ) - await expect(attemptUpdateMod).rejects.toThrow('Insufficient privileges') - const attemptUpdateTriage = agent.api.com.atproto.admin.updateAccountEmail( - { - account: handle, - email: 'new@email.com', - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders('triage'), - }, - ) - await expect(attemptUpdateTriage).rejects.toThrow('Insufficient privileges') - }) - it('disallows duplicate email addresses and handles', async () => { const email = 'bob@test.com' const handle = 'bob.test' @@ -567,21 +542,10 @@ describe('account', () => { }) await expect(tryUnauthed).rejects.toThrow('Authentication Required') - const tryAsModerator = agent.api.com.atproto.admin.updateAccountPassword( - { did, password: 'new-admin-pass' }, - { - headers: network.pds.adminAuthHeaders('moderator'), - encoding: 'application/json', - }, - ) - await expect(tryAsModerator).rejects.toThrow( - 'Must be an admin to update an account password', - ) - await agent.api.com.atproto.admin.updateAccountPassword( { did, password: 'new-admin-password' }, { - headers: network.pds.adminAuthHeaders('admin'), + headers: network.pds.adminAuthHeaders(), encoding: 'application/json', }, ) diff --git a/packages/pds/tests/handles.test.ts b/packages/pds/tests/handles.test.ts index 3d6e5ecb41f..aaac2daa3df 100644 --- a/packages/pds/tests/handles.test.ts +++ b/packages/pds/tests/handles.test.ts @@ -249,27 +249,5 @@ describe('handles', () => { handle: 'bob-alt.test', }) await expect(attempt2).rejects.toThrow('Authentication Required') - const attempt3 = agent.api.com.atproto.admin.updateAccountHandle( - { - did: bob, - handle: 'bob-alt.test', - }, - { - headers: network.pds.adminAuthHeaders('moderator'), - encoding: 'application/json', - }, - ) - await expect(attempt3).rejects.toThrow('Insufficient privileges') - const attempt4 = agent.api.com.atproto.admin.updateAccountHandle( - { - did: bob, - handle: 'bob-alt.test', - }, - { - headers: network.pds.adminAuthHeaders('triage'), - encoding: 'application/json', - }, - ) - await expect(attempt4).rejects.toThrow('Insufficient privileges') }) }) diff --git a/packages/pds/tests/invites-admin.test.ts b/packages/pds/tests/invites-admin.test.ts index ae3b0b0df5b..c4462847eb3 100644 --- a/packages/pds/tests/invites-admin.test.ts +++ b/packages/pds/tests/invites-admin.test.ts @@ -177,31 +177,6 @@ describe('pds admin invite views', () => { expect(aliceView.data.invites?.length).toBe(6) }) - it('does not allow triage moderators to disable invites.', async () => { - const attemptDisableInvites = - agent.api.com.atproto.admin.disableInviteCodes( - { codes: ['x'], accounts: [alice] }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders('triage'), - }, - ) - await expect(attemptDisableInvites).rejects.toThrow( - 'Insufficient privileges', - ) - }) - - it('does not allow non-admin moderators to create invites.', async () => { - const attemptCreateInvite = agent.api.com.atproto.server.createInviteCode( - { useCount: 5, forAccount: alice }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders('moderator'), - }, - ) - await expect(attemptCreateInvite).rejects.toThrow('Insufficient privileges') - }) - it('disables an account from getting additional invite codes', async () => { await agent.api.com.atproto.admin.disableAccountInvites( { account: carol }, @@ -255,17 +230,6 @@ describe('pds admin invite views', () => { expect(res.every((row) => row.disabled === 1)) }) - it('does not allow triage moderators to disable account invites', async () => { - const attempt = agent.api.com.atproto.admin.disableAccountInvites( - { account: alice }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders('triage'), - }, - ) - await expect(attempt).rejects.toThrow('Insufficient privileges') - }) - it('re-enables an accounts invites', async () => { await agent.api.com.atproto.admin.enableAccountInvites( { account: carol }, @@ -284,15 +248,4 @@ describe('pds admin invite views', () => { ) expect(invRes.data.codes.length).toBeGreaterThan(0) }) - - it('does not allow triage moderators to enable account invites', async () => { - const attempt = agent.api.com.atproto.admin.enableAccountInvites( - { account: alice }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders('triage'), - }, - ) - await expect(attempt).rejects.toThrow('Insufficient privileges') - }) }) diff --git a/packages/pds/tests/moderation.test.ts b/packages/pds/tests/moderation.test.ts index 4d3acfe2e25..e847929ed32 100644 --- a/packages/pds/tests/moderation.test.ts +++ b/packages/pds/tests/moderation.test.ts @@ -57,14 +57,14 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders('moderator'), + headers: network.pds.adminAuthHeaders(), }, ) const res = await agent.api.com.atproto.admin.getSubjectStatus( { did: repoSubject.did, }, - { headers: network.pds.adminAuthHeaders('moderator') }, + { headers: network.pds.adminAuthHeaders() }, ) expect(res.data.subject.did).toEqual(sc.dids.bob) expect(res.data.takedown?.applied).toBe(true) @@ -79,14 +79,14 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders('moderator'), + headers: network.pds.adminAuthHeaders(), }, ) const res = await agent.api.com.atproto.admin.getSubjectStatus( { did: repoSubject.did, }, - { headers: network.pds.adminAuthHeaders('moderator') }, + { headers: network.pds.adminAuthHeaders() }, ) expect(res.data.subject.did).toEqual(sc.dids.bob) expect(res.data.takedown?.applied).toBe(false) @@ -101,14 +101,14 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders('moderator'), + headers: network.pds.adminAuthHeaders(), }, ) const res = await agent.api.com.atproto.admin.getSubjectStatus( { uri: recordSubject.uri, }, - { headers: network.pds.adminAuthHeaders('moderator') }, + { headers: network.pds.adminAuthHeaders() }, ) expect(res.data.subject.uri).toEqual(recordSubject.uri) expect(res.data.takedown?.applied).toBe(true) @@ -123,48 +123,20 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders('moderator'), + headers: network.pds.adminAuthHeaders(), }, ) const res = await agent.api.com.atproto.admin.getSubjectStatus( { uri: recordSubject.uri, }, - { headers: network.pds.adminAuthHeaders('moderator') }, + { headers: network.pds.adminAuthHeaders() }, ) expect(res.data.subject.uri).toEqual(recordSubject.uri) expect(res.data.takedown?.applied).toBe(false) expect(res.data.takedown?.ref).toBeUndefined() }) - it('does not allow non-full moderators to update subject state', async () => { - const subject = { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.bob, - } - const attemptTakedownTriage = - agent.api.com.atproto.admin.updateSubjectStatus( - { - subject, - takedown: { applied: true }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders('triage'), - }, - ) - await expect(attemptTakedownTriage).rejects.toThrow( - 'Must be a full moderator to update subject state', - ) - const res = await agent.api.com.atproto.admin.getSubjectStatus( - { - did: subject.did, - }, - { headers: network.pds.adminAuthHeaders('moderator') }, - ) - expect(res.data.takedown?.applied).toBe(false) - }) - describe('blob takedown', () => { it('takes down blobs', async () => { await agent.api.com.atproto.admin.updateSubjectStatus( @@ -182,7 +154,7 @@ describe('moderation', () => { did: blobSubject.did, blob: blobSubject.cid, }, - { headers: network.pds.adminAuthHeaders('moderator') }, + { headers: network.pds.adminAuthHeaders() }, ) expect(res.data.subject.did).toEqual(blobSubject.did) expect(res.data.subject.cid).toEqual(blobSubject.cid) @@ -272,11 +244,6 @@ describe('moderation', () => { headers: sc.getHeaders(sc.dids.bob), }) await expect(attempt2).rejects.toThrow('Blob not found') - // non-admin role, disallow - const attempt3 = agent.api.com.atproto.sync.getBlob(blobParams, { - headers: network.pds.adminAuthHeaders('moderator'), - }) - await expect(attempt3).rejects.toThrow('Blob not found') // logged-in as account, allow const res1 = await agent.api.com.atproto.sync.getBlob(blobParams, { headers: sc.getHeaders(sc.dids.carol), @@ -284,7 +251,7 @@ describe('moderation', () => { expect(res1.data.byteLength).toBeGreaterThan(9000) // admin role, allow const res2 = await agent.api.com.atproto.sync.getBlob(blobParams, { - headers: network.pds.adminAuthHeaders('admin'), + headers: network.pds.adminAuthHeaders(), }) expect(res2.data.byteLength).toBeGreaterThan(9000) // revert takedown diff --git a/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap index 285c24cf8f0..1a8c8b4d0be 100644 --- a/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap @@ -4,7 +4,7 @@ exports[`proxies admin requests creates reports of a repo. 1`] = ` Array [ Object { "createdAt": "1970-01-01T00:00:00.000Z", - "id": 4, + "id": 1, "reasonType": "com.atproto.moderation.defs#reasonSpam", "reportedBy": "user(0)", "subject": Object { @@ -14,7 +14,7 @@ Array [ }, Object { "createdAt": "1970-01-01T00:00:00.000Z", - "id": 6, + "id": 3, "reason": "impersonation", "reasonType": "com.atproto.moderation.defs#reasonOther", "reportedBy": "user(2)", @@ -30,11 +30,12 @@ exports[`proxies admin requests fetches a list of events. 1`] = ` Array [ Object { "createdAt": "1970-01-01T00:00:00.000Z", - "createdBy": "did:example:admin", + "createdBy": "user(1)", + "creatorHandle": "testmod.test", "event": Object { "$type": "com.atproto.admin.defs#modEventAcknowledge", }, - "id": 9, + "id": 6, "subject": Object { "$type": "com.atproto.admin.defs#repoRef", "did": "user(0)", @@ -44,14 +45,14 @@ Array [ }, Object { "createdAt": "1970-01-01T00:00:00.000Z", - "createdBy": "user(1)", + "createdBy": "user(2)", "creatorHandle": "carol.test", "event": Object { "$type": "com.atproto.admin.defs#modEventReport", "comment": "impersonation", "reportType": "com.atproto.moderation.defs#reasonOther", }, - "id": 6, + "id": 3, "subject": Object { "$type": "com.atproto.admin.defs#repoRef", "did": "user(0)", @@ -61,7 +62,7 @@ Array [ }, Object { "createdAt": "1970-01-01T00:00:00.000Z", - "createdBy": "user(2)", + "createdBy": "user(3)", "event": Object { "$type": "com.atproto.admin.defs#modEventTag", "add": Array [ @@ -70,7 +71,7 @@ Array [ ], "remove": Array [], }, - "id": 5, + "id": 2, "subject": Object { "$type": "com.atproto.admin.defs#repoRef", "did": "user(0)", @@ -80,13 +81,13 @@ Array [ }, Object { "createdAt": "1970-01-01T00:00:00.000Z", - "createdBy": "user(3)", + "createdBy": "user(4)", "creatorHandle": "alice.test", "event": Object { "$type": "com.atproto.admin.defs#modEventReport", "reportType": "com.atproto.moderation.defs#reasonSpam", }, - "id": 4, + "id": 1, "subject": Object { "$type": "com.atproto.admin.defs#repoRef", "did": "user(0)", @@ -100,99 +101,58 @@ Array [ exports[`proxies admin requests fetches event details. 1`] = ` Object { "createdAt": "1970-01-01T00:00:00.000Z", - "createdBy": "user(1)", + "createdBy": "user(2)", "event": Object { - "$type": "com.atproto.admin.defs#modEventLabel", - "comment": "[AutoModerator]: Applying labels", - "createLabelVals": Array [ - "test-label", - "test-label-2", + "$type": "com.atproto.admin.defs#modEventTag", + "add": Array [ + "lang:en", + "lang:i", ], - "negateLabelVals": Array [], + "remove": Array [], }, "id": 2, "subject": Object { - "$type": "com.atproto.admin.defs#recordView", - "blobCids": Array [ - "cids(1)", - ], - "cid": "cids(0)", + "$type": "com.atproto.admin.defs#repoView", + "did": "user(0)", + "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "moderation": Object {}, - "repo": Object { - "did": "user(0)", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "moderation": Object { - "subjectStatus": Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "id": 1, - "lastReportedAt": "1970-01-01T00:00:00.000Z", - "lastReviewedAt": "1970-01-01T00:00:00.000Z", - "lastReviewedBy": "did:example:admin", - "reviewState": "com.atproto.admin.defs#reviewClosed", - "subject": Object { - "$type": "com.atproto.admin.defs#repoRef", - "did": "user(0)", - }, - "subjectBlobCids": Array [], - "subjectRepoHandle": "bob.test", - "tags": Array [ - "lang:en", - "lang:i", - ], - "takendown": false, - "updatedAt": "1970-01-01T00:00:00.000Z", + "moderation": Object { + "subjectStatus": Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "id": 1, + "lastReportedAt": "1970-01-01T00:00:00.000Z", + "lastReviewedAt": "1970-01-01T00:00:00.000Z", + "lastReviewedBy": "user(1)", + "reviewState": "com.atproto.admin.defs#reviewClosed", + "subject": Object { + "$type": "com.atproto.admin.defs#repoRef", + "did": "user(0)", }, + "subjectBlobCids": Array [], + "subjectRepoHandle": "bob.test", + "tags": Array [ + "lang:en", + "lang:i", + ], + "takendown": false, + "updatedAt": "1970-01-01T00:00:00.000Z", }, - "relatedRecords": Array [ - Object { - "$type": "app.bsky.actor.profile", - "avatar": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(3)", - }, - "size": 3976, - }, - "description": "hi im bob label_me", - "displayName": "bobby", - }, - ], }, - "uri": "record(0)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(1)", - }, - "size": 4114, - }, + "relatedRecords": Array [ + Object { + "$type": "app.bsky.actor.profile", + "avatar": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(0)", }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(1)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(1)", + "size": 3976, }, + "description": "hi im bob label_me", + "displayName": "bobby", }, - "text": "hear that label_me label_me_2", - }, + ], }, "subjectBlobCids": Array [], "subjectBlobs": Array [], @@ -211,7 +171,7 @@ Array [ ], "remove": Array [], }, - "id": 8, + "id": 5, "subject": Object { "$type": "com.atproto.repo.strongRef", "cid": "cids(0)", @@ -222,11 +182,12 @@ Array [ }, Object { "createdAt": "1970-01-01T00:00:00.000Z", - "createdBy": "did:example:admin", + "createdBy": "user(1)", + "creatorHandle": "testmod.test", "event": Object { "$type": "com.atproto.admin.defs#modEventAcknowledge", }, - "id": 7, + "id": 4, "subject": Object { "$type": "com.atproto.repo.strongRef", "cid": "cids(0)", @@ -250,7 +211,7 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "id": 4, "lastReviewedAt": "1970-01-01T00:00:00.000Z", - "lastReviewedBy": "did:example:admin", + "lastReviewedBy": "user(1)", "reviewState": "com.atproto.admin.defs#reviewClosed", "subject": Object { "$type": "com.atproto.repo.strongRef", @@ -287,13 +248,17 @@ Object { "usedAt": "1970-01-01T00:00:00.000Z", "usedBy": "user(2)", }, + Object { + "usedAt": "1970-01-01T00:00:00.000Z", + "usedBy": "user(3)", + }, Object { "usedAt": "1970-01-01T00:00:00.000Z", "usedBy": "user(0)", }, Object { "usedAt": "1970-01-01T00:00:00.000Z", - "usedBy": "user(3)", + "usedBy": "user(4)", }, ], }, @@ -304,7 +269,7 @@ Object { "id": 1, "lastReportedAt": "1970-01-01T00:00:00.000Z", "lastReviewedAt": "1970-01-01T00:00:00.000Z", - "lastReviewedBy": "did:example:admin", + "lastReviewedBy": "user(1)", "reviewState": "com.atproto.admin.defs#reviewClosed", "subject": Object { "$type": "com.atproto.admin.defs#repoRef", @@ -413,11 +378,11 @@ Array [ exports[`proxies admin requests takes actions and resolves reports 1`] = ` Object { "createdAt": "1970-01-01T00:00:00.000Z", - "createdBy": "did:example:admin", + "createdBy": "user(0)", "event": Object { "$type": "com.atproto.admin.defs#modEventAcknowledge", }, - "id": 7, + "id": 4, "subject": Object { "$type": "com.atproto.repo.strongRef", "cid": "cids(0)", @@ -430,11 +395,11 @@ Object { exports[`proxies admin requests takes actions and resolves reports 2`] = ` Object { "createdAt": "1970-01-01T00:00:00.000Z", - "createdBy": "did:example:admin", + "createdBy": "user(1)", "event": Object { "$type": "com.atproto.admin.defs#modEventAcknowledge", }, - "id": 9, + "id": 6, "subject": Object { "$type": "com.atproto.admin.defs#repoRef", "did": "user(0)", diff --git a/packages/pds/tests/proxied/admin.test.ts b/packages/pds/tests/proxied/admin.test.ts index 3637b22878c..46bef49cbaf 100644 --- a/packages/pds/tests/proxied/admin.test.ts +++ b/packages/pds/tests/proxied/admin.test.ts @@ -8,12 +8,13 @@ import { import { forSnapshot } from '../_util' import { NotFoundError } from '@atproto/api/src/client/types/app/bsky/feed/getPostThread' -// @TODO skipping during appview v2 buildout, as appview frontends no longer contains moderation endpoints -describe.skip('proxies admin requests', () => { +describe('proxies admin requests', () => { let network: TestNetwork let agent: AtpAgent let sc: SeedClient + let moderator: string + beforeAll(async () => { network = await TestNetwork.create({ dbPostgresSchema: 'proxy_admin', @@ -35,6 +36,15 @@ describe.skip('proxies admin requests', () => { inviteCode: invite.code, addModLabels: network.bsky, }) + const modAccount = await sc.createAccount('moderator', { + handle: 'testmod.test', + email: 'testmod@test.com', + password: 'testmod-pass', + inviteCode: invite.code, + }) + moderator = modAccount.did + network.ozone.addModeratorDid(moderator) + await network.processAll() }) @@ -112,7 +122,7 @@ describe.skip('proxies admin requests', () => { reason: 'Y', }, { - headers: network.pds.adminAuthHeaders(), + headers: sc.getHeaders(moderator), encoding: 'application/json', }, ) @@ -129,7 +139,7 @@ describe.skip('proxies admin requests', () => { reason: 'Y', }, { - headers: network.pds.adminAuthHeaders(), + headers: sc.getHeaders(moderator), encoding: 'application/json', }, ) @@ -142,14 +152,15 @@ describe.skip('proxies admin requests', () => { { subject: sc.posts[sc.dids.bob][1].ref.uriStr, }, - { headers: network.pds.adminAuthHeaders() }, + { headers: sc.getHeaders(moderator) }, ) expect(forSnapshot(result.events)).toMatchSnapshot() }) + it('fetches repo details.', async () => { const { data: result } = await agent.api.com.atproto.admin.getRepo( { did: sc.dids.eve }, - { headers: network.pds.adminAuthHeaders() }, + { headers: sc.getHeaders(moderator) }, ) expect(forSnapshot(result)).toMatchSnapshot() }) @@ -158,7 +169,7 @@ describe.skip('proxies admin requests', () => { const post = sc.posts[sc.dids.bob][1] const { data: result } = await agent.api.com.atproto.admin.getRecord( { uri: post.ref.uriStr }, - { headers: network.pds.adminAuthHeaders() }, + { headers: sc.getHeaders(moderator) }, ) expect(forSnapshot(result)).toMatchSnapshot() }) @@ -167,7 +178,7 @@ describe.skip('proxies admin requests', () => { const { data: result } = await agent.api.com.atproto.admin.getModerationEvent( { id: 2 }, - { headers: network.pds.adminAuthHeaders() }, + { headers: sc.getHeaders(moderator) }, ) expect(forSnapshot(result)).toMatchSnapshot() }) @@ -176,7 +187,7 @@ describe.skip('proxies admin requests', () => { const { data: result } = await agent.api.com.atproto.admin.queryModerationEvents( { subject: sc.dids.bob }, - { headers: network.pds.adminAuthHeaders() }, + { headers: sc.getHeaders(moderator) }, ) expect(forSnapshot(result.events)).toMatchSnapshot() }) @@ -184,7 +195,7 @@ describe.skip('proxies admin requests', () => { it('searches repos.', async () => { const { data: result } = await agent.api.com.atproto.admin.searchRepos( { term: 'alice' }, - { headers: network.pds.adminAuthHeaders() }, + { headers: sc.getHeaders(moderator) }, ) expect(forSnapshot(result.repos)).toMatchSnapshot() }) @@ -192,12 +203,12 @@ describe.skip('proxies admin requests', () => { it('passes through errors.', async () => { const tryGetRepo = agent.api.com.atproto.admin.getRepo( { did: 'did:does:not:exist' }, - { headers: network.pds.adminAuthHeaders() }, + { headers: sc.getHeaders(moderator) }, ) await expect(tryGetRepo).rejects.toThrow('Repo not found') const tryGetRecord = agent.api.com.atproto.admin.getRecord( { uri: 'at://did:does:not:exist/bad.collection.name/badrkey' }, - { headers: network.pds.adminAuthHeaders() }, + { headers: sc.getHeaders(moderator) }, ) await expect(tryGetRecord).rejects.toThrow('Record not found') }) @@ -217,7 +228,7 @@ describe.skip('proxies admin requests', () => { negateLabelVals: ['cats'], }, { - headers: network.pds.adminAuthHeaders(), + headers: sc.getHeaders(moderator), encoding: 'application/json', }, ) @@ -230,7 +241,7 @@ describe.skip('proxies admin requests', () => { }, ) await expect(tryGetProfileAppview).rejects.toThrow( - 'Account has been taken down', + 'Account has been suspended', ) // reverse action await agent.api.com.atproto.admin.emitModerationEvent( @@ -246,7 +257,7 @@ describe.skip('proxies admin requests', () => { reason: 'X', }, { - headers: network.pds.adminAuthHeaders(), + headers: sc.getHeaders(moderator), encoding: 'application/json', }, ) @@ -280,7 +291,7 @@ describe.skip('proxies admin requests', () => { negateLabelVals: ['cats'], }, { - headers: network.pds.adminAuthHeaders(), + headers: sc.getHeaders(moderator), encoding: 'application/json', }, ) @@ -304,7 +315,7 @@ describe.skip('proxies admin requests', () => { reason: 'X', }, { - headers: network.pds.adminAuthHeaders(), + headers: sc.getHeaders(moderator), encoding: 'application/json', }, ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a1610aac275..47fabe9593b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -660,6 +660,9 @@ importers: axios: specifier: ^0.27.2 version: 0.27.2 + nodemailer: + specifier: ^6.8.0 + version: 6.8.0 packages/pds: dependencies: