Skip to content

Commit

Permalink
Entryway sending email via account service auth, deprecate mod servic…
Browse files Browse the repository at this point in the history
…e email flow (#2271)

* allow sending email via account service auth, deprecate mod service email flow

* tests on account service auth
  • Loading branch information
devinivy authored Mar 5, 2024
1 parent abf8011 commit f0058e8
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 19 deletions.
43 changes: 27 additions & 16 deletions packages/pds/src/api/com/atproto/admin/sendEmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@ import { authPassthru } from './util'

export default function (server: Server, ctx: AppContext) {
server.com.atproto.admin.sendEmail({
auth: ctx.authVerifier.role,
auth: ctx.authVerifier.roleOrAccountService,
handler: async ({ req, input, auth }) => {
if (!auth.credentials.admin && !auth.credentials.moderator) {
if (auth.credentials.type === 'role' && !auth.credentials.moderator) {
throw new AuthRequiredError('Insufficient privileges')
}
if (
auth.credentials.type === 'account_service' &&
auth.credentials.did !== input.body.recipientDid
) {
throw new AuthRequiredError('Insufficient privileges')
}

Expand All @@ -32,21 +38,26 @@ export default function (server: Server, ctx: AppContext) {
{ content },
{ subject, to: userInfo.email },
)
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,

if (auth.credentials.type === 'role') {
// @TODO this behavior is deprecated, remove after service auth takes effect
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,
},
createdBy: senderDid,
},
{ ...authPassthru(req), encoding: 'application/json' },
)
{ ...authPassthru(req), encoding: 'application/json' },
)
}

return {
encoding: 'application/json',
body: { sent: true },
Expand Down
46 changes: 46 additions & 0 deletions packages/pds/src/auth-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ type AccessOutput = {
artifacts: string
}

type AccountServiceOutput = {
credentials: {
type: 'account_service'
did: string
}
artifacts: string
}

type AccessCheckedOutput = AccessOutput & {
credentials: {
pdsDid: string | null
Expand Down Expand Up @@ -271,6 +279,34 @@ export class AuthVerifier {
}
}

accountService = async (reqCtx: ReqCtx): Promise<AccountServiceOutput> => {
const token = bearerTokenFromReq(reqCtx.req)
if (!token) {
throw new AuthRequiredError('missing jwt', 'MissingJwt')
}
const payload = await verifyServiceJwt(
token,
this._pdsServiceDid,
(did, forceRefresh) =>
this.idResolver.did.resolveAtprotoKey(did, forceRefresh),
)
const found = await this.db.db
.selectFrom('user_account')
.where('did', '=', payload.iss)
.select('did')
.executeTakeFirst()
if (!found) {
throw new AuthRequiredError('Unknown account', 'UntrustedIss')
}
return {
credentials: {
type: 'account_service',
did: payload.iss,
},
artifacts: token,
}
}

roleOrAdminService = async (
reqCtx: ReqCtx,
): Promise<RoleOutput | AdminServiceOutput> => {
Expand All @@ -281,6 +317,16 @@ export class AuthVerifier {
}
}

roleOrAccountService = async (
reqCtx: ReqCtx,
): Promise<RoleOutput | AccountServiceOutput> => {
if (isBearerToken(reqCtx.req)) {
return this.accountService(reqCtx)
} else {
return this.role(reqCtx)
}
}

async validateBearerToken(
req: express.Request,
scopes: AuthScope[],
Expand Down
78 changes: 75 additions & 3 deletions packages/pds/tests/admin-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ describe('admin auth', () => {

modServiceKey = await Secp256k1Keypair.create()
const origResolve = network.pds.ctx.idResolver.did.resolveAtprotoKey
network.pds.ctx.idResolver.did.resolveAtprotoKey = async (
network.pds.ctx.idResolver.did.resolveAtprotoKey = async function (
did: string,
forceRefresh?: boolean,
) => {
) {
if (did === modServiceDid || did === altModDid) {
return modServiceKey.did()
}
return origResolve(did, forceRefresh)
return origResolve.call(this, did, forceRefresh)
}

agent = network.pds.getClient()
Expand Down Expand Up @@ -142,4 +142,76 @@ describe('admin auth', () => {
'jwt audience does not match service did',
)
})

describe('account service auth', () => {
it('succeeds when account exists', async () => {
const headers = await network.pds.ctx.serviceAuthHeaders(
sc.dids.alice,
network.pds.ctx.cfg.service.did,
)
const attempt = agent.api.com.atproto.admin.sendEmail(
{
subject: 'Subject',
content: 'Content',
recipientDid: sc.dids.alice,
senderDid: sc.dids.alice,
},
{
...headers,
encoding: 'application/json',
},
)
await expect(attempt).resolves.toBeDefined()
})

it("fails on account that doesn't exist", async () => {
await agent.api.com.atproto.admin.deleteAccount(
{ did: sc.dids.dan },
{
encoding: 'application/json',
headers: network.pds.adminAuthHeaders(),
},
)
await network.pds.processAll()
const headers = await network.pds.ctx.serviceAuthHeaders(
sc.dids.dan,
network.pds.ctx.cfg.service.did,
)
const attempt = agent.api.com.atproto.admin.sendEmail(
{
subject: 'Subject',
content: 'Content',
recipientDid: sc.dids.dan,
senderDid: sc.dids.dan,
},
{
...headers,
encoding: 'application/json',
},
)
await expect(attempt).rejects.toThrow('Unknown account')
})

it('fails on bad audience', async () => {
const headers = await network.pds.ctx.serviceAuthHeaders(
sc.dids.alice,
sc.dids.bob,
)
const attempt = agent.api.com.atproto.admin.sendEmail(
{
subject: 'Subject',
content: 'Content',
recipientDid: sc.dids.alice,
senderDid: sc.dids.alice,
},
{
...headers,
encoding: 'application/json',
},
)
await expect(attempt).rejects.toThrow(
'jwt audience does not match service did',
)
})
})
})

0 comments on commit f0058e8

Please sign in to comment.