feat(subsonic): per-user encrypted app-password foundation
Subsonic auth (t=md5(password+salt), legacy p=) needs a recoverable secret, but login passwords are stored as a one-way argon2 hash. Add a separate, per-user app-password: high-entropy, random, and encrypted at rest with a Fernet key derived from SUBSONIC_SECRET_KEY (never stored in the DB). - SubsonicPasswordCipher + generate_subsonic_password in core.security - users.subsonic_password_enc column (+ Alembic migration), repo + port methods - SubsonicAuthService: verify (t+s / p / p=enc:) and rotate/reveal lifecycle - self-service GET/POST /users/me/subsonic-password + admin rotate endpoint - domain SubsonicCredentials + SubsonicCipher port; deps wiring Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+10
-1
@@ -9,7 +9,8 @@ from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Query, status
|
||||
|
||||
from app.api.deps import SuperUser, UserServiceDep
|
||||
from app.api.deps import SubsonicAuthServiceDep, SuperUser, UserServiceDep
|
||||
from app.api.schemas.subsonic import SubsonicPasswordResponse
|
||||
from app.api.schemas.user import (
|
||||
CreateUserRequest,
|
||||
ResetPasswordRequest,
|
||||
@@ -81,6 +82,14 @@ async def deactivate_user(
|
||||
return UserResponse.from_entity(await users.deactivate(user_id))
|
||||
|
||||
|
||||
@router.post("/users/{user_id}/subsonic-password", response_model=SubsonicPasswordResponse)
|
||||
async def rotate_user_subsonic_password(
|
||||
user_id: uuid.UUID, _admin: SuperUser, subsonic: SubsonicAuthServiceDep
|
||||
) -> SubsonicPasswordResponse:
|
||||
"""Rotate any user's Subsonic app-password and return the new plaintext."""
|
||||
return SubsonicPasswordResponse(password=await subsonic.rotate(user_id))
|
||||
|
||||
|
||||
@router.get("/services")
|
||||
async def list_services(_admin: SuperUser) -> Any: ...
|
||||
|
||||
|
||||
Reference in New Issue
Block a user