Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into reply-qp-moderation
Browse files Browse the repository at this point in the history
* origin/main:
  Fix calls from pds containing content-type but no body (#2725)
  Version packages (#2712)
  Service auth method binding (lxm) (#2663)
  Fix getActorLikes documentation to reflect auth required (#2593)
  exact matches always show profile regardless of block status (#2653)
  • Loading branch information
estrattonbailey committed Aug 19, 2024
2 parents 5cf7898 + f9a2f3e commit 2d661f7
Show file tree
Hide file tree
Showing 97 changed files with 2,496 additions and 605 deletions.
5 changes: 0 additions & 5 deletions .changeset/giant-starfishes-fry.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/happy-eggs-swim.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/hot-cows-scream.md

This file was deleted.

5 changes: 5 additions & 0 deletions .changeset/wild-beans-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atproto/pds": patch
---

Fix calls from pds containing content-type but no body
2 changes: 1 addition & 1 deletion .github/workflows/build-and-push-bsky-ghcr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ on:
push:
branches:
- main
- divy/mod-full-thread
- service-auth-scopes
env:
REGISTRY: ghcr.io
USERNAME: ${{ github.actor }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/build-and-push-ozone-aws.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ on:
push:
branches:
- main
- service-auth-scopes
env:
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-and-push-pds-ghcr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ on:
push:
branches:
- main
- divy/starter-packs
- divy/fix-pds-calls-no-body
env:
REGISTRY: ghcr.io
USERNAME: ${{ github.actor }}
Expand Down
2 changes: 1 addition & 1 deletion lexicons/app/bsky/feed/getActorLikes.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"defs": {
"main": {
"type": "query",
"description": "Get a list of posts liked by an actor. Does not require auth.",
"description": "Get a list of posts liked by an actor. Requires auth, actor must be the requesting account.",
"parameters": {
"type": "params",
"required": ["actor"],
Expand Down
7 changes: 7 additions & 0 deletions packages/bsky/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# @atproto/bsky

## 0.0.76

### Patch Changes

- Updated dependencies [[`acbacbbd5`](https://github.com/bluesky-social/atproto/commit/acbacbbd5621473b14ee7a6a3132f675806d23b1), [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae), [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae)]:
- @atproto/xrpc-server@0.6.2

## 0.0.75

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/bsky/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@atproto/bsky",
"version": "0.0.75",
"version": "0.0.76",
"license": "MIT",
"description": "Reference implementation of app.bsky App View (Bluesky API)",
"keywords": [
Expand Down
11 changes: 10 additions & 1 deletion packages/bsky/src/api/app/bsky/actor/getProfiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,20 @@ import {
Hydrator,
} from '../../../../hydration/hydrator'
import { Views } from '../../../../views'
import { ids } from '../../../../lexicon/lexicons'

export default function (server: Server, ctx: AppContext) {
const getProfile = createPipeline(skeleton, hydration, noRules, presentation)
server.app.bsky.actor.getProfiles({
auth: ctx.authVerifier.standardOptional,
auth: ctx.authVerifier.standardOptionalParameterized({
lxmCheck: (method) => {
if (!method) return false
return (
method === ids.AppBskyActorGetProfiles ||
method.startsWith('chat.bsky.')
)
},
}),
handler: async ({ auth, params, req }) => {
const viewer = auth.credentials.iss
const labelers = ctx.reqLabelers(req)
Expand Down
13 changes: 9 additions & 4 deletions packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,15 @@ const hydration = async (
}

const noBlocks = (inputs: RulesFnInput<Context, Params, Skeleton>) => {
const { ctx, skeleton, hydration } = inputs
skeleton.dids = skeleton.dids.filter(
(did) => !ctx.views.viewerBlockExists(did, hydration),
)
const { ctx, skeleton, hydration, params } = inputs
skeleton.dids = skeleton.dids.filter((did) => {
const actor = hydration.actors?.get(did)
if (!actor) return false
// Always display exact matches so that users can find profiles that they have blocked
const term = (params.q ?? params.term ?? '').toLowerCase()
const isExactMatch = actor.handle?.toLowerCase() === term
return isExactMatch || !ctx.views.viewerBlockExists(did, hydration)
})
return skeleton
}

Expand Down
13 changes: 12 additions & 1 deletion packages/bsky/src/api/app/bsky/feed/getFeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
unpackIdentityServices,
} from '../../../../data-plane'
import { resHeaders } from '../../../util'
import { ids } from '../../../../lexicon/lexicons'

export default function (server: Server, ctx: AppContext) {
const getFeed = createPipeline(
Expand All @@ -38,7 +39,17 @@ export default function (server: Server, ctx: AppContext) {
presentation,
)
server.app.bsky.feed.getFeed({
auth: ctx.authVerifier.standardOptionalAnyAud,
auth: ctx.authVerifier.standardOptionalParameterized({
lxmCheck: (method) => {
return (
method !== undefined &&
[ids.AppBskyFeedGetFeedSkeleton, ids.AppBskyFeedGetFeed].includes(
method,
)
)
},
skipAudCheck: true,
}),
handler: async ({ params, auth, req }) => {
const viewer = auth.credentials.iss
const labelers = ctx.reqLabelers(req)
Expand Down
10 changes: 9 additions & 1 deletion packages/bsky/src/api/app/bsky/feed/getPosts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,19 @@ import {
import { Views } from '../../../../views'
import { uriToDid as creatorFromUri } from '../../../../util/uris'
import { resHeaders } from '../../../util'
import { ids } from '../../../../lexicon/lexicons'

export default function (server: Server, ctx: AppContext) {
const getPosts = createPipeline(skeleton, hydration, noBlocks, presentation)
server.app.bsky.feed.getPosts({
auth: ctx.authVerifier.standardOptional,
auth: ctx.authVerifier.standardOptionalParameterized({
lxmCheck: (method) => {
if (!method) return false
return (
method === ids.AppBskyFeedGetPosts || method.startsWith('chat.bsky.')
)
},
}),
handler: async ({ params, auth, req }) => {
const viewer = auth.credentials.iss
const labelers = ctx.reqLabelers(req)
Expand Down
129 changes: 78 additions & 51 deletions packages/bsky/src/auth-verifier.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
AuthRequiredError,
parseReqNsid,
verifyJwt as verifyServiceJwt,
} from '@atproto/xrpc-server'
import * as ui8 from 'uint8arrays'
Expand All @@ -17,6 +18,11 @@ type ReqCtx = {
req: express.Request
}

type StandardAuthOpts = {
skipAudCheck?: boolean
lxmCheck?: (method?: string) => boolean
}

export enum RoleStatus {
Valid,
Invalid,
Expand Down Expand Up @@ -80,61 +86,55 @@ export class AuthVerifier {
}

// verifiers (arrow fns to preserve scope)

standard = async (ctx: ReqCtx): Promise<StandardOutput> => {
// @TODO remove! basic auth + did supported just for testing.
if (isBasicToken(ctx.req)) {
const aud = this.ownDid
const iss = ctx.req.headers['appview-as-did']
if (typeof iss !== 'string' || !iss.startsWith('did:')) {
throw new AuthRequiredError('bad issuer')
}
if (!this.parseRoleCreds(ctx.req).admin) {
throw new AuthRequiredError('bad credentials')
}
return {
credentials: { type: 'standard', iss, aud },
standardOptionalParameterized =
(opts: StandardAuthOpts) =>
async (ctx: ReqCtx): Promise<StandardOutput | NullOutput> => {
// @TODO remove! basic auth + did supported just for testing.
if (isBasicToken(ctx.req)) {
const aud = this.ownDid
const iss = ctx.req.headers['appview-as-did']
if (typeof iss !== 'string' || !iss.startsWith('did:')) {
throw new AuthRequiredError('bad issuer')
}
if (!this.parseRoleCreds(ctx.req).admin) {
throw new AuthRequiredError('bad credentials')
}
return {
credentials: { type: 'standard', iss, aud },
}
} else if (isBearerToken(ctx.req)) {
const { iss, aud } = await this.verifyServiceJwt(ctx, {
lxmCheck: opts.lxmCheck,
iss: null,
aud: null,
})
if (!opts.skipAudCheck && !this.standardAudienceDids.has(aud)) {
throw new AuthRequiredError(
'jwt audience does not match service did',
'BadJwtAudience',
)
}
return {
credentials: {
type: 'standard',
iss,
aud,
},
}
} else {
return this.nullCreds()
}
}
const { iss, aud } = await this.verifyServiceJwt(ctx, {
aud: null,
iss: null,
})
if (!this.standardAudienceDids.has(aud)) {
throw new AuthRequiredError(
'jwt audience does not match service did',
'BadJwtAudience',
)
}
return {
credentials: {
type: 'standard',
iss,
aud,
},
}
}

standardOptional = async (
ctx: ReqCtx,
): Promise<StandardOutput | NullOutput> => {
if (isBearerToken(ctx.req) || isBasicToken(ctx.req)) {
return this.standard(ctx)
}
return this.nullCreds()
}
standardOptional: (ctx: ReqCtx) => Promise<StandardOutput | NullOutput> =
this.standardOptionalParameterized({})

standardOptionalAnyAud = async (
ctx: ReqCtx,
): Promise<StandardOutput | NullOutput> => {
if (!isBearerToken(ctx.req)) {
return this.nullCreds()
standard = async (ctx: ReqCtx): Promise<StandardOutput> => {
const output = await this.standardOptional(ctx)
if (output.credentials.type === 'none') {
throw new AuthRequiredError(undefined, 'AuthMissing')
}
const { iss, aud } = await this.verifyServiceJwt(ctx, {
aud: null,
iss: null,
})
return { credentials: { type: 'standard', iss, aud } }
return output as StandardOutput
}

role = (ctx: ReqCtx): RoleOutput => {
Expand Down Expand Up @@ -215,7 +215,11 @@ export class AuthVerifier {

async verifyServiceJwt(
reqCtx: ReqCtx,
opts: { aud: string | null; iss: string[] | null },
opts: {
iss: string[] | null
aud: string | null
lxmCheck?: (method?: string) => boolean
},
) {
const getSigningKey = async (
iss: string,
Expand Down Expand Up @@ -243,17 +247,40 @@ export class AuthVerifier {
}
return didKey
}
const assertLxmCheck = () => {
const lxm = parseReqNsid(reqCtx.req)
if (
(opts.lxmCheck && !opts.lxmCheck(payload.lxm)) ||
(!opts.lxmCheck && payload.lxm !== lxm)
) {
throw new AuthRequiredError(
payload.lxm !== undefined
? `bad jwt lexicon method ("lxm"). must match: ${lxm}`
: `missing jwt lexicon method ("lxm"). must match: ${lxm}`,
'BadJwtLexiconMethod',
)
}
}

const jwtStr = bearerTokenFromReq(reqCtx.req)
if (!jwtStr) {
throw new AuthRequiredError('missing jwt', 'MissingJwt')
}
// if validating additional scopes, skip scope check in initial validation & follow up afterwards
const payload = await verifyServiceJwt(
jwtStr,
opts.aud,
null,
getSigningKey,
)
if (
!payload.iss.endsWith('#atproto_labeler') ||
payload.lxm !== undefined
) {
// @TODO currently permissive of labelers who dont set lxm yet.
// we'll allow ozone self-hosters to upgrade before removing this condition.
assertLxmCheck()
}
return { iss: payload.iss, aud: payload.aud }
}

Expand Down
Loading

0 comments on commit 2d661f7

Please sign in to comment.