feat(passwd): mcpctl passwd + RBAC-gated password change
Restores the lost `mcpctl passwd` command and builds the backend it needs. Backend (mcpd): - POST /api/v1/users/me/password — self-service change, requires current password. Gated by a new `set-own-password` operation. - PUT /api/v1/users/:id/password — admin reset of another user, gated by edit:users (admins have edit:*). Added users name-resolver for CUID→email. - UserService.setPassword/verifyPassword; UserRepository.update accepts passwordHash + findByIdWithHash. RBAC, no exceptions: self password change is a default, admin-revocable permission. Every new user gets a `self-<id>` RbacDefinition granting `set-own-password`, seeded on create + bootstrap, gated by the `allowSelfPasswordChange` system setting (stored in the mcpctl-system-settings secret, default ON; admins disable globally or revoke per-user). CLI: src/cli/src/commands/passwd.ts (self vs admin paths) + completions. Tests: users-password route tests (8), auth-bootstrap grant assertion, passwd live smoke test. Full suite 2214 passing; zero new lint errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -92,6 +92,7 @@ interface MockDeps {
|
||||
getByName: ReturnType<typeof vi.fn>;
|
||||
update: ReturnType<typeof vi.fn>;
|
||||
delete: ReturnType<typeof vi.fn>;
|
||||
upsertByName: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
rbacService: {
|
||||
canAccess: ReturnType<typeof vi.fn>;
|
||||
@@ -131,6 +132,7 @@ function createMockDeps(): MockDeps {
|
||||
getByName: vi.fn(async () => null),
|
||||
update: vi.fn(async () => makeRbacDef()),
|
||||
delete: vi.fn(async () => {}),
|
||||
upsertByName: vi.fn(async () => makeRbacDef()),
|
||||
},
|
||||
rbacService: {
|
||||
canAccess: vi.fn(async () => false),
|
||||
@@ -223,6 +225,13 @@ describe('Auth Bootstrap', () => {
|
||||
|
||||
// Verify auto-login was called
|
||||
expect(deps.authService.login).toHaveBeenCalledWith('admin@example.com', 'securepass123');
|
||||
|
||||
// Verify the admin also got the default self password-change permission
|
||||
expect(deps.rbacDefinitionService.upsertByName).toHaveBeenCalledWith({
|
||||
name: 'self-user-1',
|
||||
subjects: [{ kind: 'User', name: 'admin@example.com' }],
|
||||
roleBindings: [{ role: 'run', action: 'set-own-password' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('passes name when provided', async () => {
|
||||
|
||||
Reference in New Issue
Block a user