diff --git a/lexicons/com/atproto/admin/updateAccountPassword.json b/lexicons/com/atproto/admin/updateAccountPassword.json new file mode 100644 index 00000000000..76c69fec0b6 --- /dev/null +++ b/lexicons/com/atproto/admin/updateAccountPassword.json @@ -0,0 +1,21 @@ +{ + "lexicon": 1, + "id": "com.atproto.admin.updateAccountPassword", + "defs": { + "main": { + "type": "procedure", + "description": "Update the password for a user account as an administrator.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["did", "password"], + "properties": { + "did": { "type": "string", "format": "did" }, + "password": { "type": "string" } + } + } + } + } + } +} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index 5029db88701..846c379b7a8 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -29,6 +29,7 @@ import * as ComAtprotoAdminSearchRepos from './types/com/atproto/admin/searchRep import * as ComAtprotoAdminSendEmail from './types/com/atproto/admin/sendEmail' import * as ComAtprotoAdminUpdateAccountEmail from './types/com/atproto/admin/updateAccountEmail' import * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle' +import * as ComAtprotoAdminUpdateAccountPassword from './types/com/atproto/admin/updateAccountPassword' import * as ComAtprotoAdminUpdateCommunicationTemplate from './types/com/atproto/admin/updateCommunicationTemplate' import * as ComAtprotoAdminUpdateSubjectStatus from './types/com/atproto/admin/updateSubjectStatus' import * as ComAtprotoIdentityGetRecommendedDidCredentials from './types/com/atproto/identity/getRecommendedDidCredentials' @@ -182,6 +183,7 @@ export * as ComAtprotoAdminSearchRepos from './types/com/atproto/admin/searchRep export * as ComAtprotoAdminSendEmail from './types/com/atproto/admin/sendEmail' export * as ComAtprotoAdminUpdateAccountEmail from './types/com/atproto/admin/updateAccountEmail' export * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle' +export * as ComAtprotoAdminUpdateAccountPassword from './types/com/atproto/admin/updateAccountPassword' export * as ComAtprotoAdminUpdateCommunicationTemplate from './types/com/atproto/admin/updateCommunicationTemplate' export * as ComAtprotoAdminUpdateSubjectStatus from './types/com/atproto/admin/updateSubjectStatus' export * as ComAtprotoIdentityGetRecommendedDidCredentials from './types/com/atproto/identity/getRecommendedDidCredentials' @@ -654,6 +656,17 @@ export class ComAtprotoAdminNS { }) } + updateAccountPassword( + data?: ComAtprotoAdminUpdateAccountPassword.InputSchema, + opts?: ComAtprotoAdminUpdateAccountPassword.CallOptions, + ): Promise { + return this._service.xrpc + .call('com.atproto.admin.updateAccountPassword', opts?.qp, data, opts) + .catch((e) => { + throw ComAtprotoAdminUpdateAccountPassword.toKnownErr(e) + }) + } + updateCommunicationTemplate( data?: ComAtprotoAdminUpdateCommunicationTemplate.InputSchema, opts?: ComAtprotoAdminUpdateCommunicationTemplate.CallOptions, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 88c27c79b18..77bedd176bb 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -1863,6 +1863,33 @@ export const schemaDict = { }, }, }, + ComAtprotoAdminUpdateAccountPassword: { + lexicon: 1, + id: 'com.atproto.admin.updateAccountPassword', + defs: { + main: { + type: 'procedure', + description: + 'Update the password for a user account as an administrator.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['did', 'password'], + properties: { + did: { + type: 'string', + format: 'did', + }, + password: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, ComAtprotoAdminUpdateCommunicationTemplate: { lexicon: 1, id: 'com.atproto.admin.updateCommunicationTemplate', @@ -8860,6 +8887,8 @@ export const ids = { ComAtprotoAdminSendEmail: 'com.atproto.admin.sendEmail', ComAtprotoAdminUpdateAccountEmail: 'com.atproto.admin.updateAccountEmail', ComAtprotoAdminUpdateAccountHandle: 'com.atproto.admin.updateAccountHandle', + ComAtprotoAdminUpdateAccountPassword: + 'com.atproto.admin.updateAccountPassword', ComAtprotoAdminUpdateCommunicationTemplate: 'com.atproto.admin.updateCommunicationTemplate', ComAtprotoAdminUpdateSubjectStatus: 'com.atproto.admin.updateSubjectStatus', diff --git a/packages/api/src/client/types/com/atproto/admin/updateAccountPassword.ts b/packages/api/src/client/types/com/atproto/admin/updateAccountPassword.ts new file mode 100644 index 00000000000..99ef3881c37 --- /dev/null +++ b/packages/api/src/client/types/com/atproto/admin/updateAccountPassword.ts @@ -0,0 +1,33 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { Headers, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface QueryParams {} + +export interface InputSchema { + did: string + password: string + [k: string]: unknown +} + +export interface CallOptions { + headers?: Headers + qp?: QueryParams + encoding: 'application/json' +} + +export interface Response { + success: boolean + headers: Headers +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index 6b3b6de582a..cf2c613e686 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -30,6 +30,7 @@ import * as ComAtprotoAdminSearchRepos from './types/com/atproto/admin/searchRep import * as ComAtprotoAdminSendEmail from './types/com/atproto/admin/sendEmail' import * as ComAtprotoAdminUpdateAccountEmail from './types/com/atproto/admin/updateAccountEmail' import * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle' +import * as ComAtprotoAdminUpdateAccountPassword from './types/com/atproto/admin/updateAccountPassword' import * as ComAtprotoAdminUpdateCommunicationTemplate from './types/com/atproto/admin/updateCommunicationTemplate' import * as ComAtprotoAdminUpdateSubjectStatus from './types/com/atproto/admin/updateSubjectStatus' import * as ComAtprotoIdentityGetRecommendedDidCredentials from './types/com/atproto/identity/getRecommendedDidCredentials' @@ -444,6 +445,17 @@ export class ComAtprotoAdminNS { return this._server.xrpc.method(nsid, cfg) } + updateAccountPassword( + cfg: ConfigOf< + AV, + ComAtprotoAdminUpdateAccountPassword.Handler>, + ComAtprotoAdminUpdateAccountPassword.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.updateAccountPassword' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + updateCommunicationTemplate( cfg: ConfigOf< AV, diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 88c27c79b18..77bedd176bb 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -1863,6 +1863,33 @@ export const schemaDict = { }, }, }, + ComAtprotoAdminUpdateAccountPassword: { + lexicon: 1, + id: 'com.atproto.admin.updateAccountPassword', + defs: { + main: { + type: 'procedure', + description: + 'Update the password for a user account as an administrator.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['did', 'password'], + properties: { + did: { + type: 'string', + format: 'did', + }, + password: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, ComAtprotoAdminUpdateCommunicationTemplate: { lexicon: 1, id: 'com.atproto.admin.updateCommunicationTemplate', @@ -8860,6 +8887,8 @@ export const ids = { ComAtprotoAdminSendEmail: 'com.atproto.admin.sendEmail', ComAtprotoAdminUpdateAccountEmail: 'com.atproto.admin.updateAccountEmail', ComAtprotoAdminUpdateAccountHandle: 'com.atproto.admin.updateAccountHandle', + ComAtprotoAdminUpdateAccountPassword: + 'com.atproto.admin.updateAccountPassword', ComAtprotoAdminUpdateCommunicationTemplate: 'com.atproto.admin.updateCommunicationTemplate', ComAtprotoAdminUpdateSubjectStatus: 'com.atproto.admin.updateSubjectStatus', diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountPassword.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountPassword.ts new file mode 100644 index 00000000000..948568f0d3d --- /dev/null +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountPassword.ts @@ -0,0 +1,39 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + did: string + password: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | void +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/ozone/src/lexicon/index.ts b/packages/ozone/src/lexicon/index.ts index 6b3b6de582a..cf2c613e686 100644 --- a/packages/ozone/src/lexicon/index.ts +++ b/packages/ozone/src/lexicon/index.ts @@ -30,6 +30,7 @@ import * as ComAtprotoAdminSearchRepos from './types/com/atproto/admin/searchRep import * as ComAtprotoAdminSendEmail from './types/com/atproto/admin/sendEmail' import * as ComAtprotoAdminUpdateAccountEmail from './types/com/atproto/admin/updateAccountEmail' import * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle' +import * as ComAtprotoAdminUpdateAccountPassword from './types/com/atproto/admin/updateAccountPassword' import * as ComAtprotoAdminUpdateCommunicationTemplate from './types/com/atproto/admin/updateCommunicationTemplate' import * as ComAtprotoAdminUpdateSubjectStatus from './types/com/atproto/admin/updateSubjectStatus' import * as ComAtprotoIdentityGetRecommendedDidCredentials from './types/com/atproto/identity/getRecommendedDidCredentials' @@ -444,6 +445,17 @@ export class ComAtprotoAdminNS { return this._server.xrpc.method(nsid, cfg) } + updateAccountPassword( + cfg: ConfigOf< + AV, + ComAtprotoAdminUpdateAccountPassword.Handler>, + ComAtprotoAdminUpdateAccountPassword.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.updateAccountPassword' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + updateCommunicationTemplate( cfg: ConfigOf< AV, diff --git a/packages/ozone/src/lexicon/lexicons.ts b/packages/ozone/src/lexicon/lexicons.ts index 88c27c79b18..77bedd176bb 100644 --- a/packages/ozone/src/lexicon/lexicons.ts +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -1863,6 +1863,33 @@ export const schemaDict = { }, }, }, + ComAtprotoAdminUpdateAccountPassword: { + lexicon: 1, + id: 'com.atproto.admin.updateAccountPassword', + defs: { + main: { + type: 'procedure', + description: + 'Update the password for a user account as an administrator.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['did', 'password'], + properties: { + did: { + type: 'string', + format: 'did', + }, + password: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, ComAtprotoAdminUpdateCommunicationTemplate: { lexicon: 1, id: 'com.atproto.admin.updateCommunicationTemplate', @@ -8860,6 +8887,8 @@ export const ids = { ComAtprotoAdminSendEmail: 'com.atproto.admin.sendEmail', ComAtprotoAdminUpdateAccountEmail: 'com.atproto.admin.updateAccountEmail', ComAtprotoAdminUpdateAccountHandle: 'com.atproto.admin.updateAccountHandle', + ComAtprotoAdminUpdateAccountPassword: + 'com.atproto.admin.updateAccountPassword', ComAtprotoAdminUpdateCommunicationTemplate: 'com.atproto.admin.updateCommunicationTemplate', ComAtprotoAdminUpdateSubjectStatus: 'com.atproto.admin.updateSubjectStatus', diff --git a/packages/ozone/src/lexicon/types/com/atproto/admin/updateAccountPassword.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/updateAccountPassword.ts new file mode 100644 index 00000000000..948568f0d3d --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/updateAccountPassword.ts @@ -0,0 +1,39 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + did: string + password: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | void +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/account-manager/index.ts b/packages/pds/src/account-manager/index.ts index d071550ee69..89fb4ac00b1 100644 --- a/packages/pds/src/account-manager/index.ts +++ b/packages/pds/src/account-manager/index.ts @@ -370,6 +370,11 @@ export class AccountManager { 'reset_password', opts.token, ) + await this.updateAccountPassword({ did, password: opts.password }) + } + + async updateAccountPassword(opts: { did: string; password: string }) { + const { did } = opts const passwordScrypt = await scrypt.genSaltAndHash(opts.password) await this.db.transaction(async (dbTxn) => Promise.all([ diff --git a/packages/pds/src/api/com/atproto/admin/index.ts b/packages/pds/src/api/com/atproto/admin/index.ts index cca12226f4b..b0911afa66c 100644 --- a/packages/pds/src/api/com/atproto/admin/index.ts +++ b/packages/pds/src/api/com/atproto/admin/index.ts @@ -15,6 +15,7 @@ import disableInviteCodes from './disableInviteCodes' import getInviteCodes from './getInviteCodes' import updateAccountHandle from './updateAccountHandle' import updateAccountEmail from './updateAccountEmail' +import updateAccountPassword from './updateAccountPassword' import sendEmail from './sendEmail' import deleteAccount from './deleteAccount' import queryModerationStatuses from './queryModerationStatuses' @@ -40,6 +41,7 @@ export default function (server: Server, ctx: AppContext) { getInviteCodes(server, ctx) updateAccountHandle(server, ctx) updateAccountEmail(server, ctx) + updateAccountPassword(server, ctx) sendEmail(server, ctx) deleteAccount(server, ctx) listCommunicationTemplates(server, ctx) diff --git a/packages/pds/src/api/com/atproto/admin/updateAccountPassword.ts b/packages/pds/src/api/com/atproto/admin/updateAccountPassword.ts new file mode 100644 index 00000000000..294286c3873 --- /dev/null +++ b/packages/pds/src/api/com/atproto/admin/updateAccountPassword.ts @@ -0,0 +1,28 @@ +import { AuthRequiredError } from '@atproto/xrpc-server' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { authPassthru } from '../../../proxy' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.admin.updateAccountPassword({ + auth: ctx.authVerifier.role, + handler: async ({ input, auth, req }) => { + if (!auth.credentials.admin) { + throw new AuthRequiredError( + 'Must be an admin to update an account password', + ) + } + + if (ctx.entrywayAgent) { + await ctx.entrywayAgent.com.atproto.admin.updateAccountPassword( + input.body, + authPassthru(req, true), + ) + return + } + + const { did, password } = input.body + await ctx.accountManager.updateAccountPassword({ did, password }) + }, + }) +} diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 6b3b6de582a..cf2c613e686 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -30,6 +30,7 @@ import * as ComAtprotoAdminSearchRepos from './types/com/atproto/admin/searchRep import * as ComAtprotoAdminSendEmail from './types/com/atproto/admin/sendEmail' import * as ComAtprotoAdminUpdateAccountEmail from './types/com/atproto/admin/updateAccountEmail' import * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle' +import * as ComAtprotoAdminUpdateAccountPassword from './types/com/atproto/admin/updateAccountPassword' import * as ComAtprotoAdminUpdateCommunicationTemplate from './types/com/atproto/admin/updateCommunicationTemplate' import * as ComAtprotoAdminUpdateSubjectStatus from './types/com/atproto/admin/updateSubjectStatus' import * as ComAtprotoIdentityGetRecommendedDidCredentials from './types/com/atproto/identity/getRecommendedDidCredentials' @@ -444,6 +445,17 @@ export class ComAtprotoAdminNS { return this._server.xrpc.method(nsid, cfg) } + updateAccountPassword( + cfg: ConfigOf< + AV, + ComAtprotoAdminUpdateAccountPassword.Handler>, + ComAtprotoAdminUpdateAccountPassword.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.updateAccountPassword' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + updateCommunicationTemplate( cfg: ConfigOf< AV, diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 88c27c79b18..77bedd176bb 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -1863,6 +1863,33 @@ export const schemaDict = { }, }, }, + ComAtprotoAdminUpdateAccountPassword: { + lexicon: 1, + id: 'com.atproto.admin.updateAccountPassword', + defs: { + main: { + type: 'procedure', + description: + 'Update the password for a user account as an administrator.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['did', 'password'], + properties: { + did: { + type: 'string', + format: 'did', + }, + password: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, ComAtprotoAdminUpdateCommunicationTemplate: { lexicon: 1, id: 'com.atproto.admin.updateCommunicationTemplate', @@ -8860,6 +8887,8 @@ export const ids = { ComAtprotoAdminSendEmail: 'com.atproto.admin.sendEmail', ComAtprotoAdminUpdateAccountEmail: 'com.atproto.admin.updateAccountEmail', ComAtprotoAdminUpdateAccountHandle: 'com.atproto.admin.updateAccountHandle', + ComAtprotoAdminUpdateAccountPassword: + 'com.atproto.admin.updateAccountPassword', ComAtprotoAdminUpdateCommunicationTemplate: 'com.atproto.admin.updateCommunicationTemplate', ComAtprotoAdminUpdateSubjectStatus: 'com.atproto.admin.updateSubjectStatus', diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/updateAccountPassword.ts b/packages/pds/src/lexicon/types/com/atproto/admin/updateAccountPassword.ts new file mode 100644 index 00000000000..948568f0d3d --- /dev/null +++ b/packages/pds/src/lexicon/types/com/atproto/admin/updateAccountPassword.ts @@ -0,0 +1,39 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + did: string + password: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | void +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/tests/account.test.ts b/packages/pds/tests/account.test.ts index 70ca5abf741..743248814f8 100644 --- a/packages/pds/tests/account.test.ts +++ b/packages/pds/tests/account.test.ts @@ -559,4 +559,46 @@ describe('account', () => { }), ).resolves.toBeDefined() }) + + it('allows an admin to update password', async () => { + const tryUnauthed = agent.api.com.atproto.admin.updateAccountPassword({ + did, + password: 'new-admin-pass', + }) + await expect(tryUnauthed).rejects.toThrow('Authentication Required') + + const tryAsModerator = agent.api.com.atproto.admin.updateAccountPassword( + { did, password: 'new-admin-pass' }, + { + headers: network.pds.adminAuthHeaders('moderator'), + encoding: 'application/json', + }, + ) + await expect(tryAsModerator).rejects.toThrow( + 'Must be an admin to update an account password', + ) + + await agent.api.com.atproto.admin.updateAccountPassword( + { did, password: 'new-admin-password' }, + { + headers: network.pds.adminAuthHeaders('admin'), + encoding: 'application/json', + }, + ) + + // old password fails + await expect( + agent.api.com.atproto.server.createSession({ + identifier: did, + password, + }), + ).rejects.toThrow('Invalid identifier or password') + + await expect( + agent.api.com.atproto.server.createSession({ + identifier: did, + password: 'new-admin-password', + }), + ).resolves.toBeDefined() + }) })