From 6a26396b03ae3038e389417f83315e5bfe92f7ce Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 11:46:00 -0600 Subject: [PATCH 01/50] tidy bsky auth --- packages/bsky/src/auth-verifier.ts | 24 ++++++++++++------------ packages/bsky/src/index.ts | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/bsky/src/auth-verifier.ts b/packages/bsky/src/auth-verifier.ts index 7798efa99b2..69335772390 100644 --- a/packages/bsky/src/auth-verifier.ts +++ b/packages/bsky/src/auth-verifier.ts @@ -45,9 +45,9 @@ type RoleOutput = { } } -type AdminServiceOutput = { +type ModServiceOutput = { credentials: { - type: 'admin_service' + type: 'mod_service' aud: string iss: string } @@ -55,18 +55,18 @@ type AdminServiceOutput = { export type AuthVerifierOpts = { ownDid: string - adminDid: string + modServiceDid: string adminPasses: string[] } export class AuthVerifier { public ownDid: string - public adminDid: string + public modServiceDid: string private adminPasses: Set constructor(public dataplane: DataPlaneClient, opts: AuthVerifierOpts) { this.ownDid = opts.ownDid - this.adminDid = opts.adminDid + this.modServiceDid = opts.modServiceDid this.adminPasses = new Set(opts.adminPasses) } @@ -159,17 +159,17 @@ export class AuthVerifier { } } - adminService = async (reqCtx: ReqCtx): Promise => { + adminService = async (reqCtx: ReqCtx): Promise => { const { iss, aud } = await this.verifyServiceJwt(reqCtx, { aud: this.ownDid, - iss: [this.adminDid], + iss: [this.modServiceDid], }) - return { credentials: { type: 'admin_service', aud, iss } } + return { credentials: { type: 'mod_service', aud, iss } } } roleOrAdminService = async ( reqCtx: ReqCtx, - ): Promise => { + ): Promise => { if (isBearerToken(reqCtx.req)) { return this.adminService(reqCtx) } else { @@ -236,16 +236,16 @@ export class AuthVerifier { } parseCreds( - creds: StandardOutput | RoleOutput | AdminServiceOutput | NullOutput, + creds: StandardOutput | RoleOutput | ModServiceOutput | NullOutput, ) { const viewer = creds.credentials.type === 'standard' ? creds.credentials.iss : null const canViewTakedowns = (creds.credentials.type === 'role' && creds.credentials.admin) || - creds.credentials.type === 'admin_service' + creds.credentials.type === 'mod_service' const canPerformTakedown = (creds.credentials.type === 'role' && creds.credentials.admin) || - creds.credentials.type === 'admin_service' + creds.credentials.type === 'mod_service' return { viewer, canViewTakedowns, diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index a968d85b584..b11fb42a453 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -100,7 +100,7 @@ export class BskyAppView { const authVerifier = new AuthVerifier(dataplane, { ownDid: config.serverDid, - adminDid: config.modServiceDid, + modServiceDid: config.modServiceDid, adminPasses: config.adminPasswords, }) From 125f721e851190427c1f6d7f2d9d76cbed5c9a88 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 11:46:15 -0600 Subject: [PATCH 02/50] hook up new auth verifier --- packages/ozone/src/auth-verifier.ts | 202 ++++++++++++++++++++++++++++ packages/ozone/src/config/config.ts | 12 ++ packages/ozone/src/config/env.ts | 6 +- packages/ozone/src/context.ts | 43 ++---- 4 files changed, 233 insertions(+), 30 deletions(-) create mode 100644 packages/ozone/src/auth-verifier.ts diff --git a/packages/ozone/src/auth-verifier.ts b/packages/ozone/src/auth-verifier.ts new file mode 100644 index 00000000000..a2fe5955f51 --- /dev/null +++ b/packages/ozone/src/auth-verifier.ts @@ -0,0 +1,202 @@ +import express from 'express' +import * as ui8 from 'uint8arrays' +import { IdResolver } from '@atproto/identity' +import { AuthRequiredError, verifyJwt } from '@atproto/xrpc-server' + +type ReqCtx = { + req: express.Request +} + +type RoleOutput = { + credentials: { + type: 'role' + isModerator: boolean + isTriage: true + } +} + +type ModeratorOutput = { + credentials: { + type: 'moderator' + aud: string + iss: string + isModerator: boolean + isTriage: true + } +} + +type StandardOutput = { + credentials: { + type: 'standard' + aud: string + iss: string + isModerator: boolean + isTriage: boolean + } +} + +type NullOutput = { + credentials: { + type: 'null' + iss: null + } +} + +export type AuthVerifierOpts = { + serviceDid: string + moderators: string[] + triage: string[] + adminPassword: string + moderatorPassword: string + triagePassword: string +} + +export class AuthVerifier { + serviceDid: string + moderators: string[] + triage: string[] + private adminPassword: string + private moderatorPassword: string + private triagePassword: string + + constructor(public idResolver: IdResolver, opts: AuthVerifierOpts) { + this.serviceDid = opts.serviceDid + 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) + } else { + return this.role(reqCtx) + } + } + + moderator = async (reqCtx: ReqCtx): Promise => { + const creds = await this.standard(reqCtx) + if (!creds.credentials.isTriage) { + throw new AuthRequiredError('not a moderator account') + } + return { + credentials: { + ...creds.credentials, + type: 'moderator', + isTriage: true, + }, + } + } + + standard = async (reqCtx: ReqCtx): Promise => { + const getSigningKey = async ( + did: string, + forceRefresh: boolean, + ): Promise => { + const atprotoData = await this.idResolver.did.resolveAtprotoData( + did, + forceRefresh, + ) + return atprotoData.signingKey + } + + const jwtStr = getJwtStrFromReq(reqCtx.req) + if (!jwtStr) { + throw new AuthRequiredError('missing jwt', 'MissingJwt') + } + const payload = await verifyJwt(jwtStr, this.serviceDid, getSigningKey) + const iss = payload.iss + const isModerator = this.moderators.includes(iss) + const isTriage = isModerator || this.triage.includes(iss) + return { + credentials: { + type: 'standard', + iss, + aud: payload.aud, + isModerator, + isTriage, + }, + } + } + + standardOptional = async ( + reqCtx: ReqCtx, + ): Promise => { + if (isBearerToken(reqCtx.req)) { + return this.standard(reqCtx) + } + return this.nullCreds() + } + + role = async (reqCtx: ReqCtx): Promise => { + const parsed = parseBasicAuth(reqCtx.req.headers.authorization ?? '') + const { username, password } = parsed ?? {} + if (username === 'admin') { + throw new AuthRequiredError() + } + if (password === this.triagePassword) { + return { + credentials: { + type: 'role', + isModerator: false, + isTriage: true, + }, + } + } else if ( + password === this.moderatorPassword || + password === this.adminPassword + ) { + return { + credentials: { + type: 'role', + isModerator: true, + isTriage: true, + }, + } + } else { + throw new AuthRequiredError() + } + } + + nullCreds(): NullOutput { + return { + credentials: { + type: 'null', + iss: null, + }, + } + } +} + +const BEARER = 'Bearer ' +const BASIC = 'Basic ' + +const isBearerToken = (req: express.Request): boolean => { + return req.headers.authorization?.startsWith(BEARER) ?? false +} + +export const getJwtStrFromReq = (req: express.Request): string | null => { + const { authorization } = req.headers + if (!authorization?.startsWith(BEARER)) { + return null + } + return authorization.slice(BEARER.length).trim() +} + +export const parseBasicAuth = ( + token: string, +): { username: string; password: string } | null => { + if (!token.startsWith(BASIC)) return null + const b64 = token.slice(BASIC.length) + let parsed: string[] + try { + parsed = ui8.toString(ui8.fromString(b64, 'base64pad'), 'utf8').split(':') + } catch (err) { + return null + } + const [username, password] = parsed + if (!username || !password) return null + return { username, password } +} diff --git a/packages/ozone/src/config/config.ts b/packages/ozone/src/config/config.ts index 32ed8ba5cb5..3f9c38f96c8 100644 --- a/packages/ozone/src/config/config.ts +++ b/packages/ozone/src/config/config.ts @@ -43,12 +43,18 @@ export const envToCfg = (env: OzoneEnvironment): OzoneConfig => { plcUrl: env.didPlcUrl, } + const accessCfg: OzoneConfig['access'] = { + moderators: env.moderatorDids, + triage: env.triageDids, + } + return { service: serviceCfg, db: dbCfg, appview: appviewCfg, pds: pdsCfg, identity: identityCfg, + access: accessCfg, } } @@ -58,6 +64,7 @@ export type OzoneConfig = { appview: AppviewConfig pds: PdsConfig | null identity: IdentityConfig + access: AccessConfig } export type ServiceConfig = { @@ -88,3 +95,8 @@ export type PdsConfig = { export type IdentityConfig = { plcUrl: string } + +export type AccessConfig = { + moderators: string[] + triage: string[] +} diff --git a/packages/ozone/src/config/env.ts b/packages/ozone/src/config/env.ts index b0ad10074eb..fcb6eb31d26 100644 --- a/packages/ozone/src/config/env.ts +++ b/packages/ozone/src/config/env.ts @@ -1,4 +1,4 @@ -import { envInt, envStr } from '@atproto/common' +import { envInt, envList, envStr } from '@atproto/common' export const readEnv = (): OzoneEnvironment => { return { @@ -17,6 +17,8 @@ export const readEnv = (): OzoneEnvironment => { dbPoolMaxUses: envInt('OZONE_DB_POOL_MAX_USES'), dbPoolIdleTimeoutMs: envInt('OZONE_DB_POOL_IDLE_TIMEOUT_MS'), didPlcUrl: envStr('OZONE_DID_PLC_URL'), + 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'), @@ -40,6 +42,8 @@ export type OzoneEnvironment = { dbPoolMaxUses?: number dbPoolIdleTimeoutMs?: number didPlcUrl?: string + moderatorDids: string[] + triageDids: string[] adminPassword?: string moderatorPassword?: string triagePassword?: string diff --git a/packages/ozone/src/context.ts b/packages/ozone/src/context.ts index 00ef4bd71ba..726a500ea38 100644 --- a/packages/ozone/src/context.ts +++ b/packages/ozone/src/context.ts @@ -6,7 +6,6 @@ import { createServiceAuthHeaders } from '@atproto/xrpc-server' import { Database } from './db' import { OzoneConfig, OzoneSecrets } from './config' import { ModerationService, ModerationServiceCreator } from './mod-service' -import * as auth from './auth' import { BackgroundQueue } from './background' import assert from 'assert' import { EventPusher } from './daemon' @@ -15,6 +14,7 @@ import { CommunicationTemplateService, CommunicationTemplateServiceCreator, } from './communication-service/template' +import { AuthVerifier } from './auth-verifier' export type AppContextOptions = { db: Database @@ -27,6 +27,7 @@ export type AppContextOptions = { idResolver: IdResolver backgroundQueue: BackgroundQueue sequencer: Sequencer + authVerifier: AuthVerifier } export class AppContext { @@ -81,6 +82,15 @@ export class AppContext { const sequencer = new Sequencer(db) + const authVerifier = new AuthVerifier(idResolver, { + serviceDid: cfg.service.did, + moderators: cfg.access.moderators, + triage: cfg.access.triage, + adminPassword: secrets.adminPassword, + moderatorPassword: secrets.moderatorPassword, + triagePassword: secrets.triagePassword, + }) + return new AppContext( { db, @@ -93,6 +103,7 @@ export class AppContext { idResolver, backgroundQueue, sequencer, + authVerifier, ...(overrides ?? {}), }, secrets, @@ -151,34 +162,8 @@ export class AppContext { return this.opts.sequencer } - get authVerifier() { - return auth.authVerifier(this.idResolver, { aud: this.cfg.service.did }) - } - - get authVerifierAnyAudience() { - return auth.authVerifier(this.idResolver, { aud: null }) - } - - get authOptionalVerifierAnyAudience() { - return auth.authOptionalVerifier(this.idResolver, { aud: null }) - } - - get authOptionalVerifier() { - return auth.authOptionalVerifier(this.idResolver, { - aud: this.cfg.service.did, - }) - } - - get authOptionalAccessOrRoleVerifier() { - return auth.authOptionalAccessOrRoleVerifier( - this.idResolver, - this.secrets, - this.cfg.service.did, - ) - } - - get roleVerifier() { - return auth.roleVerifier(this.secrets) + get authVerifier(): AuthVerifier { + return this.opts.authVerifier } async serviceAuthHeaders(aud: string) { From 36da1d90d48736afe93645c433dcf669a9bc74c4 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 12:12:15 -0600 Subject: [PATCH 03/50] update auth throughout ozone --- packages/dev-env/src/ozone.ts | 3 + .../api/admin/createCommunicationTemplate.ts | 4 +- .../api/admin/deleteCommunicationTemplate.ts | 4 +- .../src/api/admin/emitModerationEvent.ts | 8 +- .../ozone/src/api/admin/getModerationEvent.ts | 2 +- packages/ozone/src/api/admin/getRecord.ts | 4 +- packages/ozone/src/api/admin/getRepo.ts | 4 +- .../api/admin/listCommunicationTemplates.ts | 4 +- .../src/api/admin/queryModerationEvents.ts | 2 +- .../src/api/admin/queryModerationStatuses.ts | 2 +- packages/ozone/src/api/admin/searchRepos.ts | 2 +- .../api/admin/updateCommunicationTemplate.ts | 4 +- packages/ozone/src/api/index.ts | 2 + .../ozone/src/api/moderation/createReport.ts | 4 +- packages/ozone/src/api/proxied.ts | 74 +++++++++ packages/ozone/src/api/temp/fetchLabels.ts | 4 +- packages/ozone/src/auth-verifier.ts | 60 ++++--- packages/ozone/src/auth.ts | 147 ------------------ packages/ozone/src/config/config.ts | 2 + packages/ozone/src/config/env.ts | 2 + packages/ozone/src/context.ts | 1 + 21 files changed, 146 insertions(+), 193 deletions(-) create mode 100644 packages/ozone/src/api/proxied.ts delete mode 100644 packages/ozone/src/auth.ts diff --git a/packages/dev-env/src/ozone.ts b/packages/dev-env/src/ozone.ts index 4988a888f61..b14fe308fc2 100644 --- a/packages/dev-env/src/ozone.ts +++ b/packages/dev-env/src/ozone.ts @@ -44,6 +44,9 @@ export class TestOzone { adminPassword: ADMIN_PASSWORD, moderatorPassword: MOD_PASSWORD, triagePassword: TRIAGE_PASSWORD, + adminDids: [], + moderatorDids: [], + triageDids: [], } // 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/ozone/src/api/admin/createCommunicationTemplate.ts b/packages/ozone/src/api/admin/createCommunicationTemplate.ts index 94d0f4325d9..f05db2d71f2 100644 --- a/packages/ozone/src/api/admin/createCommunicationTemplate.ts +++ b/packages/ozone/src/api/admin/createCommunicationTemplate.ts @@ -4,13 +4,13 @@ import AppContext from '../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.createCommunicationTemplate({ - auth: ctx.roleVerifier, + auth: ctx.authVerifier.modOrRole, handler: async ({ input, auth }) => { const access = auth.credentials const db = ctx.db const { createdBy, ...template } = input.body - if (!access.admin) { + if (!access.isAdmin) { throw new AuthRequiredError( 'Must be an admin to create a communication template', ) diff --git a/packages/ozone/src/api/admin/deleteCommunicationTemplate.ts b/packages/ozone/src/api/admin/deleteCommunicationTemplate.ts index cb72bb46216..b70028e710d 100644 --- a/packages/ozone/src/api/admin/deleteCommunicationTemplate.ts +++ b/packages/ozone/src/api/admin/deleteCommunicationTemplate.ts @@ -4,13 +4,13 @@ import AppContext from '../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.deleteCommunicationTemplate({ - auth: ctx.roleVerifier, + auth: ctx.authVerifier.modOrRole, handler: async ({ input, auth }) => { const access = auth.credentials const db = ctx.db const { id } = input.body - if (!access.admin) { + if (!access.isAdmin) { throw new AuthRequiredError( 'Must be an admin to delete a communication template', ) diff --git a/packages/ozone/src/api/admin/emitModerationEvent.ts b/packages/ozone/src/api/admin/emitModerationEvent.ts index ef4c5fd2822..473269bffde 100644 --- a/packages/ozone/src/api/admin/emitModerationEvent.ts +++ b/packages/ozone/src/api/admin/emitModerationEvent.ts @@ -11,7 +11,7 @@ import { ModerationLangService } from '../../mod-service/lang' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.emitModerationEvent({ - auth: ctx.roleVerifier, + auth: ctx.authVerifier.modOrRole, handler: async ({ input, auth }) => { const access = auth.credentials const db = ctx.db @@ -29,7 +29,7 @@ export default function (server: Server, ctx: AppContext) { // if less than moderator access then can only take ack and escalation actions if (isTakedownEvent || isReverseTakedownEvent) { - if (!access.moderator) { + if (!access.isModerator) { throw new AuthRequiredError( 'Must be a full moderator to take this type of action', ) @@ -37,7 +37,7 @@ export default function (server: Server, ctx: AppContext) { // Non admins should not be able to take down feed generators if ( - !access.admin && + !access.isAdmin && subject.recordPath?.includes('app.bsky.feed.generator/') ) { throw new AuthRequiredError( @@ -46,7 +46,7 @@ export default function (server: Server, ctx: AppContext) { } } // if less than moderator access then can not apply labels - if (!access.moderator && isLabelEvent) { + if (!access.isModerator && isLabelEvent) { throw new AuthRequiredError('Must be a full moderator to label content') } diff --git a/packages/ozone/src/api/admin/getModerationEvent.ts b/packages/ozone/src/api/admin/getModerationEvent.ts index e02757c79a3..fc6b433789e 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.roleVerifier, + auth: ctx.authVerifier.modOrRole, 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 fbe9a1229d5..061fc87a0d6 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.roleVerifier, + auth: ctx.authVerifier.modOrRole, handler: async ({ params, auth }) => { const db = ctx.db @@ -22,7 +22,7 @@ export default function (server: Server, ctx: AppContext) { record.repo = addAccountInfoToRepoView( record.repo, accountInfo, - auth.credentials.moderator, + auth.credentials.isModerator, ) return { diff --git a/packages/ozone/src/api/admin/getRepo.ts b/packages/ozone/src/api/admin/getRepo.ts index 5da30f24524..bd0c03c13c6 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.roleVerifier, + auth: ctx.authVerifier.modOrRole, handler: async ({ params, auth }) => { const { did } = params const db = ctx.db @@ -20,7 +20,7 @@ export default function (server: Server, ctx: AppContext) { const repo = addAccountInfoToRepoViewDetail( partialRepo, accountInfo, - auth.credentials.moderator, + auth.credentials.isModerator, ) return { encoding: 'application/json', diff --git a/packages/ozone/src/api/admin/listCommunicationTemplates.ts b/packages/ozone/src/api/admin/listCommunicationTemplates.ts index b2b4d191b39..d8a88947895 100644 --- a/packages/ozone/src/api/admin/listCommunicationTemplates.ts +++ b/packages/ozone/src/api/admin/listCommunicationTemplates.ts @@ -4,12 +4,12 @@ import AppContext from '../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.listCommunicationTemplates({ - auth: ctx.roleVerifier, + auth: ctx.authVerifier.modOrRole, handler: async ({ auth }) => { const access = auth.credentials const db = ctx.db - if (!access.moderator) { + if (!access.isModerator) { throw new AuthRequiredError( 'Must be a full moderator to view list of communication template', ) diff --git a/packages/ozone/src/api/admin/queryModerationEvents.ts b/packages/ozone/src/api/admin/queryModerationEvents.ts index 670cda96cbc..959ee2dcd37 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.roleVerifier, + auth: ctx.authVerifier.modOrRole, handler: async ({ params }) => { const { subject, diff --git a/packages/ozone/src/api/admin/queryModerationStatuses.ts b/packages/ozone/src/api/admin/queryModerationStatuses.ts index fc491339ffa..2c4e0d0dd10 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.roleVerifier, + auth: ctx.authVerifier.modOrRole, handler: async ({ params }) => { const { subject, diff --git a/packages/ozone/src/api/admin/searchRepos.ts b/packages/ozone/src/api/admin/searchRepos.ts index fcdfc1b6d85..6026a5ccdc9 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.roleVerifier, + auth: ctx.authVerifier.modOrRole, 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 3800748fc30..1b1b124e7f9 100644 --- a/packages/ozone/src/api/admin/updateCommunicationTemplate.ts +++ b/packages/ozone/src/api/admin/updateCommunicationTemplate.ts @@ -4,13 +4,13 @@ import AppContext from '../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.updateCommunicationTemplate({ - auth: ctx.roleVerifier, + auth: ctx.authVerifier.modOrRole, handler: async ({ input, auth }) => { const access = auth.credentials const db = ctx.db const { id, updatedBy, ...template } = input.body - if (!access.admin) { + if (!access.isAdmin) { throw new AuthRequiredError( 'Must be an admin to update a communication template', ) diff --git a/packages/ozone/src/api/index.ts b/packages/ozone/src/api/index.ts index 54e52ffe292..c0bb0649f71 100644 --- a/packages/ozone/src/api/index.ts +++ b/packages/ozone/src/api/index.ts @@ -15,6 +15,7 @@ import createCommunicationTemplate from './admin/createCommunicationTemplate' import updateCommunicationTemplate from './admin/updateCommunicationTemplate' import deleteCommunicationTemplate from './admin/deleteCommunicationTemplate' import listCommunicationTemplates from './admin/listCommunicationTemplates' +import proxied from './proxied' export * as health from './health' @@ -36,5 +37,6 @@ export default function (server: Server, ctx: AppContext) { createCommunicationTemplate(server, ctx) updateCommunicationTemplate(server, ctx) deleteCommunicationTemplate(server, ctx) + proxied(server, ctx) return server } diff --git a/packages/ozone/src/api/moderation/createReport.ts b/packages/ozone/src/api/moderation/createReport.ts index e87b957e8d0..3abf5080e7d 100644 --- a/packages/ozone/src/api/moderation/createReport.ts +++ b/packages/ozone/src/api/moderation/createReport.ts @@ -9,10 +9,10 @@ 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.authOptionalAccessOrRoleVerifier, + auth: ctx.authVerifier.standardOptionalOrRole, handler: async ({ input, auth }) => { const requester = - 'did' in auth.credentials ? auth.credentials.did : ctx.cfg.service.did + 'iss' in auth.credentials ? auth.credentials.iss : ctx.cfg.service.did const { reasonType, reason } = input.body const subject = subjectFromInput(input.body.subject) diff --git a/packages/ozone/src/api/proxied.ts b/packages/ozone/src/api/proxied.ts new file mode 100644 index 00000000000..b7eef005650 --- /dev/null +++ b/packages/ozone/src/api/proxied.ts @@ -0,0 +1,74 @@ +import { Server } from '../lexicon' +import AppContext from '../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.actor.getProfile({ + auth: ctx.authVerifier.modOrRole, + handler: async (request) => { + const res = await ctx.appviewAgent.api.app.bsky.actor.getProfile( + request.params, + await ctx.appviewAuth(), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.feed.getAuthorFeed({ + auth: ctx.authVerifier.modOrRole, + handler: async (request) => { + const res = await ctx.appviewAgent.api.app.bsky.feed.getAuthorFeed( + request.params, + await ctx.appviewAuth(), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.feed.getPostThread({ + auth: ctx.authVerifier.modOrRole, + handler: async (request) => { + const res = await ctx.appviewAgent.api.app.bsky.feed.getPostThread( + request.params, + await ctx.appviewAuth(), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.graph.getFollows({ + auth: ctx.authVerifier.modOrRole, + handler: async (request) => { + const res = await ctx.appviewAgent.api.app.bsky.graph.getFollows( + request.params, + await ctx.appviewAuth(), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.graph.getFollowers({ + auth: ctx.authVerifier.modOrRole, + handler: async (request) => { + const res = await ctx.appviewAgent.api.app.bsky.graph.getFollowers( + request.params, + await ctx.appviewAuth(), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/ozone/src/api/temp/fetchLabels.ts b/packages/ozone/src/api/temp/fetchLabels.ts index fd0331487d1..890fafdf95c 100644 --- a/packages/ozone/src/api/temp/fetchLabels.ts +++ b/packages/ozone/src/api/temp/fetchLabels.ts @@ -8,13 +8,13 @@ import { export default function (server: Server, ctx: AppContext) { server.com.atproto.temp.fetchLabels({ - auth: ctx.authOptionalAccessOrRoleVerifier, + auth: ctx.authVerifier.standardOptionalOrRole, handler: async ({ auth, params }) => { const { limit } = params const since = params.since !== undefined ? new Date(params.since).toISOString() : '' const includeUnspeccedTakedowns = - auth.credentials.type === 'role' && auth.credentials.admin + auth.credentials.type === 'null' ? false : auth.credentials.isAdmin const labelRes = await ctx.db.db .selectFrom('label') .selectAll() diff --git a/packages/ozone/src/auth-verifier.ts b/packages/ozone/src/auth-verifier.ts index a2fe5955f51..8ea3c58c59c 100644 --- a/packages/ozone/src/auth-verifier.ts +++ b/packages/ozone/src/auth-verifier.ts @@ -10,6 +10,7 @@ type ReqCtx = { type RoleOutput = { credentials: { type: 'role' + isAdmin: boolean isModerator: boolean isTriage: true } @@ -20,6 +21,7 @@ type ModeratorOutput = { type: 'moderator' aud: string iss: string + isAdmin: boolean isModerator: boolean isTriage: true } @@ -30,6 +32,7 @@ type StandardOutput = { type: 'standard' aud: string iss: string + isAdmin: boolean isModerator: boolean isTriage: boolean } @@ -44,6 +47,7 @@ type NullOutput = { export type AuthVerifierOpts = { serviceDid: string + admins: string[] moderators: string[] triage: string[] adminPassword: string @@ -53,6 +57,7 @@ export type AuthVerifierOpts = { export class AuthVerifier { serviceDid: string + admins: string[] moderators: string[] triage: string[] private adminPassword: string @@ -61,6 +66,7 @@ export class AuthVerifier { constructor(public idResolver: IdResolver, opts: AuthVerifierOpts) { this.serviceDid = opts.serviceDid + this.admins = opts.admins this.moderators = opts.moderators this.triage = opts.triage this.adminPassword = opts.adminPassword @@ -108,13 +114,15 @@ export class AuthVerifier { } const payload = await verifyJwt(jwtStr, this.serviceDid, getSigningKey) const iss = payload.iss - const isModerator = this.moderators.includes(iss) + const isAdmin = this.admins.includes(iss) + const isModerator = isAdmin || this.moderators.includes(iss) const isTriage = isModerator || this.triage.includes(iss) return { credentials: { type: 'standard', iss, aud: payload.aud, + isAdmin, isModerator, isTriage, }, @@ -130,34 +138,38 @@ export class AuthVerifier { return this.nullCreds() } + standardOptionalOrRole = async ( + reqCtx: ReqCtx, + ): Promise => { + if (isBearerToken(reqCtx.req)) { + return this.standard(reqCtx) + } else if (isBasicToken(reqCtx.req)) { + return this.role(reqCtx) + } else { + return this.nullCreds() + } + } + role = async (reqCtx: ReqCtx): Promise => { const parsed = parseBasicAuth(reqCtx.req.headers.authorization ?? '') const { username, password } = parsed ?? {} - if (username === 'admin') { + if (username !== 'admin') { throw new AuthRequiredError() } - if (password === this.triagePassword) { - return { - credentials: { - type: 'role', - isModerator: false, - isTriage: true, - }, - } - } else if ( - password === this.moderatorPassword || - password === this.adminPassword - ) { - return { - credentials: { - type: 'role', - isModerator: true, - isTriage: true, - }, - } - } else { + const isAdmin = password === this.adminPassword + const isModerator = isAdmin || password === this.moderatorPassword + const isTriage = isModerator || password === this.triagePassword + if (!isTriage) { throw new AuthRequiredError() } + return { + credentials: { + type: 'role', + isAdmin, + isModerator, + isTriage, + }, + } } nullCreds(): NullOutput { @@ -177,6 +189,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 +} + export const getJwtStrFromReq = (req: express.Request): string | null => { const { authorization } = req.headers if (!authorization?.startsWith(BEARER)) { diff --git a/packages/ozone/src/auth.ts b/packages/ozone/src/auth.ts deleted file mode 100644 index e996f068c49..00000000000 --- a/packages/ozone/src/auth.ts +++ /dev/null @@ -1,147 +0,0 @@ -import express from 'express' -import * as uint8arrays from 'uint8arrays' -import { AuthRequiredError, verifyJwt } from '@atproto/xrpc-server' -import { IdResolver } from '@atproto/identity' -import { OzoneSecrets } from './config' - -const BASIC = 'Basic ' -const BEARER = 'Bearer ' - -export const authVerifier = ( - idResolver: IdResolver, - opts: { aud: string | null }, -) => { - const getSigningKey = async ( - did: string, - forceRefresh: boolean, - ): Promise => { - const atprotoData = await idResolver.did.resolveAtprotoData( - did, - forceRefresh, - ) - return atprotoData.signingKey - } - - return async (reqCtx: { req: express.Request; res: express.Response }) => { - const jwtStr = getJwtStrFromReq(reqCtx.req) - if (!jwtStr) { - throw new AuthRequiredError('missing jwt', 'MissingJwt') - } - const payload = await verifyJwt(jwtStr, opts.aud, getSigningKey) - return { credentials: { did: payload.iss }, artifacts: { aud: opts.aud } } - } -} - -export const authOptionalVerifier = ( - idResolver: IdResolver, - opts: { aud: string | null }, -) => { - const verifyAccess = authVerifier(idResolver, opts) - return async (reqCtx: { req: express.Request; res: express.Response }) => { - if (!reqCtx.req.headers.authorization) { - return { credentials: { did: null } } - } - return verifyAccess(reqCtx) - } -} - -export const authOptionalAccessOrRoleVerifier = ( - idResolver: IdResolver, - secrets: OzoneSecrets, - serverDid: string, -) => { - const verifyAccess = authVerifier(idResolver, { aud: serverDid }) - const verifyRole = roleVerifier(secrets) - return async (ctx: { req: express.Request; res: express.Response }) => { - const defaultUnAuthorizedCredentials = { - credentials: { did: null, type: 'unauthed' as const }, - } - if (!ctx.req.headers.authorization) { - return defaultUnAuthorizedCredentials - } - // For non-admin tokens, we don't want to consider alternative verifiers and let it fail if it fails - const isRoleAuthToken = ctx.req.headers.authorization?.startsWith(BASIC) - if (isRoleAuthToken) { - const result = await verifyRole(ctx) - return { - ...result, - credentials: { - type: 'role' as const, - ...result.credentials, - }, - } - } - const result = await verifyAccess(ctx) - return { - ...result, - credentials: { - type: 'access' as const, - ...result.credentials, - }, - } - } -} - -export const roleVerifier = - (secrets: OzoneSecrets) => - async (reqCtx: { req: express.Request; res: express.Response }) => { - const credentials = getRoleCredentials(secrets, reqCtx.req) - if (!credentials.valid) { - throw new AuthRequiredError() - } - return { credentials } - } - -export const getRoleCredentials = ( - secrets: OzoneSecrets, - req: express.Request, -) => { - const parsed = parseBasicAuth(req.headers.authorization || '') - const { username, password } = parsed ?? {} - if (username === 'admin' && password === secrets.triagePassword) { - return { valid: true, admin: false, moderator: false, triage: true } - } - if (username === 'admin' && password === secrets.moderatorPassword) { - return { valid: true, admin: false, moderator: true, triage: true } - } - if (username === 'admin' && password === secrets.adminPassword) { - return { valid: true, admin: true, moderator: true, triage: true } - } - return { valid: false, admin: false, moderator: false, triage: false } -} - -export const parseBasicAuth = ( - token: string, -): { username: string; password: string } | null => { - if (!token.startsWith(BASIC)) return null - const b64 = token.slice(BASIC.length) - let parsed: string[] - try { - parsed = uint8arrays - .toString(uint8arrays.fromString(b64, 'base64pad'), 'utf8') - .split(':') - } catch (err) { - return null - } - const [username, password] = parsed - if (!username || !password) return null - return { username, password } -} - -export const buildBasicAuth = (username: string, password: string): string => { - return ( - BASIC + - uint8arrays.toString( - uint8arrays.fromString(`${username}:${password}`, 'utf8'), - 'base64pad', - ) - ) -} - -export const getJwtStrFromReq = (req: express.Request): string | null => { - const { authorization } = req.headers - if (!authorization?.startsWith(BEARER)) { - return null - } - return authorization.slice(BEARER.length).trim() -} diff --git a/packages/ozone/src/config/config.ts b/packages/ozone/src/config/config.ts index 3f9c38f96c8..e9ffc942b99 100644 --- a/packages/ozone/src/config/config.ts +++ b/packages/ozone/src/config/config.ts @@ -44,6 +44,7 @@ export const envToCfg = (env: OzoneEnvironment): OzoneConfig => { } const accessCfg: OzoneConfig['access'] = { + admins: env.adminDids, moderators: env.moderatorDids, triage: env.triageDids, } @@ -97,6 +98,7 @@ export type IdentityConfig = { } export type AccessConfig = { + admins: string[] moderators: string[] triage: string[] } diff --git a/packages/ozone/src/config/env.ts b/packages/ozone/src/config/env.ts index fcb6eb31d26..c0e99a4625b 100644 --- a/packages/ozone/src/config/env.ts +++ b/packages/ozone/src/config/env.ts @@ -17,6 +17,7 @@ export const readEnv = (): OzoneEnvironment => { dbPoolMaxUses: envInt('OZONE_DB_POOL_MAX_USES'), dbPoolIdleTimeoutMs: envInt('OZONE_DB_POOL_IDLE_TIMEOUT_MS'), didPlcUrl: envStr('OZONE_DID_PLC_URL'), + adminDids: envList('OZONE_ADMIN_DIDS'), moderatorDids: envList('OZONE_MODERATOR_DIDS'), triageDids: envList('OZONE_TRIAGE_DIDS'), adminPassword: envStr('OZONE_ADMIN_PASSWORD'), @@ -42,6 +43,7 @@ export type OzoneEnvironment = { dbPoolMaxUses?: number dbPoolIdleTimeoutMs?: number didPlcUrl?: string + adminDids: string[] moderatorDids: string[] triageDids: string[] adminPassword?: string diff --git a/packages/ozone/src/context.ts b/packages/ozone/src/context.ts index 726a500ea38..75de011b4bb 100644 --- a/packages/ozone/src/context.ts +++ b/packages/ozone/src/context.ts @@ -84,6 +84,7 @@ export class AppContext { const authVerifier = new AuthVerifier(idResolver, { serviceDid: cfg.service.did, + admins: cfg.access.admins, moderators: cfg.access.moderators, triage: cfg.access.triage, adminPassword: secrets.adminPassword, From 32b3de8556fb1641fff12ada398da913c5ce171f Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 12:38:25 -0600 Subject: [PATCH 04/50] handle mod signing keys --- .../api/com/atproto/admin/getAccountInfos.ts | 2 +- .../api/com/atproto/admin/getSubjectStatus.ts | 2 +- .../com/atproto/admin/updateSubjectStatus.ts | 2 +- packages/bsky/src/auth-verifier.ts | 39 ++++++++++++++----- packages/common-web/src/did-doc.ts | 21 ++++++++-- .../api/com/atproto/admin/getAccountInfo.ts | 2 +- .../api/com/atproto/admin/getSubjectStatus.ts | 2 +- .../com/atproto/admin/updateSubjectStatus.ts | 2 +- packages/pds/src/auth-verifier.ts | 37 +++++++++++------- packages/xrpc-server/src/auth.ts | 2 +- 10 files changed, 78 insertions(+), 33 deletions(-) diff --git a/packages/bsky/src/api/com/atproto/admin/getAccountInfos.ts b/packages/bsky/src/api/com/atproto/admin/getAccountInfos.ts index e01be2d6383..cee84d53177 100644 --- a/packages/bsky/src/api/com/atproto/admin/getAccountInfos.ts +++ b/packages/bsky/src/api/com/atproto/admin/getAccountInfos.ts @@ -5,7 +5,7 @@ import { INVALID_HANDLE } from '@atproto/syntax' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getAccountInfos({ - auth: ctx.authVerifier.roleOrAdminService, + auth: ctx.authVerifier.roleOrModService, handler: async ({ params }) => { const { dids } = params const actors = await ctx.hydrator.actor.getActors(dids, true) diff --git a/packages/bsky/src/api/com/atproto/admin/getSubjectStatus.ts b/packages/bsky/src/api/com/atproto/admin/getSubjectStatus.ts index 2ca7bcdc2c9..3be4c1c0185 100644 --- a/packages/bsky/src/api/com/atproto/admin/getSubjectStatus.ts +++ b/packages/bsky/src/api/com/atproto/admin/getSubjectStatus.ts @@ -5,7 +5,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.roleOrAdminService, + auth: ctx.authVerifier.roleOrModService, handler: async ({ params }) => { const { did, uri, blob } = params diff --git a/packages/bsky/src/api/com/atproto/admin/updateSubjectStatus.ts b/packages/bsky/src/api/com/atproto/admin/updateSubjectStatus.ts index bb78832aa93..8256efbe7e5 100644 --- a/packages/bsky/src/api/com/atproto/admin/updateSubjectStatus.ts +++ b/packages/bsky/src/api/com/atproto/admin/updateSubjectStatus.ts @@ -10,7 +10,7 @@ import { isMain as isStrongRef } from '../../../../lexicon/types/com/atproto/rep export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.updateSubjectStatus({ - auth: ctx.authVerifier.roleOrAdminService, + auth: ctx.authVerifier.roleOrModService, handler: async ({ input, auth }) => { const { canPerformTakedown } = ctx.authVerifier.parseCreds(auth) if (!canPerformTakedown) { diff --git a/packages/bsky/src/auth-verifier.ts b/packages/bsky/src/auth-verifier.ts index 69335772390..02d89680636 100644 --- a/packages/bsky/src/auth-verifier.ts +++ b/packages/bsky/src/auth-verifier.ts @@ -83,13 +83,21 @@ export class AuthVerifier { if (!this.parseRoleCreds(ctx.req).admin) { throw new AuthRequiredError('bad credentials') } - return { credentials: { type: 'standard', iss, aud } } + return { + credentials: { type: 'standard', iss, aud }, + } } const { iss, aud } = await this.verifyServiceJwt(ctx, { aud: this.ownDid, iss: null, }) - return { credentials: { type: 'standard', iss, aud } } + return { + credentials: { + type: 'standard', + iss, + aud, + }, + } } standardOptional = async ( @@ -159,19 +167,19 @@ export class AuthVerifier { } } - adminService = async (reqCtx: ReqCtx): Promise => { + modService = async (reqCtx: ReqCtx): Promise => { const { iss, aud } = await this.verifyServiceJwt(reqCtx, { aud: this.ownDid, - iss: [this.modServiceDid], + iss: [this.modServiceDid, `${this.modServiceDid}#atproto-mod`], }) return { credentials: { type: 'mod_service', aud, iss } } } - roleOrAdminService = async ( + roleOrModService = async ( reqCtx: ReqCtx, ): Promise => { if (isBearerToken(reqCtx.req)) { - return this.adminService(reqCtx) + return this.modService(reqCtx) } else { return this.role(reqCtx) } @@ -195,12 +203,13 @@ export class AuthVerifier { opts: { aud: string | null; iss: string[] | null }, ) { const getSigningKey = async ( - did: string, + iss: string, _forceRefresh: boolean, // @TODO consider propagating to dataplane ): Promise => { - if (opts.iss !== null && !opts.iss.includes(did)) { + if (opts.iss !== null && !opts.iss.includes(iss)) { throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss') } + const [did, serviceId] = iss.split('#') let identity: GetIdentityByDidResponse try { identity = await this.dataplane.getIdentityByDid({ did }) @@ -211,7 +220,8 @@ export class AuthVerifier { throw err } const keys = unpackIdentityKeys(identity.keys) - const didKey = getKeyAsDidKey(keys, { id: 'atproto' }) + const keyId = serviceId === 'atproto-mod' ? 'atproto-mod-key' : 'atproto' + const didKey = getKeyAsDidKey(keys, { id: keyId }) if (!didKey) { throw new AuthRequiredError('missing or bad key') } @@ -226,6 +236,12 @@ export class AuthVerifier { return { iss: payload.iss, aud: payload.aud } } + isModService(iss: string): boolean { + return [this.modServiceDid, `${this.modServiceDid}#atproto-mod`].includes( + iss, + ) + } + nullCreds(): NullOutput { return { credentials: { @@ -242,10 +258,13 @@ export class AuthVerifier { creds.credentials.type === 'standard' ? creds.credentials.iss : null const canViewTakedowns = (creds.credentials.type === 'role' && creds.credentials.admin) || - creds.credentials.type === 'mod_service' + creds.credentials.type === 'mod_service' || + (creds.credentials.type === 'standard' && + this.isModService(creds.credentials.iss)) const canPerformTakedown = (creds.credentials.type === 'role' && creds.credentials.admin) || creds.credentials.type === 'mod_service' + return { viewer, canViewTakedowns, diff --git a/packages/common-web/src/did-doc.ts b/packages/common-web/src/did-doc.ts index 541e10d0937..1b84c1787a4 100644 --- a/packages/common-web/src/did-doc.ts +++ b/packages/common-web/src/did-doc.ts @@ -27,6 +27,13 @@ export const getHandle = (doc: DidDocument): string | undefined => { // @NOTE we parse to type/publicKeyMultibase to avoid the dependency on @atproto/crypto export const getSigningKey = ( doc: DidDocument, +): { type: string; publicKeyMultibase: string } | undefined => { + return getVerificationMaterial(doc, 'atproto') +} + +export const getVerificationMaterial = ( + doc: DidDocument, + keyId: string, ): { type: string; publicKeyMultibase: string } | undefined => { const did = getDid(doc) let keys = doc.verificationMethod @@ -36,7 +43,7 @@ export const getSigningKey = ( keys = [keys] } const found = keys.find( - (key) => key.id === '#atproto' || key.id === `${did}#atproto`, + (key) => key.id === `#${keyId}` || key.id === `${did}#${keyId}`, ) if (!found?.publicKeyMultibase) return undefined return { @@ -44,12 +51,20 @@ export const getSigningKey = ( publicKeyMultibase: found.publicKeyMultibase, } } -export const getSigningDidKey = (doc: DidDocument): string | undefined => { - const parsed = getSigningKey(doc) + +export const getDidKeyForId = ( + doc: DidDocument, + keyId: string, +): string | undefined => { + const parsed = getVerificationMaterial(doc, keyId) if (!parsed) return return `did:key:${parsed.publicKeyMultibase}` } +export const getSigningDidKey = (doc: DidDocument): string | undefined => { + return getDidKeyForId(doc, 'atproto') +} + export const getPdsEndpoint = (doc: DidDocument): string | undefined => { return getServiceEndpoint(doc, { id: '#atproto_pds', diff --git a/packages/pds/src/api/com/atproto/admin/getAccountInfo.ts b/packages/pds/src/api/com/atproto/admin/getAccountInfo.ts index e258f9714b2..7269bc0dfa9 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.roleOrAdminService, + auth: ctx.authVerifier.roleOrModService, 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/getSubjectStatus.ts b/packages/pds/src/api/com/atproto/admin/getSubjectStatus.ts index 767714cec36..505e3dde4e9 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.roleOrAdminService, + auth: ctx.authVerifier.roleOrModService, handler: async ({ params }) => { const { did, uri, blob } = params let body: OutputSchema | null = null diff --git a/packages/pds/src/api/com/atproto/admin/updateSubjectStatus.ts b/packages/pds/src/api/com/atproto/admin/updateSubjectStatus.ts index 29991da2b2c..018d447d6eb 100644 --- a/packages/pds/src/api/com/atproto/admin/updateSubjectStatus.ts +++ b/packages/pds/src/api/com/atproto/admin/updateSubjectStatus.ts @@ -11,7 +11,7 @@ import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.updateSubjectStatus({ - auth: ctx.authVerifier.roleOrAdminService, + 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) { diff --git a/packages/pds/src/auth-verifier.ts b/packages/pds/src/auth-verifier.ts index 668791c187f..410b254d5b6 100644 --- a/packages/pds/src/auth-verifier.ts +++ b/packages/pds/src/auth-verifier.ts @@ -12,6 +12,7 @@ import * as jose from 'jose' import KeyEncoder from 'key-encoder' import { AccountManager } from './account-manager' import { softDeleted } from './db' +import { getDidKeyForId } from '@atproto/common' type ReqCtx = { req: express.Request @@ -44,9 +45,9 @@ type RoleOutput = { } } -type AdminServiceOutput = { +type ModServiceOutput = { credentials: { - type: 'service' + type: 'mod_service' aud: string iss: string } @@ -97,7 +98,7 @@ export type AuthVerifierOpts = { dids: { pds: string entryway?: string - admin?: string + modService?: string } } @@ -253,28 +254,28 @@ export class AuthVerifier { } } - adminService = async (reqCtx: ReqCtx): Promise => { - if (!this.dids.admin) { + modService = async (reqCtx: ReqCtx): Promise => { + if (!this.dids.modService) { throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss') } const payload = await this.verifyServiceJwt(reqCtx, { aud: this.dids.entryway ?? this.dids.pds, - iss: [this.dids.admin], + iss: [this.dids.modService, `${this.dids.modService}#atproto-mod`], }) return { credentials: { - type: 'service', + type: 'mod_service', aud: payload.aud, iss: payload.iss, }, } } - roleOrAdminService = async ( + roleOrModService = async ( reqCtx: ReqCtx, - ): Promise => { + ): Promise => { if (isBearerToken(reqCtx.req)) { - return this.adminService(reqCtx) + return this.modService(reqCtx) } else { return this.role(reqCtx) } @@ -337,13 +338,23 @@ export class AuthVerifier { opts: { aud: string | null; iss: string[] | null }, ) { const getSigningKey = async ( - did: string, + iss: string, forceRefresh: boolean, ): Promise => { - if (opts.iss !== null && !opts.iss.includes(did)) { + if (opts.iss !== null && !opts.iss.includes(iss)) { throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss') } - return this.idResolver.did.resolveAtprotoKey(did, forceRefresh) + const [did, serviceId] = iss.split('#') + const didDoc = await this.idResolver.did.resolve(did, forceRefresh) + if (!didDoc) { + throw new AuthRequiredError('could not resolve iss did') + } + const keyId = serviceId === 'atproto-mod' ? 'atproto-mod-key' : 'atproto' + const didKey = getDidKeyForId(didDoc, keyId) + if (!didKey) { + throw new AuthRequiredError('missing or bad key in did doc') + } + return didKey } const jwtStr = bearerTokenFromReq(reqCtx.req) diff --git a/packages/xrpc-server/src/auth.ts b/packages/xrpc-server/src/auth.ts index db6471aa23e..32373248f51 100644 --- a/packages/xrpc-server/src/auth.ts +++ b/packages/xrpc-server/src/auth.ts @@ -48,7 +48,7 @@ const jsonToB64Url = (json: Record): string => { export const verifyJwt = async ( jwtStr: string, ownDid: string | null, // null indicates to skip the audience check - getSigningKey: (did: string, forceRefresh: boolean) => Promise, + getSigningKey: (iss: string, forceRefresh: boolean) => Promise, ): Promise => { const parts = jwtStr.split('.') if (parts.length !== 3) { From b1f07d54471d11902c6d52edbc5a2005de606032 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 13:18:56 -0600 Subject: [PATCH 05/50] add client proxy heads to pds --- packages/common-web/src/did-doc.ts | 4 +- .../pds/src/api/app/bsky/actor/getProfile.ts | 13 ++--- .../pds/src/api/app/bsky/actor/getProfiles.ts | 9 +--- .../src/api/app/bsky/actor/getSuggestions.ts | 7 +-- .../src/api/app/bsky/actor/searchActors.ts | 7 +-- .../app/bsky/actor/searchActorsTypeahead.ts | 7 +-- .../src/api/app/bsky/feed/getActorFeeds.ts | 7 +-- .../src/api/app/bsky/feed/getActorLikes.ts | 13 ++--- .../src/api/app/bsky/feed/getAuthorFeed.ts | 13 ++--- packages/pds/src/api/app/bsky/feed/getFeed.ts | 13 ++--- packages/pds/src/pipethrough.ts | 47 +++++++++++++++---- 11 files changed, 72 insertions(+), 68 deletions(-) diff --git a/packages/common-web/src/did-doc.ts b/packages/common-web/src/did-doc.ts index 541e10d0937..29699ec2d1c 100644 --- a/packages/common-web/src/did-doc.ts +++ b/packages/common-web/src/did-doc.ts @@ -73,7 +73,7 @@ export const getNotifEndpoint = (doc: DidDocument): string | undefined => { export const getServiceEndpoint = ( doc: DidDocument, - opts: { id: string; type: string }, + opts: { id: string; type?: string }, ) => { const did = getDid(doc) let services = doc.service @@ -86,7 +86,7 @@ export const getServiceEndpoint = ( (service) => service.id === opts.id || service.id === `${did}${opts.id}`, ) if (!found) return undefined - if (found.type !== opts.type) { + if (opts.type && found.type !== opts.type) { return undefined } if (typeof found.serviceEndpoint !== 'string') { diff --git a/packages/pds/src/api/app/bsky/actor/getProfile.ts b/packages/pds/src/api/app/bsky/actor/getProfile.ts index 74de7f3af6d..a63e2a556d7 100644 --- a/packages/pds/src/api/app/bsky/actor/getProfile.ts +++ b/packages/pds/src/api/app/bsky/actor/getProfile.ts @@ -1,6 +1,5 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' import { OutputSchema } from '../../../../lexicon/types/app/bsky/actor/getProfile' import { LocalViewer, @@ -15,16 +14,10 @@ export default function (server: Server, ctx: AppContext) { const { bskyAppView } = ctx.cfg if (!bskyAppView) return server.app.bsky.actor.getProfile({ - auth: ctx.authVerifier.accessOrRole, + auth: ctx.authVerifier.access, handler: async ({ req, auth, params }) => { - const requester = - auth.credentials.type === 'access' ? auth.credentials.did : null - const res = await pipethrough( - bskyAppView.url, - METHOD_NSID, - params, - requester ? await ctx.appviewAuthHeaders(requester) : authPassthru(req), - ) + const requester = auth.credentials.did + const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) if (!requester) { return res } diff --git a/packages/pds/src/api/app/bsky/actor/getProfiles.ts b/packages/pds/src/api/app/bsky/actor/getProfiles.ts index 3ab9338c7c5..67a304280f8 100644 --- a/packages/pds/src/api/app/bsky/actor/getProfiles.ts +++ b/packages/pds/src/api/app/bsky/actor/getProfiles.ts @@ -15,15 +15,10 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.actor.getProfiles({ auth: ctx.authVerifier.access, - handler: async ({ auth, params }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did - const res = await pipethrough( - bskyAppView.url, - METHOD_NSID, - params, - await ctx.appviewAuthHeaders(requester), - ) + const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) return handleReadAfterWrite( ctx, METHOD_NSID, diff --git a/packages/pds/src/api/app/bsky/actor/getSuggestions.ts b/packages/pds/src/api/app/bsky/actor/getSuggestions.ts index 6bfd65adf74..d085dd097bd 100644 --- a/packages/pds/src/api/app/bsky/actor/getSuggestions.ts +++ b/packages/pds/src/api/app/bsky/actor/getSuggestions.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.actor.getSuggestions({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.actor.getSuggestions', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/actor/searchActors.ts b/packages/pds/src/api/app/bsky/actor/searchActors.ts index 53c97566818..fd3c2cd731b 100644 --- a/packages/pds/src/api/app/bsky/actor/searchActors.ts +++ b/packages/pds/src/api/app/bsky/actor/searchActors.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.actor.searchActors({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.actor.searchActors', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts index 51e778b24ee..fbbb911e3bd 100644 --- a/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.actor.searchActorsTypeahead({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.actor.searchActorsTypeahead', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts b/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts index 123bce1785b..1bd9fd4fd17 100644 --- a/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts +++ b/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getActorFeeds({ auth: ctx.authVerifier.access, - handler: async ({ auth, params }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.feed.getActorFeeds', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/feed/getActorLikes.ts b/packages/pds/src/api/app/bsky/feed/getActorLikes.ts index d8e2f839904..9cc5223937d 100644 --- a/packages/pds/src/api/app/bsky/feed/getActorLikes.ts +++ b/packages/pds/src/api/app/bsky/feed/getActorLikes.ts @@ -1,6 +1,5 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' import { OutputSchema } from '../../../../lexicon/types/app/bsky/feed/getAuthorFeed' import { LocalViewer, @@ -15,16 +14,10 @@ export default function (server: Server, ctx: AppContext) { const { bskyAppView } = ctx.cfg if (!bskyAppView) return server.app.bsky.feed.getActorLikes({ - auth: ctx.authVerifier.accessOrRole, + auth: ctx.authVerifier.access, handler: async ({ req, params, auth }) => { - const requester = - auth.credentials.type === 'access' ? auth.credentials.did : null - const res = await pipethrough( - bskyAppView.url, - METHOD_NSID, - params, - requester ? await ctx.appviewAuthHeaders(requester) : authPassthru(req), - ) + const requester = auth.credentials.did + const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) if (!requester) { return res diff --git a/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts index c90760bbfd9..2296fa2221c 100644 --- a/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts @@ -1,6 +1,5 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' import { OutputSchema } from '../../../../lexicon/types/app/bsky/feed/getAuthorFeed' import { isReasonRepost } from '../../../../lexicon/types/app/bsky/feed/defs' import { @@ -16,16 +15,10 @@ export default function (server: Server, ctx: AppContext) { const { bskyAppView } = ctx.cfg if (!bskyAppView) return server.app.bsky.feed.getAuthorFeed({ - auth: ctx.authVerifier.accessOrRole, + auth: ctx.authVerifier.access, handler: async ({ req, params, auth }) => { - const requester = - auth.credentials.type === 'access' ? auth.credentials.did : null - const res = await pipethrough( - bskyAppView.url, - METHOD_NSID, - params, - requester ? await ctx.appviewAuthHeaders(requester) : authPassthru(req), - ) + const requester = auth.credentials.did + const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) if (!requester) { return res } diff --git a/packages/pds/src/api/app/bsky/feed/getFeed.ts b/packages/pds/src/api/app/bsky/feed/getFeed.ts index 89c84b18ba8..a785304e4b0 100644 --- a/packages/pds/src/api/app/bsky/feed/getFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getFeed.ts @@ -16,18 +16,13 @@ export default function (server: Server, ctx: AppContext) { { feed: params.feed }, await ctx.appviewAuthHeaders(requester), ) - const serviceAuthHeaders = await ctx.serviceAuthHeaders( - requester, - feed.view.did, - ) - // forward accept-language header to upstream services - serviceAuthHeaders.headers['accept-language'] = - req.headers['accept-language'] return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.feed.getFeed', params, - serviceAuthHeaders, + requester, + feed.view.did, ) }, }) diff --git a/packages/pds/src/pipethrough.ts b/packages/pds/src/pipethrough.ts index a4c11856502..a8123cbbb0a 100644 --- a/packages/pds/src/pipethrough.ts +++ b/packages/pds/src/pipethrough.ts @@ -1,22 +1,35 @@ +import express from 'express' import * as ui8 from 'uint8arrays' import { jsonToLex } from '@atproto/lexicon' -import { HandlerPipeThrough } from '@atproto/xrpc-server' -import { CallOptions, ResponseType, XRPCError } from '@atproto/xrpc' +import { HandlerPipeThrough, InvalidRequestError } from '@atproto/xrpc-server' +import { ResponseType, XRPCError } from '@atproto/xrpc' import { lexicons } from './lexicon/lexicons' import { httpLogger } from './logger' -import { noUndefinedVals } from '@atproto/common' +import { getServiceEndpoint, noUndefinedVals } from '@atproto/common' +import AppContext from './context' export const pipethrough = async ( - serviceUrl: string, + ctx: AppContext, + req: express.Request, nsid: string, params: Record, - opts?: CallOptions, + requester: string, + audOverride?: string, ): Promise => { + const proxyTo = await parseProxyHeader(ctx, req) + const serviceUrl = proxyTo?.serviceUrl ?? ctx.cfg.bskyAppView?.url + const aud = audOverride ?? proxyTo?.did ?? ctx.cfg.bskyAppView?.did + if (!serviceUrl || !aud) { + throw new InvalidRequestError(`No service configured for ${nsid}`) + } + const reqHeaders = await ctx.serviceAuthHeaders(requester, aud) + // forward accept-language header to upstream services + reqHeaders.headers['accept-language'] = req.headers['accept-language'] const url = constructUrl(serviceUrl, nsid, params) let res: Response let buffer: ArrayBuffer try { - res = await fetch(url, opts) + res = await fetch(url, reqHeaders) buffer = await res.arrayBuffer() } catch (err) { httpLogger.warn({ err }, 'pipethrough network error') @@ -35,11 +48,29 @@ export const pipethrough = async ( const encoding = res.headers.get('content-type') ?? 'application/json' const repoRevHeader = res.headers.get('atproto-repo-rev') const contentLanguage = res.headers.get('content-language') - const headers = noUndefinedVals({ + const resHeaders = noUndefinedVals({ ['atproto-repo-rev']: repoRevHeader ?? undefined, ['content-language']: contentLanguage ?? undefined, }) - return { encoding, buffer, headers } + return { encoding, buffer, headers: resHeaders } +} + +export const parseProxyHeader = async ( + ctx: AppContext, + req: express.Request, +): Promise<{ did: string; serviceUrl: string } | undefined> => { + const proxyTo = req.header('atproto-proxy') + if (!proxyTo) return + const [did, serviceId] = proxyTo.split('#') + const didDoc = await ctx.idResolver.did.resolve(did) + if (!didDoc) { + throw new InvalidRequestError('could not resolve proxy did') + } + const serviceUrl = getServiceEndpoint(didDoc, { id: `#${serviceId}` }) + if (!serviceUrl) { + throw new InvalidRequestError('could not resolve proxy did service url') + } + return { did, serviceUrl } } export const constructUrl = ( From ef236a40ab403724c4fc90616efeead745f27b28 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 13:26:52 -0600 Subject: [PATCH 06/50] hook up rest of routes --- .../src/api/app/bsky/feed/getFeedGenerator.ts | 7 +++--- .../api/app/bsky/feed/getFeedGenerators.ts | 7 +++--- .../pds/src/api/app/bsky/feed/getLikes.ts | 9 ++------ .../pds/src/api/app/bsky/feed/getListFeed.ts | 7 +++--- .../src/api/app/bsky/feed/getPostThread.ts | 22 +++---------------- .../pds/src/api/app/bsky/feed/getPosts.ts | 9 ++------ .../src/api/app/bsky/feed/getRepostedBy.ts | 7 +++--- .../api/app/bsky/feed/getSuggestedFeeds.ts | 7 +++--- .../pds/src/api/app/bsky/feed/getTimeline.ts | 9 ++------ .../pds/src/api/app/bsky/feed/searchPosts.ts | 7 +++--- .../pds/src/api/app/bsky/graph/getBlocks.ts | 7 +++--- .../src/api/app/bsky/graph/getFollowers.ts | 11 +++++----- .../pds/src/api/app/bsky/graph/getFollows.ts | 11 +++++----- .../pds/src/api/app/bsky/graph/getList.ts | 9 ++------ .../src/api/app/bsky/graph/getListBlocks.ts | 7 +++--- .../src/api/app/bsky/graph/getListMutes.ts | 7 +++--- .../pds/src/api/app/bsky/graph/getLists.ts | 9 ++------ .../pds/src/api/app/bsky/graph/getMutes.ts | 9 ++------ .../bsky/graph/getSuggestedFollowsByActor.ts | 7 +++--- .../app/bsky/notification/getUnreadCount.ts | 7 +++--- .../bsky/notification/listNotifications.ts | 7 +++--- .../unspecced/getPopularFeedGenerators.ts | 7 +++--- .../bsky/unspecced/getTaggedSuggestions.ts | 7 +++--- .../pds/src/api/com/atproto/repo/getRecord.ts | 8 ++----- packages/pds/src/pipethrough.ts | 6 +++-- 25 files changed, 87 insertions(+), 123 deletions(-) diff --git a/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts b/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts index f71ea74117f..e46eff5cf40 100644 --- a/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts +++ b/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getFeedGenerator({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.feed.getFeedGenerator', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts b/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts index c07c3dac228..828f6cc204d 100644 --- a/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts +++ b/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getFeedGenerators({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.feed.getFeedGenerators', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/feed/getLikes.ts b/packages/pds/src/api/app/bsky/feed/getLikes.ts index 90a96681c85..263864fb286 100644 --- a/packages/pds/src/api/app/bsky/feed/getLikes.ts +++ b/packages/pds/src/api/app/bsky/feed/getLikes.ts @@ -7,14 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getLikes({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - return pipethrough( - bskyAppView.url, - 'app.bsky.feed.getLikes', - params, - await ctx.appviewAuthHeaders(requester), - ) + return pipethrough(ctx, req, 'app.bsky.feed.getLikes', params, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getListFeed.ts b/packages/pds/src/api/app/bsky/feed/getListFeed.ts index 3447a721904..91bf837f3dc 100644 --- a/packages/pds/src/api/app/bsky/feed/getListFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getListFeed.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getListFeed({ auth: ctx.authVerifier.access, - handler: async ({ auth, params }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.feed.getListFeed', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/feed/getPostThread.ts b/packages/pds/src/api/app/bsky/feed/getPostThread.ts index da09523875b..e8dc8557626 100644 --- a/packages/pds/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/pds/src/api/app/bsky/feed/getPostThread.ts @@ -3,7 +3,6 @@ import { AtUri } from '@atproto/syntax' import { Headers, XRPCError } from '@atproto/xrpc' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' import { ThreadViewPost, isThreadViewPost, @@ -30,27 +29,12 @@ export default function (server: Server, ctx: AppContext) { const { bskyAppView } = ctx.cfg if (!bskyAppView) return server.app.bsky.feed.getPostThread({ - auth: ctx.authVerifier.accessOrRole, + auth: ctx.authVerifier.access, handler: async ({ req, params, auth }) => { - const requester = - auth.credentials.type === 'access' ? auth.credentials.did : null - - if (!requester) { - return pipethrough( - bskyAppView.url, - METHOD_NSID, - params, - authPassthru(req), - ) - } + const requester = auth.credentials.did try { - const res = await pipethrough( - bskyAppView.url, - METHOD_NSID, - params, - await ctx.appviewAuthHeaders(requester), - ) + const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) return await handleReadAfterWrite( ctx, diff --git a/packages/pds/src/api/app/bsky/feed/getPosts.ts b/packages/pds/src/api/app/bsky/feed/getPosts.ts index 89d0d08587d..2d3ee4b5b8c 100644 --- a/packages/pds/src/api/app/bsky/feed/getPosts.ts +++ b/packages/pds/src/api/app/bsky/feed/getPosts.ts @@ -7,14 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getPosts({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - return pipethrough( - bskyAppView.url, - 'app.bsky.feed.getPosts', - params, - await ctx.appviewAuthHeaders(requester), - ) + return pipethrough(ctx, req, 'app.bsky.feed.getPosts', params, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts b/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts index 971d150824c..5e8a7900dd4 100644 --- a/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getRepostedBy({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.feed.getRepostedBy', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts b/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts index 6da81787533..92b68e597b8 100644 --- a/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts +++ b/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getSuggestedFeeds({ auth: ctx.authVerifier.access, - handler: async ({ auth, params }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.feed.getSuggestedFeeds', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/feed/getTimeline.ts b/packages/pds/src/api/app/bsky/feed/getTimeline.ts index 90fc5bac42f..d2c2a8e6019 100644 --- a/packages/pds/src/api/app/bsky/feed/getTimeline.ts +++ b/packages/pds/src/api/app/bsky/feed/getTimeline.ts @@ -15,14 +15,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getTimeline({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - const res = await pipethrough( - bskyAppView.url, - METHOD_NSID, - params, - await ctx.appviewAuthHeaders(requester), - ) + const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) return await handleReadAfterWrite( ctx, METHOD_NSID, diff --git a/packages/pds/src/api/app/bsky/feed/searchPosts.ts b/packages/pds/src/api/app/bsky/feed/searchPosts.ts index 7cc09c864e5..99ca276cddc 100644 --- a/packages/pds/src/api/app/bsky/feed/searchPosts.ts +++ b/packages/pds/src/api/app/bsky/feed/searchPosts.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.searchPosts({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.feed.searchPosts', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/graph/getBlocks.ts b/packages/pds/src/api/app/bsky/graph/getBlocks.ts index 1b29f9b62d2..96899f2e907 100644 --- a/packages/pds/src/api/app/bsky/graph/getBlocks.ts +++ b/packages/pds/src/api/app/bsky/graph/getBlocks.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getBlocks({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.graph.getBlocks', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/graph/getFollowers.ts b/packages/pds/src/api/app/bsky/graph/getFollowers.ts index 0a158f2bbe5..a5b5305dd3e 100644 --- a/packages/pds/src/api/app/bsky/graph/getFollowers.ts +++ b/packages/pds/src/api/app/bsky/graph/getFollowers.ts @@ -1,21 +1,20 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' import { pipethrough } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { const { bskyAppView } = ctx.cfg if (!bskyAppView) return server.app.bsky.graph.getFollowers({ - auth: ctx.authVerifier.accessOrRole, + auth: ctx.authVerifier.access, handler: async ({ req, params, auth }) => { - const requester = - auth.credentials.type === 'access' ? auth.credentials.did : null + const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.graph.getFollowers', params, - requester ? await ctx.appviewAuthHeaders(requester) : authPassthru(req), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/graph/getFollows.ts b/packages/pds/src/api/app/bsky/graph/getFollows.ts index 6802acda888..e67deeb7dda 100644 --- a/packages/pds/src/api/app/bsky/graph/getFollows.ts +++ b/packages/pds/src/api/app/bsky/graph/getFollows.ts @@ -1,21 +1,20 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' import { pipethrough } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { const { bskyAppView } = ctx.cfg if (!bskyAppView) return server.app.bsky.graph.getFollows({ - auth: ctx.authVerifier.accessOrRole, + auth: ctx.authVerifier.access, handler: async ({ req, params, auth }) => { - const requester = - auth.credentials.type === 'access' ? auth.credentials.did : null + const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.graph.getFollows', params, - requester ? await ctx.appviewAuthHeaders(requester) : authPassthru(req), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/graph/getList.ts b/packages/pds/src/api/app/bsky/graph/getList.ts index 6ef1dbf7ee0..3a7ec9bcae8 100644 --- a/packages/pds/src/api/app/bsky/graph/getList.ts +++ b/packages/pds/src/api/app/bsky/graph/getList.ts @@ -7,14 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getList({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - return pipethrough( - bskyAppView.url, - 'app.bsky.graph.getList', - params, - await ctx.appviewAuthHeaders(requester), - ) + return pipethrough(ctx, req, 'app.bsky.graph.getList', params, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getListBlocks.ts b/packages/pds/src/api/app/bsky/graph/getListBlocks.ts index d9aed6e7cd6..b2e982f4e95 100644 --- a/packages/pds/src/api/app/bsky/graph/getListBlocks.ts +++ b/packages/pds/src/api/app/bsky/graph/getListBlocks.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getListBlocks({ auth: ctx.authVerifier.access, - handler: async ({ auth, params }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.graph.getListBlocks', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/graph/getListMutes.ts b/packages/pds/src/api/app/bsky/graph/getListMutes.ts index 575c09d5b1a..38ef27df8c0 100644 --- a/packages/pds/src/api/app/bsky/graph/getListMutes.ts +++ b/packages/pds/src/api/app/bsky/graph/getListMutes.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getListMutes({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.graph.getListMutes', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/graph/getLists.ts b/packages/pds/src/api/app/bsky/graph/getLists.ts index c824c9cdb4b..b9d9df274bb 100644 --- a/packages/pds/src/api/app/bsky/graph/getLists.ts +++ b/packages/pds/src/api/app/bsky/graph/getLists.ts @@ -7,14 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getLists({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - return pipethrough( - bskyAppView.url, - 'app.bsky.graph.getLists', - params, - await ctx.appviewAuthHeaders(requester), - ) + return pipethrough(ctx, req, 'app.bsky.graph.getLists', params, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getMutes.ts b/packages/pds/src/api/app/bsky/graph/getMutes.ts index d422237dd0f..0a1a87f31de 100644 --- a/packages/pds/src/api/app/bsky/graph/getMutes.ts +++ b/packages/pds/src/api/app/bsky/graph/getMutes.ts @@ -7,14 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getMutes({ auth: ctx.authVerifier.access, - handler: async ({ auth, params }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did - return pipethrough( - bskyAppView.url, - 'app.bsky.graph.getMutes', - params, - await ctx.appviewAuthHeaders(requester), - ) + return pipethrough(ctx, req, 'app.bsky.graph.getMutes', params, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts b/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts index dfe453be8f6..21c77ebc212 100644 --- a/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +++ b/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getSuggestedFollowsByActor({ auth: ctx.authVerifier.access, - handler: async ({ auth, params }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.graph.getSuggestedFollowsByActor', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts b/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts index d6b8a235ba3..b845942234c 100644 --- a/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts +++ b/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.notification.getUnreadCount({ auth: ctx.authVerifier.access, - handler: async ({ auth, params }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.notification.getUnreadCount', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/notification/listNotifications.ts b/packages/pds/src/api/app/bsky/notification/listNotifications.ts index 005473eb6f4..88492ef2322 100644 --- a/packages/pds/src/api/app/bsky/notification/listNotifications.ts +++ b/packages/pds/src/api/app/bsky/notification/listNotifications.ts @@ -7,13 +7,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.notification.listNotifications({ auth: ctx.authVerifier.access, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.notification.listNotifications', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts index da7be6fb649..0045c8d59e4 100644 --- a/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -8,13 +8,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.unspecced.getPopularFeedGenerators({ auth: ctx.authVerifier.access, - handler: async ({ auth, params }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.unspecced.getPopularFeedGenerators', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/app/bsky/unspecced/getTaggedSuggestions.ts b/packages/pds/src/api/app/bsky/unspecced/getTaggedSuggestions.ts index 68e84985441..d1b05bad671 100644 --- a/packages/pds/src/api/app/bsky/unspecced/getTaggedSuggestions.ts +++ b/packages/pds/src/api/app/bsky/unspecced/getTaggedSuggestions.ts @@ -8,13 +8,14 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.unspecced.getTaggedSuggestions({ auth: ctx.authVerifier.access, - handler: async ({ auth, params }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did return pipethrough( - bskyAppView.url, + ctx, + req, 'app.bsky.unspecced.getTaggedSuggestions', params, - await ctx.appviewAuthHeaders(requester), + requester, ) }, }) diff --git a/packages/pds/src/api/com/atproto/repo/getRecord.ts b/packages/pds/src/api/com/atproto/repo/getRecord.ts index 3d8b44099d4..de517dd807f 100644 --- a/packages/pds/src/api/com/atproto/repo/getRecord.ts +++ b/packages/pds/src/api/com/atproto/repo/getRecord.ts @@ -5,7 +5,7 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { pipethrough } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { - server.com.atproto.repo.getRecord(async ({ params }) => { + server.com.atproto.repo.getRecord(async ({ req, params }) => { const { repo, collection, rkey, cid } = params const did = await ctx.accountManager.getDidForActor(repo) @@ -32,10 +32,6 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError(`Could not locate record`) } - return await pipethrough( - ctx.cfg.bskyAppView.url, - 'com.atproto.repo.getRecord', - params, - ) + return await pipethrough(ctx, req, 'com.atproto.repo.getRecord', params) }) } diff --git a/packages/pds/src/pipethrough.ts b/packages/pds/src/pipethrough.ts index a8123cbbb0a..a25ac4604c9 100644 --- a/packages/pds/src/pipethrough.ts +++ b/packages/pds/src/pipethrough.ts @@ -13,7 +13,7 @@ export const pipethrough = async ( req: express.Request, nsid: string, params: Record, - requester: string, + requester?: string, audOverride?: string, ): Promise => { const proxyTo = await parseProxyHeader(ctx, req) @@ -22,7 +22,9 @@ export const pipethrough = async ( if (!serviceUrl || !aud) { throw new InvalidRequestError(`No service configured for ${nsid}`) } - const reqHeaders = await ctx.serviceAuthHeaders(requester, aud) + const reqHeaders = requester + ? await ctx.serviceAuthHeaders(requester, aud) + : { headers: {} } // forward accept-language header to upstream services reqHeaders.headers['accept-language'] = req.headers['accept-language'] const url = constructUrl(serviceUrl, nsid, params) From bfbb58674be02af26568dc4710e62421a7ee9439 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 14:54:35 -0600 Subject: [PATCH 07/50] simplify pipethrough & add some SSRF protection --- packages/dev-env/src/pds.ts | 1 + .../pds/src/api/app/bsky/actor/getProfile.ts | 4 ++-- .../pds/src/api/app/bsky/actor/getProfiles.ts | 4 ++-- .../src/api/app/bsky/actor/getSuggestions.ts | 10 ++-------- .../pds/src/api/app/bsky/actor/searchActors.ts | 10 ++-------- .../api/app/bsky/actor/searchActorsTypeahead.ts | 10 ++-------- .../pds/src/api/app/bsky/feed/getActorFeeds.ts | 10 ++-------- .../pds/src/api/app/bsky/feed/getActorLikes.ts | 4 ++-- .../pds/src/api/app/bsky/feed/getAuthorFeed.ts | 4 ++-- packages/pds/src/api/app/bsky/feed/getFeed.ts | 9 +-------- .../src/api/app/bsky/feed/getFeedGenerator.ts | 10 ++-------- .../src/api/app/bsky/feed/getFeedGenerators.ts | 10 ++-------- packages/pds/src/api/app/bsky/feed/getLikes.ts | 4 ++-- .../pds/src/api/app/bsky/feed/getListFeed.ts | 10 ++-------- .../pds/src/api/app/bsky/feed/getPostThread.ts | 4 ++-- packages/pds/src/api/app/bsky/feed/getPosts.ts | 4 ++-- .../pds/src/api/app/bsky/feed/getRepostedBy.ts | 10 ++-------- .../src/api/app/bsky/feed/getSuggestedFeeds.ts | 10 ++-------- .../pds/src/api/app/bsky/feed/getTimeline.ts | 4 ++-- .../pds/src/api/app/bsky/feed/searchPosts.ts | 10 ++-------- .../pds/src/api/app/bsky/graph/getBlocks.ts | 10 ++-------- .../pds/src/api/app/bsky/graph/getFollowers.ts | 10 ++-------- .../pds/src/api/app/bsky/graph/getFollows.ts | 10 ++-------- packages/pds/src/api/app/bsky/graph/getList.ts | 4 ++-- .../pds/src/api/app/bsky/graph/getListBlocks.ts | 10 ++-------- .../pds/src/api/app/bsky/graph/getListMutes.ts | 10 ++-------- packages/pds/src/api/app/bsky/graph/getLists.ts | 4 ++-- packages/pds/src/api/app/bsky/graph/getMutes.ts | 4 ++-- .../bsky/graph/getSuggestedFollowsByActor.ts | 10 ++-------- .../api/app/bsky/notification/getUnreadCount.ts | 10 ++-------- .../app/bsky/notification/listNotifications.ts | 10 ++-------- .../bsky/unspecced/getPopularFeedGenerators.ts | 10 ++-------- .../app/bsky/unspecced/getTaggedSuggestions.ts | 10 ++-------- .../pds/src/api/com/atproto/repo/getRecord.ts | 2 +- packages/pds/src/config/config.ts | 2 ++ packages/pds/src/config/env.ts | 2 ++ packages/pds/src/pipethrough.ts | 17 +++++++++++++---- 37 files changed, 82 insertions(+), 195 deletions(-) diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index 44b8a063fce..1e76b4341a1 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -35,6 +35,7 @@ export class TestPds { await fs.mkdir(dataDirectory, { recursive: true }) const env: pds.ServerEnvironment = { + devMode: true, port, dataDirectory: dataDirectory, blobstoreDiskLocation: blobstoreLoc, diff --git a/packages/pds/src/api/app/bsky/actor/getProfile.ts b/packages/pds/src/api/app/bsky/actor/getProfile.ts index a63e2a556d7..f05465d267a 100644 --- a/packages/pds/src/api/app/bsky/actor/getProfile.ts +++ b/packages/pds/src/api/app/bsky/actor/getProfile.ts @@ -15,9 +15,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.actor.getProfile({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) + const res = await pipethrough(ctx, req, requester) if (!requester) { return res } diff --git a/packages/pds/src/api/app/bsky/actor/getProfiles.ts b/packages/pds/src/api/app/bsky/actor/getProfiles.ts index 67a304280f8..3d1b2f1f579 100644 --- a/packages/pds/src/api/app/bsky/actor/getProfiles.ts +++ b/packages/pds/src/api/app/bsky/actor/getProfiles.ts @@ -15,10 +15,10 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.actor.getProfiles({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) + const res = await pipethrough(ctx, req, requester) return handleReadAfterWrite( ctx, METHOD_NSID, diff --git a/packages/pds/src/api/app/bsky/actor/getSuggestions.ts b/packages/pds/src/api/app/bsky/actor/getSuggestions.ts index d085dd097bd..fadcac2e9fc 100644 --- a/packages/pds/src/api/app/bsky/actor/getSuggestions.ts +++ b/packages/pds/src/api/app/bsky/actor/getSuggestions.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.actor.getSuggestions({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.actor.getSuggestions', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/actor/searchActors.ts b/packages/pds/src/api/app/bsky/actor/searchActors.ts index fd3c2cd731b..777f4e7a2d0 100644 --- a/packages/pds/src/api/app/bsky/actor/searchActors.ts +++ b/packages/pds/src/api/app/bsky/actor/searchActors.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.actor.searchActors({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.actor.searchActors', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts index fbbb911e3bd..58d5df0d049 100644 --- a/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.actor.searchActorsTypeahead({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.actor.searchActorsTypeahead', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts b/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts index 1bd9fd4fd17..16082a916e8 100644 --- a/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts +++ b/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getActorFeeds({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.feed.getActorFeeds', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getActorLikes.ts b/packages/pds/src/api/app/bsky/feed/getActorLikes.ts index 9cc5223937d..3d17bff8e05 100644 --- a/packages/pds/src/api/app/bsky/feed/getActorLikes.ts +++ b/packages/pds/src/api/app/bsky/feed/getActorLikes.ts @@ -15,9 +15,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getActorLikes({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) + const res = await pipethrough(ctx, req, requester) if (!requester) { return res diff --git a/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts index 2296fa2221c..88f646842a5 100644 --- a/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts @@ -16,9 +16,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getAuthorFeed({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) + const res = await pipethrough(ctx, req, requester) if (!requester) { return res } diff --git a/packages/pds/src/api/app/bsky/feed/getFeed.ts b/packages/pds/src/api/app/bsky/feed/getFeed.ts index a785304e4b0..aedbe5b4f23 100644 --- a/packages/pds/src/api/app/bsky/feed/getFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getFeed.ts @@ -16,14 +16,7 @@ export default function (server: Server, ctx: AppContext) { { feed: params.feed }, await ctx.appviewAuthHeaders(requester), ) - return pipethrough( - ctx, - req, - 'app.bsky.feed.getFeed', - params, - requester, - feed.view.did, - ) + return pipethrough(ctx, req, requester, feed.view.did) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts b/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts index e46eff5cf40..278ee9af7c2 100644 --- a/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts +++ b/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getFeedGenerator({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.feed.getFeedGenerator', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts b/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts index 828f6cc204d..2aa79b03853 100644 --- a/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts +++ b/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getFeedGenerators({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.feed.getFeedGenerators', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getLikes.ts b/packages/pds/src/api/app/bsky/feed/getLikes.ts index 263864fb286..226b10feb9d 100644 --- a/packages/pds/src/api/app/bsky/feed/getLikes.ts +++ b/packages/pds/src/api/app/bsky/feed/getLikes.ts @@ -7,9 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getLikes({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough(ctx, req, 'app.bsky.feed.getLikes', params, requester) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getListFeed.ts b/packages/pds/src/api/app/bsky/feed/getListFeed.ts index 91bf837f3dc..2d059eb442f 100644 --- a/packages/pds/src/api/app/bsky/feed/getListFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getListFeed.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getListFeed({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.feed.getListFeed', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getPostThread.ts b/packages/pds/src/api/app/bsky/feed/getPostThread.ts index e8dc8557626..c6231d4bf04 100644 --- a/packages/pds/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/pds/src/api/app/bsky/feed/getPostThread.ts @@ -30,11 +30,11 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getPostThread({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth, params }) => { const requester = auth.credentials.did try { - const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) + const res = await pipethrough(ctx, req, requester) return await handleReadAfterWrite( ctx, diff --git a/packages/pds/src/api/app/bsky/feed/getPosts.ts b/packages/pds/src/api/app/bsky/feed/getPosts.ts index 2d3ee4b5b8c..a07be55a516 100644 --- a/packages/pds/src/api/app/bsky/feed/getPosts.ts +++ b/packages/pds/src/api/app/bsky/feed/getPosts.ts @@ -7,9 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getPosts({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough(ctx, req, 'app.bsky.feed.getPosts', params, requester) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts b/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts index 5e8a7900dd4..3b8cff2f4cf 100644 --- a/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getRepostedBy({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.feed.getRepostedBy', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts b/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts index 92b68e597b8..002b24ef3f0 100644 --- a/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts +++ b/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getSuggestedFeeds({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.feed.getSuggestedFeeds', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/feed/getTimeline.ts b/packages/pds/src/api/app/bsky/feed/getTimeline.ts index d2c2a8e6019..05f0a0ea90f 100644 --- a/packages/pds/src/api/app/bsky/feed/getTimeline.ts +++ b/packages/pds/src/api/app/bsky/feed/getTimeline.ts @@ -15,9 +15,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.getTimeline({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - const res = await pipethrough(ctx, req, METHOD_NSID, params, requester) + const res = await pipethrough(ctx, req, requester) return await handleReadAfterWrite( ctx, METHOD_NSID, diff --git a/packages/pds/src/api/app/bsky/feed/searchPosts.ts b/packages/pds/src/api/app/bsky/feed/searchPosts.ts index 99ca276cddc..45ad78ad401 100644 --- a/packages/pds/src/api/app/bsky/feed/searchPosts.ts +++ b/packages/pds/src/api/app/bsky/feed/searchPosts.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.feed.searchPosts({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.feed.searchPosts', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getBlocks.ts b/packages/pds/src/api/app/bsky/graph/getBlocks.ts index 96899f2e907..12cc5603eb7 100644 --- a/packages/pds/src/api/app/bsky/graph/getBlocks.ts +++ b/packages/pds/src/api/app/bsky/graph/getBlocks.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getBlocks({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.graph.getBlocks', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getFollowers.ts b/packages/pds/src/api/app/bsky/graph/getFollowers.ts index a5b5305dd3e..78f433598aa 100644 --- a/packages/pds/src/api/app/bsky/graph/getFollowers.ts +++ b/packages/pds/src/api/app/bsky/graph/getFollowers.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getFollowers({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.graph.getFollowers', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getFollows.ts b/packages/pds/src/api/app/bsky/graph/getFollows.ts index e67deeb7dda..3c62584ee98 100644 --- a/packages/pds/src/api/app/bsky/graph/getFollows.ts +++ b/packages/pds/src/api/app/bsky/graph/getFollows.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getFollows({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.graph.getFollows', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getList.ts b/packages/pds/src/api/app/bsky/graph/getList.ts index 3a7ec9bcae8..0b39c8eeae5 100644 --- a/packages/pds/src/api/app/bsky/graph/getList.ts +++ b/packages/pds/src/api/app/bsky/graph/getList.ts @@ -7,9 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getList({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough(ctx, req, 'app.bsky.graph.getList', params, requester) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getListBlocks.ts b/packages/pds/src/api/app/bsky/graph/getListBlocks.ts index b2e982f4e95..472a7c6f24c 100644 --- a/packages/pds/src/api/app/bsky/graph/getListBlocks.ts +++ b/packages/pds/src/api/app/bsky/graph/getListBlocks.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getListBlocks({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.graph.getListBlocks', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getListMutes.ts b/packages/pds/src/api/app/bsky/graph/getListMutes.ts index 38ef27df8c0..f77f35155a5 100644 --- a/packages/pds/src/api/app/bsky/graph/getListMutes.ts +++ b/packages/pds/src/api/app/bsky/graph/getListMutes.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getListMutes({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.graph.getListMutes', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getLists.ts b/packages/pds/src/api/app/bsky/graph/getLists.ts index b9d9df274bb..8aabf93ba78 100644 --- a/packages/pds/src/api/app/bsky/graph/getLists.ts +++ b/packages/pds/src/api/app/bsky/graph/getLists.ts @@ -7,9 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getLists({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough(ctx, req, 'app.bsky.graph.getLists', params, requester) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getMutes.ts b/packages/pds/src/api/app/bsky/graph/getMutes.ts index 0a1a87f31de..0d3c131d28e 100644 --- a/packages/pds/src/api/app/bsky/graph/getMutes.ts +++ b/packages/pds/src/api/app/bsky/graph/getMutes.ts @@ -7,9 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getMutes({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough(ctx, req, 'app.bsky.graph.getMutes', params, requester) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts b/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts index 21c77ebc212..1c3a7d4ed86 100644 --- a/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +++ b/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.graph.getSuggestedFollowsByActor({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.graph.getSuggestedFollowsByActor', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts b/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts index b845942234c..d5f09f92bb3 100644 --- a/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts +++ b/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.notification.getUnreadCount({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.notification.getUnreadCount', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/notification/listNotifications.ts b/packages/pds/src/api/app/bsky/notification/listNotifications.ts index 88492ef2322..68d08fde137 100644 --- a/packages/pds/src/api/app/bsky/notification/listNotifications.ts +++ b/packages/pds/src/api/app/bsky/notification/listNotifications.ts @@ -7,15 +7,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.notification.listNotifications({ auth: ctx.authVerifier.access, - handler: async ({ req, params, auth }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.notification.listNotifications', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts index 0045c8d59e4..0d53ffff5ad 100644 --- a/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -8,15 +8,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.unspecced.getPopularFeedGenerators({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.unspecced.getPopularFeedGenerators', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/unspecced/getTaggedSuggestions.ts b/packages/pds/src/api/app/bsky/unspecced/getTaggedSuggestions.ts index d1b05bad671..c9d9ed90dfe 100644 --- a/packages/pds/src/api/app/bsky/unspecced/getTaggedSuggestions.ts +++ b/packages/pds/src/api/app/bsky/unspecced/getTaggedSuggestions.ts @@ -8,15 +8,9 @@ export default function (server: Server, ctx: AppContext) { if (!bskyAppView) return server.app.bsky.unspecced.getTaggedSuggestions({ auth: ctx.authVerifier.access, - handler: async ({ req, auth, params }) => { + handler: async ({ req, auth }) => { const requester = auth.credentials.did - return pipethrough( - ctx, - req, - 'app.bsky.unspecced.getTaggedSuggestions', - params, - requester, - ) + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/repo/getRecord.ts b/packages/pds/src/api/com/atproto/repo/getRecord.ts index de517dd807f..5a0d0fae441 100644 --- a/packages/pds/src/api/com/atproto/repo/getRecord.ts +++ b/packages/pds/src/api/com/atproto/repo/getRecord.ts @@ -32,6 +32,6 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError(`Could not locate record`) } - return await pipethrough(ctx, req, 'com.atproto.repo.getRecord', params) + return await pipethrough(ctx, req) }) } diff --git a/packages/pds/src/config/config.ts b/packages/pds/src/config/config.ts index 8ca807d39b5..bbdb5ee797c 100644 --- a/packages/pds/src/config/config.ts +++ b/packages/pds/src/config/config.ts @@ -24,6 +24,7 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { termsOfServiceUrl: env.termsOfServiceUrl, acceptingImports: env.acceptingImports ?? true, blobUploadLimit: env.blobUploadLimit ?? 5 * 1024 * 1024, // 5mb + devMode: env.devMode ?? false, } const dbLoc = (name: string) => { @@ -280,6 +281,7 @@ export type ServiceConfig = { termsOfServiceUrl?: string acceptingImports: boolean blobUploadLimit: number + devMode: boolean } export type DatabaseConfig = { diff --git a/packages/pds/src/config/env.ts b/packages/pds/src/config/env.ts index fb5aed8232f..a334c0c51d3 100644 --- a/packages/pds/src/config/env.ts +++ b/packages/pds/src/config/env.ts @@ -11,6 +11,7 @@ export const readEnv = (): ServerEnvironment => { termsOfServiceUrl: envStr('PDS_TERMS_OF_SERVICE_URL'), acceptingImports: envBool('PDS_ACCEPTING_REPO_IMPORTS'), blobUploadLimit: envInt('PDS_BLOB_UPLOAD_LIMIT'), + devMode: envBool('PDS_DEV_MODE'), // database dataDirectory: envStr('PDS_DATA_DIRECTORY'), @@ -118,6 +119,7 @@ export type ServerEnvironment = { termsOfServiceUrl?: string acceptingImports?: boolean blobUploadLimit?: number + devMode?: boolean // database dataDirectory?: string diff --git a/packages/pds/src/pipethrough.ts b/packages/pds/src/pipethrough.ts index a25ac4604c9..5dcffd31d99 100644 --- a/packages/pds/src/pipethrough.ts +++ b/packages/pds/src/pipethrough.ts @@ -1,5 +1,6 @@ import express from 'express' import * as ui8 from 'uint8arrays' +import net from 'node:net' import { jsonToLex } from '@atproto/lexicon' import { HandlerPipeThrough, InvalidRequestError } from '@atproto/xrpc-server' import { ResponseType, XRPCError } from '@atproto/xrpc' @@ -11,8 +12,6 @@ import AppContext from './context' export const pipethrough = async ( ctx: AppContext, req: express.Request, - nsid: string, - params: Record, requester?: string, audOverride?: string, ): Promise => { @@ -20,14 +19,17 @@ export const pipethrough = async ( const serviceUrl = proxyTo?.serviceUrl ?? ctx.cfg.bskyAppView?.url const aud = audOverride ?? proxyTo?.did ?? ctx.cfg.bskyAppView?.did if (!serviceUrl || !aud) { - throw new InvalidRequestError(`No service configured for ${nsid}`) + throw new InvalidRequestError(`No service configured for ${req.path}`) + } + const url = new URL(req.originalUrl, serviceUrl) + if (!ctx.cfg.service.devMode && !isSafeUrl(url)) { + throw new InvalidRequestError(`Invalid service url: ${url.toString()}`) } const reqHeaders = requester ? await ctx.serviceAuthHeaders(requester, aud) : { headers: {} } // forward accept-language header to upstream services reqHeaders.headers['accept-language'] = req.headers['accept-language'] - const url = constructUrl(serviceUrl, nsid, params) let res: Response let buffer: ArrayBuffer try { @@ -98,6 +100,13 @@ export const constructUrl = ( return uri.toString() } +const isSafeUrl = (url: URL) => { + if (url.protocol !== 'https:') return false + if (!url.hostname || url.hostname === 'localhost') return false + if (net.isIP(url.hostname) === 0) return false + return true +} + export const parseRes = (nsid: string, res: HandlerPipeThrough): T => { const buffer = new Uint8Array(res.buffer) const json = safeParseJson(ui8.toString(buffer, 'utf8')) From 92d92685032fc640a4fb53340ff583a3686bd2bf Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 15:40:03 -0600 Subject: [PATCH 08/50] tests --- packages/pds/src/pipethrough.ts | 3 + .../pds/tests/proxied/proxy-header.test.ts | 168 ++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 packages/pds/tests/proxied/proxy-header.test.ts diff --git a/packages/pds/src/pipethrough.ts b/packages/pds/src/pipethrough.ts index 5dcffd31d99..90929499a47 100644 --- a/packages/pds/src/pipethrough.ts +++ b/packages/pds/src/pipethrough.ts @@ -66,6 +66,9 @@ export const parseProxyHeader = async ( const proxyTo = req.header('atproto-proxy') if (!proxyTo) return const [did, serviceId] = proxyTo.split('#') + if (!serviceId) { + throw new InvalidRequestError('no service id specified') + } const didDoc = await ctx.idResolver.did.resolve(did) if (!didDoc) { throw new InvalidRequestError('could not resolve proxy did') diff --git a/packages/pds/tests/proxied/proxy-header.test.ts b/packages/pds/tests/proxied/proxy-header.test.ts new file mode 100644 index 00000000000..d00dc3bb342 --- /dev/null +++ b/packages/pds/tests/proxied/proxy-header.test.ts @@ -0,0 +1,168 @@ +import http from 'node:http' +import assert from 'node:assert' +import express from 'express' +import axios from 'axios' +import * as plc from '@did-plc/lib' +import { SeedClient, TestNetworkNoAppView, usersSeed } from '@atproto/dev-env' +import getPort from 'get-port' +import { Keypair } from '@atproto/crypto' +import { verifyJwt } from '@atproto/xrpc-server' + +describe('proxy header', () => { + let network: TestNetworkNoAppView + let sc: SeedClient + + let alice: string + + let proxyServer: ProxyServer + + beforeAll(async () => { + network = await TestNetworkNoAppView.create({ + dbPostgresSchema: 'proxy_header', + }) + sc = network.getSeedClient() + await usersSeed(sc) + + proxyServer = await ProxyServer.create( + network.pds.ctx.plcClient, + network.pds.ctx.plcRotationKey, + 'atproto_test', + ) + + alice = sc.dids.alice + await network.processAll() + }) + + afterAll(async () => { + await proxyServer.close() + await network.close() + }) + + const assertAxiosErr = async (promise: Promise, msg: string) => { + try { + await promise + } catch (err) { + if (!axios.isAxiosError(err)) { + throw err + } + expect(err.response?.data?.['message']).toEqual(msg) + return + } + throw new Error('no error thrown') + } + + it('proxies requests based on header', async () => { + const path = `/xrpc/app.bsky.actor.getProfile?actor=${alice}` + await axios.get(`${network.pds.url}${path}`, { + headers: { + ...sc.getHeaders(alice), + 'atproto-proxy': `${proxyServer.did}#atproto_test`, + }, + }) + const req = proxyServer.requests.at(-1) + assert(req) + expect(req.url).toEqual(path) + assert(req.auth) + const verified = await verifyJwt( + req.auth.replace('Bearer ', ''), + proxyServer.did, + (iss) => network.pds.ctx.idResolver.did.resolveAtprotoKey(iss, true), + ) + expect(verified.aud).toBe(proxyServer.did) + expect(verified.iss).toBe(alice) + }) + + it('fails on a non-existant did', async () => { + const path = `/xrpc/app.bsky.actor.getProfile?actor=${alice}` + const attempt = axios.get(`${network.pds.url}${path}`, { + headers: { + ...sc.getHeaders(alice), + 'atproto-proxy': `did:plc:12345678123456781234578#atproto_test`, + }, + }) + await assertAxiosErr(attempt, 'could not resolve proxy did') + expect(proxyServer.requests.length).toBe(1) + }) + + it('fails when a service is not specified', async () => { + const path = `/xrpc/app.bsky.actor.getProfile?actor=${alice}` + const attempt = axios.get(`${network.pds.url}${path}`, { + headers: { + ...sc.getHeaders(alice), + 'atproto-proxy': proxyServer.did, + }, + }) + await assertAxiosErr(attempt, 'no service id specified') + expect(proxyServer.requests.length).toBe(1) + }) + + it('fails on a non-existant service', async () => { + const path = `/xrpc/app.bsky.actor.getProfile?actor=${alice}` + const attempt = axios.get(`${network.pds.url}${path}`, { + headers: { + ...sc.getHeaders(alice), + 'atproto-proxy': `${proxyServer.did}#atproto_bad`, + }, + }) + await assertAxiosErr(attempt, 'could not resolve proxy did service url') + expect(proxyServer.requests.length).toBe(1) + }) +}) + +type ProxyReq = { + url: string + auth: string | undefined +} + +class ProxyServer { + constructor( + public server: http.Server, + public url: string, + public did: string, + public requests: ProxyReq[], + ) {} + + static async create( + plcClient: plc.Client, + keypair: Keypair, + serviceId: string, + ): Promise { + const requests: ProxyReq[] = [] + const app = express() + app.get('*', (req, res) => { + requests.push({ + url: req.url, + auth: req.header('authorization'), + }) + res.sendStatus(200) + }) + const port = await getPort() + const server = app.listen(port) + const url = `http://localhost:${port}` + const plcOp = await plc.signOperation( + { + type: 'plc_operation', + rotationKeys: [keypair.did()], + alsoKnownAs: [], + verificationMethods: {}, + services: { + [serviceId]: { + type: 'TestAtprotoService', + endpoint: url, + }, + }, + prev: null, + }, + keypair, + ) + const did = await plc.didForCreateOp(plcOp) + await plcClient.sendOperation(did, plcOp) + return new ProxyServer(server, url, did, requests) + } + + close(): Promise { + return new Promise((resolve) => { + this.server.close(() => resolve()) + }) + } +} From 199b754fbcf2f902633ce100a9b8af382b5d4a12 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 16:12:31 -0600 Subject: [PATCH 09/50] fix bad var --- packages/pds/src/context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index a9fe996056f..4f852af1497 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -199,7 +199,7 @@ export class AppContext { dids: { pds: cfg.service.did, entryway: cfg.entryway?.did, - admin: cfg.modService?.did, + modService: cfg.modService?.did, }, }) From 5cea30c3606d24fb89ad964e35f0f3ca22c504ce Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 17:23:23 -0600 Subject: [PATCH 10/50] remove basic auth in ozone --- .../api/admin/createCommunicationTemplate.ts | 2 +- .../api/admin/deleteCommunicationTemplate.ts | 2 +- .../src/api/admin/emitModerationEvent.ts | 5 +- .../ozone/src/api/admin/getModerationEvent.ts | 2 +- packages/ozone/src/api/admin/getRecord.ts | 2 +- packages/ozone/src/api/admin/getRepo.ts | 2 +- .../api/admin/listCommunicationTemplates.ts | 2 +- .../src/api/admin/queryModerationEvents.ts | 2 +- .../src/api/admin/queryModerationStatuses.ts | 2 +- packages/ozone/src/api/admin/searchRepos.ts | 2 +- .../api/admin/updateCommunicationTemplate.ts | 2 +- .../ozone/src/api/moderation/createReport.ts | 3 +- packages/ozone/src/api/proxied.ts | 10 ++-- packages/ozone/src/api/temp/fetchLabels.ts | 2 +- packages/ozone/src/auth-verifier.ts | 46 ++++++------------- packages/ozone/src/config/env.ts | 4 -- packages/ozone/src/config/secrets.ts | 6 --- packages/ozone/src/context.ts | 2 - 18 files changed, 33 insertions(+), 65 deletions(-) diff --git a/packages/ozone/src/api/admin/createCommunicationTemplate.ts b/packages/ozone/src/api/admin/createCommunicationTemplate.ts index f05db2d71f2..0f7b794fcb4 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.moderator, 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..409a8bc9ba9 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.moderator, 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 473269bffde..2417388ed69 100644 --- a/packages/ozone/src/api/admin/emitModerationEvent.ts +++ b/packages/ozone/src/api/admin/emitModerationEvent.ts @@ -11,12 +11,13 @@ import { ModerationLangService } from '../../mod-service/lang' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.emitModerationEvent({ - auth: ctx.authVerifier.modOrRole, + auth: ctx.authVerifier.moderator, handler: async ({ input, auth }) => { const access = auth.credentials + const createdBy = access.iss 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..cd1657b6a4d 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.moderator, 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..34b94c21b2f 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.moderator, 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..2812d5de3c4 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.moderator, 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..4ed69157e1d 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.moderator, 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..f9cfbe65cd1 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.moderator, 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..a97a998fc1d 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.moderator, 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..e33595de3bd 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.moderator, 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..b206dbe46ef 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.moderator, handler: async ({ input, auth }) => { const access = auth.credentials const db = ctx.db 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 b7eef005650..eaa2c33f40b 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.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, @@ -31,7 +31,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, @@ -45,7 +45,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, @@ -59,7 +59,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, diff --git a/packages/ozone/src/api/temp/fetchLabels.ts b/packages/ozone/src/api/temp/fetchLabels.ts index 890fafdf95c..352fde3de91 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 8ea3c58c59c..22032bf55bc 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,16 +66,6 @@ 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) - } else { - return this.role(reqCtx) - } } moderator = async (reqCtx: ReqCtx): Promise => { @@ -138,36 +124,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 e86b520815d..6462a9f1b6a 100644 --- a/packages/ozone/src/config/env.ts +++ b/packages/ozone/src/config/env.ts @@ -22,8 +22,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'), } } @@ -49,7 +47,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 e9332620993..46758444762 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( From 7561b93ccf9184cd510ef2cce66c214b1c9cd1eb Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 17:54:06 -0600 Subject: [PATCH 11/50] wip --- packages/dev-env/src/moderator-client.ts | 87 ++++++++++++++++++++++++ packages/dev-env/src/ozone.ts | 84 ++++++++++++++++------- packages/dev-env/src/seed/client.ts | 52 -------------- packages/dev-env/src/types.ts | 8 ++- packages/dev-env/src/util.ts | 23 +++++++ packages/ozone/src/api/admin/util.ts | 1 + packages/ozone/tests/get-repo.test.ts | 26 ++++--- 7 files changed, 195 insertions(+), 86 deletions(-) create mode 100644 packages/dev-env/src/moderator-client.ts diff --git a/packages/dev-env/src/moderator-client.ts b/packages/dev-env/src/moderator-client.ts new file mode 100644 index 00000000000..90550992d6b --- /dev/null +++ b/packages/dev-env/src/moderator-client.ts @@ -0,0 +1,87 @@ +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' + +export class ModeratorClient { + agent: AtpAgent + constructor(public ozone: TestOzone) { + this.agent = ozone.getClient() + } + + async getEvent(id: number) { + const result = await this.agent.api.com.atproto.admin.getModerationEvent( + { id }, + { + headers: await this.ozone.modHeaders(), + }, + ) + return result.data + } + + async queryModerationStatuses(input: QueryStatusesParams) { + const result = + await this.agent.api.com.atproto.admin.queryModerationStatuses(input, { + headers: await this.ozone.modHeaders(), + }) + return result.data + } + + async queryModerationEvents(input: QueryEventsParams) { + const result = await this.agent.api.com.atproto.admin.queryModerationEvents( + input, + { + headers: await this.ozone.modHeaders(), + }, + ) + return result.data + } + + 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: await this.ozone.modHeaders(), + }, + ) + 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: await this.ozone.modHeaders(), + }, + ) + return result.data + } +} diff --git a/packages/dev-env/src/ozone.ts b/packages/dev-env/src/ozone.ts index 0e0578c7935..39f69962505 100644 --- a/packages/dev-env/src/ozone.ts +++ b/packages/dev-env/src/ozone.ts @@ -4,8 +4,11 @@ import * as ozone from '@atproto/ozone' import { AtpAgent } from '@atproto/api' import { Secp256k1Keypair } from '@atproto/crypto' import { Client as PlcClient } 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' +import { createServiceJwt } from '@atproto/xrpc-server' 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 { @@ -31,6 +37,24 @@ export class TestOzone { }) } + 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}` const env: ozone.OzoneEnvironment = { @@ -42,11 +66,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 ..." @@ -75,7 +101,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 { @@ -86,23 +112,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' = 'admin') { + 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/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/src/api/admin/util.ts b/packages/ozone/src/api/admin/util.ts index b4df0664327..06b64e45862 100644 --- a/packages/ozone/src/api/admin/util.ts +++ b/packages/ozone/src/api/admin/util.ts @@ -17,6 +17,7 @@ export const getPdsAccountInfo = async ( const res = await agent.api.com.atproto.admin.getAccountInfo({ did }, auth) return res.data } catch (err) { + console.log('ERR: ', err) return null } } diff --git a/packages/ozone/tests/get-repo.test.ts b/packages/ozone/tests/get-repo.test.ts index 1e0491465f5..f34a24e8d3b 100644 --- a/packages/ozone/tests/get-repo.test.ts +++ b/packages/ozone/tests/get-repo.test.ts @@ -5,18 +5,24 @@ import { REASONSPAM, } from '../src/lexicon/types/com/atproto/moderation/defs' import { forSnapshot } from './_util' +import { TestOzone } from '@atproto/dev-env/src/ozone' +import { ModeratorClient } from '@atproto/dev-env/src/moderator-client' describe('admin get repo 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_repo', }) - agent = network.pds.getClient() + ozone = network.ozone + agent = ozone.getClient() sc = network.getSeedClient() + modClient = ozone.getModClient() await basicSeed(sc) await network.processAll() }) @@ -26,7 +32,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 +56,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 +68,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 +76,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 +96,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() @@ -112,7 +118,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 +130,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') }) From cb53fdca79fca0c9789b1eefe170540538abde21 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 17:55:55 -0600 Subject: [PATCH 12/50] fix key parsing in pds --- packages/common-web/src/did-doc.ts | 11 ++--------- packages/pds/src/auth-verifier.ts | 10 +++++++--- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/common-web/src/did-doc.ts b/packages/common-web/src/did-doc.ts index 1b84c1787a4..715c364b3ca 100644 --- a/packages/common-web/src/did-doc.ts +++ b/packages/common-web/src/did-doc.ts @@ -52,19 +52,12 @@ export const getVerificationMaterial = ( } } -export const getDidKeyForId = ( - doc: DidDocument, - keyId: string, -): string | undefined => { - const parsed = getVerificationMaterial(doc, keyId) +export const getSigningDidKey = (doc: DidDocument): string | undefined => { + const parsed = getSigningKey(doc) if (!parsed) return return `did:key:${parsed.publicKeyMultibase}` } -export const getSigningDidKey = (doc: DidDocument): string | undefined => { - return getDidKeyForId(doc, 'atproto') -} - export const getPdsEndpoint = (doc: DidDocument): string | undefined => { return getServiceEndpoint(doc, { id: '#atproto_pds', diff --git a/packages/pds/src/auth-verifier.ts b/packages/pds/src/auth-verifier.ts index 410b254d5b6..ea2ebeb868d 100644 --- a/packages/pds/src/auth-verifier.ts +++ b/packages/pds/src/auth-verifier.ts @@ -5,14 +5,14 @@ import { InvalidRequestError, verifyJwt as verifyServiceJwt, } from '@atproto/xrpc-server' -import { IdResolver } from '@atproto/identity' +import { IdResolver, getDidKeyFromMultibase } from '@atproto/identity' import * as ui8 from 'uint8arrays' import express from 'express' import * as jose from 'jose' import KeyEncoder from 'key-encoder' import { AccountManager } from './account-manager' import { softDeleted } from './db' -import { getDidKeyForId } from '@atproto/common' +import { getVerificationMaterial } from '@atproto/common' type ReqCtx = { req: express.Request @@ -350,7 +350,11 @@ export class AuthVerifier { throw new AuthRequiredError('could not resolve iss did') } const keyId = serviceId === 'atproto-mod' ? 'atproto-mod-key' : 'atproto' - const didKey = getDidKeyForId(didDoc, keyId) + const parsedKey = getVerificationMaterial(didDoc, keyId) + if (!parsedKey) { + throw new AuthRequiredError('missing or bad key in did doc') + } + const didKey = getDidKeyFromMultibase(parsedKey) if (!didKey) { throw new AuthRequiredError('missing or bad key in did doc') } From 26b35571b84f5e444f7458fef7559915ab186e1e Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 19:01:10 -0600 Subject: [PATCH 13/50] fix up all ozone tests --- packages/dev-env/src/index.ts | 2 + packages/dev-env/src/moderator-client.ts | 95 +++- packages/dev-env/src/ozone.ts | 2 +- .../__snapshots__/get-record.test.ts.snap | 8 +- .../tests/__snapshots__/get-repo.test.ts.snap | 4 +- packages/ozone/tests/get-record.test.ts | 24 +- packages/ozone/tests/get-repo.test.ts | 14 +- .../ozone/tests/moderation-appeals.test.ts | 76 +--- .../ozone/tests/moderation-events.test.ts | 210 ++++----- .../tests/moderation-status-tags.test.ts | 47 +- .../ozone/tests/moderation-statuses.test.ts | 95 ++-- packages/ozone/tests/moderation.test.ts | 427 ++++++------------ packages/ozone/tests/repo-search.test.ts | 13 +- 13 files changed, 419 insertions(+), 598 deletions(-) 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/moderator-client.ts b/packages/dev-env/src/moderator-client.ts index 90550992d6b..cd21e6c1bd6 100644 --- a/packages/dev-env/src/moderator-client.ts +++ b/packages/dev-env/src/moderator-client.ts @@ -4,69 +4,79 @@ import { QueryParams as QueryStatusesParams } from '@atproto/api/src/client/type 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) { + async getEvent(id: number, role?: ModLevel) { const result = await this.agent.api.com.atproto.admin.getModerationEvent( { id }, { - headers: await this.ozone.modHeaders(), + headers: await this.ozone.modHeaders(role), }, ) return result.data } - async queryModerationStatuses(input: QueryStatusesParams) { + async queryModerationStatuses(input: QueryStatusesParams, role?: ModLevel) { const result = await this.agent.api.com.atproto.admin.queryModerationStatuses(input, { - headers: await this.ozone.modHeaders(), + headers: await this.ozone.modHeaders(role), }) return result.data } - async queryModerationEvents(input: QueryEventsParams) { + async queryModerationEvents(input: QueryEventsParams, role?: ModLevel) { const result = await this.agent.api.com.atproto.admin.queryModerationEvents( input, { - headers: await this.ozone.modHeaders(), + headers: await this.ozone.modHeaders(role), }, ) return result.data } - async emitModerationEvent(opts: { - event: TakeActionInput['event'] - subject: TakeActionInput['subject'] - reason?: string - createdBy?: string - meta?: TakeActionInput['meta'] - }) { + 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, createdBy, reason }, + { event, subject, subjectBlobCids, createdBy, reason }, { encoding: 'application/json', - headers: await this.ozone.modHeaders(), + headers: await this.ozone.modHeaders(role), }, ) return result.data } - async reverseModerationAction(opts: { - id: number - subject: TakeActionInput['subject'] - reason?: string - createdBy?: string - }) { + 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( { @@ -79,9 +89,50 @@ export class ModeratorClient { }, { encoding: 'application/json', - headers: await this.ozone.modHeaders(), + 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/ozone.ts b/packages/dev-env/src/ozone.ts index 39f69962505..addcc7c04ee 100644 --- a/packages/dev-env/src/ozone.ts +++ b/packages/dev-env/src/ozone.ts @@ -128,7 +128,7 @@ export class TestOzone { this.ctx.cfg.access.triage.push(did) } - async modHeaders(role: 'admin' | 'moderator' | 'triage' = 'admin') { + async modHeaders(role: 'admin' | 'moderator' | 'triage' = 'moderator') { const account = role === 'admin' ? this.adminAccnt 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/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 f34a24e8d3b..45ca3f37190 100644 --- a/packages/ozone/tests/get-repo.test.ts +++ b/packages/ozone/tests/get-repo.test.ts @@ -1,17 +1,22 @@ -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, REASONSPAM, } from '../src/lexicon/types/com/atproto/moderation/defs' import { forSnapshot } from './_util' -import { TestOzone } from '@atproto/dev-env/src/ozone' -import { ModeratorClient } from '@atproto/dev-env/src/moderator-client' describe('admin get repo view', () => { let network: TestNetwork let ozone: TestOzone let agent: AtpAgent + let pdsAgent: AtpAgent let sc: SeedClient let modClient: ModeratorClient @@ -21,6 +26,7 @@ describe('admin get repo view', () => { }) ozone = network.ozone agent = ozone.getClient() + pdsAgent = network.pds.getClient() sc = network.getSeedClient() modClient = ozone.getModClient() await basicSeed(sc) @@ -107,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', 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 fbe571a8172..a4b54708aed 100644 --- a/packages/ozone/tests/moderation-events.test.ts +++ b/packages/ozone/tests/moderation-events.test.ts @@ -1,9 +1,11 @@ import assert from 'node:assert' -import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' -import AtpAgent, { - ComAtprotoAdminDefs, - ComAtprotoAdminEmitModerationEvent, -} from '@atproto/api' +import { + TestNetwork, + SeedClient, + basicSeed, + ModeratorClient, +} from '@atproto/dev-env' +import { ComAtprotoAdminDefs } from '@atproto/api' import { forSnapshot } from './_util' import { REASONAPPEAL, @@ -13,23 +15,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 = { @@ -52,25 +39,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, }) } } @@ -79,9 +60,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() @@ -94,16 +74,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 () => { @@ -112,66 +92,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, }) @@ -184,15 +162,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) @@ -203,49 +181,49 @@ 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[5].id) }) 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', @@ -257,9 +235,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', @@ -271,36 +248,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 ({ @@ -310,7 +282,7 @@ describe('moderation-events', () => { add: string[] remove: string[] }) => - emitModerationEvent({ + modClient.emitModerationEvent({ event: { $type: 'com.atproto.admin.defs#modEventTag', comment: 'X', @@ -321,43 +293,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() }) }) @@ -366,7 +330,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', }, @@ -376,18 +340,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.adminAccnt.did, event: { $type: 'com.atproto.admin.defs#modEventTakedown', }, @@ -397,7 +356,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', }, @@ -406,15 +365,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.adminAccnt.did, event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown', }, 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 527611d5313..fa8679f50f3 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' @@ -12,21 +17,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 = { @@ -49,25 +41,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, }) } } @@ -76,9 +62,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() @@ -90,26 +75,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 () => { @@ -121,13 +106,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) @@ -139,13 +124,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({ @@ -164,7 +148,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', }, @@ -176,11 +160,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, @@ -190,7 +172,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', }, @@ -199,13 +181,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..61d1c30c76e 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,6 +11,7 @@ describe('admin repo search view', () => { let network: TestNetwork let agent: AtpAgent let sc: SeedClient + let modClient: ModeratorClient let headers: { [s: string]: string } beforeAll(async () => { @@ -14,8 +20,9 @@ describe('admin repo search view', () => { }) agent = network.pds.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', From e3bfb17202edce5e16b5025f435cf90645e86a69 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 19:24:06 -0600 Subject: [PATCH 14/50] fix admin auth test --- packages/bsky/src/auth-verifier.ts | 7 ++-- packages/ozone/src/api/well-known.ts | 4 +-- packages/pds/src/auth-verifier.ts | 5 ++- packages/pds/tests/admin-auth.test.ts | 49 +++++++++++++++++++-------- 4 files changed, 41 insertions(+), 24 deletions(-) diff --git a/packages/bsky/src/auth-verifier.ts b/packages/bsky/src/auth-verifier.ts index 02d89680636..4c5311d73fe 100644 --- a/packages/bsky/src/auth-verifier.ts +++ b/packages/bsky/src/auth-verifier.ts @@ -170,7 +170,7 @@ export class AuthVerifier { modService = async (reqCtx: ReqCtx): Promise => { const { iss, aud } = await this.verifyServiceJwt(reqCtx, { aud: this.ownDid, - iss: [this.modServiceDid, `${this.modServiceDid}#atproto-mod`], + iss: [this.modServiceDid, `${this.modServiceDid}#atproto_mod`], }) return { credentials: { type: 'mod_service', aud, iss } } } @@ -209,7 +209,7 @@ export class AuthVerifier { if (opts.iss !== null && !opts.iss.includes(iss)) { throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss') } - const [did, serviceId] = iss.split('#') + const [did, keyId = 'atproto'] = iss.split('#') let identity: GetIdentityByDidResponse try { identity = await this.dataplane.getIdentityByDid({ did }) @@ -220,7 +220,6 @@ export class AuthVerifier { throw err } const keys = unpackIdentityKeys(identity.keys) - const keyId = serviceId === 'atproto-mod' ? 'atproto-mod-key' : 'atproto' const didKey = getKeyAsDidKey(keys, { id: keyId }) if (!didKey) { throw new AuthRequiredError('missing or bad key') @@ -237,7 +236,7 @@ export class AuthVerifier { } isModService(iss: string): boolean { - return [this.modServiceDid, `${this.modServiceDid}#atproto-mod`].includes( + return [this.modServiceDid, `${this.modServiceDid}#atproto_mod`].includes( iss, ) } diff --git a/packages/ozone/src/api/well-known.ts b/packages/ozone/src/api/well-known.ts index 9cbfa9efe53..29a9f6d3636 100644 --- a/packages/ozone/src/api/well-known.ts +++ b/packages/ozone/src/api/well-known.ts @@ -15,7 +15,7 @@ export const createRouter = (ctx: AppContext): express.Router => { id: ctx.cfg.service.did, verificationMethod: [ { - id: `${ctx.cfg.service.did}#atproto`, + id: `${ctx.cfg.service.did}#atproto_mod`, type: 'Multikey', controller: ctx.cfg.service.did, publicKeyMultibase: ctx.signingKey.did().replace('did:key:', ''), @@ -23,7 +23,7 @@ export const createRouter = (ctx: AppContext): express.Router => { ], service: [ { - id: '#atproto_mod', + id: '#atproto_mod_srvc', type: 'AtprotoModerationService', serviceEndpoint: `https://${hostname}`, }, diff --git a/packages/pds/src/auth-verifier.ts b/packages/pds/src/auth-verifier.ts index ea2ebeb868d..5f1b11fd29c 100644 --- a/packages/pds/src/auth-verifier.ts +++ b/packages/pds/src/auth-verifier.ts @@ -260,7 +260,7 @@ export class AuthVerifier { } const payload = await this.verifyServiceJwt(reqCtx, { aud: this.dids.entryway ?? this.dids.pds, - iss: [this.dids.modService, `${this.dids.modService}#atproto-mod`], + iss: [this.dids.modService, `${this.dids.modService}#atproto_mod`], }) return { credentials: { @@ -344,12 +344,11 @@ export class AuthVerifier { if (opts.iss !== null && !opts.iss.includes(iss)) { throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss') } - const [did, serviceId] = iss.split('#') + const [did, keyId = 'atproto'] = iss.split('#') const didDoc = await this.idResolver.did.resolve(did, forceRefresh) if (!didDoc) { throw new AuthRequiredError('could not resolve iss did') } - const keyId = serviceId === 'atproto-mod' ? 'atproto-mod-key' : 'atproto' const parsedKey = getVerificationMaterial(didDoc, keyId) if (!parsedKey) { throw new AuthRequiredError('missing or bad key in did doc') diff --git a/packages/pds/tests/admin-auth.test.ts b/packages/pds/tests/admin-auth.test.ts index dffd9261874..191f7a5daf4 100644 --- a/packages/pds/tests/admin-auth.test.ts +++ b/packages/pds/tests/admin-auth.test.ts @@ -1,7 +1,8 @@ import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import AtpAgent from '@atproto/api' -import { Secp256k1Keypair } from '@atproto/crypto' +import { Keypair, Secp256k1Keypair } from '@atproto/crypto' import { createServiceAuthHeaders } from '@atproto/xrpc-server' +import * as plc from '@did-plc/lib' import usersSeed from './seeds/users' import { RepoRef } from '../src/lexicon/types/com/atproto/admin/defs' @@ -12,32 +13,50 @@ describe('admin auth', () => { let repoSubject: RepoRef - const modServiceDid = 'did:example:mod' - const altModDid = 'did:example:alt' + let modServiceDid: string + let altModDid: string let modServiceKey: Secp256k1Keypair let pdsDid: string + const opAndDid = async (handle: string, key: Keypair) => { + const op = await plc.signOperation( + { + type: 'plc_operation', + alsoKnownAs: [handle], + verificationMethods: { + atproto: key.did(), + }, + rotationKeys: [key.did()], + services: {}, + prev: null, + }, + key, + ) + const did = await plc.didForCreateOp(op) + return { op, did } + } + beforeAll(async () => { + // kinda goofy but we need to know the dids before creating the testnet for the PDS's config + modServiceKey = await Secp256k1Keypair.create() + const modServiceInfo = await opAndDid('mod.test', modServiceKey) + const altModInfo = await opAndDid('alt-mod.test', modServiceKey) + modServiceDid = modServiceInfo.did + altModDid = altModInfo.did + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'pds_admin_auth', pds: { - modServiceDid, + modServiceDid: modServiceInfo.did, + modServiceUrl: 'https://mod.invalid', }, }) pdsDid = network.pds.ctx.cfg.service.did - modServiceKey = await Secp256k1Keypair.create() - const origResolve = network.pds.ctx.idResolver.did.resolveAtprotoKey - network.pds.ctx.idResolver.did.resolveAtprotoKey = async ( - did: string, - forceRefresh?: boolean, - ) => { - if (did === modServiceDid || did === altModDid) { - return modServiceKey.did() - } - return origResolve(did, forceRefresh) - } + const plcClient = network.plc.getClient() + await plcClient.sendOperation(modServiceInfo.did, modServiceInfo.op) + await plcClient.sendOperation(altModInfo.did, altModInfo.op) agent = network.pds.getClient() sc = network.getSeedClient() From a642063979df3e9d10f8e0cdc250fd9132046084 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 19:24:41 -0600 Subject: [PATCH 15/50] rename test --- .../pds/tests/{admin-auth.test.ts => moderator-auth.test.ts} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename packages/pds/tests/{admin-auth.test.ts => moderator-auth.test.ts} (98%) diff --git a/packages/pds/tests/admin-auth.test.ts b/packages/pds/tests/moderator-auth.test.ts similarity index 98% rename from packages/pds/tests/admin-auth.test.ts rename to packages/pds/tests/moderator-auth.test.ts index 191f7a5daf4..0b89b4f0fd7 100644 --- a/packages/pds/tests/admin-auth.test.ts +++ b/packages/pds/tests/moderator-auth.test.ts @@ -6,7 +6,7 @@ import * as plc from '@did-plc/lib' import usersSeed from './seeds/users' import { RepoRef } from '../src/lexicon/types/com/atproto/admin/defs' -describe('admin auth', () => { +describe('moderator auth', () => { let network: TestNetworkNoAppView let agent: AtpAgent let sc: SeedClient @@ -45,7 +45,7 @@ describe('admin auth', () => { altModDid = altModInfo.did network = await TestNetworkNoAppView.create({ - dbPostgresSchema: 'pds_admin_auth', + dbPostgresSchema: 'pds_moderator_auth', pds: { modServiceDid: modServiceInfo.did, modServiceUrl: 'https://mod.invalid', From bac2b57bbac02a62c5c0ddbd4b9dbd30f00ba3c0 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 20:31:36 -0600 Subject: [PATCH 16/50] fix ozone test --- .../ozone/tests/communication-templates.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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() From d599dd749ad54640ca0976bf1051e9d5fa952b89 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 20:52:29 -0600 Subject: [PATCH 17/50] clean up tokens in pds --- .../pds/src/api/app/bsky/actor/getProfile.ts | 2 +- .../src/api/app/bsky/feed/getActorLikes.ts | 2 +- .../src/api/app/bsky/feed/getAuthorFeed.ts | 2 +- .../src/api/app/bsky/feed/getPostThread.ts | 2 +- .../src/api/app/bsky/graph/getFollowers.ts | 2 +- .../pds/src/api/app/bsky/graph/getFollows.ts | 2 +- .../admin/createCommunicationTemplate.ts | 2 +- .../api/com/atproto/admin/deleteAccount.ts | 8 +- .../admin/deleteCommunicationTemplate.ts | 2 +- .../atproto/admin/disableAccountInvites.ts | 9 +- .../com/atproto/admin/disableInviteCodes.ts | 9 +- .../com/atproto/admin/emitModerationEvent.ts | 2 +- .../com/atproto/admin/enableAccountInvites.ts | 9 +- .../api/com/atproto/admin/getAccountInfo.ts | 2 +- .../api/com/atproto/admin/getInviteCodes.ts | 2 +- .../com/atproto/admin/getModerationEvent.ts | 2 +- .../src/api/com/atproto/admin/getRecord.ts | 2 +- .../pds/src/api/com/atproto/admin/getRepo.ts | 2 +- .../api/com/atproto/admin/getSubjectStatus.ts | 2 +- .../admin/listCommunicationTemplates.ts | 2 +- .../atproto/admin/queryModerationEvents.ts | 2 +- .../atproto/admin/queryModerationStatuses.ts | 2 +- .../src/api/com/atproto/admin/searchRepos.ts | 2 +- .../com/atproto/admin/updateAccountEmail.ts | 9 +- .../com/atproto/admin/updateAccountHandle.ts | 10 +- .../atproto/admin/updateAccountPassword.ts | 11 +-- .../admin/updateCommunicationTemplate.ts | 2 +- .../com/atproto/admin/updateSubjectStatus.ts | 13 +-- .../com/atproto/server/createInviteCode.ts | 9 +- .../com/atproto/server/createInviteCodes.ts | 9 +- .../atproto/sync/deprecated/getCheckout.ts | 2 +- .../com/atproto/sync/deprecated/getHead.ts | 2 +- .../pds/src/api/com/atproto/sync/getBlob.ts | 2 +- .../pds/src/api/com/atproto/sync/getBlocks.ts | 2 +- .../api/com/atproto/sync/getLatestCommit.ts | 2 +- .../pds/src/api/com/atproto/sync/getRecord.ts | 2 +- .../pds/src/api/com/atproto/sync/getRepo.ts | 2 +- .../pds/src/api/com/atproto/sync/listBlobs.ts | 2 +- packages/pds/src/auth-verifier.ts | 95 ++++++------------- packages/pds/src/config/env.ts | 4 - packages/pds/src/config/secrets.ts | 5 - packages/pds/src/context.ts | 2 - 42 files changed, 85 insertions(+), 173 deletions(-) diff --git a/packages/pds/src/api/app/bsky/actor/getProfile.ts b/packages/pds/src/api/app/bsky/actor/getProfile.ts index 74de7f3af6d..69ed5b6f3ec 100644 --- a/packages/pds/src/api/app/bsky/actor/getProfile.ts +++ b/packages/pds/src/api/app/bsky/actor/getProfile.ts @@ -15,7 +15,7 @@ export default function (server: Server, ctx: AppContext) { const { bskyAppView } = ctx.cfg if (!bskyAppView) return server.app.bsky.actor.getProfile({ - auth: ctx.authVerifier.accessOrRole, + auth: ctx.authVerifier.access, handler: async ({ req, auth, params }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null diff --git a/packages/pds/src/api/app/bsky/feed/getActorLikes.ts b/packages/pds/src/api/app/bsky/feed/getActorLikes.ts index d8e2f839904..6312f6b1b02 100644 --- a/packages/pds/src/api/app/bsky/feed/getActorLikes.ts +++ b/packages/pds/src/api/app/bsky/feed/getActorLikes.ts @@ -15,7 +15,7 @@ export default function (server: Server, ctx: AppContext) { const { bskyAppView } = ctx.cfg if (!bskyAppView) return server.app.bsky.feed.getActorLikes({ - auth: ctx.authVerifier.accessOrRole, + auth: ctx.authVerifier.access, handler: async ({ req, params, auth }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null diff --git a/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts index c90760bbfd9..7b09f97732b 100644 --- a/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts @@ -16,7 +16,7 @@ export default function (server: Server, ctx: AppContext) { const { bskyAppView } = ctx.cfg if (!bskyAppView) return server.app.bsky.feed.getAuthorFeed({ - auth: ctx.authVerifier.accessOrRole, + auth: ctx.authVerifier.access, handler: async ({ req, params, auth }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null diff --git a/packages/pds/src/api/app/bsky/feed/getPostThread.ts b/packages/pds/src/api/app/bsky/feed/getPostThread.ts index da09523875b..0efb06e7d63 100644 --- a/packages/pds/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/pds/src/api/app/bsky/feed/getPostThread.ts @@ -30,7 +30,7 @@ export default function (server: Server, ctx: AppContext) { const { bskyAppView } = ctx.cfg if (!bskyAppView) return server.app.bsky.feed.getPostThread({ - auth: ctx.authVerifier.accessOrRole, + auth: ctx.authVerifier.access, handler: async ({ req, params, auth }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null diff --git a/packages/pds/src/api/app/bsky/graph/getFollowers.ts b/packages/pds/src/api/app/bsky/graph/getFollowers.ts index 0a158f2bbe5..adc58520530 100644 --- a/packages/pds/src/api/app/bsky/graph/getFollowers.ts +++ b/packages/pds/src/api/app/bsky/graph/getFollowers.ts @@ -7,7 +7,7 @@ export default function (server: Server, ctx: AppContext) { const { bskyAppView } = ctx.cfg if (!bskyAppView) return server.app.bsky.graph.getFollowers({ - auth: ctx.authVerifier.accessOrRole, + auth: ctx.authVerifier.access, handler: async ({ req, params, auth }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null diff --git a/packages/pds/src/api/app/bsky/graph/getFollows.ts b/packages/pds/src/api/app/bsky/graph/getFollows.ts index 6802acda888..e021c8f1d36 100644 --- a/packages/pds/src/api/app/bsky/graph/getFollows.ts +++ b/packages/pds/src/api/app/bsky/graph/getFollows.ts @@ -7,7 +7,7 @@ export default function (server: Server, ctx: AppContext) { const { bskyAppView } = ctx.cfg if (!bskyAppView) return server.app.bsky.graph.getFollows({ - auth: ctx.authVerifier.accessOrRole, + auth: ctx.authVerifier.access, handler: async ({ req, params, auth }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null diff --git a/packages/pds/src/api/com/atproto/admin/createCommunicationTemplate.ts b/packages/pds/src/api/com/atproto/admin/createCommunicationTemplate.ts index 7b6939270a2..484c5022c80 100644 --- a/packages/pds/src/api/com/atproto/admin/createCommunicationTemplate.ts +++ b/packages/pds/src/api/com/atproto/admin/createCommunicationTemplate.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { const { moderationAgent } = ctx if (!moderationAgent) return server.com.atproto.admin.createCommunicationTemplate({ - auth: ctx.authVerifier.role, + auth: ctx.authVerifier.access, handler: async ({ req, input }) => { const { data: result } = await moderationAgent.com.atproto.admin.createCommunicationTemplate( 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/deleteCommunicationTemplate.ts b/packages/pds/src/api/com/atproto/admin/deleteCommunicationTemplate.ts index d10c2564571..170ff027e6e 100644 --- a/packages/pds/src/api/com/atproto/admin/deleteCommunicationTemplate.ts +++ b/packages/pds/src/api/com/atproto/admin/deleteCommunicationTemplate.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { const { moderationAgent } = ctx if (!moderationAgent) return server.com.atproto.admin.deleteCommunicationTemplate({ - auth: ctx.authVerifier.role, + auth: ctx.authVerifier.access, handler: async ({ req, input }) => { await moderationAgent.com.atproto.admin.deleteCommunicationTemplate( input.body, 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/emitModerationEvent.ts b/packages/pds/src/api/com/atproto/admin/emitModerationEvent.ts index 65bb4c36d0e..658f1061000 100644 --- a/packages/pds/src/api/com/atproto/admin/emitModerationEvent.ts +++ b/packages/pds/src/api/com/atproto/admin/emitModerationEvent.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { const { moderationAgent } = ctx if (!moderationAgent) return server.com.atproto.admin.emitModerationEvent({ - auth: ctx.authVerifier.role, + auth: ctx.authVerifier.access, handler: async ({ req, input }) => { const { data: result } = await moderationAgent.com.atproto.admin.emitModerationEvent( diff --git a/packages/pds/src/api/com/atproto/admin/enableAccountInvites.ts b/packages/pds/src/api/com/atproto/admin/enableAccountInvites.ts index 7d809e114d8..e8f8adecc55 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.access, + 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/getModerationEvent.ts b/packages/pds/src/api/com/atproto/admin/getModerationEvent.ts index a5e579baa58..887019d82e6 100644 --- a/packages/pds/src/api/com/atproto/admin/getModerationEvent.ts +++ b/packages/pds/src/api/com/atproto/admin/getModerationEvent.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { const { moderationAgent } = ctx if (!moderationAgent) return server.com.atproto.admin.getModerationEvent({ - auth: ctx.authVerifier.role, + auth: ctx.authVerifier.moderator, handler: async ({ req, params }) => { const { data } = await moderationAgent.com.atproto.admin.getModerationEvent( diff --git a/packages/pds/src/api/com/atproto/admin/getRecord.ts b/packages/pds/src/api/com/atproto/admin/getRecord.ts index 3cff5508683..20b2da29da4 100644 --- a/packages/pds/src/api/com/atproto/admin/getRecord.ts +++ b/packages/pds/src/api/com/atproto/admin/getRecord.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { const { moderationAgent } = ctx if (!moderationAgent) return server.com.atproto.admin.getRecord({ - auth: ctx.authVerifier.role, + auth: ctx.authVerifier.moderator, handler: async ({ req, params }) => { const { data: recordDetailAppview } = await moderationAgent.com.atproto.admin.getRecord( diff --git a/packages/pds/src/api/com/atproto/admin/getRepo.ts b/packages/pds/src/api/com/atproto/admin/getRepo.ts index 880b407ce79..c2bc9eb1552 100644 --- a/packages/pds/src/api/com/atproto/admin/getRepo.ts +++ b/packages/pds/src/api/com/atproto/admin/getRepo.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { const { moderationAgent } = ctx if (!moderationAgent) return server.com.atproto.admin.getRepo({ - auth: ctx.authVerifier.role, + auth: ctx.authVerifier.moderator, handler: async ({ req, params }) => { const res = await moderationAgent.com.atproto.admin.getRepo( params, 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/listCommunicationTemplates.ts b/packages/pds/src/api/com/atproto/admin/listCommunicationTemplates.ts index dfe3a74bce8..6b715b79110 100644 --- a/packages/pds/src/api/com/atproto/admin/listCommunicationTemplates.ts +++ b/packages/pds/src/api/com/atproto/admin/listCommunicationTemplates.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { const { moderationAgent } = ctx if (!moderationAgent) return server.com.atproto.admin.listCommunicationTemplates({ - auth: ctx.authVerifier.role, + auth: ctx.authVerifier.access, handler: async ({ req }) => { const { data: result } = await moderationAgent.com.atproto.admin.listCommunicationTemplates( diff --git a/packages/pds/src/api/com/atproto/admin/queryModerationEvents.ts b/packages/pds/src/api/com/atproto/admin/queryModerationEvents.ts index 2d33ca6d466..866bd611ed5 100644 --- a/packages/pds/src/api/com/atproto/admin/queryModerationEvents.ts +++ b/packages/pds/src/api/com/atproto/admin/queryModerationEvents.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { const { moderationAgent } = ctx if (!moderationAgent) return server.com.atproto.admin.queryModerationEvents({ - auth: ctx.authVerifier.role, + auth: ctx.authVerifier.access, handler: async ({ req, params }) => { const { data: result } = await moderationAgent.com.atproto.admin.queryModerationEvents( diff --git a/packages/pds/src/api/com/atproto/admin/queryModerationStatuses.ts b/packages/pds/src/api/com/atproto/admin/queryModerationStatuses.ts index c31125ce114..30999631c41 100644 --- a/packages/pds/src/api/com/atproto/admin/queryModerationStatuses.ts +++ b/packages/pds/src/api/com/atproto/admin/queryModerationStatuses.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { const { moderationAgent } = ctx if (!moderationAgent) return server.com.atproto.admin.queryModerationStatuses({ - auth: ctx.authVerifier.role, + auth: ctx.authVerifier.access, handler: async ({ req, params }) => { const { data } = await moderationAgent.com.atproto.admin.queryModerationStatuses( diff --git a/packages/pds/src/api/com/atproto/admin/searchRepos.ts b/packages/pds/src/api/com/atproto/admin/searchRepos.ts index d09ff7b2327..14eb73aa84f 100644 --- a/packages/pds/src/api/com/atproto/admin/searchRepos.ts +++ b/packages/pds/src/api/com/atproto/admin/searchRepos.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { const { moderationAgent } = ctx if (!moderationAgent) return server.com.atproto.admin.searchRepos({ - auth: ctx.authVerifier.role, + auth: ctx.authVerifier.access, handler: async ({ req, params }) => { const { data: result } = await moderationAgent.com.atproto.admin.searchRepos( 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/updateCommunicationTemplate.ts b/packages/pds/src/api/com/atproto/admin/updateCommunicationTemplate.ts index c548a83bf03..a634fde2534 100644 --- a/packages/pds/src/api/com/atproto/admin/updateCommunicationTemplate.ts +++ b/packages/pds/src/api/com/atproto/admin/updateCommunicationTemplate.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { const { moderationAgent } = ctx if (!moderationAgent) return server.com.atproto.admin.updateCommunicationTemplate({ - auth: ctx.authVerifier.role, + auth: ctx.authVerifier.access, handler: async ({ req, input }) => { const { data: result } = await moderationAgent.com.atproto.admin.updateCommunicationTemplate( 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 5f1b11fd29c..0e60633c0d2 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() } } @@ -271,13 +243,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) } } @@ -368,36 +340,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 } } @@ -411,6 +370,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 fb5aed8232f..9cc971b047d 100644 --- a/packages/pds/src/config/env.ts +++ b/packages/pds/src/config/env.ts @@ -96,8 +96,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'), @@ -201,8 +199,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 4f852af1497..b0457767a7c 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, From 08dc9a997cddfd7f37cb912b7af758018a85bd39 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 21:09:22 -0600 Subject: [PATCH 18/50] fix up pds tests --- packages/dev-env/src/bsky.ts | 4 +- packages/dev-env/src/const.ts | 2 - packages/dev-env/src/pds.ts | 26 +++------ .../com/atproto/admin/enableAccountInvites.ts | 2 +- .../src/api/com/atproto/admin/sendEmail.ts | 12 ++--- packages/pds/tests/account-deletion.test.ts | 17 ++---- packages/pds/tests/account.test.ts | 38 +------------ packages/pds/tests/handles.test.ts | 22 -------- packages/pds/tests/invites-admin.test.ts | 47 ---------------- packages/pds/tests/moderation.test.ts | 53 ++++--------------- 10 files changed, 30 insertions(+), 193 deletions(-) diff --git a/packages/dev-env/src/bsky.ts b/packages/dev-env/src/bsky.ts index 461ef4d07df..2d2c18e4df3 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/pds.ts b/packages/dev-env/src/pds.ts index 44b8a063fce..f939db07dbc 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( @@ -40,8 +35,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', @@ -71,22 +64,19 @@ export class TestPds { return new AtpAgent({ service: `http://localhost:${this.port}` }) } - 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/pds/src/api/com/atproto/admin/enableAccountInvites.ts b/packages/pds/src/api/com/atproto/admin/enableAccountInvites.ts index e8f8adecc55..d65bd781194 100644 --- a/packages/pds/src/api/com/atproto/admin/enableAccountInvites.ts +++ b/packages/pds/src/api/com/atproto/admin/enableAccountInvites.ts @@ -4,7 +4,7 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.enableAccountInvites({ - auth: ctx.authVerifier.access, + auth: ctx.authVerifier.moderator, handler: async ({ input }) => { if (ctx.cfg.entryway) { throw new InvalidRequestError( diff --git a/packages/pds/src/api/com/atproto/admin/sendEmail.ts b/packages/pds/src/api/com/atproto/admin/sendEmail.ts index e23d6bea5c1..d66f2dad94f 100644 --- a/packages/pds/src/api/com/atproto/admin/sendEmail.ts +++ b/packages/pds/src/api/com/atproto/admin/sendEmail.ts @@ -1,16 +1,14 @@ -import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' +import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { authPassthru, resultPassthru } from '../../../proxy' +// @TODO this needs to be refactored to come from ozone + export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.sendEmail({ - auth: ctx.authVerifier.role, - handler: async ({ req, input, auth }) => { - if (!auth.credentials.admin && !auth.credentials.moderator) { - throw new AuthRequiredError('Insufficient privileges') - } - + auth: ctx.authVerifier.moderator, + handler: async ({ req, input }) => { const { content, recipientDid, 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 From 87478697d9646b4148c6a86579679e6fd1d7a3bb Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 21:14:31 -0600 Subject: [PATCH 19/50] fix up ozone tests --- packages/ozone/tests/moderation-events.test.ts | 4 ++-- packages/ozone/tests/repo-search.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ozone/tests/moderation-events.test.ts b/packages/ozone/tests/moderation-events.test.ts index a4b54708aed..48c3a7a1ea0 100644 --- a/packages/ozone/tests/moderation-events.test.ts +++ b/packages/ozone/tests/moderation-events.test.ts @@ -346,7 +346,7 @@ describe('moderation-events', () => { types: ['com.atproto.admin.defs#modEventTakedown'], }) expect(result.events[0]).toMatchObject({ - createdBy: network.ozone.adminAccnt.did, + createdBy: network.ozone.moderatorAccnt.did, event: { $type: 'com.atproto.admin.defs#modEventTakedown', }, @@ -370,7 +370,7 @@ describe('moderation-events', () => { subject: post.ref.uriStr, }) expect(result.events[0]).toMatchObject({ - createdBy: network.ozone.adminAccnt.did, + createdBy: network.ozone.moderatorAccnt.did, event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown', }, diff --git a/packages/ozone/tests/repo-search.test.ts b/packages/ozone/tests/repo-search.test.ts index 61d1c30c76e..8b999b261b8 100644 --- a/packages/ozone/tests/repo-search.test.ts +++ b/packages/ozone/tests/repo-search.test.ts @@ -18,7 +18,7 @@ describe('admin repo search view', () => { 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) From 88c24121783c09a7a869c2b5513b5a6abc5a5edd Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 22:01:02 -0600 Subject: [PATCH 20/50] add pipethrough to write routes --- .../pds/src/api/app/bsky/graph/muteActor.ts | 11 +- .../src/api/app/bsky/graph/muteActorList.ts | 11 +- .../pds/src/api/app/bsky/graph/unmuteActor.ts | 11 +- .../src/api/app/bsky/graph/unmuteActorList.ts | 11 +- .../api/app/bsky/notification/updateSeen.ts | 11 +- .../admin/createCommunicationTemplate.ts | 19 +-- .../admin/deleteCommunicationTemplate.ts | 14 +- .../com/atproto/admin/emitModerationEvent.ts | 19 +-- .../com/atproto/admin/getModerationEvent.ts | 19 +-- .../src/api/com/atproto/admin/getRecord.ts | 19 +-- .../pds/src/api/com/atproto/admin/getRepo.ts | 18 +- .../admin/listCommunicationTemplates.ts | 19 +-- .../atproto/admin/queryModerationEvents.ts | 19 +-- .../atproto/admin/queryModerationStatuses.ts | 19 +-- .../src/api/com/atproto/admin/searchRepos.ts | 19 +-- .../admin/updateCommunicationTemplate.ts | 20 +-- .../com/atproto/moderation/createReport.ts | 22 +-- packages/pds/src/pipethrough.ts | 154 ++++++++++++------ 18 files changed, 174 insertions(+), 261 deletions(-) diff --git a/packages/pds/src/api/app/bsky/graph/muteActor.ts b/packages/pds/src/api/app/bsky/graph/muteActor.ts index c88a05b9eaf..9613e8736d0 100644 --- a/packages/pds/src/api/app/bsky/graph/muteActor.ts +++ b/packages/pds/src/api/app/bsky/graph/muteActor.ts @@ -1,18 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { pipethroughProcedure } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { - const { appViewAgent } = ctx - if (!appViewAgent) return server.app.bsky.graph.muteActor({ auth: ctx.authVerifier.access, - handler: async ({ auth, input }) => { + handler: async ({ auth, input, req }) => { const requester = auth.credentials.did - - await appViewAgent.api.app.bsky.graph.muteActor(input.body, { - ...(await ctx.appviewAuthHeaders(requester)), - encoding: 'application/json', - }) + await pipethroughProcedure(ctx, req, input.body, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/muteActorList.ts b/packages/pds/src/api/app/bsky/graph/muteActorList.ts index 74c2357d3d9..41e74ccd713 100644 --- a/packages/pds/src/api/app/bsky/graph/muteActorList.ts +++ b/packages/pds/src/api/app/bsky/graph/muteActorList.ts @@ -1,18 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { pipethroughProcedure } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { - const { appViewAgent } = ctx - if (!appViewAgent) return server.app.bsky.graph.muteActorList({ auth: ctx.authVerifier.access, - handler: async ({ auth, input }) => { + handler: async ({ auth, input, req }) => { const requester = auth.credentials.did - - await appViewAgent.api.app.bsky.graph.muteActorList(input.body, { - ...(await ctx.appviewAuthHeaders(requester)), - encoding: 'application/json', - }) + await pipethroughProcedure(ctx, req, input.body, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/unmuteActor.ts b/packages/pds/src/api/app/bsky/graph/unmuteActor.ts index e73c5d08e5a..8d9898f442e 100644 --- a/packages/pds/src/api/app/bsky/graph/unmuteActor.ts +++ b/packages/pds/src/api/app/bsky/graph/unmuteActor.ts @@ -1,18 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { pipethroughProcedure } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { - const { appViewAgent } = ctx - if (!appViewAgent) return server.app.bsky.graph.unmuteActor({ auth: ctx.authVerifier.access, - handler: async ({ auth, input }) => { + handler: async ({ auth, input, req }) => { const requester = auth.credentials.did - - await appViewAgent.api.app.bsky.graph.unmuteActor(input.body, { - ...(await ctx.appviewAuthHeaders(requester)), - encoding: 'application/json', - }) + await pipethroughProcedure(ctx, req, input.body, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/graph/unmuteActorList.ts b/packages/pds/src/api/app/bsky/graph/unmuteActorList.ts index e36afeaf0a3..dc6e254fbb4 100644 --- a/packages/pds/src/api/app/bsky/graph/unmuteActorList.ts +++ b/packages/pds/src/api/app/bsky/graph/unmuteActorList.ts @@ -1,18 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { pipethroughProcedure } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { - const { appViewAgent } = ctx - if (!appViewAgent) return server.app.bsky.graph.unmuteActorList({ auth: ctx.authVerifier.access, - handler: async ({ auth, input }) => { + handler: async ({ auth, input, req }) => { const requester = auth.credentials.did - - await appViewAgent.api.app.bsky.graph.unmuteActorList(input.body, { - ...(await ctx.appviewAuthHeaders(requester)), - encoding: 'application/json', - }) + await pipethroughProcedure(ctx, req, input.body, requester) }, }) } diff --git a/packages/pds/src/api/app/bsky/notification/updateSeen.ts b/packages/pds/src/api/app/bsky/notification/updateSeen.ts index 18a0ea3fa11..e7a97f3eddd 100644 --- a/packages/pds/src/api/app/bsky/notification/updateSeen.ts +++ b/packages/pds/src/api/app/bsky/notification/updateSeen.ts @@ -1,18 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { pipethroughProcedure } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { - const { appViewAgent } = ctx - if (!appViewAgent) return server.app.bsky.notification.updateSeen({ auth: ctx.authVerifier.access, - handler: async ({ input, auth }) => { + handler: async ({ input, auth, req }) => { const requester = auth.credentials.did - - await appViewAgent.api.app.bsky.notification.updateSeen(input.body, { - ...(await ctx.appviewAuthHeaders(requester)), - encoding: 'application/json', - }) + await pipethroughProcedure(ctx, req, input.body, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/createCommunicationTemplate.ts b/packages/pds/src/api/com/atproto/admin/createCommunicationTemplate.ts index 7b6939270a2..d3d1ebfd968 100644 --- a/packages/pds/src/api/com/atproto/admin/createCommunicationTemplate.ts +++ b/packages/pds/src/api/com/atproto/admin/createCommunicationTemplate.ts @@ -1,22 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethroughProcedure } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { - const { moderationAgent } = ctx - if (!moderationAgent) return server.com.atproto.admin.createCommunicationTemplate({ - auth: ctx.authVerifier.role, - handler: async ({ req, input }) => { - const { data: result } = - await moderationAgent.com.atproto.admin.createCommunicationTemplate( - input.body, - authPassthru(req, true), - ) - return { - encoding: 'application/json', - body: result, - } + auth: ctx.authVerifier.access, + handler: async ({ req, input, auth }) => { + const requester = auth.credentials.did + return pipethroughProcedure(ctx, req, input.body, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/deleteCommunicationTemplate.ts b/packages/pds/src/api/com/atproto/admin/deleteCommunicationTemplate.ts index d10c2564571..7a612dc1162 100644 --- a/packages/pds/src/api/com/atproto/admin/deleteCommunicationTemplate.ts +++ b/packages/pds/src/api/com/atproto/admin/deleteCommunicationTemplate.ts @@ -1,17 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethroughProcedure } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { - const { moderationAgent } = ctx - if (!moderationAgent) return server.com.atproto.admin.deleteCommunicationTemplate({ - auth: ctx.authVerifier.role, - handler: async ({ req, input }) => { - await moderationAgent.com.atproto.admin.deleteCommunicationTemplate( - input.body, - authPassthru(req, true), - ) + auth: ctx.authVerifier.access, + handler: async ({ req, input, auth }) => { + const requester = auth.credentials.did + await pipethroughProcedure(ctx, req, input.body, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/emitModerationEvent.ts b/packages/pds/src/api/com/atproto/admin/emitModerationEvent.ts index 65bb4c36d0e..1658720d674 100644 --- a/packages/pds/src/api/com/atproto/admin/emitModerationEvent.ts +++ b/packages/pds/src/api/com/atproto/admin/emitModerationEvent.ts @@ -1,22 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethroughProcedure } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { - const { moderationAgent } = ctx - if (!moderationAgent) return server.com.atproto.admin.emitModerationEvent({ - auth: ctx.authVerifier.role, - handler: async ({ req, input }) => { - const { data: result } = - await moderationAgent.com.atproto.admin.emitModerationEvent( - input.body, - authPassthru(req, true), - ) - return { - encoding: 'application/json', - body: result, - } + auth: ctx.authVerifier.access, + handler: async ({ req, input, auth }) => { + const requester = auth.credentials.did + return pipethroughProcedure(ctx, req, input.body, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/getModerationEvent.ts b/packages/pds/src/api/com/atproto/admin/getModerationEvent.ts index a5e579baa58..9d81d7dee59 100644 --- a/packages/pds/src/api/com/atproto/admin/getModerationEvent.ts +++ b/packages/pds/src/api/com/atproto/admin/getModerationEvent.ts @@ -1,22 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethrough } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { - const { moderationAgent } = ctx - if (!moderationAgent) return server.com.atproto.admin.getModerationEvent({ - auth: ctx.authVerifier.role, - handler: async ({ req, params }) => { - const { data } = - await moderationAgent.com.atproto.admin.getModerationEvent( - params, - authPassthru(req), - ) - return { - encoding: 'application/json', - body: data, - } + auth: ctx.authVerifier.access, + handler: async ({ req, auth }) => { + const requester = auth.credentials.did + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/getRecord.ts b/packages/pds/src/api/com/atproto/admin/getRecord.ts index 3cff5508683..52c7686b5b1 100644 --- a/packages/pds/src/api/com/atproto/admin/getRecord.ts +++ b/packages/pds/src/api/com/atproto/admin/getRecord.ts @@ -1,22 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethrough } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { - const { moderationAgent } = ctx - if (!moderationAgent) return server.com.atproto.admin.getRecord({ - auth: ctx.authVerifier.role, - handler: async ({ req, params }) => { - const { data: recordDetailAppview } = - await moderationAgent.com.atproto.admin.getRecord( - params, - authPassthru(req), - ) - return { - encoding: 'application/json', - body: recordDetailAppview, - } + auth: ctx.authVerifier.access, + handler: async ({ req, auth }) => { + const requester = auth.credentials.did + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/getRepo.ts b/packages/pds/src/api/com/atproto/admin/getRepo.ts index 880b407ce79..d380570c16b 100644 --- a/packages/pds/src/api/com/atproto/admin/getRepo.ts +++ b/packages/pds/src/api/com/atproto/admin/getRepo.ts @@ -1,21 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethrough } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { - const { moderationAgent } = ctx - if (!moderationAgent) return server.com.atproto.admin.getRepo({ - auth: ctx.authVerifier.role, - handler: async ({ req, params }) => { - const res = await moderationAgent.com.atproto.admin.getRepo( - params, - authPassthru(req), - ) - return { - encoding: 'application/json', - body: res.data, - } + auth: ctx.authVerifier.access, + handler: async ({ req, auth }) => { + const requester = auth.credentials.did + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/listCommunicationTemplates.ts b/packages/pds/src/api/com/atproto/admin/listCommunicationTemplates.ts index dfe3a74bce8..520e0b68c97 100644 --- a/packages/pds/src/api/com/atproto/admin/listCommunicationTemplates.ts +++ b/packages/pds/src/api/com/atproto/admin/listCommunicationTemplates.ts @@ -1,22 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethrough } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { - const { moderationAgent } = ctx - if (!moderationAgent) return server.com.atproto.admin.listCommunicationTemplates({ - auth: ctx.authVerifier.role, - handler: async ({ req }) => { - const { data: result } = - await moderationAgent.com.atproto.admin.listCommunicationTemplates( - {}, - authPassthru(req, true), - ) - return { - encoding: 'application/json', - body: result, - } + auth: ctx.authVerifier.access, + handler: async ({ req, auth }) => { + const requester = auth.credentials.did + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/queryModerationEvents.ts b/packages/pds/src/api/com/atproto/admin/queryModerationEvents.ts index 2d33ca6d466..87e173917a6 100644 --- a/packages/pds/src/api/com/atproto/admin/queryModerationEvents.ts +++ b/packages/pds/src/api/com/atproto/admin/queryModerationEvents.ts @@ -1,22 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethrough } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { - const { moderationAgent } = ctx - if (!moderationAgent) return server.com.atproto.admin.queryModerationEvents({ - auth: ctx.authVerifier.role, - handler: async ({ req, params }) => { - const { data: result } = - await moderationAgent.com.atproto.admin.queryModerationEvents( - params, - authPassthru(req), - ) - return { - encoding: 'application/json', - body: result, - } + auth: ctx.authVerifier.access, + handler: async ({ req, auth }) => { + const requester = auth.credentials.did + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/queryModerationStatuses.ts b/packages/pds/src/api/com/atproto/admin/queryModerationStatuses.ts index c31125ce114..b10c4c0efc0 100644 --- a/packages/pds/src/api/com/atproto/admin/queryModerationStatuses.ts +++ b/packages/pds/src/api/com/atproto/admin/queryModerationStatuses.ts @@ -1,22 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethrough } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { - const { moderationAgent } = ctx - if (!moderationAgent) return server.com.atproto.admin.queryModerationStatuses({ - auth: ctx.authVerifier.role, - handler: async ({ req, params }) => { - const { data } = - await moderationAgent.com.atproto.admin.queryModerationStatuses( - params, - authPassthru(req), - ) - return { - encoding: 'application/json', - body: data, - } + auth: ctx.authVerifier.access, + handler: async ({ req, auth }) => { + const requester = auth.credentials.did + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/searchRepos.ts b/packages/pds/src/api/com/atproto/admin/searchRepos.ts index d09ff7b2327..e3d67470e29 100644 --- a/packages/pds/src/api/com/atproto/admin/searchRepos.ts +++ b/packages/pds/src/api/com/atproto/admin/searchRepos.ts @@ -1,22 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethrough } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { - const { moderationAgent } = ctx - if (!moderationAgent) return server.com.atproto.admin.searchRepos({ - auth: ctx.authVerifier.role, - handler: async ({ req, params }) => { - const { data: result } = - await moderationAgent.com.atproto.admin.searchRepos( - params, - authPassthru(req), - ) - return { - encoding: 'application/json', - body: result, - } + auth: ctx.authVerifier.access, + handler: async ({ req, auth }) => { + const requester = auth.credentials.did + return pipethrough(ctx, req, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/admin/updateCommunicationTemplate.ts b/packages/pds/src/api/com/atproto/admin/updateCommunicationTemplate.ts index c548a83bf03..ef7d05667b2 100644 --- a/packages/pds/src/api/com/atproto/admin/updateCommunicationTemplate.ts +++ b/packages/pds/src/api/com/atproto/admin/updateCommunicationTemplate.ts @@ -1,23 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from '../../../proxy' +import { pipethroughProcedure } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { - const { moderationAgent } = ctx - if (!moderationAgent) return server.com.atproto.admin.updateCommunicationTemplate({ - auth: ctx.authVerifier.role, - handler: async ({ req, input }) => { - const { data: result } = - await moderationAgent.com.atproto.admin.updateCommunicationTemplate( - input.body, - authPassthru(req, true), - ) - - return { - encoding: 'application/json', - body: result, - } + auth: ctx.authVerifier.access, + handler: async ({ req, input, auth }) => { + const requester = auth.credentials.did + return pipethroughProcedure(ctx, req, input.body, requester) }, }) } diff --git a/packages/pds/src/api/com/atproto/moderation/createReport.ts b/packages/pds/src/api/com/atproto/moderation/createReport.ts index 64ed5c20005..f5b65fbd0cb 100644 --- a/packages/pds/src/api/com/atproto/moderation/createReport.ts +++ b/packages/pds/src/api/com/atproto/moderation/createReport.ts @@ -1,29 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { InvalidRequestError } from '@atproto/xrpc-server' +import { pipethroughProcedure } from '../../../../pipethrough' export default function (server: Server, ctx: AppContext) { server.com.atproto.moderation.createReport({ auth: ctx.authVerifier.accessCheckTakedown, - handler: async ({ input, auth }) => { + handler: async ({ req, input, auth }) => { const requester = auth.credentials.did - if (!ctx.reportingAgent) { - throw new InvalidRequestError( - 'Your hosting service is not configured with a moderation provider. If this seems in error, reach out to your hosting provider.', - ) - } - const { data: result } = - await ctx.reportingAgent.com.atproto.moderation.createReport( - input.body, - { - ...(await ctx.reportingAuthHeaders(requester)), - encoding: 'application/json', - }, - ) - return { - encoding: 'application/json', - body: result, - } + return pipethroughProcedure(ctx, req, input.body, requester) }, }) } diff --git a/packages/pds/src/pipethrough.ts b/packages/pds/src/pipethrough.ts index 90929499a47..595584b9388 100644 --- a/packages/pds/src/pipethrough.ts +++ b/packages/pds/src/pipethrough.ts @@ -1,23 +1,110 @@ import express from 'express' import * as ui8 from 'uint8arrays' import net from 'node:net' -import { jsonToLex } from '@atproto/lexicon' +import { LexValue, jsonToLex, stringifyLex } from '@atproto/lexicon' import { HandlerPipeThrough, InvalidRequestError } from '@atproto/xrpc-server' import { ResponseType, XRPCError } from '@atproto/xrpc' -import { lexicons } from './lexicon/lexicons' +import { ids, lexicons } from './lexicon/lexicons' import { httpLogger } from './logger' import { getServiceEndpoint, noUndefinedVals } from '@atproto/common' import AppContext from './context' +const defaultService = ( + ctx: AppContext, + path: string, +): { url: string; did: string } | null => { + const nsid = path.replace('/xrpc/', '') + switch (nsid) { + case ids.ComAtprotoAdminCreateCommunicationTemplate: + case ids.ComAtprotoAdminDeleteCommunicationTemplate: + case ids.ComAtprotoAdminEmitModerationEvent: + case ids.ComAtprotoAdminGetModerationEvent: + case ids.ComAtprotoAdminGetRecord: + case ids.ComAtprotoAdminGetRepo: + case ids.ComAtprotoAdminListCommunicationTemplates: + case ids.ComAtprotoAdminQueryModerationEvents: + case ids.ComAtprotoAdminQueryModerationStatuses: + case ids.ComAtprotoAdminSearchRepos: + case ids.ComAtprotoAdminUpdateCommunicationTemplate: + return ctx.cfg.modService + case ids.ComAtprotoModerationCreateReport: + return ctx.cfg.reportService + default: + return ctx.cfg.bskyAppView + } +} + export const pipethrough = async ( ctx: AppContext, req: express.Request, requester?: string, audOverride?: string, ): Promise => { + const { url, headers } = await createUrlAndHeaders( + ctx, + req, + requester, + audOverride, + ) + const reqInit: RequestInit = { + headers, + } + return doProxy(url, reqInit) +} + +export const pipethroughProcedure = async ( + ctx: AppContext, + req: express.Request, + body: LexValue, + requester?: string, + audOverride?: string, +) => { + const { url, headers } = await createUrlAndHeaders( + ctx, + req, + requester, + audOverride, + ) + const reqInit: RequestInit & { duplex: string } = { + method: 'post', + headers, + body: new TextEncoder().encode(stringifyLex(body)), + duplex: 'half', + } + return doProxy(url, reqInit) +} + +export const parseProxyHeader = async ( + ctx: AppContext, + req: express.Request, +): Promise<{ did: string; serviceUrl: string } | undefined> => { + const proxyTo = req.header('atproto-proxy') + if (!proxyTo) return + const [did, serviceId] = proxyTo.split('#') + if (!serviceId) { + throw new InvalidRequestError('no service id specified') + } + const didDoc = await ctx.idResolver.did.resolve(did) + if (!didDoc) { + throw new InvalidRequestError('could not resolve proxy did') + } + const serviceUrl = getServiceEndpoint(didDoc, { id: `#${serviceId}` }) + if (!serviceUrl) { + throw new InvalidRequestError('could not resolve proxy did service url') + } + return { did, serviceUrl } +} + +export const createUrlAndHeaders = async ( + ctx: AppContext, + req: express.Request, + requester?: string, + audOverride?: string, +): Promise<{ url: URL; headers: { authorization?: string } }> => { const proxyTo = await parseProxyHeader(ctx, req) - const serviceUrl = proxyTo?.serviceUrl ?? ctx.cfg.bskyAppView?.url - const aud = audOverride ?? proxyTo?.did ?? ctx.cfg.bskyAppView?.did + const defaultProxy = defaultService(ctx, req.path) + const serviceUrl = proxyTo?.serviceUrl ?? defaultProxy?.url + const aud = audOverride ?? proxyTo?.did ?? defaultProxy?.did if (!serviceUrl || !aud) { throw new InvalidRequestError(`No service configured for ${req.path}`) } @@ -25,15 +112,20 @@ export const pipethrough = async ( if (!ctx.cfg.service.devMode && !isSafeUrl(url)) { throw new InvalidRequestError(`Invalid service url: ${url.toString()}`) } - const reqHeaders = requester - ? await ctx.serviceAuthHeaders(requester, aud) - : { headers: {} } + const headers = requester + ? (await ctx.serviceAuthHeaders(requester, aud)).headers + : {} // forward accept-language header to upstream services - reqHeaders.headers['accept-language'] = req.headers['accept-language'] + headers['accept-language'] = req.headers['accept-language'] + headers['content-type'] = req.headers['content-type'] + return { url, headers } +} + +export const doProxy = async (url: URL, reqInit: RequestInit) => { let res: Response let buffer: ArrayBuffer try { - res = await fetch(url, reqHeaders) + res = await fetch(url, reqInit) buffer = await res.arrayBuffer() } catch (err) { httpLogger.warn({ err }, 'pipethrough network error') @@ -59,50 +151,6 @@ export const pipethrough = async ( return { encoding, buffer, headers: resHeaders } } -export const parseProxyHeader = async ( - ctx: AppContext, - req: express.Request, -): Promise<{ did: string; serviceUrl: string } | undefined> => { - const proxyTo = req.header('atproto-proxy') - if (!proxyTo) return - const [did, serviceId] = proxyTo.split('#') - if (!serviceId) { - throw new InvalidRequestError('no service id specified') - } - const didDoc = await ctx.idResolver.did.resolve(did) - if (!didDoc) { - throw new InvalidRequestError('could not resolve proxy did') - } - const serviceUrl = getServiceEndpoint(didDoc, { id: `#${serviceId}` }) - if (!serviceUrl) { - throw new InvalidRequestError('could not resolve proxy did service url') - } - return { did, serviceUrl } -} - -export const constructUrl = ( - serviceUrl: string, - nsid: string, - params?: Record, -): string => { - const uri = new URL(serviceUrl) - uri.pathname = `/xrpc/${nsid}` - - for (const [key, value] of Object.entries(params ?? {})) { - if (value === undefined) { - continue - } else if (Array.isArray(value)) { - for (const item of value) { - uri.searchParams.append(key, String(item)) - } - } else { - uri.searchParams.set(key, String(value)) - } - } - - return uri.toString() -} - const isSafeUrl = (url: URL) => { if (url.protocol !== 'https:') return false if (!url.hostname || url.hostname === 'localhost') return false From a52f7b66db6a75237a526886b9b457e92e4fae1e Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 29 Feb 2024 22:56:33 -0600 Subject: [PATCH 21/50] reenable proxied admin test --- packages/dev-env/src/network.ts | 1 + packages/ozone/src/api/admin/util.ts | 3 +- .../proxied/__snapshots__/admin.test.ts.snap | 167 +++++++----------- packages/pds/tests/proxied/admin.test.ts | 44 +++-- 4 files changed, 96 insertions(+), 119 deletions(-) diff --git a/packages/dev-env/src/network.ts b/packages/dev-env/src/network.ts index c90e2c181f5..fdb723e68ea 100644 --- a/packages/dev-env/src/network.ts +++ b/packages/dev-env/src/network.ts @@ -112,6 +112,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/ozone/src/api/admin/util.ts b/packages/ozone/src/api/admin/util.ts index 06b64e45862..004f6eca908 100644 --- a/packages/ozone/src/api/admin/util.ts +++ b/packages/ozone/src/api/admin/util.ts @@ -16,8 +16,7 @@ export const getPdsAccountInfo = async ( try { const res = await agent.api.com.atproto.admin.getAccountInfo({ did }, auth) return res.data - } catch (err) { - console.log('ERR: ', err) + } catch { return null } } 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..ba9de578a99 100644 --- a/packages/pds/tests/proxied/admin.test.ts +++ b/packages/pds/tests/proxied/admin.test.ts @@ -9,11 +9,13 @@ 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 +37,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 +123,7 @@ describe.skip('proxies admin requests', () => { reason: 'Y', }, { - headers: network.pds.adminAuthHeaders(), + headers: sc.getHeaders(moderator), encoding: 'application/json', }, ) @@ -129,7 +140,7 @@ describe.skip('proxies admin requests', () => { reason: 'Y', }, { - headers: network.pds.adminAuthHeaders(), + headers: sc.getHeaders(moderator), encoding: 'application/json', }, ) @@ -142,14 +153,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 +170,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 +179,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 +188,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 +196,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 +204,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 +229,7 @@ describe.skip('proxies admin requests', () => { negateLabelVals: ['cats'], }, { - headers: network.pds.adminAuthHeaders(), + headers: sc.getHeaders(moderator), encoding: 'application/json', }, ) @@ -230,7 +242,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 +258,7 @@ describe.skip('proxies admin requests', () => { reason: 'X', }, { - headers: network.pds.adminAuthHeaders(), + headers: sc.getHeaders(moderator), encoding: 'application/json', }, ) @@ -280,7 +292,7 @@ describe.skip('proxies admin requests', () => { negateLabelVals: ['cats'], }, { - headers: network.pds.adminAuthHeaders(), + headers: sc.getHeaders(moderator), encoding: 'application/json', }, ) @@ -304,7 +316,7 @@ describe.skip('proxies admin requests', () => { reason: 'X', }, { - headers: network.pds.adminAuthHeaders(), + headers: sc.getHeaders(moderator), encoding: 'application/json', }, ) From 9b322c7ac418f1c4acf5573453ae3008d4a13d45 Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 4 Mar 2024 15:20:39 -0600 Subject: [PATCH 22/50] add moderator accounts to ozone in dev-env --- packages/dev-env/src/mock/index.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/dev-env/src/mock/index.ts b/packages/dev-env/src/mock/index.ts index f115a1112ef..97e4fac370a 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({ From f7ef546f7b9f51d4af4e2c3ef58eb50da8f80214 Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 4 Mar 2024 16:05:53 -0600 Subject: [PATCH 23/50] update did doc id values --- packages/bsky/src/auth-verifier.ts | 13 ++++++++----- packages/ozone/src/api/well-known.ts | 6 +++--- packages/pds/src/auth-verifier.ts | 6 ++++-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/bsky/src/auth-verifier.ts b/packages/bsky/src/auth-verifier.ts index 4c5311d73fe..03766c55151 100644 --- a/packages/bsky/src/auth-verifier.ts +++ b/packages/bsky/src/auth-verifier.ts @@ -170,7 +170,7 @@ export class AuthVerifier { modService = async (reqCtx: ReqCtx): Promise => { const { iss, aud } = await this.verifyServiceJwt(reqCtx, { aud: this.ownDid, - iss: [this.modServiceDid, `${this.modServiceDid}#atproto_mod`], + iss: [this.modServiceDid, `${this.modServiceDid}#atproto_labeler`], }) return { credentials: { type: 'mod_service', aud, iss } } } @@ -209,7 +209,9 @@ export class AuthVerifier { if (opts.iss !== null && !opts.iss.includes(iss)) { throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss') } - const [did, keyId = 'atproto'] = iss.split('#') + const [did, serviceId] = iss.split('#') + const keyId = + serviceId === 'atproto-labeler' ? 'atproto-label' : 'atproto' let identity: GetIdentityByDidResponse try { identity = await this.dataplane.getIdentityByDid({ did }) @@ -236,9 +238,10 @@ export class AuthVerifier { } isModService(iss: string): boolean { - return [this.modServiceDid, `${this.modServiceDid}#atproto_mod`].includes( - iss, - ) + return [ + this.modServiceDid, + `${this.modServiceDid}#atproto_labeler`, + ].includes(iss) } nullCreds(): NullOutput { diff --git a/packages/ozone/src/api/well-known.ts b/packages/ozone/src/api/well-known.ts index 29a9f6d3636..17ee9ad3752 100644 --- a/packages/ozone/src/api/well-known.ts +++ b/packages/ozone/src/api/well-known.ts @@ -15,7 +15,7 @@ export const createRouter = (ctx: AppContext): express.Router => { id: ctx.cfg.service.did, verificationMethod: [ { - id: `${ctx.cfg.service.did}#atproto_mod`, + id: `${ctx.cfg.service.did}#atproto_label`, type: 'Multikey', controller: ctx.cfg.service.did, publicKeyMultibase: ctx.signingKey.did().replace('did:key:', ''), @@ -23,8 +23,8 @@ export const createRouter = (ctx: AppContext): express.Router => { ], service: [ { - id: '#atproto_mod_srvc', - type: 'AtprotoModerationService', + id: '#atproto_labeler', + type: 'AtprotoLabeler', serviceEndpoint: `https://${hostname}`, }, ], diff --git a/packages/pds/src/auth-verifier.ts b/packages/pds/src/auth-verifier.ts index 5f1b11fd29c..6625ea9629c 100644 --- a/packages/pds/src/auth-verifier.ts +++ b/packages/pds/src/auth-verifier.ts @@ -260,7 +260,7 @@ export class AuthVerifier { } const payload = await this.verifyServiceJwt(reqCtx, { aud: this.dids.entryway ?? this.dids.pds, - iss: [this.dids.modService, `${this.dids.modService}#atproto_mod`], + iss: [this.dids.modService, `${this.dids.modService}#atproto_labeler`], }) return { credentials: { @@ -344,7 +344,9 @@ export class AuthVerifier { if (opts.iss !== null && !opts.iss.includes(iss)) { throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss') } - const [did, keyId = 'atproto'] = iss.split('#') + const [did, serviceId] = iss.split('#') + const keyId = + serviceId === 'atproto_labeler' ? 'atproto_label' : 'atproto' const didDoc = await this.idResolver.did.resolve(did, forceRefresh) if (!didDoc) { throw new AuthRequiredError('could not resolve iss did') From 0482a92fe46f301560d898977509694deef52e3a Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 4 Mar 2024 16:13:34 -0600 Subject: [PATCH 24/50] null creds string -> `none` --- packages/bsky/src/auth-verifier.ts | 4 ++-- packages/ozone/src/auth-verifier.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bsky/src/auth-verifier.ts b/packages/bsky/src/auth-verifier.ts index 03766c55151..b305aad1ef1 100644 --- a/packages/bsky/src/auth-verifier.ts +++ b/packages/bsky/src/auth-verifier.ts @@ -25,7 +25,7 @@ export enum RoleStatus { type NullOutput = { credentials: { - type: 'null' + type: 'none' iss: null } } @@ -247,7 +247,7 @@ export class AuthVerifier { nullCreds(): NullOutput { return { credentials: { - type: 'null', + type: 'none', iss: null, }, } diff --git a/packages/ozone/src/auth-verifier.ts b/packages/ozone/src/auth-verifier.ts index 8ea3c58c59c..48ca241e6ef 100644 --- a/packages/ozone/src/auth-verifier.ts +++ b/packages/ozone/src/auth-verifier.ts @@ -40,7 +40,7 @@ type StandardOutput = { type NullOutput = { credentials: { - type: 'null' + type: 'none' iss: null } } @@ -175,7 +175,7 @@ export class AuthVerifier { nullCreds(): NullOutput { return { credentials: { - type: 'null', + type: 'none', iss: null, }, } From 5df31de214d873d0fb40cd7fb42d130a8b046e04 Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 4 Mar 2024 18:49:03 -0600 Subject: [PATCH 25/50] fix fetchLabels auth check --- packages/ozone/src/api/temp/fetchLabels.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ozone/src/api/temp/fetchLabels.ts b/packages/ozone/src/api/temp/fetchLabels.ts index 890fafdf95c..b2fbfbb846a 100644 --- a/packages/ozone/src/api/temp/fetchLabels.ts +++ b/packages/ozone/src/api/temp/fetchLabels.ts @@ -14,7 +14,7 @@ export default function (server: Server, ctx: AppContext) { const since = params.since !== undefined ? new Date(params.since).toISOString() : '' const includeUnspeccedTakedowns = - auth.credentials.type === 'null' ? false : auth.credentials.isAdmin + auth.credentials.type === 'none' ? false : auth.credentials.isAdmin const labelRes = await ctx.db.db .selectFrom('label') .selectAll() From dd891d4608a0fa28144ffcc786339365af40f490 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Tue, 5 Mar 2024 18:01:05 +0000 Subject: [PATCH 26/50] :sparkles: Add a couple more proxied requests that we use in ozone ui --- packages/ozone/src/api/proxied.ts | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/packages/ozone/src/api/proxied.ts b/packages/ozone/src/api/proxied.ts index b7eef005650..a2d5040989e 100644 --- a/packages/ozone/src/api/proxied.ts +++ b/packages/ozone/src/api/proxied.ts @@ -16,6 +16,20 @@ export default function (server: Server, ctx: AppContext) { }, }) + server.app.bsky.actor.getProfiles({ + auth: ctx.authVerifier.modOrRole, + handler: async (request) => { + const res = await ctx.appviewAgent.api.app.bsky.actor.getProfiles( + request.params, + await ctx.appviewAuth(), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + server.app.bsky.feed.getAuthorFeed({ auth: ctx.authVerifier.modOrRole, handler: async (request) => { @@ -44,6 +58,20 @@ export default function (server: Server, ctx: AppContext) { }, }) + server.app.bsky.feed.getFeedGenerator({ + auth: ctx.authVerifier.modOrRole, + handler: async (request) => { + const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( + request.params, + await ctx.appviewAuth(), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + server.app.bsky.graph.getFollows({ auth: ctx.authVerifier.modOrRole, handler: async (request) => { @@ -71,4 +99,18 @@ export default function (server: Server, ctx: AppContext) { } }, }) + + server.app.bsky.graph.getList({ + auth: ctx.authVerifier.modOrRole, + handler: async (request) => { + const res = await ctx.appviewAgent.api.app.bsky.graph.getList( + request.params, + await ctx.appviewAuth(), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) } From 2ca4feebba7fb36ab95fb6f0701be896a62d153f Mon Sep 17 00:00:00 2001 From: Jake Gold <52801504+Jacob2161@users.noreply.github.com> Date: Thu, 29 Feb 2024 14:16:50 -0800 Subject: [PATCH 27/50] Add runit to the services/bsky Dockerfile (#2254) add runit to the services/bsky Dockerfile --- services/bsky/Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/bsky/Dockerfile b/services/bsky/Dockerfile index 84422945ae0..a15e6aa2ee7 100644 --- a/services/bsky/Dockerfile +++ b/services/bsky/Dockerfile @@ -35,7 +35,10 @@ WORKDIR services/bsky # Uses assets from build stage to reduce build size FROM node:20.11-alpine -RUN apk add --update dumb-init +# dumb-init is used to handle signals properly. +# runit is installed so it can be (optionally) used for logging via svlogd. +RUN apk add --update dumb-init runit + # Avoid zombie processes, handle signal forwarding ENTRYPOINT ["dumb-init", "--"] From 6ba5f6c67fb7fc8a8fd5b3fc4f1fbc84638f21b3 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 1 Mar 2024 12:44:52 -0600 Subject: [PATCH 28/50] Improve tag detection (#2260) * Allow tags to lead with and contain only numbers * Break tags on other whitespace characters * Export regexes from rich text detection * Add test * Add test * Disallow number-only tags * Avoid combining enclosing screen chars * Allow full-width number sign * Clarify tests * Fix punctuation edge case * Reorder * Simplify, add another test * Another test, comment --- .changeset/chatty-cows-kick.md | 5 +++ .changeset/lovely-pandas-pretend.md | 5 +++ .changeset/quick-ducks-joke.md | 5 +++ packages/api/src/index.ts | 1 + packages/api/src/rich-text/detection.ts | 18 ++++++++--- packages/api/src/rich-text/util.ts | 11 +++++++ .../api/tests/rich-text-detection.test.ts | 31 +++++++++++++++++-- 7 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 .changeset/chatty-cows-kick.md create mode 100644 .changeset/lovely-pandas-pretend.md create mode 100644 .changeset/quick-ducks-joke.md create mode 100644 packages/api/src/rich-text/util.ts diff --git a/.changeset/chatty-cows-kick.md b/.changeset/chatty-cows-kick.md new file mode 100644 index 00000000000..76bd82f015d --- /dev/null +++ b/.changeset/chatty-cows-kick.md @@ -0,0 +1,5 @@ +--- +'@atproto/api': patch +--- + +Export regex from rich text detection diff --git a/.changeset/lovely-pandas-pretend.md b/.changeset/lovely-pandas-pretend.md new file mode 100644 index 00000000000..3a75be2877b --- /dev/null +++ b/.changeset/lovely-pandas-pretend.md @@ -0,0 +1,5 @@ +--- +'@atproto/api': patch +--- + +Disallow rare unicode whitespace characters from tags diff --git a/.changeset/quick-ducks-joke.md b/.changeset/quick-ducks-joke.md new file mode 100644 index 00000000000..923b2fe2fe0 --- /dev/null +++ b/.changeset/quick-ducks-joke.md @@ -0,0 +1,5 @@ +--- +'@atproto/api': patch +--- + +Allow tags to lead with numbers diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 9e407142aba..87cf1ccf01a 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -14,6 +14,7 @@ export * from './agent' export * from './rich-text/rich-text' export * from './rich-text/sanitization' export * from './rich-text/unicode' +export * from './rich-text/util' export * from './moderation' export * from './moderation/types' export { LABELS } from './moderation/const/labels' diff --git a/packages/api/src/rich-text/detection.ts b/packages/api/src/rich-text/detection.ts index 7b5444a68a5..22c5db1b087 100644 --- a/packages/api/src/rich-text/detection.ts +++ b/packages/api/src/rich-text/detection.ts @@ -1,6 +1,12 @@ import TLDs from 'tlds' import { AppBskyRichtextFacet } from '../client' import { UnicodeString } from './unicode' +import { + URL_REGEX, + MENTION_REGEX, + TAG_REGEX, + TRAILING_PUNCTUATION_REGEX, +} from './util' export type Facet = AppBskyRichtextFacet.Main @@ -9,7 +15,7 @@ export function detectFacets(text: UnicodeString): Facet[] | undefined { const facets: Facet[] = [] { // mentions - const re = /(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)/g + const re = MENTION_REGEX while ((match = re.exec(text.utf16))) { if (!isValidDomain(match[3]) && !match[3].endsWith('.test')) { continue // probably not a handle @@ -33,8 +39,7 @@ export function detectFacets(text: UnicodeString): Facet[] | undefined { } { // links - const re = - /(^|\s|\()((https?:\/\/[\S]+)|((?[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim + const re = URL_REGEX while ((match = re.exec(text.utf16))) { let uri = match[2] if (!uri.startsWith('http')) { @@ -70,11 +75,14 @@ export function detectFacets(text: UnicodeString): Facet[] | undefined { } } { - const re = /(^|\s)#((?!\ufe0f)[^\d\s]\S*)(?=\s)?/g + const re = TAG_REGEX while ((match = re.exec(text.utf16))) { let [, leading, tag] = match - tag = tag.trim().replace(/\p{P}+$/gu, '') // strip ending punctuation + if (!tag) continue + + // strip ending punctuation and any spaces + tag = tag.trim().replace(TRAILING_PUNCTUATION_REGEX, '') if (tag.length === 0 || tag.length > 64) continue diff --git a/packages/api/src/rich-text/util.ts b/packages/api/src/rich-text/util.ts new file mode 100644 index 00000000000..ab50c66212d --- /dev/null +++ b/packages/api/src/rich-text/util.ts @@ -0,0 +1,11 @@ +export const MENTION_REGEX = /(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)/g +export const URL_REGEX = + /(^|\s|\()((https?:\/\/[\S]+)|((?[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim +export const TRAILING_PUNCTUATION_REGEX = /\p{P}+$/gu + +/** + * `\ufe0f` emoji modifier + * `\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2` zero-width spaces (likely incomplete) + */ +export const TAG_REGEX = + /(^|\s)[##]((?!\ufe0f)[^\s\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2]*[^\d\s\p{P}\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2]+[^\s\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2]*)?/gu diff --git a/packages/api/tests/rich-text-detection.test.ts b/packages/api/tests/rich-text-detection.test.ts index 0eafb65a3b1..084b5440a48 100644 --- a/packages/api/tests/rich-text-detection.test.ts +++ b/packages/api/tests/rich-text-detection.test.ts @@ -218,7 +218,7 @@ describe('detectFacets', () => { } }) - it('correctly detects tags inline', async () => { + describe('correctly detects tags inline', () => { const inputs: [ string, string[], @@ -234,11 +234,13 @@ describe('detectFacets', () => { ], ], ['#1', [], []], + ['#1a', ['1a'], [{ byteStart: 0, byteEnd: 3 }]], ['#tag', ['tag'], [{ byteStart: 0, byteEnd: 4 }]], ['body #tag', ['tag'], [{ byteStart: 5, byteEnd: 9 }]], ['#tag body', ['tag'], [{ byteStart: 0, byteEnd: 4 }]], ['body #tag body', ['tag'], [{ byteStart: 5, byteEnd: 9 }]], ['body #1', [], []], + ['body #1a', ['1a'], [{ byteStart: 5, byteEnd: 8 }]], ['body #a1', ['a1'], [{ byteStart: 5, byteEnd: 8 }]], ['#', [], []], ['#?', [], []], @@ -254,12 +256,18 @@ describe('detectFacets', () => { [], [], ], + [ + 'body #thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!', + ['thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'], + [{ byteStart: 5, byteEnd: 70 }], + ], [ 'its a #double#rainbow', ['double#rainbow'], [{ byteStart: 6, byteEnd: 21 }], ], ['##hashash', ['#hashash'], [{ byteStart: 0, byteEnd: 9 }]], + ['##', [], []], ['some #n0n3s@n5e!', ['n0n3s@n5e'], [{ byteStart: 5, byteEnd: 15 }]], [ 'works #with,punctuation', @@ -319,9 +327,26 @@ describe('detectFacets', () => { }, ], ], + ['no match (\\u200B): #​', [], []], + ['no match (\\u200Ba): #​a', [], []], + ['match (a\\u200Bb): #a​b', ['a'], [{ byteStart: 18, byteEnd: 20 }]], + ['match (ab\\u200B): #ab​', ['ab'], [{ byteStart: 18, byteEnd: 21 }]], + ['no match (\\u20e2tag): #⃢tag', [], []], + ['no match (a\\u20e2b): #a⃢b', ['a'], [{ byteStart: 21, byteEnd: 23 }]], + [ + 'match full width number sign (tag): #tag', + ['tag'], + [{ byteStart: 36, byteEnd: 42 }], + ], + [ + 'match full width number sign (tag): ##️⃣tag', + ['#️⃣tag'], + [{ byteStart: 36, byteEnd: 49 }], + ], + ['no match 1?: #1?', [], []], ] - for (const [input, tags, indices] of inputs) { + it.each(inputs)('%s', async (input, tags, indices) => { const rt = new RichText({ text: input }) await rt.detectFacets(agent) @@ -340,7 +365,7 @@ describe('detectFacets', () => { expect(detectedTags).toEqual(tags) expect(detectedIndices).toEqual(indices) - } + }) }) }) From 9b2500e895f1109f42efcf7d456f4cf606e50901 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 13:03:04 -0600 Subject: [PATCH 29/50] Version packages (#2261) Co-authored-by: github-actions[bot] --- .changeset/chatty-cows-kick.md | 5 ----- .changeset/lovely-pandas-pretend.md | 5 ----- .changeset/quick-ducks-joke.md | 5 ----- packages/api/CHANGELOG.md | 10 ++++++++++ packages/api/package.json | 2 +- packages/bsky/CHANGELOG.md | 7 +++++++ packages/bsky/package.json | 2 +- packages/dev-env/CHANGELOG.md | 10 ++++++++++ packages/dev-env/package.json | 2 +- packages/ozone/CHANGELOG.md | 7 +++++++ packages/ozone/package.json | 2 +- packages/pds/CHANGELOG.md | 7 +++++++ packages/pds/package.json | 2 +- 13 files changed, 46 insertions(+), 20 deletions(-) delete mode 100644 .changeset/chatty-cows-kick.md delete mode 100644 .changeset/lovely-pandas-pretend.md delete mode 100644 .changeset/quick-ducks-joke.md diff --git a/.changeset/chatty-cows-kick.md b/.changeset/chatty-cows-kick.md deleted file mode 100644 index 76bd82f015d..00000000000 --- a/.changeset/chatty-cows-kick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@atproto/api': patch ---- - -Export regex from rich text detection diff --git a/.changeset/lovely-pandas-pretend.md b/.changeset/lovely-pandas-pretend.md deleted file mode 100644 index 3a75be2877b..00000000000 --- a/.changeset/lovely-pandas-pretend.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@atproto/api': patch ---- - -Disallow rare unicode whitespace characters from tags diff --git a/.changeset/quick-ducks-joke.md b/.changeset/quick-ducks-joke.md deleted file mode 100644 index 923b2fe2fe0..00000000000 --- a/.changeset/quick-ducks-joke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@atproto/api': patch ---- - -Allow tags to lead with numbers diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index 48e4edc018b..a5bac9a4eaa 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,15 @@ # @atproto/api +## 0.10.4 + +### Patch Changes + +- [#2260](https://github.com/bluesky-social/atproto/pull/2260) [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Export regex from rich text detection + +- [#2260](https://github.com/bluesky-social/atproto/pull/2260) [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Disallow rare unicode whitespace characters from tags + +- [#2260](https://github.com/bluesky-social/atproto/pull/2260) [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Allow tags to lead with numbers + ## 0.10.3 ### Patch Changes diff --git a/packages/api/package.json b/packages/api/package.json index 4f07d5a01a1..dbb18da1786 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.10.3", + "version": "0.10.4", "license": "MIT", "description": "Client library for atproto and Bluesky", "keywords": [ diff --git a/packages/bsky/CHANGELOG.md b/packages/bsky/CHANGELOG.md index 27155388ef9..102c40050eb 100644 --- a/packages/bsky/CHANGELOG.md +++ b/packages/bsky/CHANGELOG.md @@ -1,5 +1,12 @@ # @atproto/bsky +## 0.0.36 + +### Patch Changes + +- Updated dependencies [[`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f)]: + - @atproto/api@0.10.4 + ## 0.0.35 ### Patch Changes diff --git a/packages/bsky/package.json b/packages/bsky/package.json index 8ffa5e0911f..dd081d3c209 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/bsky", - "version": "0.0.35", + "version": "0.0.36", "license": "MIT", "description": "Reference implementation of app.bsky App View (Bluesky API)", "keywords": [ diff --git a/packages/dev-env/CHANGELOG.md b/packages/dev-env/CHANGELOG.md index 814f89dd619..15a6ad3171f 100644 --- a/packages/dev-env/CHANGELOG.md +++ b/packages/dev-env/CHANGELOG.md @@ -1,5 +1,15 @@ # @atproto/dev-env +## 0.2.36 + +### Patch Changes + +- Updated dependencies [[`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f)]: + - @atproto/api@0.10.4 + - @atproto/bsky@0.0.36 + - @atproto/ozone@0.0.15 + - @atproto/pds@0.4.4 + ## 0.2.35 ### Patch Changes diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index 5924c18bfd0..46899cdb763 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/dev-env", - "version": "0.2.35", + "version": "0.2.36", "license": "MIT", "description": "Local development environment helper for atproto development", "keywords": [ diff --git a/packages/ozone/CHANGELOG.md b/packages/ozone/CHANGELOG.md index 7f27390ce91..0953945b4b2 100644 --- a/packages/ozone/CHANGELOG.md +++ b/packages/ozone/CHANGELOG.md @@ -1,5 +1,12 @@ # @atproto/ozone +## 0.0.15 + +### Patch Changes + +- Updated dependencies [[`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f)]: + - @atproto/api@0.10.4 + ## 0.0.14 ### Patch Changes diff --git a/packages/ozone/package.json b/packages/ozone/package.json index 3a3d3a690ff..3840f4b49ed 100644 --- a/packages/ozone/package.json +++ b/packages/ozone/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/ozone", - "version": "0.0.14", + "version": "0.0.15", "license": "MIT", "description": "Backend service for moderating the Bluesky network.", "keywords": [ diff --git a/packages/pds/CHANGELOG.md b/packages/pds/CHANGELOG.md index 3a323988102..1a3e87286f3 100644 --- a/packages/pds/CHANGELOG.md +++ b/packages/pds/CHANGELOG.md @@ -1,5 +1,12 @@ # @atproto/pds +## 0.4.4 + +### Patch Changes + +- Updated dependencies [[`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f)]: + - @atproto/api@0.10.4 + ## 0.4.3 ### Patch Changes diff --git a/packages/pds/package.json b/packages/pds/package.json index f345bbcbf63..062578aa531 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/pds", - "version": "0.4.3", + "version": "0.4.4", "license": "MIT", "description": "Reference implementation of atproto Personal Data Server (PDS)", "keywords": [ From c76fd03caa1eed36d9d26feba1271521c51629a4 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Mon, 4 Mar 2024 17:31:50 +0100 Subject: [PATCH 30/50] :bug: Increment attempt count after each attempt to push ozone event (#2239) --- packages/ozone/src/daemon/event-pusher.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ozone/src/daemon/event-pusher.ts b/packages/ozone/src/daemon/event-pusher.ts index faaee4529ed..01570595c0d 100644 --- a/packages/ozone/src/daemon/event-pusher.ts +++ b/packages/ozone/src/daemon/event-pusher.ts @@ -205,7 +205,7 @@ export class EventPusher { ? { confirmedAt: new Date() } : { lastAttempted: new Date(), - attempts: evt.attempts ?? 0 + 1, + attempts: (evt.attempts ?? 0) + 1, }, ) .where('subjectDid', '=', evt.subjectDid) @@ -244,7 +244,7 @@ export class EventPusher { ? { confirmedAt: new Date() } : { lastAttempted: new Date(), - attempts: evt.attempts ?? 0 + 1, + attempts: (evt.attempts ?? 0) + 1, }, ) .where('subjectUri', '=', evt.subjectUri) @@ -284,7 +284,7 @@ export class EventPusher { ? { confirmedAt: new Date() } : { lastAttempted: new Date(), - attempts: evt.attempts ?? 0 + 1, + attempts: (evt.attempts ?? 0) + 1, }, ) .where('subjectDid', '=', evt.subjectDid) From 87f00f21938655322be58ab66a838dd9edc22bc6 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Tue, 5 Mar 2024 14:27:25 -0500 Subject: [PATCH 31/50] Ozone delegates email sending to actor's pds (#2272) * ozone delegates email sending to user's pds * lexicon: add content field to mod email event * test email sending via mod event --- lexicons/com/atproto/admin/defs.json | 4 ++ packages/api/src/client/lexicons.ts | 4 ++ .../client/types/com/atproto/admin/defs.ts | 2 + packages/bsky/src/lexicon/lexicons.ts | 4 ++ .../lexicon/types/com/atproto/admin/defs.ts | 2 + packages/dev-env/src/ozone.ts | 1 + .../src/api/admin/emitModerationEvent.ts | 19 +++++ packages/ozone/src/config/config.ts | 2 + packages/ozone/src/config/env.ts | 4 +- packages/ozone/src/context.ts | 14 ++-- packages/ozone/src/daemon/context.ts | 14 ++-- packages/ozone/src/lexicon/lexicons.ts | 4 ++ .../lexicon/types/com/atproto/admin/defs.ts | 2 + packages/ozone/src/mod-service/index.ts | 71 +++++++++++++++++-- packages/ozone/src/mod-service/views.ts | 16 ++--- .../ozone/tests/moderation-events.test.ts | 49 +++++++++++++ .../src/api/com/atproto/admin/sendEmail.ts | 44 ++++-------- packages/pds/src/lexicon/lexicons.ts | 4 ++ .../lexicon/types/com/atproto/admin/defs.ts | 2 + 19 files changed, 205 insertions(+), 57 deletions(-) diff --git a/lexicons/com/atproto/admin/defs.json b/lexicons/com/atproto/admin/defs.json index e1315eb7473..d2056f77cf8 100644 --- a/lexicons/com/atproto/admin/defs.json +++ b/lexicons/com/atproto/admin/defs.json @@ -585,6 +585,10 @@ "type": "string", "description": "The subject line of the email sent to the user." }, + "content": { + "type": "string", + "description": "The content of the email sent to the user." + }, "comment": { "type": "string", "description": "Additional comment about the outgoing comm." diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 14e4c1cb81e..72ac23230f4 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -897,6 +897,10 @@ export const schemaDict = { type: 'string', description: 'The subject line of the email sent to the user.', }, + content: { + type: 'string', + description: 'The content of the email sent to the user.', + }, comment: { type: 'string', description: 'Additional comment about the outgoing comm.', diff --git a/packages/api/src/client/types/com/atproto/admin/defs.ts b/packages/api/src/client/types/com/atproto/admin/defs.ts index 4e3d35a869f..2de71524005 100644 --- a/packages/api/src/client/types/com/atproto/admin/defs.ts +++ b/packages/api/src/client/types/com/atproto/admin/defs.ts @@ -704,6 +704,8 @@ export function validateModEventUnmute(v: unknown): ValidationResult { export interface ModEventEmail { /** The subject line of the email sent to the user. */ subjectLine: string + /** The content of the email sent to the user. */ + content?: string /** Additional comment about the outgoing comm. */ comment?: string [k: string]: unknown diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 14e4c1cb81e..72ac23230f4 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -897,6 +897,10 @@ export const schemaDict = { type: 'string', description: 'The subject line of the email sent to the user.', }, + content: { + type: 'string', + description: 'The content of the email sent to the user.', + }, comment: { type: 'string', description: 'Additional comment about the outgoing comm.', diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts index a713a635635..acfda37abbc 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts @@ -704,6 +704,8 @@ export function validateModEventUnmute(v: unknown): ValidationResult { export interface ModEventEmail { /** The subject line of the email sent to the user. */ subjectLine: string + /** The content of the email sent to the user. */ + content?: string /** Additional comment about the outgoing comm. */ comment?: string [k: string]: unknown diff --git a/packages/dev-env/src/ozone.ts b/packages/dev-env/src/ozone.ts index 0e0578c7935..278ad8d2b2b 100644 --- a/packages/dev-env/src/ozone.ts +++ b/packages/dev-env/src/ozone.ts @@ -34,6 +34,7 @@ export class TestOzone { const port = config.port || (await getPort()) const url = `http://localhost:${port}` const env: ozone.OzoneEnvironment = { + devMode: true, version: '0.0.0', port, didPlcUrl: config.plcUrl, diff --git a/packages/ozone/src/api/admin/emitModerationEvent.ts b/packages/ozone/src/api/admin/emitModerationEvent.ts index 473269bffde..f1bdbab3462 100644 --- a/packages/ozone/src/api/admin/emitModerationEvent.ts +++ b/packages/ozone/src/api/admin/emitModerationEvent.ts @@ -2,12 +2,14 @@ import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../lexicon' import AppContext from '../../context' import { + isModEventEmail, isModEventLabel, isModEventReverseTakedown, isModEventTakedown, } from '../../lexicon/types/com/atproto/admin/defs' import { subjectFromInput } from '../../mod-service/subject' import { ModerationLangService } from '../../mod-service/lang' +import { retryHttp } from '../../util' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.emitModerationEvent({ @@ -75,6 +77,23 @@ export default function (server: Server, ctx: AppContext) { } } + if (isModEventEmail(event) && event.content) { + // sending email prior to logging the event to avoid a long transaction below + if (!subject.isRepo()) { + throw new InvalidRequestError( + 'Email can only be sent to a repo subject', + ) + } + const { content, subjectLine } = event + await retryHttp(() => + ctx.modService(db).sendEmail({ + subject: subjectLine, + content, + recipientDid: subject.did, + }), + ) + } + const moderationEvent = await db.transaction(async (dbTxn) => { const moderationTxn = ctx.modService(dbTxn) diff --git a/packages/ozone/src/config/config.ts b/packages/ozone/src/config/config.ts index 54abb29e21e..3a3cc62f663 100644 --- a/packages/ozone/src/config/config.ts +++ b/packages/ozone/src/config/config.ts @@ -13,6 +13,7 @@ export const envToCfg = (env: OzoneEnvironment): OzoneConfig => { publicUrl: env.publicUrl, did: env.serverDid, version: env.version, + devMode: env.devMode, } assert(env.dbPostgresUrl) @@ -79,6 +80,7 @@ export type ServiceConfig = { publicUrl: string did: string version?: string + devMode?: boolean } export type DatabaseConfig = { diff --git a/packages/ozone/src/config/env.ts b/packages/ozone/src/config/env.ts index e86b520815d..554a0f38ba9 100644 --- a/packages/ozone/src/config/env.ts +++ b/packages/ozone/src/config/env.ts @@ -1,8 +1,9 @@ -import { envInt, envList, envStr } from '@atproto/common' +import { envBool, envInt, envList, envStr } from '@atproto/common' export const readEnv = (): OzoneEnvironment => { return { nodeEnv: envStr('NODE_ENV'), + devMode: envBool('OZONE_DEV_MODE'), version: envStr('OZONE_VERSION'), port: envInt('OZONE_PORT'), publicUrl: envStr('OZONE_PUBLIC_URL'), @@ -30,6 +31,7 @@ export const readEnv = (): OzoneEnvironment => { export type OzoneEnvironment = { nodeEnv?: string + devMode?: boolean version?: string port?: number publicUrl?: string diff --git a/packages/ozone/src/context.ts b/packages/ozone/src/context.ts index e9332620993..db8d123d5e0 100644 --- a/packages/ozone/src/context.ts +++ b/packages/ozone/src/context.ts @@ -59,8 +59,6 @@ export class AppContext { aud, keypair: signingKey, }) - const appviewAuth = async () => - cfg.appview.did ? createAuthHeaders(cfg.appview.did) : undefined const backgroundQueue = new BackgroundQueue(db) const eventPusher = new EventPusher(db, createAuthHeaders, { @@ -68,11 +66,17 @@ export class AppContext { pds: cfg.pds ?? undefined, }) + const idResolver = new IdResolver({ + plcUrl: cfg.identity.plcUrl, + }) + const modService = ModerationService.creator( + cfg, backgroundQueue, + idResolver, eventPusher, appviewAgent, - appviewAuth, + createAuthHeaders, cfg.service.did, overrides?.imgInvalidator, cfg.cdn.paths, @@ -80,10 +84,6 @@ export class AppContext { const communicationTemplateService = CommunicationTemplateService.creator() - const idResolver = new IdResolver({ - plcUrl: cfg.identity.plcUrl, - }) - const sequencer = new Sequencer(db) const authVerifier = new AuthVerifier(idResolver, { diff --git a/packages/ozone/src/daemon/context.ts b/packages/ozone/src/daemon/context.ts index 5af19d89bc4..3ed0596c2ed 100644 --- a/packages/ozone/src/daemon/context.ts +++ b/packages/ozone/src/daemon/context.ts @@ -7,6 +7,7 @@ import { EventPusher } from './event-pusher' import { EventReverser } from './event-reverser' import { ModerationService, ModerationServiceCreator } from '../mod-service' import { BackgroundQueue } from '../background' +import { IdResolver } from '@atproto/identity' export type DaemonContextOptions = { db: Database @@ -39,21 +40,26 @@ export class DaemonContext { keypair: signingKey, }) - const appviewAuth = async () => - cfg.appview.did ? createAuthHeaders(cfg.appview.did) : undefined - const eventPusher = new EventPusher(db, createAuthHeaders, { appview: cfg.appview, pds: cfg.pds ?? undefined, }) + const backgroundQueue = new BackgroundQueue(db) + const idResolver = new IdResolver({ + plcUrl: cfg.identity.plcUrl, + }) + const modService = ModerationService.creator( + cfg, backgroundQueue, + idResolver, eventPusher, appviewAgent, - appviewAuth, + createAuthHeaders, cfg.service.did, ) + const eventReverser = new EventReverser(db, modService) return new DaemonContext({ diff --git a/packages/ozone/src/lexicon/lexicons.ts b/packages/ozone/src/lexicon/lexicons.ts index 14e4c1cb81e..72ac23230f4 100644 --- a/packages/ozone/src/lexicon/lexicons.ts +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -897,6 +897,10 @@ export const schemaDict = { type: 'string', description: 'The subject line of the email sent to the user.', }, + content: { + type: 'string', + description: 'The content of the email sent to the user.', + }, comment: { type: 'string', description: 'Additional comment about the outgoing comm.', diff --git a/packages/ozone/src/lexicon/types/com/atproto/admin/defs.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/defs.ts index a713a635635..acfda37abbc 100644 --- a/packages/ozone/src/lexicon/types/com/atproto/admin/defs.ts +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/defs.ts @@ -704,6 +704,8 @@ export function validateModEventUnmute(v: unknown): ValidationResult { export interface ModEventEmail { /** The subject line of the email sent to the user. */ subjectLine: string + /** The content of the email sent to the user. */ + content?: string /** Additional comment about the outgoing comm. */ comment?: string [k: string]: unknown diff --git a/packages/ozone/src/mod-service/index.ts b/packages/ozone/src/mod-service/index.ts index d0690fce57a..1ca793d5601 100644 --- a/packages/ozone/src/mod-service/index.ts +++ b/packages/ozone/src/mod-service/index.ts @@ -1,9 +1,13 @@ +import net from 'node:net' +import { Insertable, sql } from 'kysely' import { CID } from 'multiformats/cid' import { AtUri, INVALID_HANDLE } from '@atproto/syntax' import { InvalidRequestError } from '@atproto/xrpc-server' import { addHoursToDate } from '@atproto/common' +import { IdResolver } from '@atproto/identity' +import AtpAgent from '@atproto/api' import { Database } from '../db' -import { AppviewAuth, ModerationViews } from './views' +import { AuthHeaders, ModerationViews } from './views' import { Main as StrongRef } from '../lexicon/types/com/atproto/repo/strongRef' import { isModEventComment, @@ -30,9 +34,7 @@ import { } from './types' import { ModerationEvent } from '../db/schema/moderation_event' import { StatusKeyset, TimeIdKeyset, paginate } from '../db/pagination' -import AtpAgent from '@atproto/api' import { Label } from '../lexicon/types/com/atproto/label/defs' -import { Insertable, sql } from 'kysely' import { ModSubject, RecordSubject, @@ -46,26 +48,31 @@ import { BackgroundQueue } from '../background' import { EventPusher } from '../daemon' import { ImageInvalidator } from '../image-invalidator' import { httpLogger as log } from '../logger' +import { OzoneConfig } from '../config' export type ModerationServiceCreator = (db: Database) => ModerationService export class ModerationService { constructor( public db: Database, + public cfg: OzoneConfig, public backgroundQueue: BackgroundQueue, + public idResolver: IdResolver, public eventPusher: EventPusher, public appviewAgent: AtpAgent, - private appviewAuth: AppviewAuth, + private createAuthHeaders: (aud: string) => Promise, public serverDid: string, public imgInvalidator?: ImageInvalidator, public cdnPaths?: string[], ) {} static creator( + cfg: OzoneConfig, backgroundQueue: BackgroundQueue, + idResolver: IdResolver, eventPusher: EventPusher, appviewAgent: AtpAgent, - appviewAuth: AppviewAuth, + createAuthHeaders: (aud: string) => Promise, serverDid: string, imgInvalidator?: ImageInvalidator, cdnPaths?: string[], @@ -73,17 +80,21 @@ export class ModerationService { return (db: Database) => new ModerationService( db, + cfg, backgroundQueue, + idResolver, eventPusher, appviewAgent, - appviewAuth, + createAuthHeaders, serverDid, imgInvalidator, cdnPaths, ) } - views = new ModerationViews(this.db, this.appviewAgent, this.appviewAuth) + views = new ModerationViews(this.db, this.appviewAgent, () => + this.createAuthHeaders(this.cfg.appview.did), + ) async getEvent(id: number): Promise { return await this.db.db @@ -291,6 +302,9 @@ export class ModerationService { if (isModEventEmail(event)) { meta.subjectLine = event.subjectLine + if (event.content) { + meta.content = event.content + } } const subjectInfo = subject.info() @@ -903,6 +917,49 @@ export class ModerationService { ) .execute() } + + async sendEmail(opts: { + content: string + recipientDid: string + subject: string + }) { + const { subject, content, recipientDid } = opts + const { pds } = await this.idResolver.did.resolveAtprotoData(recipientDid) + const url = new URL(pds) + if (!this.cfg.service.devMode && !isSafeUrl(url)) { + throw new InvalidRequestError('Invalid pds service in DID doc') + } + const agent = new AtpAgent({ service: url }) + const { data: serverInfo } = + await agent.api.com.atproto.server.describeServer() + if (serverInfo.did !== `did:web:${url.hostname}`) { + // @TODO do bidirectional check once implemented. in the meantime, + // matching did to hostname we're talking to is pretty good. + throw new InvalidRequestError('Invalid pds service in DID doc') + } + const { data: delivery } = await agent.api.com.atproto.admin.sendEmail( + { + subject, + content, + recipientDid, + senderDid: this.cfg.service.did, + }, + { + encoding: 'application/json', + ...(await this.createAuthHeaders(serverInfo.did)), + }, + ) + if (!delivery.sent) { + throw new InvalidRequestError('Email was accepted but not sent') + } + } +} + +const isSafeUrl = (url: URL) => { + if (url.protocol !== 'https:') return false + if (!url.hostname || url.hostname === 'localhost') return false + if (net.isIP(url.hostname) === 0) return false + return true } const TAKEDOWNS = ['pds_takedown' as const, 'appview_takedown' as const] diff --git a/packages/ozone/src/mod-service/views.ts b/packages/ozone/src/mod-service/views.ts index f1188968cbe..498091a8bd0 100644 --- a/packages/ozone/src/mod-service/views.ts +++ b/packages/ozone/src/mod-service/views.ts @@ -26,20 +26,17 @@ import { REASONOTHER } from '../lexicon/types/com/atproto/moderation/defs' import { subjectFromEventRow, subjectFromStatusRow } from './subject' import { formatLabel } from './util' -export type AppviewAuth = () => Promise< - | { - headers: { - authorization: string - } - } - | undefined -> +export type AuthHeaders = { + headers: { + authorization: string + } +} export class ModerationViews { constructor( private db: Database, private appviewAgent: AtpAgent, - private appviewAuth: AppviewAuth, + private appviewAuth: () => Promise, ) {} async getAccoutInfosByDid(dids: string[]): Promise> { @@ -154,6 +151,7 @@ export class ModerationViews { eventView.event = { ...eventView.event, subjectLine: event.meta?.subjectLine ?? '', + content: event.meta?.content, } } diff --git a/packages/ozone/tests/moderation-events.test.ts b/packages/ozone/tests/moderation-events.test.ts index fbe571a8172..1212a6049e6 100644 --- a/packages/ozone/tests/moderation-events.test.ts +++ b/packages/ozone/tests/moderation-events.test.ts @@ -1,4 +1,6 @@ import assert from 'node:assert' +import EventEmitter, { once } from 'node:events' +import Mail from 'nodemailer/lib/mailer' import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import AtpAgent, { ComAtprotoAdminDefs, @@ -422,4 +424,51 @@ describe('moderation-events', () => { }) }) }) + + describe('email event', () => { + let sendMailOriginal + const mailCatcher = new EventEmitter() + const getMailFrom = async (promise): Promise => { + const result = await Promise.all([once(mailCatcher, 'mail'), promise]) + return result[0][0] + } + + beforeAll(() => { + const mailer = network.pds.ctx.moderationMailer + // Catch emails for use in tests + sendMailOriginal = mailer.transporter.sendMail + mailer.transporter.sendMail = async (opts) => { + const result = await sendMailOriginal.call(mailer.transporter, opts) + mailCatcher.emit('mail', opts) + return result + } + }) + + afterAll(() => { + network.pds.ctx.moderationMailer.transporter.sendMail = sendMailOriginal + }) + + it('sends email via pds.', async () => { + const mail = await getMailFrom( + emitModerationEvent({ + event: { + $type: 'com.atproto.admin.defs#modEventEmail', + comment: 'Reaching out to Alice', + subjectLine: 'Hello', + content: 'Hey Alice, how are you?', + }, + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.alice, + }, + createdBy: sc.dids.bob, + }), + ) + expect(mail).toEqual({ + to: 'alice@test.com', + subject: 'Hello', + html: 'Hey Alice, how are you?', + }) + }) + }) }) diff --git a/packages/pds/src/api/com/atproto/admin/sendEmail.ts b/packages/pds/src/api/com/atproto/admin/sendEmail.ts index e23d6bea5c1..f6d8cce8d19 100644 --- a/packages/pds/src/api/com/atproto/admin/sendEmail.ts +++ b/packages/pds/src/api/com/atproto/admin/sendEmail.ts @@ -1,23 +1,23 @@ +import assert from 'node:assert' import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru, resultPassthru } from '../../../proxy' +import { resultPassthru } from '../../../proxy' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.sendEmail({ - auth: ctx.authVerifier.role, - handler: async ({ req, input, auth }) => { - if (!auth.credentials.admin && !auth.credentials.moderator) { + auth: ctx.authVerifier.roleOrAdminService, + handler: async ({ input, auth }) => { + if (auth.credentials.type === 'role' && !auth.credentials.moderator) { throw new AuthRequiredError('Insufficient privileges') } const { content, recipientDid, - senderDid, - subject = 'Message from Bluesky moderator', - comment, + subject = 'Message via your PDS', } = input.body + const account = await ctx.accountManager.getAccount(recipientDid, { includeDeactivated: true, includeTakenDown: true, @@ -27,11 +27,15 @@ export default function (server: Server, ctx: AppContext) { } if (ctx.entrywayAgent) { + assert(ctx.cfg.entryway) return resultPassthru( - await ctx.entrywayAgent.com.atproto.admin.sendEmail( - input.body, - authPassthru(req, true), - ), + await ctx.entrywayAgent.com.atproto.admin.sendEmail(input.body, { + encoding: 'application/json', + ...(await ctx.serviceAuthHeaders( + recipientDid, + ctx.cfg.entryway?.did, + )), + }), ) } @@ -44,24 +48,6 @@ export default function (server: Server, ctx: AppContext) { { subject, to: account.email }, ) - if (ctx.moderationAgent) { - await ctx.moderationAgent.api.com.atproto.admin.emitModerationEvent( - { - event: { - $type: 'com.atproto.admin.defs#modEventEmail', - subjectLine: subject, - comment, - }, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: recipientDid, - }, - createdBy: senderDid, - }, - { ...authPassthru(req), encoding: 'application/json' }, - ) - } - return { encoding: 'application/json', body: { sent: true }, diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 14e4c1cb81e..72ac23230f4 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -897,6 +897,10 @@ export const schemaDict = { type: 'string', description: 'The subject line of the email sent to the user.', }, + content: { + type: 'string', + description: 'The content of the email sent to the user.', + }, comment: { type: 'string', description: 'Additional comment about the outgoing comm.', diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/defs.ts b/packages/pds/src/lexicon/types/com/atproto/admin/defs.ts index a713a635635..acfda37abbc 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/defs.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/defs.ts @@ -704,6 +704,8 @@ export function validateModEventUnmute(v: unknown): ValidationResult { export interface ModEventEmail { /** The subject line of the email sent to the user. */ subjectLine: string + /** The content of the email sent to the user. */ + content?: string /** Additional comment about the outgoing comm. */ comment?: string [k: string]: unknown From c273f4612821dacbf07dca7c47b9f4b9a9f2f3ff Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 13:32:13 -0600 Subject: [PATCH 32/50] add dev dep for nodemailer in ozone --- packages/ozone/package.json | 3 ++- pnpm-lock.yaml | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ozone/package.json b/packages/ozone/package.json index 3840f4b49ed..2ba78f68729 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/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: From 8341c7a10b53c5d3fa477199152486e62a37e241 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 13:34:49 -0600 Subject: [PATCH 33/50] fix auth verifier method --- packages/pds/src/api/com/atproto/admin/sendEmail.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/src/api/com/atproto/admin/sendEmail.ts b/packages/pds/src/api/com/atproto/admin/sendEmail.ts index f6d8cce8d19..6e30159c204 100644 --- a/packages/pds/src/api/com/atproto/admin/sendEmail.ts +++ b/packages/pds/src/api/com/atproto/admin/sendEmail.ts @@ -6,7 +6,7 @@ import { resultPassthru } from '../../../proxy' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.sendEmail({ - auth: ctx.authVerifier.roleOrAdminService, + auth: ctx.authVerifier.roleOrModService, handler: async ({ input, auth }) => { if (auth.credentials.type === 'role' && !auth.credentials.moderator) { throw new AuthRequiredError('Insufficient privileges') From f9361057b3892b7f504a5ec0fb4db1086c450004 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 13:36:14 -0600 Subject: [PATCH 34/50] build branch --- .github/workflows/build-and-push-bsky-ghcr.yaml | 2 +- .github/workflows/build-and-push-ozone-aws.yaml | 2 +- .github/workflows/build-and-push-pds-ghcr.yaml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-push-bsky-ghcr.yaml b/.github/workflows/build-and-push-bsky-ghcr.yaml index f1bf0bd10f5..cfa76dc5a46 100644 --- a/.github/workflows/build-and-push-bsky-ghcr.yaml +++ b/.github/workflows/build-and-push-bsky-ghcr.yaml @@ -3,7 +3,7 @@ on: push: branches: - main - - appview-v2 + - 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 b934d192b6f..34c461a0b42 100644 --- a/.github/workflows/build-and-push-ozone-aws.yaml +++ b/.github/workflows/build-and-push-ozone-aws.yaml @@ -3,7 +3,7 @@ on: push: branches: - main - - ozone-cdn-invalidation + - 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 b11230ab531..988e6b02f84 100644 --- a/.github/workflows/build-and-push-pds-ghcr.yaml +++ b/.github/workflows/build-and-push-pds-ghcr.yaml @@ -3,6 +3,7 @@ on: push: branches: - main + - pds-proxy-headers env: REGISTRY: ghcr.io USERNAME: ${{ github.actor }} From 971b4b7605da57a712e22b01a1c93ce03c30ccdc Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 13:37:07 -0600 Subject: [PATCH 35/50] build branch --- .github/workflows/build-and-push-bsky-ghcr.yaml | 1 - .github/workflows/build-and-push-ozone-aws.yaml | 2 +- .github/workflows/build-and-push-pds-ghcr.yaml | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) 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..cebd6e6aab2 100644 --- a/.github/workflows/build-and-push-ozone-aws.yaml +++ b/.github/workflows/build-and-push-ozone-aws.yaml @@ -3,7 +3,7 @@ on: push: branches: - main - - pds-proxy-headers + - rm-basic-auth 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..2621c532f72 100644 --- a/.github/workflows/build-and-push-pds-ghcr.yaml +++ b/.github/workflows/build-and-push-pds-ghcr.yaml @@ -3,7 +3,7 @@ on: push: branches: - main - - pds-proxy-headers + - rm-basic-auth env: REGISTRY: ghcr.io USERNAME: ${{ github.actor }} From 5f3c91b63b79255188e7e3eab173d0586a736bfc Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 13:58:18 -0600 Subject: [PATCH 36/50] fix url check --- packages/pds/src/pipethrough.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/src/pipethrough.ts b/packages/pds/src/pipethrough.ts index 595584b9388..a33e719eda8 100644 --- a/packages/pds/src/pipethrough.ts +++ b/packages/pds/src/pipethrough.ts @@ -154,7 +154,7 @@ export const doProxy = async (url: URL, reqInit: RequestInit) => { const isSafeUrl = (url: URL) => { if (url.protocol !== 'https:') return false if (!url.hostname || url.hostname === 'localhost') return false - if (net.isIP(url.hostname) === 0) return false + if (net.isIP(url.hostname) !== 0) return false return true } From 037f163cdb9dffb418a8363b3c7d3551fd2eebf4 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 15:32:30 -0600 Subject: [PATCH 37/50] better error handling for get account infos --- packages/ozone/src/mod-service/views.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/ozone/src/mod-service/views.ts b/packages/ozone/src/mod-service/views.ts index 498091a8bd0..f65fd68b480 100644 --- a/packages/ozone/src/mod-service/views.ts +++ b/packages/ozone/src/mod-service/views.ts @@ -25,6 +25,7 @@ import { import { REASONOTHER } from '../lexicon/types/com/atproto/moderation/defs' import { subjectFromEventRow, subjectFromStatusRow } from './subject' import { formatLabel } from './util' +import { httpLogger as log } from '../logger' export type AuthHeaders = { headers: { @@ -43,15 +44,20 @@ export class ModerationViews { if (dids.length === 0) return new Map() const auth = await this.appviewAuth() if (!auth) return new Map() - const res = await this.appviewAgent.api.com.atproto.admin.getAccountInfos( - { - dids: dedupeStrs(dids), - }, - auth, - ) - return res.data.infos.reduce((acc, cur) => { - return acc.set(cur.did, cur) - }, new Map()) + try { + const res = await this.appviewAgent.api.com.atproto.admin.getAccountInfos( + { + dids: dedupeStrs(dids), + }, + auth, + ) + return res.data.infos.reduce((acc, cur) => { + return acc.set(cur.did, cur) + }, new Map()) + } catch (err) { + log.error({ err, dids }, 'failed to resolve account infos from appview') + return new Map() + } } async repos(dids: string[]): Promise> { From fc1c40dff8bb14d8a6cfbba7a0b18a8efaa76057 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 15:33:12 -0600 Subject: [PATCH 38/50] fix labeler service id --- packages/bsky/src/auth-verifier.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bsky/src/auth-verifier.ts b/packages/bsky/src/auth-verifier.ts index b305aad1ef1..2936b1cd28b 100644 --- a/packages/bsky/src/auth-verifier.ts +++ b/packages/bsky/src/auth-verifier.ts @@ -211,7 +211,7 @@ export class AuthVerifier { } const [did, serviceId] = iss.split('#') const keyId = - serviceId === 'atproto-labeler' ? 'atproto-label' : 'atproto' + serviceId === 'atproto_labeler' ? 'atproto_label' : 'atproto' let identity: GetIdentityByDidResponse try { identity = await this.dataplane.getIdentityByDid({ did }) From 5e1c5fd7d3d7acebbf5c03d9a765f6f45082469c Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 15:50:18 -0600 Subject: [PATCH 39/50] fix iss on auth headers --- packages/ozone/src/context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ozone/src/context.ts b/packages/ozone/src/context.ts index db8d123d5e0..e7168881bac 100644 --- a/packages/ozone/src/context.ts +++ b/packages/ozone/src/context.ts @@ -55,7 +55,7 @@ export class AppContext { const createAuthHeaders = (aud: string) => createServiceAuthHeaders({ - iss: cfg.service.did, + iss: `${cfg.service.did}#atproto_labeler`, aud, keypair: signingKey, }) From 82acea2356fc21a7cb180d9813b6bf7014a92448 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 15:58:58 -0600 Subject: [PATCH 40/50] fix dev-env ozone did --- packages/dev-env/src/ozone.ts | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/dev-env/src/ozone.ts b/packages/dev-env/src/ozone.ts index 278ad8d2b2b..27e240b0518 100644 --- a/packages/dev-env/src/ozone.ts +++ b/packages/dev-env/src/ozone.ts @@ -3,7 +3,7 @@ import * as ui8 from 'uint8arrays' import * as ozone from '@atproto/ozone' import { AtpAgent } from '@atproto/api' import { Secp256k1Keypair } from '@atproto/crypto' -import { Client as PlcClient } from '@did-plc/lib' +import * as plc from '@did-plc/lib' import { OzoneConfig } from './types' import { ADMIN_PASSWORD, MOD_PASSWORD, TRIAGE_PASSWORD } from './const' @@ -21,18 +21,32 @@ export class TestOzone { const signingKeyHex = ui8.toString(await serviceKeypair.export(), 'hex') let serverDid = config.serverDid if (!serverDid) { - const plcClient = new PlcClient(config.plcUrl) - serverDid = await plcClient.createDid({ - signingKey: serviceKeypair.did(), - rotationKeys: [serviceKeypair.did()], - handle: 'ozone.test', - pds: `https://pds.invalid`, - signer: serviceKeypair, - }) + const plcClient = new plc.Client(config.plcUrl) + const plcOp = await plc.signOperation( + { + type: 'plc_operation', + alsoKnownAs: [], + rotationKeys: [serviceKeypair.did()], + verificationMethods: { + atproto_label: serviceKeypair.did(), + }, + services: { + atproto_labeler: { + type: 'AtprotoLabeler', + endpoint: 'https://ozone.public.url', + }, + }, + prev: null, + }, + serviceKeypair, + ) + serverDid = await plc.didForCreateOp(plcOp) + await plcClient.sendOperation(serverDid, plcOp) } const port = config.port || (await getPort()) const url = `http://localhost:${port}` + const env: ozone.OzoneEnvironment = { devMode: true, version: '0.0.0', From 4c7db5cc644a91f092cccbc95d895e9080100106 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 16:15:10 -0600 Subject: [PATCH 41/50] fix tests & another jwt issuer --- packages/dev-env/src/network.ts | 11 ++----- packages/dev-env/src/ozone.ts | 52 +++++++++++++++++++-------------- packages/ozone/src/context.ts | 2 +- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/packages/dev-env/src/network.ts b/packages/dev-env/src/network.ts index c90e2c181f5..06fbd780060 100644 --- a/packages/dev-env/src/network.ts +++ b/packages/dev-env/src/network.ts @@ -3,12 +3,11 @@ import * as uint8arrays from 'uint8arrays' import getPort from 'get-port' import { wait } from '@atproto/common-web' import { createServiceJwt } from '@atproto/xrpc-server' -import { Client as PlcClient } from '@did-plc/lib' import { TestServerParams } from './types' import { TestPlc } from './plc' import { TestPds } from './pds' import { TestBsky } from './bsky' -import { TestOzone } from './ozone' +import { TestOzone, createOzoneDid } from './ozone' import { mockNetworkUtilities } from './util' import { TestNetworkNoAppView } from './network-no-appview' import { Secp256k1Keypair } from '@atproto/crypto' @@ -43,13 +42,7 @@ export class TestNetwork extends TestNetworkNoAppView { const ozonePort = params.ozone?.port ?? (await getPort()) const ozoneKey = await Secp256k1Keypair.create({ exportable: true }) - const ozoneDid = await new PlcClient(plc.url).createDid({ - signingKey: ozoneKey.did(), - rotationKeys: [ozoneKey.did()], - handle: 'ozone.test', - pds: `http://pds.invalid`, - signer: ozoneKey, - }) + const ozoneDid = await createOzoneDid(plc.url, ozoneKey) const bsky = await TestBsky.create({ port: bskyPort, diff --git a/packages/dev-env/src/ozone.ts b/packages/dev-env/src/ozone.ts index 27e240b0518..d06e45eba13 100644 --- a/packages/dev-env/src/ozone.ts +++ b/packages/dev-env/src/ozone.ts @@ -2,7 +2,7 @@ import getPort from 'get-port' import * as ui8 from 'uint8arrays' import * as ozone from '@atproto/ozone' import { AtpAgent } from '@atproto/api' -import { Secp256k1Keypair } from '@atproto/crypto' +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' @@ -21,27 +21,7 @@ export class TestOzone { const signingKeyHex = ui8.toString(await serviceKeypair.export(), 'hex') let serverDid = config.serverDid if (!serverDid) { - const plcClient = new plc.Client(config.plcUrl) - const plcOp = await plc.signOperation( - { - type: 'plc_operation', - alsoKnownAs: [], - rotationKeys: [serviceKeypair.did()], - verificationMethods: { - atproto_label: serviceKeypair.did(), - }, - services: { - atproto_labeler: { - type: 'AtprotoLabeler', - endpoint: 'https://ozone.public.url', - }, - }, - prev: null, - }, - serviceKeypair, - ) - serverDid = await plc.didForCreateOp(plcOp) - await plcClient.sendOperation(serverDid, plcOp) + serverDid = await createOzoneDid(config.plcUrl, serviceKeypair) } const port = config.port || (await getPort()) @@ -130,3 +110,31 @@ export class TestOzone { await this.server.destroy() } } + +export const createOzoneDid = async ( + plcUrl: string, + keypair: Keypair, +): Promise => { + const plcClient = new plc.Client(plcUrl) + const plcOp = await plc.signOperation( + { + type: 'plc_operation', + alsoKnownAs: [], + rotationKeys: [keypair.did()], + verificationMethods: { + atproto_label: keypair.did(), + }, + services: { + atproto_labeler: { + type: 'AtprotoLabeler', + endpoint: 'https://ozone.public.url', + }, + }, + prev: null, + }, + keypair, + ) + const did = await plc.didForCreateOp(plcOp) + await plcClient.sendOperation(did, plcOp) + return did +} diff --git a/packages/ozone/src/context.ts b/packages/ozone/src/context.ts index e7168881bac..5205e54f848 100644 --- a/packages/ozone/src/context.ts +++ b/packages/ozone/src/context.ts @@ -172,7 +172,7 @@ export class AppContext { } async serviceAuthHeaders(aud: string) { - const iss = this.cfg.service.did + const iss = `${this.cfg.service.did}#atproto_labeler` return createServiceAuthHeaders({ iss, aud, From 9d5d7626b4b525fecfae953d6ae567d55b9a90fc Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 16:49:56 -0600 Subject: [PATCH 42/50] fix proxy auth --- packages/ozone/src/api/proxied.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ozone/src/api/proxied.ts b/packages/ozone/src/api/proxied.ts index 8b02bc21e51..f1d0d748ea7 100644 --- a/packages/ozone/src/api/proxied.ts +++ b/packages/ozone/src/api/proxied.ts @@ -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, @@ -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, @@ -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, From 81f9d693afcd7e354f84c2d3254353bed84dbb43 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Tue, 5 Mar 2024 17:55:34 -0500 Subject: [PATCH 43/50] ozone: fix ip check --- packages/ozone/src/mod-service/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ozone/src/mod-service/index.ts b/packages/ozone/src/mod-service/index.ts index 1ca793d5601..dba57baa7b9 100644 --- a/packages/ozone/src/mod-service/index.ts +++ b/packages/ozone/src/mod-service/index.ts @@ -958,7 +958,7 @@ export class ModerationService { const isSafeUrl = (url: URL) => { if (url.protocol !== 'https:') return false if (!url.hostname || url.hostname === 'localhost') return false - if (net.isIP(url.hostname) === 0) return false + if (net.isIP(url.hostname) !== 0) return false return true } From 7be84452c3bd6021a77a81b7b5cff9cf5656afb8 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 17:14:31 -0600 Subject: [PATCH 44/50] fix aud check on pds mod service auth --- packages/pds/src/auth-verifier.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/pds/src/auth-verifier.ts b/packages/pds/src/auth-verifier.ts index 6625ea9629c..05a61ecdf5b 100644 --- a/packages/pds/src/auth-verifier.ts +++ b/packages/pds/src/auth-verifier.ts @@ -259,9 +259,20 @@ export class AuthVerifier { throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss') } const payload = await this.verifyServiceJwt(reqCtx, { - aud: this.dids.entryway ?? this.dids.pds, + aud: null, iss: [this.dids.modService, `${this.dids.modService}#atproto_labeler`], }) + if ( + payload.aud !== this.dids.pds && + (!this.dids.entryway || payload.aud !== this.dids.entryway) + ) { + throw new AuthRequiredError( + 'jwt audience does not match service did', + 'BadJwtAudience', + ) + } + + payload.aud return { credentials: { type: 'mod_service', From 592518c63b0696aa70578c0316b510b1242ea739 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 5 Mar 2024 17:16:55 -0600 Subject: [PATCH 45/50] tidy --- packages/pds/src/auth-verifier.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/pds/src/auth-verifier.ts b/packages/pds/src/auth-verifier.ts index 05a61ecdf5b..e12e0e8acfe 100644 --- a/packages/pds/src/auth-verifier.ts +++ b/packages/pds/src/auth-verifier.ts @@ -271,8 +271,6 @@ export class AuthVerifier { 'BadJwtAudience', ) } - - payload.aud return { credentials: { type: 'mod_service', From 1ce9e00eafe6586914b082fb5b0a30529298f0b7 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 5 Mar 2024 20:29:37 -0600 Subject: [PATCH 46/50] Update packages/pds/tests/proxied/admin.test.ts Co-authored-by: devin ivy --- packages/pds/tests/proxied/admin.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/pds/tests/proxied/admin.test.ts b/packages/pds/tests/proxied/admin.test.ts index ba9de578a99..46bef49cbaf 100644 --- a/packages/pds/tests/proxied/admin.test.ts +++ b/packages/pds/tests/proxied/admin.test.ts @@ -8,7 +8,6 @@ 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('proxies admin requests', () => { let network: TestNetwork let agent: AtpAgent From b4211ea1d4402cd1fe1bb37800195e101158afc5 Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 6 Mar 2024 18:21:44 -0600 Subject: [PATCH 47/50] fix pipethrough of headers --- packages/pds/src/pipethrough.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/pds/src/pipethrough.ts b/packages/pds/src/pipethrough.ts index faf4fe36aa3..0d9c00737b5 100644 --- a/packages/pds/src/pipethrough.ts +++ b/packages/pds/src/pipethrough.ts @@ -95,6 +95,12 @@ export const parseProxyHeader = async ( return { did, serviceUrl } } +const HEADERS_TO_FORWARD = [ + 'accept-language', + 'content-type', + 'atproto-labelers', +] + export const createUrlAndHeaders = async ( ctx: AppContext, req: express.Request, @@ -115,10 +121,13 @@ export const createUrlAndHeaders = async ( const headers = requester ? (await ctx.serviceAuthHeaders(requester, aud)).headers : {} - // forward accept-language header to upstream services - headers['accept-language'] = req.headers['accept-language'] - headers['content-type'] = req.headers['content-type'] - headers['atproto-labelers'] = req.headers['atproto-labelers'] + // forward select headers to upstream services + for (const header of HEADERS_TO_FORWARD) { + const val = req.headers[header] + if (val) { + headers[header] = val + } + } return { url, headers } } From e46397d3c69a9ee2ebe8d21f7b802c5e791b2a2b Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 6 Mar 2024 18:27:51 -0600 Subject: [PATCH 48/50] fix moderation status tests --- packages/ozone/tests/moderation-statuses.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/ozone/tests/moderation-statuses.test.ts b/packages/ozone/tests/moderation-statuses.test.ts index 9deaa26ab6e..1c2848d9404 100644 --- a/packages/ozone/tests/moderation-statuses.test.ts +++ b/packages/ozone/tests/moderation-statuses.test.ts @@ -160,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, }) @@ -170,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', @@ -180,7 +180,7 @@ describe('moderation-statuses', () => { }, createdBy: sc.dids.alice, }), - emitModerationEvent({ + modClient.emitModerationEvent({ subject: bobsAccount, event: { $type: 'com.atproto.admin.defs#modEventComment', @@ -197,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, }) @@ -207,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', @@ -218,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', From d9b62b906499ecf4f7af40c8695e4757de2f9f38 Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 6 Mar 2024 18:48:35 -0600 Subject: [PATCH 49/50] fix auth on ozone routes --- .../ozone/src/api/admin/createCommunicationTemplate.ts | 2 +- .../ozone/src/api/admin/deleteCommunicationTemplate.ts | 2 +- packages/ozone/src/api/admin/emitModerationEvent.ts | 7 +++++-- packages/ozone/src/api/admin/getModerationEvent.ts | 2 +- packages/ozone/src/api/admin/getRecord.ts | 2 +- packages/ozone/src/api/admin/getRepo.ts | 2 +- .../ozone/src/api/admin/listCommunicationTemplates.ts | 2 +- packages/ozone/src/api/admin/queryModerationEvents.ts | 2 +- .../ozone/src/api/admin/queryModerationStatuses.ts | 2 +- packages/ozone/src/api/admin/searchRepos.ts | 2 +- .../ozone/src/api/admin/updateCommunicationTemplate.ts | 2 +- packages/ozone/src/auth-verifier.ts | 10 ++++++++++ 12 files changed, 25 insertions(+), 12 deletions(-) diff --git a/packages/ozone/src/api/admin/createCommunicationTemplate.ts b/packages/ozone/src/api/admin/createCommunicationTemplate.ts index 0f7b794fcb4..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.moderator, + 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 409a8bc9ba9..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.moderator, + 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 ee4927bce30..f2885e67e4c 100644 --- a/packages/ozone/src/api/admin/emitModerationEvent.ts +++ b/packages/ozone/src/api/admin/emitModerationEvent.ts @@ -13,10 +13,13 @@ import { retryHttp } from '../../util' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.emitModerationEvent({ - auth: ctx.authVerifier.moderator, + auth: ctx.authVerifier.modOrAdminToken, handler: async ({ input, auth }) => { const access = auth.credentials - const createdBy = access.iss + const createdBy = + auth.credentials.type === 'moderator' + ? auth.credentials.iss + : input.body.createdBy const db = ctx.db const moderationService = ctx.modService(db) const { event } = input.body diff --git a/packages/ozone/src/api/admin/getModerationEvent.ts b/packages/ozone/src/api/admin/getModerationEvent.ts index cd1657b6a4d..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.moderator, + 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 34b94c21b2f..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.moderator, + 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 2812d5de3c4..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.moderator, + 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 4ed69157e1d..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.moderator, + 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 f9cfbe65cd1..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.moderator, + 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 a97a998fc1d..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.moderator, + 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 e33595de3bd..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.moderator, + 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 b206dbe46ef..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.moderator, + auth: ctx.authVerifier.modOrAdminToken, handler: async ({ input, auth }) => { const access = auth.credentials const db = ctx.db diff --git a/packages/ozone/src/auth-verifier.ts b/packages/ozone/src/auth-verifier.ts index 4255fdc4d82..2eec84cb683 100644 --- a/packages/ozone/src/auth-verifier.ts +++ b/packages/ozone/src/auth-verifier.ts @@ -68,6 +68,16 @@ export class AuthVerifier { this.adminPassword = opts.adminPassword } + modOrAdminToken = async ( + reqCtx: ReqCtx, + ): Promise => { + if (isBasicToken(reqCtx.req)) { + return this.adminToken(reqCtx) + } else { + return this.moderator(reqCtx) + } + } + moderator = async (reqCtx: ReqCtx): Promise => { const creds = await this.standard(reqCtx) if (!creds.credentials.isTriage) { From 4d2c17fc68843fae75a3c7f1e4542d2eb50a2871 Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 6 Mar 2024 19:02:39 -0600 Subject: [PATCH 50/50] update iss on daemon --- packages/ozone/src/daemon/context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, })