Skip to content

Commit

Permalink
Scope put prefs by AuthScope (#2573)
Browse files Browse the repository at this point in the history
scope put prefs by authscope
  • Loading branch information
dholms authored Jun 11, 2024
1 parent 708217d commit fcae4c5
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 12 deletions.
8 changes: 7 additions & 1 deletion packages/pds/src/actor-store/preference/reader.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { AuthScope } from '../../auth-verifier'
import { ActorDb } from '../db'
import { prefInScope } from './util'

export class PreferenceReader {
constructor(public db: ActorDb) {}

async getPreferences(namespace?: string): Promise<AccountPreference[]> {
async getPreferences(
namespace: string,
scope: AuthScope,
): Promise<AccountPreference[]> {
const prefsRes = await this.db.db
.selectFrom('account_pref')
.orderBy('id')
.selectAll()
.execute()
return prefsRes
.filter((pref) => !namespace || prefMatchNamespace(namespace, pref.name))
.filter((pref) => prefInScope(scope, pref.name))
.map((pref) => JSON.parse(pref.valueJson))
}
}
Expand Down
10 changes: 10 additions & 0 deletions packages/pds/src/actor-store/preference/transactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,27 @@ import {
AccountPreference,
prefMatchNamespace,
} from './reader'
import { AuthScope } from '../../auth-verifier'
import { prefInScope } from './util'

export class PreferenceTransactor extends PreferenceReader {
async putPreferences(
values: AccountPreference[],
namespace: string,
scope: AuthScope,
): Promise<void> {
this.db.assertTransaction()
if (!values.every((value) => prefMatchNamespace(namespace, value.$type))) {
throw new InvalidRequestError(
`Some preferences are not in the ${namespace} namespace`,
)
}
const notInScope = values.filter((val) => !prefInScope(scope, val.$type))
if (notInScope.length > 0) {
throw new InvalidRequestError(
`Do not have authorization to set preferences: ${notInScope.join(', ')}`,
)
}
// get all current prefs for user and prep new pref rows
const allPrefs = await this.db.db
.selectFrom('account_pref')
Expand All @@ -29,6 +38,7 @@ export class PreferenceTransactor extends PreferenceReader {
})
const allPrefIdsInNamespace = allPrefs
.filter((pref) => prefMatchNamespace(namespace, pref.name))
.filter((pref) => prefInScope(scope, pref.name))
.map((pref) => pref.id)
// replace all prefs in given namespace
if (allPrefIdsInNamespace.length) {
Expand Down
8 changes: 8 additions & 0 deletions packages/pds/src/actor-store/preference/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { AuthScope } from '../../auth-verifier'

const FULL_ACCESS_ONLY_PREFS = ['app.bsky.actor.defs#personalDetailsPref']

export const prefInScope = (scope: AuthScope, prefType: string) => {
if (scope === AuthScope.Access) return true
return !FULL_ACCESS_ONLY_PREFS.includes(prefType)
}
11 changes: 2 additions & 9 deletions packages/pds/src/api/app/bsky/actor/getPreferences.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
import { AuthScope } from '../../../../auth-verifier'

export default function (server: Server, ctx: AppContext) {
if (!ctx.cfg.bskyAppView) return
server.app.bsky.actor.getPreferences({
auth: ctx.authVerifier.accessStandard(),
handler: async ({ auth }) => {
const requester = auth.credentials.did
let preferences = await ctx.actorStore.read(requester, (store) =>
store.pref.getPreferences('app.bsky'),
const preferences = await ctx.actorStore.read(requester, (store) =>
store.pref.getPreferences('app.bsky', auth.credentials.scope),
)
if (auth.credentials.scope !== AuthScope.Access) {
// filter out personal details for app passwords
preferences = preferences.filter(
(pref) => pref.$type !== 'app.bsky.actor.defs#personalDetailsPref',
)
}
return {
encoding: 'application/json',
body: { preferences },
Expand Down
6 changes: 5 additions & 1 deletion packages/pds/src/api/app/bsky/actor/putPreferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ export default function (server: Server, ctx: AppContext) {
}
}
await ctx.actorStore.transact(requester, async (actorTxn) => {
await actorTxn.pref.putPreferences(checkedPreferences, 'app.bsky')
await actorTxn.pref.putPreferences(
checkedPreferences,
'app.bsky',
auth.credentials.scope,
)
})
},
})
Expand Down
68 changes: 67 additions & 1 deletion packages/pds/tests/preferences.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env'
import AtpAgent from '@atproto/api'
import usersSeed from './seeds/users'
import { AuthScope } from '../dist/auth-verifier'

describe('user preferences', () => {
let network: TestNetworkNoAppView
let agent: AtpAgent
let sc: SeedClient
let appPassHeaders: { authorization: string }

beforeAll(async () => {
network = await TestNetworkNoAppView.create({
Expand All @@ -14,6 +16,16 @@ describe('user preferences', () => {
agent = network.pds.getClient()
sc = network.getSeedClient()
await usersSeed(sc)
const appPass = await network.pds.ctx.accountManager.createAppPassword(
sc.dids.alice,
'test app pass',
false,
)
const res = await agent.com.atproto.server.createSession({
identifier: sc.dids.alice,
password: appPass.password,
})
appPassHeaders = { authorization: `Bearer ${res.data.accessJwt}` }
})

afterAll(async () => {
Expand Down Expand Up @@ -46,6 +58,7 @@ describe('user preferences', () => {
store.pref.putPreferences(
[{ $type: 'com.atproto.server.defs#unknown' }],
'com.atproto',
AuthScope.Access,
),
)
const { data } = await agent.api.app.bsky.actor.getPreferences(
Expand Down Expand Up @@ -96,7 +109,7 @@ describe('user preferences', () => {
// Ensure other prefs were not clobbered
const otherPrefs = await network.pds.ctx.actorStore.read(
sc.dids.alice,
(store) => store.pref.getPreferences('com.atproto'),
(store) => store.pref.getPreferences('com.atproto', AuthScope.Access),
)
expect(otherPrefs).toEqual([{ $type: 'com.atproto.server.defs#unknown' }])
})
Expand Down Expand Up @@ -178,4 +191,57 @@ describe('user preferences', () => {
'Input/preferences/1 must be an object which includes the "$type" property',
)
})

it('does not read permissioned preferences with an app password', async () => {
await agent.api.app.bsky.actor.putPreferences(
{
preferences: [
{
$type: 'app.bsky.actor.defs#personalDetailsPref',
birthDate: new Date().toISOString(),
},
],
},
{ headers: sc.getHeaders(sc.dids.alice), encoding: 'application/json' },
)
const res = await agent.api.app.bsky.actor.getPreferences(
{},
{ headers: appPassHeaders },
)
expect(res.data.preferences).toEqual([])
})

it('does not write permissioned preferences with an app password', async () => {
const tryPut = agent.api.app.bsky.actor.putPreferences(
{
preferences: [
{
$type: 'app.bsky.actor.defs#personalDetailsPref',
birthDate: new Date().toISOString(),
},
],
},
{ headers: appPassHeaders, encoding: 'application/json' },
)
await expect(tryPut).rejects.toThrow(
/Do not have authorization to set preferences/,
)
})

it('does not remove permissioned preferences with an app password', async () => {
await agent.api.app.bsky.actor.putPreferences(
{
preferences: [],
},
{ headers: appPassHeaders, encoding: 'application/json' },
)
const res = await agent.api.app.bsky.actor.getPreferences(
{},
{ headers: sc.getHeaders(sc.dids.alice) },
)
const scopedPref = res.data.preferences.find(
(pref) => pref.$type === 'app.bsky.actor.defs#personalDetailsPref',
)
expect(scopedPref).toBeDefined()
})
})

0 comments on commit fcae4c5

Please sign in to comment.