feat(jellyseer): move jellyseerr_api_key to an encrypted secret (slice 2/3)
The Jellyseerr API key was stored as plaintext in the Jellyfin service config. It is now a SecretField on the Jellyfin service, so it is encrypted at rest and rendered as a masked secret input (the generic config editor stops exposing it, and the secret editor picks it up automatically). Migration (idempotent, runs in ensure_defaults): - _migrate_jellyseerr_api_key_to_secret: for every Jellyfin service with a plaintext jellyseerr_api_key still in config, encrypt it ONCE into the secrets blob (direct UPDATE so existing encrypted secrets are preserved, not re-encrypted) and remove it from config. - _migrate_jellyseerr_into_jellyfin: standalone-jellyseerr absorption now stores the key as a secret, and decrypts the Jellyfin api_key before handing it to upsert_service (fixes a pre-existing double-encrypt on that rare path). The stats provider already reads jellyseerr_api_key from secrets-or-config, so it works before, during, and after the migration. Tests: absorbed-key lands in secrets (and existing api_key isn't corrupted); new plaintext-config -> secret migration + idempotency. 401/401 backend pass.
This commit is contained in:
@@ -73,9 +73,12 @@ def test_registry_contains_eight_service_types():
|
||||
def test_jellyseerr_absorbed_into_jellyfin():
|
||||
"""Jellyseerr is no longer its own service type (absorbed into Jellyfin)."""
|
||||
assert "jellyseerr" not in SERVICE_DEFINITIONS
|
||||
jellyfin_config = get_service_definition("jellyfin").config_schema["properties"]
|
||||
jellyfin = get_service_definition("jellyfin")
|
||||
jellyfin_config = jellyfin.config_schema["properties"]
|
||||
assert "jellyseerr_url" in jellyfin_config
|
||||
assert "jellyseerr_api_key" in jellyfin_config
|
||||
# jellyseerr_api_key moved from config to a secret field.
|
||||
assert "jellyseerr_api_key" not in jellyfin_config
|
||||
assert "jellyseerr_api_key" in {sf.key for sf in jellyfin.secret_fields}
|
||||
|
||||
|
||||
def test_backups_service_definition():
|
||||
@@ -555,10 +558,42 @@ def test_jellyseerr_migrates_into_single_jellyfin(tmp_path):
|
||||
# Jellyseerr row is gone.
|
||||
assert store.list_services("jellyseerr") == []
|
||||
|
||||
# Jellyfin config gained the absorbed fields.
|
||||
# Jellyfin config gained jellyseerr_url; the api key is now a secret.
|
||||
migrated = store.get_service(jellyfin["id"])
|
||||
assert migrated["config"]["jellyseerr_url"] == "https://jellyseerr.example.com"
|
||||
assert migrated["config"]["jellyseerr_api_key"] == "js-key"
|
||||
assert "jellyseerr_api_key" not in migrated["config"]
|
||||
assert decrypt_value(migrated["secrets"]["jellyseerr_api_key"]) == "js-key"
|
||||
# The existing Jellyfin api_key is preserved (not double-encrypted).
|
||||
assert decrypt_value(migrated["secrets"]["api_key"]) == "jf-key"
|
||||
|
||||
|
||||
def test_jellyseerr_api_key_migrates_from_config_to_secret(tmp_path):
|
||||
"""A pre-existing plaintext jellyseerr_api_key in config moves to a secret."""
|
||||
store = SettingsStore(tmp_path / "settings.sqlite")
|
||||
store.ensure_defaults()
|
||||
jellyfin = store.upsert_service(
|
||||
{
|
||||
"service_type": "jellyfin",
|
||||
"name": "Main Jellyfin",
|
||||
"config": {
|
||||
"base_url": "https://jellyfin.example.com",
|
||||
"jellyseerr_url": "https://jellyseerr.example.com",
|
||||
"jellyseerr_api_key": "plaintext-key", # legacy plaintext in config
|
||||
},
|
||||
"enabled": True,
|
||||
},
|
||||
secret_values={"api_key": "jf-key"},
|
||||
)
|
||||
|
||||
store.ensure_defaults() # runs the config->secret migration
|
||||
|
||||
migrated = store.get_service(jellyfin["id"])
|
||||
assert "jellyseerr_api_key" not in migrated["config"]
|
||||
assert decrypt_value(migrated["secrets"]["jellyseerr_api_key"]) == "plaintext-key"
|
||||
# Idempotent: a second run keeps it in secrets, doesn't wipe it.
|
||||
store.ensure_defaults()
|
||||
migrated = store.get_service(jellyfin["id"])
|
||||
assert decrypt_value(migrated["secrets"]["jellyseerr_api_key"]) == "plaintext-key"
|
||||
|
||||
|
||||
def test_jellyseerr_dropped_when_no_jellyfin(tmp_path):
|
||||
|
||||
Reference in New Issue
Block a user