Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Expose takendown profile, their follows and followers to mods #1456

Merged
merged 5 commits into from
Aug 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions packages/bsky/src/api/app/bsky/actor/getProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import { setRepoRev } from '../../../util'

export default function (server: Server, ctx: AppContext) {
server.app.bsky.actor.getProfile({
auth: ctx.authOptionalVerifier,
auth: ctx.authOptionalAccessOrRoleVerifier,
handler: async ({ auth, params, res }) => {
const { actor } = params
const requester = auth.credentials.did
const requester = 'did' in auth.credentials ? auth.credentials.did : null
const canViewTakendownProfile =
auth.credentials.type === 'role' && auth.credentials.triage
const db = ctx.db.getReplica()
const actorService = ctx.services.actor(db)

Expand All @@ -22,7 +24,7 @@ export default function (server: Server, ctx: AppContext) {
if (!actorRes) {
throw new InvalidRequestError('Profile not found')
}
if (softDeleted(actorRes)) {
if (!canViewTakendownProfile && softDeleted(actorRes)) {
throw new InvalidRequestError(
'Account has been taken down',
'AccountTakedown',
Expand All @@ -31,6 +33,7 @@ export default function (server: Server, ctx: AppContext) {
const profile = await actorService.views.profileDetailed(
actorRes,
requester,
{ includeSoftDeleted: canViewTakendownProfile },
)
if (!profile) {
throw new InvalidRequestError('Profile not found')
Expand Down
23 changes: 17 additions & 6 deletions packages/bsky/src/api/app/bsky/graph/getFollowers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,22 @@ import { notSoftDeletedClause } from '../../../../db/util'

export default function (server: Server, ctx: AppContext) {
server.app.bsky.graph.getFollowers({
auth: ctx.authOptionalVerifier,
auth: ctx.authOptionalAccessOrRoleVerifier,
handler: async ({ params, auth }) => {
const { actor, limit, cursor } = params
const requester = auth.credentials.did
const requester = 'did' in auth.credentials ? auth.credentials.did : null
const canViewTakendownProfile =
auth.credentials.type === 'role' && auth.credentials.triage
const db = ctx.db.getReplica()
const { ref } = db.db.dynamic

const actorService = ctx.services.actor(db)
const graphService = ctx.services.graph(db)

const subjectRes = await actorService.getActor(actor)
const subjectRes = await actorService.getActor(
actor,
canViewTakendownProfile,
)
if (!subjectRes) {
throw new InvalidRequestError(`Actor not found: ${actor}`)
}
Expand All @@ -25,7 +30,9 @@ export default function (server: Server, ctx: AppContext) {
.selectFrom('follow')
.where('follow.subjectDid', '=', subjectRes.did)
.innerJoin('actor as creator', 'creator.did', 'follow.creator')
.where(notSoftDeletedClause(ref('creator')))
.if(!canViewTakendownProfile, (qb) =>
qb.where(notSoftDeletedClause(ref('creator'))),
)
.whereNotExists(
graphService.blockQb(requester, [ref('follow.creator')]),
)
Expand All @@ -47,8 +54,12 @@ export default function (server: Server, ctx: AppContext) {

const followersRes = await followersReq.execute()
const [followers, subject] = await Promise.all([
actorService.views.hydrateProfiles(followersRes, requester),
actorService.views.profile(subjectRes, requester),
actorService.views.hydrateProfiles(followersRes, requester, {
includeSoftDeleted: canViewTakendownProfile,
devinivy marked this conversation as resolved.
Show resolved Hide resolved
}),
actorService.views.profile(subjectRes, requester, {
includeSoftDeleted: canViewTakendownProfile,
}),
])
if (!subject) {
throw new InvalidRequestError(`Actor not found: ${actor}`)
Expand Down
23 changes: 17 additions & 6 deletions packages/bsky/src/api/app/bsky/graph/getFollows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,22 @@ import { notSoftDeletedClause } from '../../../../db/util'

export default function (server: Server, ctx: AppContext) {
server.app.bsky.graph.getFollows({
auth: ctx.authOptionalVerifier,
auth: ctx.authOptionalAccessOrRoleVerifier,
handler: async ({ params, auth }) => {
const { actor, limit, cursor } = params
const requester = auth.credentials.did
const requester = 'did' in auth.credentials ? auth.credentials.did : null
const canViewTakendownProfile =
auth.credentials.type === 'role' && auth.credentials.triage
const db = ctx.db.getReplica()
const { ref } = db.db.dynamic

const actorService = ctx.services.actor(db)
const graphService = ctx.services.graph(db)

const creatorRes = await actorService.getActor(actor)
const creatorRes = await actorService.getActor(
actor,
canViewTakendownProfile,
)
if (!creatorRes) {
throw new InvalidRequestError(`Actor not found: ${actor}`)
}
Expand All @@ -25,7 +30,9 @@ export default function (server: Server, ctx: AppContext) {
.selectFrom('follow')
.where('follow.creator', '=', creatorRes.did)
.innerJoin('actor as subject', 'subject.did', 'follow.subjectDid')
.where(notSoftDeletedClause(ref('subject')))
.if(!canViewTakendownProfile, (qb) =>
qb.where(notSoftDeletedClause(ref('subject'))),
)
.whereNotExists(
graphService.blockQb(requester, [ref('follow.subjectDid')]),
)
Expand All @@ -47,8 +54,12 @@ export default function (server: Server, ctx: AppContext) {

const followsRes = await followsReq.execute()
const [follows, subject] = await Promise.all([
actorService.views.hydrateProfiles(followsRes, requester),
actorService.views.profile(creatorRes, requester),
actorService.views.hydrateProfiles(followsRes, requester, {
includeSoftDeleted: canViewTakendownProfile,
}),
actorService.views.profile(creatorRes, requester, {
includeSoftDeleted: canViewTakendownProfile,
}),
])
if (!subject) {
throw new InvalidRequestError(`Actor not found: ${actor}`)
Expand Down
36 changes: 36 additions & 0 deletions packages/bsky/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,42 @@ export const authOptionalVerifier =
return authVerifier(idResolver, opts)(reqCtx)
}

export const authOptionalAccessOrRoleVerifier = (
devinivy marked this conversation as resolved.
Show resolved Hide resolved
idResolver: IdResolver,
cfg: ServerConfig,
) => {
const verifyAccess = authVerifier(idResolver, { aud: cfg.serverDid })
const verifyRole = roleVerifier(cfg)
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 =
(cfg: ServerConfig) =>
async (reqCtx: { req: express.Request; res: express.Response }) => {
Expand Down
4 changes: 4 additions & 0 deletions packages/bsky/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ export class AppContext {
})
}

get authOptionalAccessOrRoleVerifier() {
return auth.authOptionalAccessOrRoleVerifier(this.idResolver, this.cfg)
}

get roleVerifier() {
return auth.roleVerifier(this.cfg)
}
Expand Down
16 changes: 12 additions & 4 deletions packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@ import { InvalidRequestError } from '@atproto/xrpc-server'
import { Server } from '../../../../../lexicon'
import { softDeleted } from '../../../../../db/util'
import AppContext from '../../../../../context'
import { authPassthru } from '../../../../../api/com/atproto/admin/util'
import { OutputSchema } from '../../../../../lexicon/types/app/bsky/actor/getProfile'
import { handleReadAfterWrite } from '../util/read-after-write'
import { LocalRecords } from '../../../../../services/local'

export default function (server: Server, ctx: AppContext) {
server.app.bsky.actor.getProfile({
auth: ctx.accessVerifier,
auth: ctx.accessOrRoleVerifier,
handler: async ({ req, auth, params }) => {
const requester = auth.credentials.did
const requester =
auth.credentials.type === 'access' ? auth.credentials.did : null
if (ctx.canProxyRead(req)) {
const res = await ctx.appviewAgent.api.app.bsky.actor.getProfile(
params,
await ctx.serviceAuthHeaders(requester),
requester
? await ctx.serviceAuthHeaders(requester)
: authPassthru(req),
)
if (res.data.did === requester) {
return await handleReadAfterWrite(
Expand All @@ -30,6 +34,9 @@ export default function (server: Server, ctx: AppContext) {
}
}

// As long as user has triage permission, we know that they are a moderator user and can see taken down profiles
const canViewTakendownProfile =
auth.credentials.type === 'role' && auth.credentials.triage
const { actor } = params
const { db, services } = ctx
const actorService = services.appView.actor(db)
Expand All @@ -39,7 +46,7 @@ export default function (server: Server, ctx: AppContext) {
if (!actorRes) {
throw new InvalidRequestError('Profile not found')
}
if (softDeleted(actorRes)) {
if (!canViewTakendownProfile && softDeleted(actorRes)) {
throw new InvalidRequestError(
'Account has been taken down',
'AccountTakedown',
Expand All @@ -48,6 +55,7 @@ export default function (server: Server, ctx: AppContext) {
const profile = await actorService.views.profileDetailed(
actorRes,
requester,
{ includeSoftDeleted: canViewTakendownProfile },
)
if (!profile) {
throw new InvalidRequestError('Profile not found')
Expand Down
29 changes: 22 additions & 7 deletions packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,40 @@ import { Server } from '../../../../../lexicon'
import { paginate, TimeCidKeyset } from '../../../../../db/pagination'
import AppContext from '../../../../../context'
import { notSoftDeletedClause } from '../../../../../db/util'
import { authPassthru } from '../../../../../api/com/atproto/admin/util'

export default function (server: Server, ctx: AppContext) {
server.app.bsky.graph.getFollowers({
auth: ctx.accessVerifier,
auth: ctx.accessOrRoleVerifier,
handler: async ({ req, params, auth }) => {
const requester = auth.credentials.did
const requester =
auth.credentials.type === 'access' ? auth.credentials.did : null
if (ctx.canProxyRead(req)) {
const res = await ctx.appviewAgent.api.app.bsky.graph.getFollowers(
params,
await ctx.serviceAuthHeaders(requester),
requester
? await ctx.serviceAuthHeaders(requester)
: authPassthru(req),
)
return {
encoding: 'application/json',
body: res.data,
}
}

const canViewTakendownProfile =
auth.credentials.type === 'role' && auth.credentials.triage
const { actor, limit, cursor } = params
const { services, db } = ctx
const { ref } = db.db.dynamic

const actorService = services.appView.actor(db)
const graphService = services.appView.graph(db)

const subjectRes = await actorService.getActor(actor)
const subjectRes = await actorService.getActor(
actor,
canViewTakendownProfile,
)
if (!subjectRes) {
throw new InvalidRequestError(`Actor not found: ${actor}`)
}
Expand All @@ -41,7 +50,9 @@ export default function (server: Server, ctx: AppContext) {
'creator_repo.did',
'follow.creator',
)
.where(notSoftDeletedClause(ref('creator_repo')))
.if(canViewTakendownProfile, (qb) =>
qb.where(notSoftDeletedClause(ref('creator_repo'))),
)
.whereNotExists(
graphService.blockQb(requester, [ref('follow.creator')]),
)
Expand All @@ -66,8 +77,12 @@ export default function (server: Server, ctx: AppContext) {

const followersRes = await followersReq.execute()
const [followers, subject] = await Promise.all([
actorService.views.hydrateProfiles(followersRes, requester),
actorService.views.profile(subjectRes, requester),
actorService.views.hydrateProfiles(followersRes, requester, {
includeSoftDeleted: canViewTakendownProfile,
}),
actorService.views.profile(subjectRes, requester, {
includeSoftDeleted: canViewTakendownProfile,
}),
])
if (!subject) {
throw new InvalidRequestError(`Actor not found: ${actor}`)
Expand Down
29 changes: 22 additions & 7 deletions packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,40 @@ import { Server } from '../../../../../lexicon'
import { paginate, TimeCidKeyset } from '../../../../../db/pagination'
import AppContext from '../../../../../context'
import { notSoftDeletedClause } from '../../../../../db/util'
import { authPassthru } from '../../../../../api/com/atproto/admin/util'

export default function (server: Server, ctx: AppContext) {
server.app.bsky.graph.getFollows({
auth: ctx.accessVerifier,
auth: ctx.accessOrRoleVerifier,
handler: async ({ req, params, auth }) => {
const requester = auth.credentials.did
const requester =
auth.credentials.type === 'access' ? auth.credentials.did : null
if (ctx.canProxyRead(req)) {
const res = await ctx.appviewAgent.api.app.bsky.graph.getFollows(
params,
await ctx.serviceAuthHeaders(requester),
requester
? await ctx.serviceAuthHeaders(requester)
: authPassthru(req),
)
return {
encoding: 'application/json',
body: res.data,
}
}

const canViewTakendownProfile =
auth.credentials.type === 'role' && auth.credentials.triage
const { actor, limit, cursor } = params
const { services, db } = ctx
const { ref } = db.db.dynamic

const actorService = services.appView.actor(db)
const graphService = services.appView.graph(db)

const creatorRes = await actorService.getActor(actor)
const creatorRes = await actorService.getActor(
actor,
canViewTakendownProfile,
)
if (!creatorRes) {
throw new InvalidRequestError(`Actor not found: ${actor}`)
}
Expand All @@ -41,7 +50,9 @@ export default function (server: Server, ctx: AppContext) {
'subject_repo.did',
'follow.subjectDid',
)
.where(notSoftDeletedClause(ref('subject_repo')))
.if(!canViewTakendownProfile, (qb) =>
qb.where(notSoftDeletedClause(ref('subject_repo'))),
)
.whereNotExists(
graphService.blockQb(requester, [ref('follow.subjectDid')]),
)
Expand All @@ -66,8 +77,12 @@ export default function (server: Server, ctx: AppContext) {

const followsRes = await followsReq.execute()
const [follows, subject] = await Promise.all([
actorService.views.hydrateProfiles(followsRes, requester),
actorService.views.profile(creatorRes, requester),
actorService.views.hydrateProfiles(followsRes, requester, {
includeSoftDeleted: canViewTakendownProfile,
}),
actorService.views.profile(creatorRes, requester, {
includeSoftDeleted: canViewTakendownProfile,
}),
])
if (!subject) {
throw new InvalidRequestError(`Actor not found: ${actor}`)
Expand Down
Loading