Skip to content

Commit 369a201

Browse files
matthieusiebendevinivyCopilot
authored
Perf: Avoid fetching account data twice in putRecord (#4107)
* Perf: Avoid fetching account data twice in `putRecord` * Apply same changes in `createRecord`/`deleteRecord`/`applyWrites` * tidy Co-authored-by: Copilot <[email protected]> * switch validation order --------- Co-authored-by: devin ivy <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 75162ff commit 369a201

File tree

7 files changed

+120
-73
lines changed

7 files changed

+120
-73
lines changed

.changeset/fair-olives-boil.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@atproto/pds": patch
3+
---
4+
5+
Allow use of handle in `applyWrites`'s `repo`

.changeset/fast-hairs-attack.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@atproto/pds": patch
3+
---
4+
5+
Perf: Avoid fetching account data twice in `putRecord`

packages/pds/src/api/com/atproto/repo/applyWrites.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,15 @@ const ratelimitPoints = ({ input }: { input: HandlerInput }) => {
3939
export default function (server: Server, ctx: AppContext) {
4040
server.com.atproto.repo.applyWrites({
4141
auth: ctx.authVerifier.authorization({
42-
checkTakedown: true,
43-
checkDeactivated: true,
42+
// @NOTE the "checkTakedown" and "checkDeactivated" checks are typically
43+
// performed during auth. However, since this method's "repo" parameter
44+
// can be a handle, we will need to fetch the account again to ensure that
45+
// the handle matches the DID from the request's credentials. In order to
46+
// avoid fetching the account twice (during auth, and then again in the
47+
// controller), the checks are disabled here:
48+
49+
// checkTakedown: true,
50+
// checkDeactivated: true,
4451
authorize: () => {
4552
// Performed in the handler as it is based on the request body
4653
},
@@ -62,10 +69,16 @@ export default function (server: Server, ctx: AppContext) {
6269
handler: async ({ input, auth }) => {
6370
const { repo, validate, swapCommit, writes } = input.body
6471

65-
const { did } = auth.credentials
66-
if (repo !== did) {
72+
const account = await ctx.authVerifier.findAccount(repo, {
73+
checkDeactivated: true,
74+
checkTakedown: true,
75+
})
76+
77+
const did = account.did
78+
if (did !== auth.credentials.did) {
6779
throw new AuthRequiredError()
6880
}
81+
6982
if (writes.length > 200) {
7083
throw new InvalidRequestError('Too many writes. Max: 200')
7184
}

packages/pds/src/api/com/atproto/repo/createRecord.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,15 @@ import {
1515
export default function (server: Server, ctx: AppContext) {
1616
server.com.atproto.repo.createRecord({
1717
auth: ctx.authVerifier.authorization({
18-
checkTakedown: true,
19-
checkDeactivated: true,
18+
// @NOTE the "checkTakedown" and "checkDeactivated" checks are typically
19+
// performed during auth. However, since this method's "repo" parameter
20+
// can be a handle, we will need to fetch the account again to ensure that
21+
// the handle matches the DID from the request's credentials. In order to
22+
// avoid fetching the account twice (during auth, and then again in the
23+
// controller), the checks are disabled here:
24+
25+
// checkTakedown: true,
26+
// checkDeactivated: true,
2027
authorize: () => {
2128
// Performed in the handler as it requires the request body
2229
},
@@ -37,26 +44,23 @@ export default function (server: Server, ctx: AppContext) {
3744
const { repo, collection, rkey, record, swapCommit, validate } =
3845
input.body
3946

47+
const account = await ctx.authVerifier.findAccount(repo, {
48+
checkDeactivated: true,
49+
checkTakedown: true,
50+
})
51+
52+
const did = account.did
53+
if (did !== auth.credentials.did) {
54+
throw new AuthRequiredError()
55+
}
56+
4057
if (auth.credentials.type === 'oauth') {
4158
auth.credentials.permissions.assertRepo({
4259
action: 'create',
4360
collection,
4461
})
4562
}
4663

47-
const account = await ctx.accountManager.getAccount(repo, {
48-
includeDeactivated: true,
49-
})
50-
51-
if (!account) {
52-
throw new InvalidRequestError(`Could not find repo: ${repo}`)
53-
} else if (account.deactivatedAt) {
54-
throw new InvalidRequestError('Account is deactivated')
55-
}
56-
const did = account.did
57-
if (did !== auth.credentials.did) {
58-
throw new AuthRequiredError()
59-
}
6064
const swapCommitCid = swapCommit ? CID.parse(swapCommit) : undefined
6165

6266
let write: PreparedCreate

packages/pds/src/api/com/atproto/repo/deleteRecord.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,15 @@ import {
1212
export default function (server: Server, ctx: AppContext) {
1313
server.com.atproto.repo.deleteRecord({
1414
auth: ctx.authVerifier.authorization({
15-
checkTakedown: true,
16-
checkDeactivated: true,
15+
// @NOTE the "checkTakedown" and "checkDeactivated" checks are typically
16+
// performed during auth. However, since this method's "repo" parameter
17+
// can be a handle, we will need to fetch the account again to ensure that
18+
// the handle matches the DID from the request's credentials. In order to
19+
// avoid fetching the account twice (during auth, and then again in the
20+
// controller), the checks are disabled here:
21+
22+
// checkTakedown: true,
23+
// checkDeactivated: true,
1724
authorize: () => {
1825
// Performed in the handler as it requires the request body
1926
},
@@ -33,6 +40,16 @@ export default function (server: Server, ctx: AppContext) {
3340
handler: async ({ input, auth }) => {
3441
const { repo, collection, rkey, swapCommit, swapRecord } = input.body
3542

43+
const account = await ctx.authVerifier.findAccount(repo, {
44+
checkDeactivated: true,
45+
checkTakedown: true,
46+
})
47+
48+
const did = account.did
49+
if (did !== auth.credentials.did) {
50+
throw new AuthRequiredError()
51+
}
52+
3653
// We can't compute permissions based on the request payload ("input") in
3754
// the 'auth' phase, so we do it here.
3855
if (auth.credentials.type === 'oauth') {
@@ -42,20 +59,6 @@ export default function (server: Server, ctx: AppContext) {
4259
})
4360
}
4461

45-
const account = await ctx.accountManager.getAccount(repo, {
46-
includeDeactivated: true,
47-
})
48-
49-
if (!account) {
50-
throw new InvalidRequestError(`Could not find repo: ${repo}`)
51-
} else if (account.deactivatedAt) {
52-
throw new InvalidRequestError('Account is deactivated')
53-
}
54-
const did = account.did
55-
if (did !== auth.credentials.did) {
56-
throw new AuthRequiredError()
57-
}
58-
5962
const swapCommitCid = swapCommit ? CID.parse(swapCommit) : undefined
6063
const swapRecordCid = swapRecord ? CID.parse(swapRecord) : undefined
6164

packages/pds/src/api/com/atproto/repo/putRecord.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,15 @@ import {
2121
export default function (server: Server, ctx: AppContext) {
2222
server.com.atproto.repo.putRecord({
2323
auth: ctx.authVerifier.authorization({
24-
checkTakedown: true,
25-
checkDeactivated: true,
24+
// @NOTE the "checkTakedown" and "checkDeactivated" checks are typically
25+
// performed during auth. However, since this method's "repo" parameter
26+
// can be a handle, we will need to fetch the account again to ensure that
27+
// the handle matches the DID from the request's credentials. In order to
28+
// avoid fetching the account twice (during auth, and then again in the
29+
// controller), the checks are disabled here:
30+
31+
// checkTakedown: true,
32+
// checkDeactivated: true,
2633
authorize: () => {
2734
// Performed in the handler as it requires the request body
2835
},
@@ -50,6 +57,16 @@ export default function (server: Server, ctx: AppContext) {
5057
swapRecord,
5158
} = input.body
5259

60+
const account = await ctx.authVerifier.findAccount(repo, {
61+
checkDeactivated: true,
62+
checkTakedown: true,
63+
})
64+
65+
const did = account.did
66+
if (did !== auth.credentials.did) {
67+
throw new AuthRequiredError()
68+
}
69+
5370
// We can't compute permissions based on the request payload ("input") in
5471
// the 'auth' phase, so we do it here.
5572
if (auth.credentials.type === 'oauth') {
@@ -63,20 +80,6 @@ export default function (server: Server, ctx: AppContext) {
6380
})
6481
}
6582

66-
const account = await ctx.accountManager.getAccount(repo, {
67-
includeDeactivated: true,
68-
})
69-
70-
if (!account) {
71-
throw new InvalidRequestError(`Could not find repo: ${repo}`)
72-
} else if (account.deactivatedAt) {
73-
throw new InvalidRequestError('Account is deactivated')
74-
}
75-
const did = account.did
76-
if (did !== auth.credentials.did) {
77-
throw new AuthRequiredError()
78-
}
79-
8083
const uri = AtUri.make(did, collection, rkey)
8184
const swapCommitCid = swapCommit ? CID.parse(swapCommit) : undefined
8285
const swapRecordCid =

packages/pds/src/auth-verifier.ts

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
verifyJwt as verifyServiceJwt,
2525
} from '@atproto/xrpc-server'
2626
import { AccountManager } from './account-manager/account-manager'
27+
import { ActorAccount } from './account-manager/helpers/account'
2728
import {
2829
AccessOutput,
2930
AdminTokenOutput,
@@ -424,30 +425,43 @@ export class AuthVerifier {
424425

425426
protected async verifyStatus(
426427
did: string,
427-
{ checkTakedown = false, checkDeactivated = false }: VerifiedOptions,
428+
options: VerifiedOptions,
428429
): Promise<void> {
429-
if (checkTakedown || checkDeactivated) {
430-
const found = await this.accountManager.getAccount(did, {
431-
includeDeactivated: true,
432-
includeTakenDown: true,
433-
})
434-
if (!found) {
435-
// will be turned into ExpiredToken for the client if proxied by entryway
436-
throw new ForbiddenError('Account not found', 'AccountNotFound')
437-
}
438-
if (checkTakedown && softDeleted(found)) {
439-
throw new AuthRequiredError(
440-
'Account has been taken down',
441-
'AccountTakedown',
442-
)
443-
}
444-
if (checkDeactivated && found.deactivatedAt) {
445-
throw new AuthRequiredError(
446-
'Account is deactivated',
447-
'AccountDeactivated',
448-
)
449-
}
430+
if (options.checkDeactivated || options.checkTakedown) {
431+
await this.findAccount(did, options)
432+
}
433+
}
434+
435+
/**
436+
* Finds an account by its handle or DID, returning possibly deactivated or
437+
* taken down accounts (unless `options.checkDeactivated` or
438+
* `options.checkTakedown` are set to true, respectively).
439+
*/
440+
public async findAccount(
441+
handleOrDid: string,
442+
options: VerifiedOptions,
443+
): Promise<ActorAccount> {
444+
const account = await this.accountManager.getAccount(handleOrDid, {
445+
includeDeactivated: true,
446+
includeTakenDown: true,
447+
})
448+
if (!account) {
449+
// will be turned into ExpiredToken for the client if proxied by entryway
450+
throw new ForbiddenError('Account not found', 'AccountNotFound')
451+
}
452+
if (options.checkTakedown && softDeleted(account)) {
453+
throw new AuthRequiredError(
454+
'Account has been taken down',
455+
'AccountTakedown',
456+
)
457+
}
458+
if (options.checkDeactivated && account.deactivatedAt) {
459+
throw new AuthRequiredError(
460+
'Account is deactivated',
461+
'AccountDeactivated',
462+
)
450463
}
464+
return account
451465
}
452466

453467
/**

0 commit comments

Comments
 (0)