Merge branch 'subsonic-new-auth-system' into develop

This commit is contained in:
emeric
2024-11-29 18:17:22 +01:00
56 changed files with 957 additions and 655 deletions
+10 -3
View File
@@ -1,16 +1,21 @@
# Subsonic API # Subsonic API
The API version implemented is 1.16.0 and has been tested on _Android_ using _Subsonic Player_, _Ultrasonic_, _Symfonium_, and _DSub_. The API version implemented is 1.16.0 and has been tested on _Android_ using _DSub_, _Subsonic Player_, _Symfonium_, _Tempo_ and _Ultrasonic_.
Folder navigation commands are supported. However, since _LMS_ does not store information for each folder, it is not possible to star/unstar folders considered as artists. Folder navigation commands are supported. However, since _LMS_ does not store information for each folder, it is not possible to star/unstar folders considered as artists.
Given the API limitations of folder navigation commands, it is recommended to place all tracks of an album in the same folder and not to mix multiple albums in the same folder. Given the API limitations of folder navigation commands, it is recommended to place all tracks of an album in the same folder and not to mix multiple albums in the same folder.
The Subsonic API is enabled by default. The Subsonic API is enabled by default.
__Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method. You may need to check your client to make sure to use the __password__ authentication method. Since logins/passwords are passed in plain text through URLs, it is highly recommended to use a unique password when using the Subsonic API. Note that this may affect the use of authentication via PAM. In any case, ensure the web server logs (and proxy logs, if applicable) are properly secured.
# OpenSubsonic API # OpenSubsonic API
OpenSubsonic is an initiative to patch and extend the legacy Subsonic API. You'll find more details in the [official documentation](https://opensubsonic.netlify.app/) OpenSubsonic is an initiative to patch and extend the legacy Subsonic API. You'll find more details in the [official documentation](https://opensubsonic.netlify.app/)
## Authentication
_LMS_ supports the newer [API Key Authentication](https://opensubsonic.netlify.app/docs/extensions/apikeyauth/). Each user can generate, regenerate, or delete their API key in the settings page.
By default, API keys can also be used as passwords, and the `user` parameter must match the owner of the API key. This fallback authentication method can be disabled by setting `api-subsonic-support-user-password-auth = false;` in `lms.conf`.
__Note__: the token+salt authentication method is not supported.
## Extra fields ## Extra fields
The following extra fields are implemented: The following extra fields are implemented:
* `Album` response: * `Album` response:
@@ -50,5 +55,7 @@ The following extra fields are implemented:
* `roles` * `roles`
## Supported extensions ## Supported extensions
* [API Key Authentication](https://opensubsonic.netlify.app/docs/extensions/apikeyauth/)
* [HTTP form POST](https://opensubsonic.netlify.app/docs/extensions/formpost/)
* [Transcode offset](https://opensubsonic.netlify.app/docs/extensions/transcodeoffset/) * [Transcode offset](https://opensubsonic.netlify.app/docs/extensions/transcodeoffset/)
* [Song Lyrics](https://opensubsonic.netlify.app/docs/extensions/songlyrics/) * [Song Lyrics](https://opensubsonic.netlify.app/docs/extensions/songlyrics/)
+10 -7
View File
@@ -289,7 +289,7 @@
<!--Settings--> <!--Settings-->
<message id="Lms.Settings.artist-release-sort-method">Method to sort artist's albums</message> <message id="Lms.Settings.artist-release-sort-method">Method to sort artist's albums</message>
<message id="Lms.Settings.audio">Audio</message> <message id="Lms.Settings.audio">Audio</message>
<message id="Lms.Settings.audio-settings-are-local">These audio settings are local to your browser!</message> <message id="Lms.Settings.audio-settings-are-local">These audio settings are local to your browser</message>
<message id="Lms.Settings.backend.internal">Internal</message> <message id="Lms.Settings.backend.internal">Internal</message>
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message> <message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
<message id="Lms.Settings.backend.listenbrainz-token">ListenBrainz API token</message> <message id="Lms.Settings.backend.listenbrainz-token">ListenBrainz API token</message>
@@ -307,6 +307,7 @@
<message id="Lms.Settings.original-date-desc">Original Release Date (Descending)</message> <message id="Lms.Settings.original-date-desc">Original Release Date (Descending)</message>
<message id="Lms.Settings.password-bad">Bad password</message> <message id="Lms.Settings.password-bad">Bad password</message>
<message id="Lms.Settings.password-must-fill-old-password">Old password must be filled in</message> <message id="Lms.Settings.password-must-fill-old-password">Old password must be filled in</message>
<message id="Lms.Settings.regen-token">Generate</message>
<message id="Lms.Settings.replaygain-mode">ReplayGain mode</message> <message id="Lms.Settings.replaygain-mode">ReplayGain mode</message>
<message id="Lms.Settings.replaygain-mode.none">No ReplayGain</message> <message id="Lms.Settings.replaygain-mode.none">No ReplayGain</message>
<message id="Lms.Settings.replaygain-mode.auto">Auto</message> <message id="Lms.Settings.replaygain-mode.auto">Auto</message>
@@ -317,13 +318,19 @@
<message id="Lms.Settings.scrobbling">Scrobbling</message> <message id="Lms.Settings.scrobbling">Scrobbling</message>
<message id="Lms.Settings.services">Services</message> <message id="Lms.Settings.services">Services</message>
<message id="Lms.Settings.settings">Settings</message> <message id="Lms.Settings.settings">Settings</message>
<message id="Lms.Settings.settings-saved">New settings saved!</message>
<message id="Lms.Settings.subsonic-artist-list-mode">Artist list mode</message> <message id="Lms.Settings.subsonic-artist-list-mode">Artist list mode</message>
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">All artists</message> <message id="Lms.Settings.subsonic-artist-list-mode.all-artists">All artists</message>
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Album artists</message> <message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Album artists</message>
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Track artists</message> <message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Track artists</message>
<message id="Lms.Settings.subsonic-api">Subsonic API</message> <message id="Lms.Settings.subsonic-api">Subsonic API</message>
<message id="Lms.Settings.user-interface">User Interface</message> <message id="Lms.Settings.subsonic-token">OpenSubsonic API key</message>
<message id="Lms.Settings.subsonic-token-usage">Use this API key as the password for clients that do not support the 'API Key Authentication' extension</message>
<message id="Lms.Settings.transcoding">Transcoding</message> <message id="Lms.Settings.transcoding">Transcoding</message>
<message id="Lms.Settings.transcoding-mode">Enable transcoding</message>
<message id="Lms.Settings.transcoding-mode.always">Always</message>
<message id="Lms.Settings.transcoding-mode.if-format-not-supported">Only when format is not supported by the browser</message>
<message id="Lms.Settings.transcoding-mode.never">Never</message>
<message id="Lms.Settings.transcoding-output-bitrate">Transcoding output bitrate</message> <message id="Lms.Settings.transcoding-output-bitrate">Transcoding output bitrate</message>
<message id="Lms.Settings.transcoding-output-format">Transcoding output format</message> <message id="Lms.Settings.transcoding-output-format">Transcoding output format</message>
<message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message> <message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
@@ -331,10 +338,6 @@
<message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message> <message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
<message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message> <message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
<message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message> <message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
<message id="Lms.Settings.transcoding-mode">Enable transcoding</message> <message id="Lms.Settings.user-interface">User Interface</message>
<message id="Lms.Settings.transcoding-mode.always">Always</message>
<message id="Lms.Settings.transcoding-mode.if-format-not-supported">Only when format is not supported by the browser</message>
<message id="Lms.Settings.transcoding-mode.never">Never</message>
<message id="Lms.Settings.settings-saved">New settings saved!</message>
</messages> </messages>
+10 -7
View File
@@ -289,7 +289,7 @@
<!--Settings--> <!--Settings-->
<message id="Lms.Settings.artist-release-sort-method">Méthode pour trier les albums de l'artiste</message> <message id="Lms.Settings.artist-release-sort-method">Méthode pour trier les albums de l'artiste</message>
<message id="Lms.Settings.audio">Audio</message> <message id="Lms.Settings.audio">Audio</message>
<message id="Lms.Settings.audio-settings-are-local">Ces paramètres audio sont locaux à votre navigateur !</message> <message id="Lms.Settings.audio-settings-are-local">Ces paramètres audio sont locaux à votre navigateur</message>
<message id="Lms.Settings.backend.internal">Interne</message> <message id="Lms.Settings.backend.internal">Interne</message>
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message> <message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
<message id="Lms.Settings.backend.listenbrainz-token">Jeton d'API ListenBrainz</message> <message id="Lms.Settings.backend.listenbrainz-token">Jeton d'API ListenBrainz</message>
@@ -307,6 +307,7 @@
<message id="Lms.Settings.original-date-desc">Date de sortie originale (Décroissant)</message> <message id="Lms.Settings.original-date-desc">Date de sortie originale (Décroissant)</message>
<message id="Lms.Settings.password-bad">Mauvais mot de passe</message> <message id="Lms.Settings.password-bad">Mauvais mot de passe</message>
<message id="Lms.Settings.password-must-fill-old-password">L'ancien mot de passe doit être renseigné</message> <message id="Lms.Settings.password-must-fill-old-password">L'ancien mot de passe doit être renseigné</message>
<message id="Lms.Settings.regen-token">Regénérer</message>
<message id="Lms.Settings.replaygain-mode">Mode ReplayGain</message> <message id="Lms.Settings.replaygain-mode">Mode ReplayGain</message>
<message id="Lms.Settings.replaygain-mode.none">Pas de ReplayGain</message> <message id="Lms.Settings.replaygain-mode.none">Pas de ReplayGain</message>
<message id="Lms.Settings.replaygain-mode.auto">Automatique</message> <message id="Lms.Settings.replaygain-mode.auto">Automatique</message>
@@ -317,13 +318,19 @@
<message id="Lms.Settings.scrobbling">Scrobbling</message> <message id="Lms.Settings.scrobbling">Scrobbling</message>
<message id="Lms.Settings.services">Services</message> <message id="Lms.Settings.services">Services</message>
<message id="Lms.Settings.settings">Paramètres</message> <message id="Lms.Settings.settings">Paramètres</message>
<message id="Lms.Settings.settings-saved">Paramètres sauvegardés !</message>
<message id="Lms.Settings.subsonic-artist-list-mode">Mode de listage des artistes</message> <message id="Lms.Settings.subsonic-artist-list-mode">Mode de listage des artistes</message>
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Tous les artistes</message> <message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Tous les artistes</message>
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Tous les artistes d'album</message> <message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Tous les artistes d'album</message>
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Tous les artistes de piste</message> <message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Tous les artistes de piste</message>
<message id="Lms.Settings.subsonic-api">API Subsonic</message> <message id="Lms.Settings.subsonic-api">API Subsonic</message>
<message id="Lms.Settings.user-interface">Interface utilisateur</message> <message id="Lms.Settings.subsonic-token">Clef pour l'API OpenSubsonic</message>
<message id="Lms.Settings.subsonic-token-usage">Utilisez cette clé d'API comme mot de passe pour les clients qui ne prennent pas en charge l'extension 'API Key Authentication'</message>
<message id="Lms.Settings.transcoding">Transcodage</message> <message id="Lms.Settings.transcoding">Transcodage</message>
<message id="Lms.Settings.transcoding-mode">Appliquer le transcodage</message>
<message id="Lms.Settings.transcoding-mode.always">Toujours</message>
<message id="Lms.Settings.transcoding-mode.if-format-not-supported">Seulement lorsque le format n'est pas pris en charge par votre navigateur</message>
<message id="Lms.Settings.transcoding-mode.never">Jamais</message>
<message id="Lms.Settings.transcoding-output-bitrate">Bitrate du transcodage</message> <message id="Lms.Settings.transcoding-output-bitrate">Bitrate du transcodage</message>
<message id="Lms.Settings.transcoding-output-format">Format du transcodage</message> <message id="Lms.Settings.transcoding-output-format">Format du transcodage</message>
<message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message> <message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
@@ -331,11 +338,7 @@
<message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message> <message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
<message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message> <message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
<message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message> <message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
<message id="Lms.Settings.transcoding-mode">Appliquer le transcodage</message> <message id="Lms.Settings.user-interface">Interface utilisateur</message>
<message id="Lms.Settings.transcoding-mode.always">Toujours</message>
<message id="Lms.Settings.transcoding-mode.if-format-not-supported">Seulement lorsque le format n'est pas pris en charge par votre navigateur</message>
<message id="Lms.Settings.transcoding-mode.never">Jamais</message>
<message id="Lms.Settings.settings-saved">Paramètres sauvegardés !</message>
<!--Wt--> <!--Wt-->
<message id="Wt.WDateTime.LessThanASecond">moins d'une seconde</message> <message id="Wt.WDateTime.LessThanASecond">moins d'une seconde</message>
+12 -9
View File
@@ -46,7 +46,7 @@
<message id="Lms.Error.error-occurred">C'è stato un errore!</message> <message id="Lms.Error.error-occurred">C'è stato un errore!</message>
<message id="Lms.Error.go-home">Ritorna alla home</message> <message id="Lms.Error.go-home">Ritorna alla home</message>
<message id="Lms.Error.release-not-found">Album non trovato</message> <message id="Lms.Error.release-not-found">Album non trovato</message>
<message id="Lms.Error.tracklist-not-found">Playlist non trovata</message>
<message id="Lms.Error.user-not-allowed">Non sei autorizzato ad eseguire questa operazione</message> <message id="Lms.Error.user-not-allowed">Non sei autorizzato ad eseguire questa operazione</message>
<message id="Lms.Error.user-not-found">Utente non trovato</message> <message id="Lms.Error.user-not-found">Utente non trovato</message>
@@ -289,7 +289,7 @@
<!--Settings--> <!--Settings-->
<message id="Lms.Settings.artist-release-sort-method">Metodo per ordinare gli album dell'artista</message> <message id="Lms.Settings.artist-release-sort-method">Metodo per ordinare gli album dell'artista</message>
<message id="Lms.Settings.audio">Audio</message> <message id="Lms.Settings.audio">Audio</message>
<message id="Lms.Settings.audio-settings-are-local">Queste impostazioni sono salvate localmente su questo dispositivo!</message> <message id="Lms.Settings.audio-settings-are-local">Queste impostazioni sono salvate localmente su questo dispositivo</message>
<message id="Lms.Settings.backend.internal">Interno</message> <message id="Lms.Settings.backend.internal">Interno</message>
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message> <message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
<message id="Lms.Settings.backend.listenbrainz-token">Token API ListenBrainz</message> <message id="Lms.Settings.backend.listenbrainz-token">Token API ListenBrainz</message>
@@ -307,6 +307,7 @@
<message id="Lms.Settings.original-date-desc">Data di uscita originale (Decrescente)</message> <message id="Lms.Settings.original-date-desc">Data di uscita originale (Decrescente)</message>
<message id="Lms.Settings.password-bad">Password errata</message> <message id="Lms.Settings.password-bad">Password errata</message>
<message id="Lms.Settings.password-must-fill-old-password">Devi inserire la password attuale</message> <message id="Lms.Settings.password-must-fill-old-password">Devi inserire la password attuale</message>
<message id="Lms.Settings.regen-token">Genera</message>
<message id="Lms.Settings.replaygain-mode">Modalità ReplayGain</message> <message id="Lms.Settings.replaygain-mode">Modalità ReplayGain</message>
<message id="Lms.Settings.replaygain-mode.none">No ReplayGain</message> <message id="Lms.Settings.replaygain-mode.none">No ReplayGain</message>
<message id="Lms.Settings.replaygain-mode.auto">Automatica</message> <message id="Lms.Settings.replaygain-mode.auto">Automatica</message>
@@ -317,13 +318,19 @@
<message id="Lms.Settings.scrobbling">Scrobbling</message> <message id="Lms.Settings.scrobbling">Scrobbling</message>
<message id="Lms.Settings.services">Servizi</message> <message id="Lms.Settings.services">Servizi</message>
<message id="Lms.Settings.settings">Impostazioni</message> <message id="Lms.Settings.settings">Impostazioni</message>
<message id="Lms.Settings.settings-saved">Nuove impostazioni salvate!</message>
<message id="Lms.Settings.subsonic-artist-list-mode">Modalità di elencazione artisti</message> <message id="Lms.Settings.subsonic-artist-list-mode">Modalità di elencazione artisti</message>
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Tutti gli artisti</message> <message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Tutti gli artisti</message>
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Artisti album</message> <message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Artisti album</message>
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Artisti tracce</message> <message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Artisti tracce</message>
<message id="Lms.Settings.subsonic-api">Subsonic API</message> <message id="Lms.Settings.subsonic-api">API Subsonic</message>
<message id="Lms.Settings.user-interface">Interfaccia utente</message> <message id="Lms.Settings.subsonic-token">Chiave API OpenSubsonic</message>
<message id="Lms.Settings.subsonic-token-usage">Utilizza questa chiave API come password per i client che non supportano l'estensione 'Autenticazione con chiave API'</message>
<message id="Lms.Settings.transcoding">Transcodifica</message> <message id="Lms.Settings.transcoding">Transcodifica</message>
<message id="Lms.Settings.transcoding-mode">Abilita transcodifica</message>
<message id="Lms.Settings.transcoding-mode.always">Sempre</message>
<message id="Lms.Settings.transcoding-mode.if-format-not-supported">Quando il formato non è supportato</message>
<message id="Lms.Settings.transcoding-mode.never">Mai</message>
<message id="Lms.Settings.transcoding-output-bitrate">Bitrate transcodifica</message> <message id="Lms.Settings.transcoding-output-bitrate">Bitrate transcodifica</message>
<message id="Lms.Settings.transcoding-output-format">Formato di transcodifica</message> <message id="Lms.Settings.transcoding-output-format">Formato di transcodifica</message>
<message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message> <message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
@@ -331,11 +338,7 @@
<message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message> <message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
<message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message> <message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
<message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message> <message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
<message id="Lms.Settings.transcoding-mode">Abilita transcodifica</message> <message id="Lms.Settings.user-interface">Interfaccia utente</message>
<message id="Lms.Settings.transcoding-mode.always">Sempre</message>
<message id="Lms.Settings.transcoding-mode.if-format-not-supported">Quando il formato non è supportato</message>
<message id="Lms.Settings.transcoding-mode.never">Mai</message>
<message id="Lms.Settings.settings-saved">Nuove impostazioni salvate!</message>
<!--Wt--> <!--Wt-->
<message id="Wt.WDateTime.LessThanASecond">meno di un secondo</message> <message id="Wt.WDateTime.LessThanASecond">meno di un secondo</message>
+11 -8
View File
@@ -315,7 +315,7 @@
<!--Settings--> <!--Settings-->
<message id="Lms.Settings.artist-release-sort-method">Metoda sortowania albumów artysty</message> <message id="Lms.Settings.artist-release-sort-method">Metoda sortowania albumów artysty</message>
<message id="Lms.Settings.audio">Dźwięk</message> <message id="Lms.Settings.audio">Dźwięk</message>
<message id="Lms.Settings.audio-settings-are-local">Te ustawienia dotyczą wyłącznie tej przeglądarki!</message> <message id="Lms.Settings.audio-settings-are-local">Te ustawienia dotyczą wyłącznie tej przeglądarki</message>
<message id="Lms.Settings.backend.internal">Wewnętrzny</message> <message id="Lms.Settings.backend.internal">Wewnętrzny</message>
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message> <message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
<message id="Lms.Settings.backend.listenbrainz-token">ListenBrainz API token</message> <message id="Lms.Settings.backend.listenbrainz-token">ListenBrainz API token</message>
@@ -333,6 +333,7 @@
<message id="Lms.Settings.original-date-desc">Oryginalna data wydania (Malejąco)</message> <message id="Lms.Settings.original-date-desc">Oryginalna data wydania (Malejąco)</message>
<message id="Lms.Settings.password-bad">Błędne hasło</message> <message id="Lms.Settings.password-bad">Błędne hasło</message>
<message id="Lms.Settings.password-must-fill-old-password">Należy podać stare hasło</message> <message id="Lms.Settings.password-must-fill-old-password">Należy podać stare hasło</message>
<message id="Lms.Settings.regen-token">Generuj</message>
<message id="Lms.Settings.replaygain-mode">Tryb ReplayGain</message> <message id="Lms.Settings.replaygain-mode">Tryb ReplayGain</message>
<message id="Lms.Settings.replaygain-mode.none">Bez ReplayGain</message> <message id="Lms.Settings.replaygain-mode.none">Bez ReplayGain</message>
<message id="Lms.Settings.replaygain-mode.auto">Auto</message> <message id="Lms.Settings.replaygain-mode.auto">Auto</message>
@@ -343,13 +344,19 @@
<message id="Lms.Settings.scrobbling">Scrobbling</message> <message id="Lms.Settings.scrobbling">Scrobbling</message>
<message id="Lms.Settings.services">Usługi</message> <message id="Lms.Settings.services">Usługi</message>
<message id="Lms.Settings.settings">Ustawienia</message> <message id="Lms.Settings.settings">Ustawienia</message>
<message id="Lms.Settings.settings-saved">Ustawienia zapisane!</message>
<message id="Lms.Settings.subsonic-artist-list-mode">Tryb listy artystów</message> <message id="Lms.Settings.subsonic-artist-list-mode">Tryb listy artystów</message>
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Wszyscy artyści</message> <message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Wszyscy artyści</message>
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Artyści z albumów</message> <message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Artyści z albumów</message>
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Artyści ze ścieżek</message> <message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Artyści ze ścieżek</message>
<message id="Lms.Settings.subsonic-api">Subsonic API</message> <message id="Lms.Settings.subsonic-api">API Subsonic</message>
<message id="Lms.Settings.user-interface">Interfejs użytkownika</message> <message id="Lms.Settings.subsonic-token">Klucz API OpenSubsonic</message>
<message id="Lms.Settings.subsonic-token-usage">Użyj tego klucza API jako hasła dla klientów, którzy nie obsługują rozszerzenia 'Uwierzytelnianie za pomocą klucza API'</message>
<message id="Lms.Settings.transcoding">Transkodowanie</message> <message id="Lms.Settings.transcoding">Transkodowanie</message>
<message id="Lms.Settings.transcoding-mode">Aktywuj transkodowanie</message>
<message id="Lms.Settings.transcoding-mode.always">Zawsze</message>
<message id="Lms.Settings.transcoding-mode.if-format-not-supported">Tylko gdy przeglądarka nie wspiera formatu</message>
<message id="Lms.Settings.transcoding-mode.never">Nigdy</message>
<message id="Lms.Settings.transcoding-output-bitrate">Przepływność transkodowania</message> <message id="Lms.Settings.transcoding-output-bitrate">Przepływność transkodowania</message>
<message id="Lms.Settings.transcoding-output-format">Format wyjściowy transkodowania</message> <message id="Lms.Settings.transcoding-output-format">Format wyjściowy transkodowania</message>
<message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message> <message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
@@ -357,11 +364,7 @@
<message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message> <message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
<message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message> <message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
<message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message> <message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
<message id="Lms.Settings.transcoding-mode">Aktywuj transkodowanie</message> <message id="Lms.Settings.user-interface">Interfejs użytkownika</message>
<message id="Lms.Settings.transcoding-mode.always">Zawsze</message>
<message id="Lms.Settings.transcoding-mode.if-format-not-supported">Tylko gdy przeglądarka nie wspiera formatu</message>
<message id="Lms.Settings.transcoding-mode.never">Nigdy</message>
<message id="Lms.Settings.settings-saved">Ustawienia zapisane!</message>
<!--Wt--> <!--Wt-->
<message id="Wt.WDateTime.LessThanASecond">mniej niż sekunda</message> <message id="Wt.WDateTime.LessThanASecond">mniej niż sekunda</message>
+8 -6
View File
@@ -289,7 +289,7 @@
<!--Settings--> <!--Settings-->
<message id="Lms.Settings.audio">音频</message> <message id="Lms.Settings.audio">音频</message>
<message id="Lms.Settings.audio-settings-are-local">这些音频设置仅保存在您的本地浏览器</message> <message id="Lms.Settings.audio-settings-are-local">这些音频设置仅保存在您的本地浏览器</message>
@@ -307,6 +307,7 @@
<message id="Lms.Settings.password-bad">无效密码</message> <message id="Lms.Settings.password-bad">无效密码</message>
<message id="Lms.Settings.password-must-fill-old-password">必须填写旧密码</message> <message id="Lms.Settings.password-must-fill-old-password">必须填写旧密码</message>
<message id="Lms.Settings.replaygain-mode">ReplayGain 模式</message> <message id="Lms.Settings.replaygain-mode">ReplayGain 模式</message>
<message id="Lms.Settings.replaygain-mode.none">没有 ReplayGain</message> <message id="Lms.Settings.replaygain-mode.none">没有 ReplayGain</message>
<message id="Lms.Settings.replaygain-mode.auto">自动模式</message> <message id="Lms.Settings.replaygain-mode.auto">自动模式</message>
@@ -317,13 +318,19 @@
<message id="Lms.Settings.settings-saved">新设置已保存!</message>
<message id="Lms.Settings.subsonic-artist-list-mode">歌手列表模式</message> <message id="Lms.Settings.subsonic-artist-list-mode">歌手列表模式</message>
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">所有歌手</message> <message id="Lms.Settings.subsonic-artist-list-mode.all-artists">所有歌手</message>
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">专辑歌手</message> <message id="Lms.Settings.subsonic-artist-list-mode.release-artists">专辑歌手</message>
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">音轨歌手</message> <message id="Lms.Settings.subsonic-artist-list-mode.track-artists">音轨歌手</message>
<message id="Lms.Settings.subsonic-api">Subsonic 应用程序接口</message> <message id="Lms.Settings.subsonic-api">Subsonic 应用程序接口</message>
<message id="Lms.Settings.transcoding">转码</message> <message id="Lms.Settings.transcoding">转码</message>
<message id="Lms.Settings.transcoding-mode">启动转码</message>
<message id="Lms.Settings.transcoding-mode.always">总是</message>
<message id="Lms.Settings.transcoding-mode.if-format-not-supported">仅当格式不支持时使用</message>
<message id="Lms.Settings.transcoding-mode.never">从不</message>
<message id="Lms.Settings.transcoding-output-bitrate">转码比特率</message> <message id="Lms.Settings.transcoding-output-bitrate">转码比特率</message>
<message id="Lms.Settings.transcoding-output-format">转码格式</message> <message id="Lms.Settings.transcoding-output-format">转码格式</message>
<message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message> <message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
@@ -331,11 +338,6 @@
<message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message> <message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
<message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message> <message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
<message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message> <message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
<message id="Lms.Settings.transcoding-mode">启动转码</message>
<message id="Lms.Settings.transcoding-mode.always">总是</message>
<message id="Lms.Settings.transcoding-mode.if-format-not-supported">仅当格式不支持时使用</message>
<message id="Lms.Settings.transcoding-mode.never">从不</message>
<message id="Lms.Settings.settings-saved">新设置已保存!</message>
<!--Wt--> <!--Wt-->
<message id="Wt.WMessageBox.Yes"></message> <message id="Wt.WMessageBox.Yes"></message>
+1
View File
@@ -19,5 +19,6 @@
<message id="Lms.template.edit-btn"><i class="fa fa-fw fa-edit"></i></message> <message id="Lms.template.edit-btn"><i class="fa fa-fw fa-edit"></i></message>
<message id="Lms.template.more-btn"><i class="fa fa-fw fa-ellipsis-v"></i></message> <message id="Lms.template.more-btn"><i class="fa fa-fw fa-ellipsis-v"></i></message>
<message id="Lms.template.play-btn"><i class="fa fa-fw fa-play"></i></message> <message id="Lms.template.play-btn"><i class="fa fa-fw fa-play"></i></message>
<message id="Lms.template.toggle-visibility-btn"><i class="fa fa-fw fa-eye"></i></message>
</messages> </messages>
+25 -3
View File
@@ -85,6 +85,25 @@
</div> </div>
${<if-has-subsonic-api>} ${<if-has-subsonic-api>}
<legend>${tr:Lms.Settings.subsonic-api}</legend> <legend>${tr:Lms.Settings.subsonic-api}</legend>
<div class="col-lg-12">
<label class="form-label" for="${id:subsonic-token}">
${tr:Lms.Settings.subsonic-token}
${<if-has-subsonic-token-usage>}
<button type="button" class="btn btn-sm p-0" data-bs-toggle="tooltip" data-bs-placement="right" title="${tr:Lms.Settings.subsonic-token-usage}">
<i class="fa fa-fw fa-info-circle" aria-hidden="true"></i>
</button>
${</if-has-subsonic-token-usage>}
</label>
<div class="input-group mb-3">
${subsonic-token-regen-btn class="btn btn-outline-secondary"}
${subsonic-token-del-btn class="btn btn-outline-warning"}
${subsonic-token class="form-control"}
${subsonic-token-visibility-btn class="btn btn-outline-secondary"}
<div class="invalid-feedback">
${subsonic-token-info class="help-block"}
</div>
</div>
</div>
<div class="col-12"> <div class="col-12">
<div class="form-check"> <div class="form-check">
${subsonic-enable-transcoding-by-default class="form-check-input"} ${subsonic-enable-transcoding-by-default class="form-check-input"}
@@ -150,9 +169,12 @@
<label class="form-label" for="${id:listenbrainz-token}"> <label class="form-label" for="${id:listenbrainz-token}">
${tr:Lms.Settings.backend.listenbrainz-token} ${tr:Lms.Settings.backend.listenbrainz-token}
</label> </label>
${listenbrainz-token class="form-control"} <div class="input-group mb-3">
<div class="invalid-feedback"> ${listenbrainz-token class="form-control"}
${listenbrainz-token-info class="help-block"} ${listenbrainz-token-visibility-btn class="btn btn-outline-secondary"}
<div class="invalid-feedback">
${listenbrainz-token-info class="help-block"}
</div>
</div> </div>
</div> </div>
${<if-has-change-password>} ${<if-has-change-password>}
+7
View File
@@ -60,6 +60,9 @@ acousticbrainz-api-base-url = "https://acousticbrainz.org";
# Authentication # Authentication
# Available backends: "internal", "PAM", "http-headers" # Available backends: "internal", "PAM", "http-headers"
authentication-backend = "internal"; authentication-backend = "internal";
# The number of bcrypt rounds to be used when backend is set to "internal". The higher the more secure
internal-password-bcrypt-round = 12;
# The header to be used to read the authentication user when backend is set to "http-headers"
http-headers-login-field = "X-Forwarded-User"; http-headers-login-field = "X-Forwarded-User";
# Max entries in the login throttler (1 entry per IP address. For IPv6, the whole /64 block is used) # Max entries in the login throttler (1 entry per IP address. For IPv6, the whole /64 block is used)
@@ -68,6 +71,10 @@ login-throttler-max-entries = 10000;
# API # API
api-subsonic = true; api-subsonic = true;
# Enable or disable user/password authentication for the Subsonic API.
# Note: Since token/salt authentication is always disabled, setting this to 'false' means only API keys can be used to access the Subsonic API.
api-subsonic-support-user-password-auth = true;
# Use this list to make the reported server version to 1.12.0 depending on the client's name # Use this list to make the reported server version to 1.12.0 depending on the client's name
# Main usage is to make auto detections for the 'p' (password) parameter work # Main usage is to make auto detections for the 'p' (password) parameter work
api-subsonic-old-server-protocol-clients = ("DSub"); api-subsonic-old-server-protocol-clients = ("DSub");
+4 -3
View File
@@ -24,7 +24,8 @@
namespace lms::core namespace lms::core
{ {
template<typename Class> // Tag can be used if you have multiple services sharing the same interface
template<typename Class, typename Tag = Class>
class Service class Service
{ {
public: public:
@@ -46,12 +47,12 @@ namespace lms::core
Class* operator->() const Class* operator->() const
{ {
return Service<Class>::get(); return Service<Class, Tag>::get();
} }
Class& operator*() const Class& operator*() const
{ {
return *Service<Class>::get(); return *Service<Class, Tag>::get();
} }
static Class* get() { return _service.get(); } static Class* get() { return _service.get(); }
+1
View File
@@ -5,6 +5,7 @@ add_executable(test-core
LiteralString.cpp LiteralString.cpp
Path.cpp Path.cpp
RecursiveSharedMutex.cpp RecursiveSharedMutex.cpp
Service.cpp
String.cpp String.cpp
TraceLogger.cpp TraceLogger.cpp
Utils.cpp Utils.cpp
+69
View File
@@ -0,0 +1,69 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <gtest/gtest.h>
#include "core/Service.hpp"
namespace lms::core::tests
{
class IMyService
{
};
class MyService : public IMyService
{
};
class MyOtherService : public IMyService
{
};
class MyServiceTag
{
};
class MyOtherServiceTag
{
};
TEST(Service, ctr)
{
EXPECT_FALSE(Service<IMyService>().exists());
EXPECT_EQ(Service<IMyService>().get(), nullptr);
Service<IMyService> myService{ std::make_unique<MyService>() };
EXPECT_TRUE(Service<IMyService>().exists());
EXPECT_EQ(Service<IMyService>().get(), myService.get());
}
TEST(Service, tags)
{
Service<IMyService, MyServiceTag> myService{ std::make_unique<MyService>() };
Service<IMyService, MyOtherServiceTag> myOtherService{ std::make_unique<MyOtherService>() };
EXPECT_FALSE(Service<IMyService>().exists());
EXPECT_EQ(Service<IMyService>().get(), nullptr);
EXPECT_TRUE((Service<IMyService, MyServiceTag>().exists()));
EXPECT_TRUE((Service<IMyService, MyOtherServiceTag>().exists()));
EXPECT_EQ((Service<IMyService, MyServiceTag>().get()), myService.get());
EXPECT_EQ((Service<IMyService, MyOtherServiceTag>().get()), myOtherService.get());
}
} // namespace lms::core::tests
+49 -13
View File
@@ -30,29 +30,65 @@
namespace lms::db namespace lms::db
{ {
AuthToken::AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user) AuthToken::AuthToken(std::string_view domain, std::string_view value, const Wt::WDateTime& expiry, std::optional<long> maxUseCount, ObjectPtr<User> user)
: _value{ value } : _domain{ domain }
, _value{ value }
, _expiry{ expiry } , _expiry{ expiry }
, _maxUseCount{ maxUseCount }
, _user{ getDboPtr(user) } , _user{ getDboPtr(user) }
{ {
} }
AuthToken::pointer AuthToken::create(Session& session, std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user) AuthToken::pointer AuthToken::create(Session& session, std::string_view domain, std::string_view value, const Wt::WDateTime& expiry, std::optional<long> maxUseCount, ObjectPtr<User> user)
{ {
return session.getDboSession()->add(std::unique_ptr<AuthToken>{ new AuthToken{ value, expiry, user } }); return session.getDboSession()->add(std::unique_ptr<AuthToken>{ new AuthToken{ domain, value, expiry, maxUseCount, user } });
} }
void AuthToken::removeExpiredTokens(Session& session, const Wt::WDateTime& now) std::size_t AuthToken::getCount(Session& session)
{
session.checkWriteTransaction();
utils::executeCommand(*session.getDboSession(), "DELETE FROM auth_token WHERE expiry < ?", now);
}
AuthToken::pointer AuthToken::find(Session& session, std::string_view value)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<AuthToken>().where("value = ?").bind(value)); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM auth_token"));
}
AuthToken::pointer AuthToken::find(Session& session, AuthTokenId id)
{
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<AuthToken>>("SELECT a_t from auth_token a_t").where("a_t.id = ?").bind(id));
}
AuthToken::pointer AuthToken::find(Session& session, std::string_view domain, std::string_view value)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->find<AuthToken>() };
query.where("domain = ?").bind(domain);
query.where("value = ?").bind(value);
return utils::fetchQuerySingleResult(query);
}
void AuthToken::find(Session& session, std::string_view domain, UserId userId, std::function<void(const AuthToken::pointer&)> visitor)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->find<AuthToken>() };
query.where("domain = ?").bind(domain);
query.where("user_id = ?").bind(userId);
utils::forEachQueryResult(query, visitor);
}
void AuthToken::removeExpiredTokens(Session& session, std::string_view domain, const Wt::WDateTime& now)
{
session.checkWriteTransaction();
utils::executeCommand(*session.getDboSession(), "DELETE FROM auth_token WHERE expiry < ? AND domain = ?", now, domain);
}
void AuthToken::clearUserTokens(Session& session, std::string_view domain, UserId user)
{
session.checkWriteTransaction();
utils::executeCommand(*session.getDboSession(), "DELETE FROM auth_token WHERE user_id = ? AND domain = ?", user, domain);
} }
} // namespace lms::db } // namespace lms::db
+27 -2
View File
@@ -35,7 +35,7 @@ namespace lms::db
{ {
namespace namespace
{ {
static constexpr Version LMS_DATABASE_VERSION{ 74 }; static constexpr Version LMS_DATABASE_VERSION{ 76 };
} }
VersionInfo::VersionInfo() VersionInfo::VersionInfo()
@@ -947,6 +947,27 @@ SELECT
utils::executeCommand(*session.getDboSession(), "UPDATE media_library SET path = rtrim(path, '/') WHERE path LIKE '%/'"); utils::executeCommand(*session.getDboSession(), "UPDATE media_library SET path = rtrim(path, '/') WHERE path LIKE '%/'");
} }
void migrateFromV74(Session& session)
{
// New auth token authentication for Subsonic API
// Previous tokens are not usable any more, no problem since they are just used for the ui's "remember me" feature
utils::executeCommand(*session.getDboSession(), "DELETE FROM auth_token");
utils::executeCommand(*session.getDboSession(), "ALTER TABLE auth_token ADD domain TEXT NOT NULL");
utils::executeCommand(*session.getDboSession(), "ALTER TABLE auth_token ADD use_count INTEGER NOT NULL");
utils::executeCommand(*session.getDboSession(), "ALTER TABLE auth_token ADD last_used TEXT");
utils::executeCommand(*session.getDboSession(), "ALTER TABLE auth_token ADD max_use_count INTEGER");
utils::executeCommand(*session.getDboSession(), "DROP INDEX IF EXISTS auth_token_user_idx");
utils::executeCommand(*session.getDboSession(), "DROP INDEX IF EXISTS auth_token_expiry_idx");
utils::executeCommand(*session.getDboSession(), "DROP INDEX IF EXISTS auth_token_value_idx");
}
void migrateFromV75(Session& session)
{
// Added a new option to set the bcrypt count to be use to hash user's passwords
utils::executeCommand(*session.getDboSession(), "ALTER TABLE user ADD bcrypt_round_count INTEGER NOT NULL DEFAULT(7)");
}
bool doDbMigration(Session& session) bool doDbMigration(Session& session)
{ {
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
@@ -997,6 +1018,8 @@ SELECT
{ 71, migrateFromV71 }, { 71, migrateFromV71 },
{ 72, migrateFromV72 }, { 72, migrateFromV72 },
{ 73, migrateFromV73 }, { 73, migrateFromV73 },
{ 74, migrateFromV74 },
{ 75, migrateFromV75 },
}; };
bool migrationPerformed{}; bool migrationPerformed{};
@@ -1028,7 +1051,9 @@ SELECT
LMS_LOG(DB, INFO, "Migrating database from version " << version << " to " << version + 1 << "..."); LMS_LOG(DB, INFO, "Migrating database from version " << version << " to " << version + 1 << "...");
auto itMigrationFunc{ migrationFunctions.find(version) }; auto itMigrationFunc{ migrationFunctions.find(version) };
assert(itMigrationFunc != std::cend(migrationFunctions)); if (itMigrationFunc == std::cend(migrationFunctions))
throw core::LmsException{ "No code found to upgrade database!" };
itMigrationFunc->second(session); itMigrationFunc->second(session);
VersionInfo::get(session).modify()->setVersion(++version); VersionInfo::get(session).modify()->setVersion(++version);
+3 -3
View File
@@ -188,9 +188,9 @@ namespace lms::db
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_user_idx ON auth_token(user_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_user_domain_idx ON auth_token(user_id, domain)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_expiry_idx ON auth_token(expiry)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_expiry_idx ON auth_token(domain, expiry)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_value_idx ON auth_token(value)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_value_idx ON auth_token(domain, value)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)");
-6
View File
@@ -97,10 +97,4 @@ namespace lms::db
assert(isAudioBitrateAllowed(bitrate)); assert(isAudioBitrateAllowed(bitrate));
_subsonicDefaultTranscodingOutputBitrate = bitrate; _subsonicDefaultTranscodingOutputBitrate = bitrate;
} }
void User::clearAuthTokens()
{
_authTokens.clear();
}
} // namespace lms::db } // namespace lms::db
@@ -19,6 +19,7 @@
#pragma once #pragma once
#include <optional>
#include <string_view> #include <string_view>
#include <Wt/Dbo/Dbo.h> #include <Wt/Dbo/Dbo.h>
@@ -26,41 +27,62 @@
#include "database/AuthTokenId.hpp" #include "database/AuthTokenId.hpp"
#include "database/Object.hpp" #include "database/Object.hpp"
#include "database/Types.hpp"
#include "database/UserId.hpp"
namespace lms::db namespace lms::db
{ {
class Session; class Session;
class User; class User;
class AuthToken final : public Object<AuthToken, AuthTokenId> class AuthToken final : public Object<AuthToken, AuthTokenId>
{ {
public: public:
AuthToken() = default; AuthToken() = default;
// Utility // Utility
static void removeExpiredTokens(Session& session, const Wt::WDateTime& now); static std::size_t getCount(Session& session);
static pointer find(Session& session, std::string_view value); static pointer find(Session& session, AuthTokenId tokenId);
static pointer find(Session& session, std::string_view domain, std::string_view value);
static void find(Session& session, std::string_view domain, UserId userId, std::function<void(const AuthToken::pointer&)> visitor);
static void removeExpiredTokens(Session& session, std::string_view domain, const Wt::WDateTime& now);
static void clearUserTokens(Session& session, std::string_view domain, UserId user);
// Accessors // Accessors
const Wt::WDateTime& getExpiry() const { return _expiry; } const Wt::WDateTime& getExpiry() const { return _expiry; }
ObjectPtr<User> getUser() const { return _user; } ObjectPtr<User> getUser() const { return _user; }
const std::string& getValue() const { return _value; } const std::string& getValue() const { return _value; }
std::size_t getUseCount() const { return _useCount; }
Wt::WDateTime getLastUsed() const { return _lastUsed; }
std::optional<std::size_t> getMaxUseCount() const { return _maxUseCount; }
// Setters
std::size_t incUseCount() { return ++_useCount; }
void setLastUsed(const Wt::WDateTime& lastUsed) { _lastUsed = lastUsed; }
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
{ {
Wt::Dbo::field(a, _domain, "domain");
Wt::Dbo::field(a, _value, "value"); Wt::Dbo::field(a, _value, "value");
Wt::Dbo::field(a, _expiry, "expiry"); Wt::Dbo::field(a, _expiry, "expiry");
Wt::Dbo::field(a, _useCount, "use_count");
Wt::Dbo::field(a, _lastUsed, "last_used");
Wt::Dbo::field(a, _maxUseCount, "max_use_count");
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
} }
private: private:
friend class Session; friend class Session;
AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user); AuthToken(std::string_view domain, std::string_view value, const Wt::WDateTime& expiry, std::optional<long> maxUseCount, ObjectPtr<User> user);
static pointer create(Session& session, std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user); static pointer create(Session& session, std::string_view domain, std::string_view value, const Wt::WDateTime& expiry, std::optional<long> maxUseCount, ObjectPtr<User> user);
std::string _domain;
std::string _value; std::string _value;
Wt::WDateTime _expiry; Wt::WDateTime _expiry;
long _useCount{};
Wt::WDateTime _lastUsed;
std::optional<long> _maxUseCount;
Wt::Dbo::ptr<User> _user; Wt::Dbo::ptr<User> _user;
}; };
} // namespace lms::db } // namespace lms::db
+5 -2
View File
@@ -42,6 +42,7 @@ namespace lms::db
public: public:
struct PasswordHash struct PasswordHash
{ {
std::size_t bcryptRoundCount;
std::string salt; std::string salt;
std::string hash; std::string hash;
}; };
@@ -91,7 +92,7 @@ namespace lms::db
// accessors // accessors
const std::string& getLoginName() const { return _loginName; } const std::string& getLoginName() const { return _loginName; }
PasswordHash getPasswordHash() const { return PasswordHash{ _passwordSalt, _passwordHash }; } PasswordHash getPasswordHash() const { return PasswordHash{ .bcryptRoundCount = static_cast<std::size_t>(_bcryptRoundCount), .salt = _passwordSalt, .hash = _passwordHash }; }
const Wt::WDateTime& getLastLogin() const { return _lastLogin; } const Wt::WDateTime& getLastLogin() const { return _lastLogin; }
std::size_t getAuthTokensCount() const { return _authTokens.size(); } std::size_t getAuthTokensCount() const { return _authTokens.size(); }
@@ -99,6 +100,7 @@ namespace lms::db
void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; } void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; }
void setPasswordHash(const PasswordHash& passwordHash) void setPasswordHash(const PasswordHash& passwordHash)
{ {
_bcryptRoundCount = passwordHash.bcryptRoundCount;
_passwordSalt = passwordHash.salt; _passwordSalt = passwordHash.salt;
_passwordHash = passwordHash.hash; _passwordHash = passwordHash.hash;
} }
@@ -108,7 +110,6 @@ namespace lms::db
void setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate); void setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate);
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; } void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
void setUIArtistReleaseSortMethod(ReleaseSortMethod method) { _uiArtistReleaseSortMethod = method; } void setUIArtistReleaseSortMethod(ReleaseSortMethod method) { _uiArtistReleaseSortMethod = method; }
void clearAuthTokens();
void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; } void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; }
void setFeedbackBackend(FeedbackBackend feedbackBackend) { _feedbackBackend = feedbackBackend; } void setFeedbackBackend(FeedbackBackend feedbackBackend) { _feedbackBackend = feedbackBackend; }
void setScrobblingBackend(ScrobblingBackend scrobblingBackend) { _scrobblingBackend = scrobblingBackend; } void setScrobblingBackend(ScrobblingBackend scrobblingBackend) { _scrobblingBackend = scrobblingBackend; }
@@ -133,6 +134,7 @@ namespace lms::db
{ {
Wt::Dbo::field(a, _type, "type"); Wt::Dbo::field(a, _type, "type");
Wt::Dbo::field(a, _loginName, "login_name"); Wt::Dbo::field(a, _loginName, "login_name");
Wt::Dbo::field(a, _bcryptRoundCount, "bcrypt_round_count");
Wt::Dbo::field(a, _passwordSalt, "password_salt"); Wt::Dbo::field(a, _passwordSalt, "password_salt");
Wt::Dbo::field(a, _passwordHash, "password_hash"); Wt::Dbo::field(a, _passwordHash, "password_hash");
Wt::Dbo::field(a, _lastLogin, "last_login"); Wt::Dbo::field(a, _lastLogin, "last_login");
@@ -156,6 +158,7 @@ namespace lms::db
static pointer create(Session& session, std::string_view loginName); static pointer create(Session& session, std::string_view loginName);
std::string _loginName; std::string _loginName;
int _bcryptRoundCount{};
std::string _passwordSalt; std::string _passwordSalt;
std::string _passwordHash; std::string _passwordHash;
Wt::WDateTime _lastLogin; Wt::WDateTime _lastLogin;
+49
View File
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Common.hpp"
#include "database/AuthToken.hpp"
namespace lms::db::tests
{
using ScopedAuthToken = ScopedEntity<db::AuthToken>;
TEST_F(DatabaseFixture, AuthTokens)
{
ScopedUser user{ session, "MyUser" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(AuthToken::getCount(session), 0);
}
ScopedAuthToken token{ session, "myDomain", "foo", Wt::WDateTime{}, std::nullopt, user.lockAndGet() };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(AuthToken::getCount(session), 1);
}
{
auto transaction{ session.createWriteTransaction() };
AuthToken::clearUserTokens(session, "nonExistingDomain", user.getId());
}
}
} // namespace lms::db::tests
+1
View File
@@ -1,5 +1,6 @@
add_executable(test-database add_executable(test-database
AuthToken.cpp
Artist.cpp Artist.cpp
Cluster.cpp Cluster.cpp
Common.cpp Common.cpp
+2
View File
@@ -20,6 +20,7 @@
#include "Common.hpp" #include "Common.hpp"
#include "core/String.hpp" #include "core/String.hpp"
#include "database/AuthToken.hpp"
#include "database/Db.hpp" #include "database/Db.hpp"
#include "database/Directory.hpp" #include "database/Directory.hpp"
#include "database/Image.hpp" #include "database/Image.hpp"
@@ -337,6 +338,7 @@ VALUES
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
EXPECT_FALSE(Artist::find(session, ArtistId{})); EXPECT_FALSE(Artist::find(session, ArtistId{}));
EXPECT_FALSE(AuthToken::find(session, AuthTokenId{}));
EXPECT_FALSE(Cluster::find(session, ClusterId{})); EXPECT_FALSE(Cluster::find(session, ClusterId{}));
EXPECT_FALSE(ClusterType::find(session, ClusterTypeId{})); EXPECT_FALSE(ClusterType::find(session, ClusterTypeId{}));
EXPECT_FALSE(Directory::find(session, DirectoryId{})); EXPECT_FALSE(Directory::find(session, DirectoryId{}));
@@ -20,7 +20,6 @@
#include "AuthTokenService.hpp" #include "AuthTokenService.hpp"
#include <Wt/Auth/HashFunction.h> #include <Wt/Auth/HashFunction.h>
#include <Wt/Auth/PasswordStrengthValidator.h>
#include <Wt/WRandom.h> #include <Wt/WRandom.h>
#include "core/Exception.hpp" #include "core/Exception.hpp"
@@ -32,72 +31,95 @@
namespace lms::auth namespace lms::auth
{ {
namespace
std::unique_ptr<IAuthTokenService> createAuthTokenService(db::Db& db, std::size_t maxThrottlerEntries)
{ {
return std::make_unique<AuthTokenService>(db, maxThrottlerEntries); AuthTokenService::AuthTokenInfo createAuthTokenInfo(const db::AuthToken::pointer& authToken)
{
return AuthTokenService::AuthTokenInfo{
.userId = authToken->getUser()->getId(),
.expiry = authToken->getExpiry(),
.lastUsed = authToken->getLastUsed(),
.useCount = authToken->getUseCount(),
.maxUseCount = authToken->getMaxUseCount(),
};
}
} // namespace
std::unique_ptr<IAuthTokenService> createAuthTokenService(db::Db& db, std::size_t maxThrottlerEntryCount)
{
return std::make_unique<AuthTokenService>(db, maxThrottlerEntryCount);
} }
static const Wt::Auth::SHA1HashFunction sha1Function; AuthTokenService::AuthTokenService(db::Db& db, std::size_t maxThrottlerEntryCount)
AuthTokenService::AuthTokenService(db::Db& db, std::size_t maxThrottlerEntries)
: AuthServiceBase{ db } : AuthServiceBase{ db }
, _loginThrottler{ maxThrottlerEntries } , _loginThrottler{ maxThrottlerEntryCount }
{ {
} }
std::string void AuthTokenService::registerDomain(core::LiteralString domain, const DomainParameters& params)
AuthTokenService::createAuthToken(db::UserId userId, const Wt::WDateTime& expiry)
{ {
const std::string secret{ Wt::WRandom::generateId(32) }; auto [it, inserted]{ _domainParameters.emplace(domain, params) };
const std::string secretHash{ sha1Function.compute(secret, {}) }; if (!inserted)
throw Exception{ "Auth token domain already registered!" };
db::Session& session{ getDbSession() };
auto transaction{ session.createWriteTransaction() };
db::User::pointer user{ db::User::find(session, userId) };
if (!user)
throw Exception{ "User deleted" };
db::AuthToken::pointer authToken{ session.create<db::AuthToken>(secretHash, expiry, user) };
LMS_LOG(UI, DEBUG, "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString());
if (user->getAuthTokensCount() >= 50)
db::AuthToken::removeExpiredTokens(session, Wt::WDateTime::currentDateTime());
return secret;
} }
std::optional<AuthTokenService::AuthTokenProcessResult::AuthTokenInfo> void AuthTokenService::createAuthToken(core::LiteralString domain, db::UserId userId, std::string_view token)
AuthTokenService::processAuthToken(std::string_view secret)
{ {
const std::string secretHash{ sha1Function.compute(std::string{ secret }, {}) }; const DomainParameters& params{ getDomainParameters(domain) };
db::Session& session{ getDbSession() };
const auto now{ Wt::WDateTime::currentDateTime() };
const auto expiry{ params.tokenDuration ? now.addSecs(std::chrono::duration_cast<std::chrono::seconds>(params.tokenDuration.value()).count()) : Wt::WDateTime{} };
{
auto transaction{ session.createWriteTransaction() };
const db::User::pointer user{ db::User::find(session, userId) };
if (!user)
throw Exception{ "User deleted" };
const db::AuthToken::pointer authToken{ session.create<db::AuthToken>(domain.str(), token, expiry, params.tokenMaxUseCount, user) };
LMS_LOG(UI, DEBUG, "Created auth token for user '" << user->getLoginName() << "', expiry = " << authToken->getExpiry().toString() << ", maxUseCount = " << (authToken->getMaxUseCount() ? std::to_string(*authToken->getMaxUseCount()) : "<unset>"));
// TODO per domain
if (user->getAuthTokensCount() >= 50)
db::AuthToken::removeExpiredTokens(session, domain.str(), Wt::WDateTime::currentDateTime());
}
}
std::optional<AuthTokenService::AuthTokenInfo> AuthTokenService::processAuthToken(core::LiteralString domain, std::string_view token)
{
db::Session& session{ getDbSession() }; db::Session& session{ getDbSession() };
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
db::AuthToken::pointer authToken{ db::AuthToken::find(session, secretHash) }; db::AuthToken::pointer authToken{ db::AuthToken::find(session, domain.str(), token) };
if (!authToken) if (!authToken)
return std::nullopt; return std::nullopt;
if (authToken->getExpiry() < Wt::WDateTime::currentDateTime()) if (authToken->getExpiry().isValid() && authToken->getExpiry() < Wt::WDateTime::currentDateTime())
{ {
authToken.remove(); authToken.remove();
return std::nullopt; return std::nullopt;
} }
LMS_LOG(UI, DEBUG, "Found auth token for user '" << authToken->getUser()->getLoginName() << "'!"); LMS_LOG(UI, DEBUG, "Found auth token for user '" << authToken->getUser()->getLoginName() << "' on domain '" << domain.str() << "'");
AuthTokenService::AuthTokenProcessResult::AuthTokenInfo res{ authToken->getUser()->getId(), authToken->getExpiry() }; AuthTokenInfo res{ createAuthTokenInfo(authToken) };
authToken.remove();
const std::size_t tokenUseCount{ authToken.modify()->incUseCount() };
authToken.modify()->setLastUsed(Wt::WDateTime::currentDateTime());
if (auto maxUseCount{ authToken->getMaxUseCount() })
{
if (*maxUseCount >= tokenUseCount)
authToken.remove();
}
return res; return res;
} }
AuthTokenService::AuthTokenProcessResult AuthTokenService::AuthTokenProcessResult AuthTokenService::processAuthToken(core::LiteralString domain, const boost::asio::ip::address& clientAddress, std::string_view tokenValue)
AuthTokenService::processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue)
{ {
// Do not waste too much resource on brute force attacks (optim) // Do not waste too much resource on brute force attacks (optim)
{ {
@@ -107,7 +129,7 @@ namespace lms::auth
return AuthTokenProcessResult{ AuthTokenProcessResult::State::Throttled }; return AuthTokenProcessResult{ AuthTokenProcessResult::State::Throttled };
} }
auto res{ processAuthToken(tokenValue) }; auto res{ processAuthToken(domain, tokenValue) };
{ {
std::unique_lock lock{ _mutex }; std::unique_lock lock{ _mutex };
@@ -122,22 +144,40 @@ namespace lms::auth
_loginThrottler.onGoodClientAttempt(clientAddress); _loginThrottler.onGoodClientAttempt(clientAddress);
onUserAuthenticated(res->userId); onUserAuthenticated(res->userId);
return AuthTokenProcessResult{ AuthTokenProcessResult::State::Granted, std::move(*res) }; return AuthTokenProcessResult{ AuthTokenProcessResult::State::Granted, res };
} }
} }
void void AuthTokenService::visitAuthTokens(core::LiteralString domain, db::UserId userId, std::function<void(const AuthTokenInfo& info, std::string_view token)> visitor)
AuthTokenService::clearAuthTokens(db::UserId userId)
{ {
db::Session& session{ getDbSession() }; db::Session& session{ getDbSession() };
auto transaction{ session.createWriteTransaction() }; {
auto transaction{ session.createReadTransaction() };
db::User::pointer user{ db::User::find(session, userId) }; db::AuthToken::find(session, domain.str(), userId, [&](const db::AuthToken::pointer& authToken) {
if (!user) const AuthTokenInfo info{ createAuthTokenInfo(authToken) };
throw Exception{ "User deleted" }; visitor(info, authToken->getValue());
});
user.modify()->clearAuthTokens(); }
} }
void AuthTokenService::clearAuthTokens(core::LiteralString domain, db::UserId userId)
{
db::Session& session{ getDbSession() };
{
auto transaction{ session.createWriteTransaction() };
db::AuthToken::clearUserTokens(session, domain.str(), userId);
}
}
const AuthTokenService::DomainParameters& AuthTokenService::getDomainParameters(core::LiteralString domain) const
{
auto it{ _domainParameters.find(domain) };
if (it == std::cend(_domainParameters))
throw Exception{ "Invalid auth token domain" };
return it->second;
}
} // namespace lms::auth } // namespace lms::auth
@@ -19,6 +19,7 @@
#pragma once #pragma once
#include <map>
#include <shared_mutex> #include <shared_mutex>
#include "services/auth/IAuthTokenService.hpp" #include "services/auth/IAuthTokenService.hpp"
@@ -36,7 +37,7 @@ namespace lms::auth
class AuthTokenService : public IAuthTokenService, public AuthServiceBase class AuthTokenService : public IAuthTokenService, public AuthServiceBase
{ {
public: public:
AuthTokenService(db::Db& db, std::size_t maxThrottlerEntries); AuthTokenService(db::Db& db, std::size_t maxThrottlerEntryCount);
AuthTokenService(const AuthTokenService&) = delete; AuthTokenService(const AuthTokenService&) = delete;
AuthTokenService& operator=(const AuthTokenService&) = delete; AuthTokenService& operator=(const AuthTokenService&) = delete;
@@ -44,13 +45,17 @@ namespace lms::auth
AuthTokenService& operator=(AuthTokenService&&) = delete; AuthTokenService& operator=(AuthTokenService&&) = delete;
private: private:
AuthTokenProcessResult processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue) override; void registerDomain(core::LiteralString domain, const DomainParameters& params) override;
std::string createAuthToken(db::UserId userId, const Wt::WDateTime& expiry) override; AuthTokenProcessResult processAuthToken(core::LiteralString domain, const boost::asio::ip::address& clientAddress, std::string_view tokenValue) override;
void clearAuthTokens(db::UserId userId) override; void visitAuthTokens(core::LiteralString domain, db::UserId userId, std::function<void(const AuthTokenInfo& info, std::string_view token)> visitor) override;
void createAuthToken(core::LiteralString domain, db::UserId userId, std::string_view token) override;
void clearAuthTokens(core::LiteralString domain, db::UserId userId) override;
std::optional<AuthTokenService::AuthTokenProcessResult::AuthTokenInfo> processAuthToken(std::string_view secret); std::optional<AuthTokenInfo> processAuthToken(core::LiteralString domain, std::string_view tokenValue);
const DomainParameters& getDomainParameters(core::LiteralString domain) const;
std::shared_mutex _mutex; std::shared_mutex _mutex;
std::map<core::LiteralString, DomainParameters> _domainParameters;
LoginThrottler _loginThrottler; LoginThrottler _loginThrottler;
}; };
} // namespace lms::auth } // namespace lms::auth
@@ -37,28 +37,24 @@ namespace lms::auth
{ {
static const Wt::Auth::SHA1HashFunction sha1Function; static const Wt::Auth::SHA1HashFunction sha1Function;
std::unique_ptr<IPasswordService> std::unique_ptr<IPasswordService> createPasswordService(std::string_view backend, db::Db& db, std::size_t maxThrottlerEntryCount)
createPasswordService(std::string_view passwordAuthenticationBackend, db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
{ {
if (passwordAuthenticationBackend == "internal") if (backend == "internal")
return std::make_unique<InternalPasswordService>(db, maxThrottlerEntries, authTokenService); return std::make_unique<InternalPasswordService>(db, maxThrottlerEntryCount);
#ifdef LMS_SUPPORT_PAM #ifdef LMS_SUPPORT_PAM
else if (passwordAuthenticationBackend == "pam") if (backend == "PAM")
return std::make_unique<PAMPasswordService>(db, maxThrottlerEntries, authTokenService); return std::make_unique<PAMPasswordService>(db, maxThrottlerEntryCount);
#endif // LMS_SUPPORT_PAM #endif // LMS_SUPPORT_PAM
throw Exception{ "Authentication backend '" + std::string{ backend } + "' not supported!" };
throw Exception{ "Authentication backend '" + std::string{ passwordAuthenticationBackend } + "' is not supported!" };
} }
PasswordServiceBase::PasswordServiceBase(db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService) PasswordServiceBase::PasswordServiceBase(db::Db& db, std::size_t maxThrottlerEntries)
: AuthServiceBase{ db } : AuthServiceBase{ db }
, _loginThrottler{ maxThrottlerEntries } , _loginThrottler{ maxThrottlerEntries }
, _authTokenService{ authTokenService }
{ {
} }
PasswordServiceBase::CheckResult PasswordServiceBase::CheckResult PasswordServiceBase::checkUserPassword(const boost::asio::ip::address& clientAddress, std::string_view loginName, std::string_view password)
PasswordServiceBase::checkUserPassword(const boost::asio::ip::address& clientAddress, std::string_view loginName, std::string_view password)
{ {
LMS_LOG(AUTH, DEBUG, "Checking password for user '" << loginName << "'"); LMS_LOG(AUTH, DEBUG, "Checking password for user '" << loginName << "'");
@@ -36,16 +36,13 @@ namespace lms::auth
class PasswordServiceBase : public IPasswordService, public AuthServiceBase class PasswordServiceBase : public IPasswordService, public AuthServiceBase
{ {
public: public:
PasswordServiceBase(db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService); PasswordServiceBase(db::Db& db, std::size_t maxThrottlerEntries);
PasswordServiceBase(const PasswordServiceBase&) = delete; PasswordServiceBase(const PasswordServiceBase&) = delete;
PasswordServiceBase& operator=(const PasswordServiceBase&) = delete; PasswordServiceBase& operator=(const PasswordServiceBase&) = delete;
PasswordServiceBase(PasswordServiceBase&&) = delete; PasswordServiceBase(PasswordServiceBase&&) = delete;
PasswordServiceBase& operator=(PasswordServiceBase&&) = delete; PasswordServiceBase& operator=(PasswordServiceBase&&) = delete;
protected:
IAuthTokenService& getAuthTokenService() { return _authTokenService; }
private: private:
virtual bool checkUserPassword(std::string_view loginName, std::string_view password) = 0; virtual bool checkUserPassword(std::string_view loginName, std::string_view password) = 0;
@@ -55,6 +52,5 @@ namespace lms::auth
std::shared_mutex _mutex; std::shared_mutex _mutex;
LoginThrottler _loginThrottler; LoginThrottler _loginThrottler;
IAuthTokenService& _authTokenService;
}; };
} // namespace lms::auth } // namespace lms::auth
@@ -38,25 +38,25 @@ namespace lms::auth
{ {
const std::string loginName{ env.headerValue(_fieldName) }; const std::string loginName{ env.headerValue(_fieldName) };
if (loginName.empty()) if (loginName.empty())
return { CheckResult::State::Denied }; return CheckResult{ .state = CheckResult::State::Denied, .userId = {} };
LMS_LOG(AUTH, DEBUG, "Extracted login name = '" << loginName << "' from HTTP header"); LMS_LOG(AUTH, DEBUG, "Extracted login name = '" << loginName << "' from HTTP header");
const db::UserId userId{ getOrCreateUser(loginName) }; const db::UserId userId{ getOrCreateUser(loginName) };
onUserAuthenticated(userId); onUserAuthenticated(userId);
return { CheckResult::State::Granted, userId }; return CheckResult{ .state = CheckResult::State::Granted, .userId = userId };
} }
HttpHeadersEnvService::CheckResult HttpHeadersEnvService::processRequest(const Wt::Http::Request& request) HttpHeadersEnvService::CheckResult HttpHeadersEnvService::processRequest(const Wt::Http::Request& request)
{ {
const std::string loginName{ request.headerValue(_fieldName) }; const std::string loginName{ request.headerValue(_fieldName) };
if (loginName.empty()) if (loginName.empty())
return { CheckResult::State::Denied }; return CheckResult{ .state = CheckResult::State::Denied, .userId = {} };
LMS_LOG(AUTH, DEBUG, "Extracted login name = '" << loginName << "' from HTTP header"); LMS_LOG(AUTH, DEBUG, "Extracted login name = '" << loginName << "' from HTTP header");
const db::UserId userId{ getOrCreateUser(loginName) }; const db::UserId userId{ getOrCreateUser(loginName) };
onUserAuthenticated(userId); onUserAuthenticated(userId);
return { CheckResult::State::Granted, userId }; return { .state = CheckResult::State::Granted, .userId = userId };
} }
} // namespace lms::auth } // namespace lms::auth
@@ -21,18 +21,21 @@
#include <Wt/WRandom.h> #include <Wt/WRandom.h>
#include "core/Exception.hpp" #include "core/IConfig.hpp"
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/User.hpp" #include "database/User.hpp"
#include "services/auth/IAuthTokenService.hpp"
#include "services/auth/Types.hpp" #include "services/auth/Types.hpp"
namespace lms::auth namespace lms::auth
{ {
InternalPasswordService::InternalPasswordService(db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService) InternalPasswordService::InternalPasswordService(db::Db& db, std::size_t maxThrottlerEntries)
: PasswordServiceBase{ db, maxThrottlerEntries, authTokenService } : PasswordServiceBase{ db, maxThrottlerEntries }
, _bcryptRoundCount{ static_cast<unsigned>(core::Service<core::IConfig>::get()->getULong("internal-password-bcrypt-round", 12)) }
{ {
if (_bcryptRoundCount < 7 || _bcryptRoundCount > 31)
throw Exception{ "\"internal-password-bcrypt-round\" must be in range 7-31" };
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::OneCharClass, 4); _validator.setMinimumLength(Wt::Auth::PasswordStrengthType::OneCharClass, 4);
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::TwoCharClass, 4); _validator.setMinimumLength(Wt::Auth::PasswordStrengthType::TwoCharClass, 4);
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::PassPhrase, 4); _validator.setMinimumLength(Wt::Auth::PasswordStrengthType::PassPhrase, 4);
@@ -55,7 +58,7 @@ namespace lms::auth
if (!user) if (!user)
{ {
LMS_LOG(AUTH, DEBUG, "hashing random stuff"); LMS_LOG(AUTH, DEBUG, "hashing random stuff");
// hash random stuff here to waste some time // hash random stuff here to waste some time, don't give clue the user does not exist
hashRandomPassword(); hashRandomPassword();
return false; return false;
} }
@@ -64,13 +67,29 @@ namespace lms::auth
passwordHash = user->getPasswordHash(); passwordHash = user->getPasswordHash();
if (passwordHash.salt.empty() || passwordHash.hash.empty()) if (passwordHash.salt.empty() || passwordHash.hash.empty())
{ {
// hash random stuff here to waste some time // hash random stuff here to waste some time, don't give clue the user has no password set
hashRandomPassword(); hashRandomPassword();
return false; return false;
} }
} }
return _hashFunc.verify(std::string{ password }, std::string{ passwordHash.salt }, std::string{ passwordHash.hash }); // Note: the round count set in the actual hash is used to verify, not the one used to construct _hashFunc
bool passwordMatched{ _hashFunc.verify(std::string{ password }, std::string{ passwordHash.salt }, std::string{ passwordHash.hash }) };
if (passwordMatched && passwordHash.bcryptRoundCount != _bcryptRoundCount)
{
LMS_LOG(AUTH, INFO, "Updating password hash for user '" << loginName << "' to match new bcrypt round count: previously " << passwordHash.bcryptRoundCount << " rounds, now " << _bcryptRoundCount << " rounds");
const db::User::PasswordHash updatedPasswordHash{ hashPassword(password) };
{
db::Session& session{ getDbSession() };
auto transaction{ session.createWriteTransaction() };
if (db::User::pointer user{ db::User::find(session, loginName) })
user.modify()->setPasswordHash(updatedPasswordHash);
}
}
return passwordMatched;
} }
bool InternalPasswordService::canSetPasswords() const bool InternalPasswordService::canSetPasswords() const
@@ -96,38 +115,37 @@ namespace lms::auth
{ {
const db::User::PasswordHash passwordHash{ hashPassword(newPassword) }; const db::User::PasswordHash passwordHash{ hashPassword(newPassword) };
db::Session& session{ getDbSession() };
auto transaction{ session.createWriteTransaction() };
db::User::pointer user{ db::User::find(session, userId) };
if (!user)
throw Exception{ "User not found!" };
switch (checkPasswordAcceptability(newPassword, PasswordValidationContext{ user->getLoginName(), user->getType() }))
{ {
case PasswordAcceptabilityResult::OK: db::Session& session{ getDbSession() };
break; auto transaction{ session.createWriteTransaction() };
case PasswordAcceptabilityResult::TooWeak:
throw PasswordTooWeakException{};
case PasswordAcceptabilityResult::MustMatchLoginName:
throw PasswordMustMatchLoginNameException{};
}
user.modify()->setPasswordHash(passwordHash); db::User::pointer user{ db::User::find(session, userId) };
getAuthTokenService().clearAuthTokens(userId); if (!user)
throw Exception{ "User not found!" };
switch (checkPasswordAcceptability(newPassword, PasswordValidationContext{ user->getLoginName(), user->getType() }))
{
case PasswordAcceptabilityResult::OK:
break;
case PasswordAcceptabilityResult::TooWeak:
throw PasswordTooWeakException{};
case PasswordAcceptabilityResult::MustMatchLoginName:
throw PasswordMustMatchLoginNameException{};
}
user.modify()->setPasswordHash(passwordHash);
}
} }
db::User::PasswordHash InternalPasswordService::hashPassword(std::string_view password) const db::User::PasswordHash InternalPasswordService::hashPassword(std::string_view password) const
{ {
const std::string salt{ Wt::WRandom::generateId(32) }; const std::string salt{ Wt::WRandom::generateId(32) };
return { salt, _hashFunc.compute(std::string{ password }, salt) }; return db::User::PasswordHash{ .bcryptRoundCount = _bcryptRoundCount, .salt = salt, .hash = _hashFunc.compute(std::string{ password }, salt) };
} }
void void InternalPasswordService::hashRandomPassword() const
InternalPasswordService::hashRandomPassword() const
{ {
hashPassword(Wt::WRandom::generateId(32)); hashPassword(Wt::WRandom::generateId(32));
} }
} // namespace lms::auth } // namespace lms::auth
@@ -24,17 +24,15 @@
#include "database/User.hpp" #include "database/User.hpp"
#include "LoginThrottler.hpp"
#include "PasswordServiceBase.hpp" #include "PasswordServiceBase.hpp"
#include "services/auth/IPasswordService.hpp"
namespace lms::auth namespace lms::auth
{ {
class IAuthTokenService;
class InternalPasswordService : public PasswordServiceBase class InternalPasswordService : public PasswordServiceBase
{ {
public: public:
InternalPasswordService(db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService); InternalPasswordService(db::Db& db, std::size_t maxThrottlerEntries);
private: private:
bool checkUserPassword(std::string_view loginName, std::string_view password) override; bool checkUserPassword(std::string_view loginName, std::string_view password) override;
@@ -46,8 +44,8 @@ namespace lms::auth
db::User::PasswordHash hashPassword(std::string_view password) const; db::User::PasswordHash hashPassword(std::string_view password) const;
void hashRandomPassword() const; void hashRandomPassword() const;
const Wt::Auth::BCryptHashFunction _hashFunc{ 7 }; // TODO parametrize this const unsigned _bcryptRoundCount;
const Wt::Auth::BCryptHashFunction _hashFunc{ static_cast<int>(_bcryptRoundCount) };
Wt::Auth::PasswordStrengthValidator _validator; Wt::Auth::PasswordStrengthValidator _validator;
}; };
} // namespace lms::auth } // namespace lms::auth
@@ -19,18 +19,21 @@
#pragma once #pragma once
#include <Wt/WDateTime.h> #include <chrono>
#include <boost/asio/ip/address.hpp> #include <memory>
#include <optional> #include <optional>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <Wt/WDateTime.h>
#include <boost/asio/ip/address.hpp>
#include "core/LiteralString.hpp"
#include "database/UserId.hpp" #include "database/UserId.hpp"
namespace lms::db namespace lms::db
{ {
class Db; class Db;
class User;
} // namespace lms::db } // namespace lms::db
namespace lms::auth namespace lms::auth
@@ -40,7 +43,15 @@ namespace lms::auth
public: public:
virtual ~IAuthTokenService() = default; virtual ~IAuthTokenService() = default;
// Auth Token services struct AuthTokenInfo
{
db::UserId userId;
Wt::WDateTime expiry;
Wt::WDateTime lastUsed; // if called by processAuthToken, value is before processing
std::size_t useCount; // if called by processAuthToken, value is before processing
std::optional<std::size_t> maxUseCount;
};
struct AuthTokenProcessResult struct AuthTokenProcessResult
{ {
enum class State enum class State
@@ -50,22 +61,25 @@ namespace lms::auth
Denied, Denied,
}; };
struct AuthTokenInfo
{
db::UserId userId;
Wt::WDateTime expiry;
};
State state{ State::Denied }; State state{ State::Denied };
std::optional<AuthTokenInfo> authTokenInfo{}; std::optional<AuthTokenInfo> authTokenInfo{};
}; };
// Provided token is only accepted once struct DomainParameters
virtual AuthTokenProcessResult processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue) = 0; {
std::optional<std::size_t> tokenMaxUseCount;
std::optional<std::chrono::seconds> tokenDuration;
};
// Returns a one time token virtual void registerDomain(core::LiteralString domain, const DomainParameters& params) = 0;
virtual std::string createAuthToken(db::UserId userid, const Wt::WDateTime& expiry) = 0;
virtual void clearAuthTokens(db::UserId userid) = 0; // Processing an auth token will make its useCount increase by 1. Token is then automatically deleted if its maxUsecount is reached
virtual AuthTokenProcessResult processAuthToken(core::LiteralString domain, const boost::asio::ip::address& clientAddress, std::string_view tokenValue) = 0;
virtual void visitAuthTokens(core::LiteralString domain, db::UserId userid, std::function<void(const AuthTokenInfo& info, std::string_view token)> visitor) = 0;
virtual void createAuthToken(core::LiteralString domain, db::UserId userid, std::string_view token) = 0;
virtual void clearAuthTokens(core::LiteralString domain, db::UserId userid) = 0;
}; };
std::unique_ptr<IAuthTokenService> createAuthTokenService(db::Db& db, std::size_t maxThrottlerEntryCount); std::unique_ptr<IAuthTokenService> createAuthTokenService(db::Db& db, std::size_t maxThrottlerEntryCount);
@@ -58,12 +58,12 @@ namespace lms::auth
}; };
State state{ State::Denied }; State state{ State::Denied };
std::optional<db::UserId> userId{}; db::UserId userId{};
}; };
virtual CheckResult processEnv(const Wt::WEnvironment& env) = 0; virtual CheckResult processEnv(const Wt::WEnvironment& env) = 0;
virtual CheckResult processRequest(const Wt::Http::Request& request) = 0; virtual CheckResult processRequest(const Wt::Http::Request& request) = 0;
}; };
std::unique_ptr<IEnvService> createEnvService(std::string_view backendName, db::Db& db); std::unique_ptr<IEnvService> createEnvService(std::string_view backend, db::Db& db);
} // namespace lms::auth } // namespace lms::auth
@@ -37,8 +37,6 @@ namespace lms::db
namespace lms::auth namespace lms::auth
{ {
class IAuthTokenService;
class IPasswordService class IPasswordService
{ {
public: public:
@@ -53,7 +51,7 @@ namespace lms::auth
Throttled, Throttled,
}; };
State state{ State::Denied }; State state{ State::Denied };
std::optional<db::UserId> userId{}; db::UserId userId{};
std::optional<Wt::WDateTime> expiry{}; std::optional<Wt::WDateTime> expiry{};
}; };
virtual CheckResult checkUserPassword(const boost::asio::ip::address& clientAddress, virtual CheckResult checkUserPassword(const boost::asio::ip::address& clientAddress,
@@ -73,5 +71,5 @@ namespace lms::auth
virtual void setPassword(db::UserId userId, std::string_view newPassword) = 0; virtual void setPassword(db::UserId userId, std::string_view newPassword) = 0;
}; };
std::unique_ptr<IPasswordService> createPasswordService(std::string_view authPasswordBackend, db::Db& db, std::size_t maxThrottlerEntryCount, IAuthTokenService& authTokenService); std::unique_ptr<IPasswordService> createPasswordService(std::string_view backend, db::Db& db, std::size_t maxThrottlerEntryCount);
} // namespace lms::auth } // namespace lms::auth
-1
View File
@@ -30,7 +30,6 @@ add_library(lmssubsonic SHARED
impl/SubsonicId.cpp impl/SubsonicId.cpp
impl/SubsonicResource.cpp impl/SubsonicResource.cpp
impl/SubsonicResponse.cpp impl/SubsonicResponse.cpp
impl/Utils.cpp
) )
target_include_directories(lmssubsonic INTERFACE target_include_directories(lmssubsonic INTERFACE
-3
View File
@@ -27,10 +27,7 @@ namespace lms::api::subsonic
{ {
struct ClientInfo struct ClientInfo
{ {
std::string ipAddress;
std::string name; std::string name;
std::string user;
std::string password;
ProtocolVersion version; ProtocolVersion version;
}; };
} // namespace lms::api::subsonic } // namespace lms::api::subsonic
+2 -1
View File
@@ -41,7 +41,8 @@ namespace lms::api::subsonic
{ {
const Wt::Http::ParameterMap& parameters; const Wt::Http::ParameterMap& parameters;
db::Session& dbSession; db::Session& dbSession;
const db::ObjectPtr<db::User> user; db::ObjectPtr<db::User> user;
std::string clientIpAddr;
ClientInfo clientInfo; ClientInfo clientInfo;
ProtocolVersion serverProtocolVersion; ProtocolVersion serverProtocolVersion;
ResponseFormat responseFormat; ResponseFormat responseFormat;
+84 -71
View File
@@ -33,7 +33,7 @@
#include "database/Db.hpp" #include "database/Db.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/User.hpp" #include "database/User.hpp"
#include "services/auth/IEnvService.hpp" #include "services/auth/IAuthTokenService.hpp"
#include "services/auth/IPasswordService.hpp" #include "services/auth/IPasswordService.hpp"
#include "ParameterParsing.hpp" #include "ParameterParsing.hpp"
@@ -41,7 +41,6 @@
#include "RequestContext.hpp" #include "RequestContext.hpp"
#include "SubsonicId.hpp" #include "SubsonicId.hpp"
#include "SubsonicResponse.hpp" #include "SubsonicResponse.hpp"
#include "Utils.hpp"
#include "entrypoints/AlbumSongLists.hpp" #include "entrypoints/AlbumSongLists.hpp"
#include "entrypoints/Bookmarks.hpp" #include "entrypoints/Bookmarks.hpp"
#include "entrypoints/Browsing.hpp" #include "entrypoints/Browsing.hpp"
@@ -132,9 +131,10 @@ namespace lms::api::subsonic
return res; return res;
} }
void checkUserTypeIsAllowed(RequestContext& context, core::EnumSet<db::UserType> allowedUserTypes) void checkUserTypeIsAllowed(const db::User::pointer& user, core::EnumSet<db::UserType> allowedUserTypes)
{ {
if (!allowedUserTypes.contains(context.user->getType())) assert(user);
if (!allowedUserTypes.contains(user->getType()))
throw UserNotAuthorizedError{}; throw UserNotAuthorizedError{};
} }
@@ -143,20 +143,24 @@ namespace lms::api::subsonic
throw NotImplementedGenericError{}; throw NotImplementedGenericError{};
} }
enum class AuthenticationMode
{
Authenticated,
Unauthenticated,
};
using RequestHandlerFunc = std::function<Response(RequestContext& context)>; using RequestHandlerFunc = std::function<Response(RequestContext& context)>;
using CheckImplementedFunc = std::function<void()>;
struct RequestEntryPointInfo struct RequestEntryPointInfo
{ {
RequestHandlerFunc func; RequestHandlerFunc func;
AuthenticationMode authMode{ AuthenticationMode::Authenticated };
core::EnumSet<db::UserType> allowedUserTypes{ db::UserType::DEMO, db::UserType::REGULAR, db::UserType::ADMIN }; core::EnumSet<db::UserType> allowedUserTypes{ db::UserType::DEMO, db::UserType::REGULAR, db::UserType::ADMIN };
CheckImplementedFunc checkFunc{};
}; };
const std::unordered_map<core::LiteralString, RequestEntryPointInfo, core::LiteralStringHash, core::LiteralStringEqual> requestEntryPoints{ const std::unordered_map<core::LiteralString, RequestEntryPointInfo, core::LiteralStringHash, core::LiteralStringEqual> requestEntryPoints{
// System // System
{ "/ping", { handlePingRequest } }, { "/ping", { handlePingRequest } },
{ "/getLicense", { handleGetLicenseRequest } }, { "/getLicense", { handleGetLicenseRequest } },
{ "/getOpenSubsonicExtensions", { handleGetOpenSubsonicExtensions } }, { "/getOpenSubsonicExtensions", { handleGetOpenSubsonicExtensions, AuthenticationMode::Unauthenticated } },
// Browsing // Browsing
{ "/getMusicFolders", { handleGetMusicFoldersRequest } }, { "/getMusicFolders", { handleGetMusicFoldersRequest } },
@@ -240,11 +244,11 @@ namespace lms::api::subsonic
// User management // User management
{ "/getUser", { handleGetUserRequest } }, { "/getUser", { handleGetUserRequest } },
{ "/getUsers", { handleGetUsersRequest, { db::UserType::ADMIN } } }, { "/getUsers", { handleGetUsersRequest, AuthenticationMode::Authenticated, { db::UserType::ADMIN } } },
{ "/createUser", { handleCreateUserRequest, { db::UserType::ADMIN }, &utils::checkSetPasswordImplemented } }, { "/createUser", { handleNotImplemented } },
{ "/updateUser", { handleUpdateUserRequest, { db::UserType::ADMIN } } }, { "/updateUser", { handleNotImplemented } },
{ "/deleteUser", { handleDeleteUserRequest, { db::UserType::ADMIN } } }, { "/deleteUser", { handleNotImplemented } },
{ "/changePassword", { handleChangePassword, { db::UserType::REGULAR, db::UserType::ADMIN }, &utils::checkSetPasswordImplemented } }, { "/changePassword", { handleNotImplemented } },
// Bookmarks // Bookmarks
{ "/getBookmarks", { handleGetBookmarks } }, { "/getBookmarks", { handleGetBookmarks } },
@@ -255,7 +259,7 @@ namespace lms::api::subsonic
// Media library scanning // Media library scanning
{ "/getScanStatus", { Scan::handleGetScanStatus } }, { "/getScanStatus", { Scan::handleGetScanStatus } },
{ "/startScan", { Scan::handleStartScan, { db::UserType::ADMIN } } }, { "/startScan", { Scan::handleStartScan, AuthenticationMode::Authenticated, { db::UserType::ADMIN } } },
}; };
using MediaRetrievalHandlerFunc = std::function<void(RequestContext&, const Wt::Http::Request&, Wt::Http::Response&)>; using MediaRetrievalHandlerFunc = std::function<void(RequestContext&, const Wt::Http::Request&, Wt::Http::Response&)>;
@@ -278,12 +282,23 @@ namespace lms::api::subsonic
TLSMonotonicMemoryResourceCleaner(const TLSMonotonicMemoryResourceCleaner&) = delete; TLSMonotonicMemoryResourceCleaner(const TLSMonotonicMemoryResourceCleaner&) = delete;
TLSMonotonicMemoryResourceCleaner& operator=(const TLSMonotonicMemoryResourceCleaner&) = delete; TLSMonotonicMemoryResourceCleaner& operator=(const TLSMonotonicMemoryResourceCleaner&) = delete;
}; };
db::User::pointer getUserFromUserId(db::Session& session, db::UserId userId)
{
auto transaction{ session.createReadTransaction() };
if (db::User::pointer user{ db::User::find(session, userId) })
return user;
throw UserNotAuthorizedError{};
}
} // namespace } // namespace
SubsonicResource::SubsonicResource(db::Db& db) SubsonicResource::SubsonicResource(db::Db& db)
: _serverProtocolVersionsByClient{ readConfigProtocolVersions() } : _serverProtocolVersionsByClient{ readConfigProtocolVersions() }
, _openSubsonicDisabledClients{ readOpenSubsonicDisabledClients() } , _openSubsonicDisabledClients{ readOpenSubsonicDisabledClients() }
, _defaultReleaseCoverClients{ readDefaultCoverClients() } , _defaultReleaseCoverClients{ readDefaultCoverClients() }
, _supportUserPasswordAuthentication{ core::Service<core::IConfig>::get()->getBool("api-subsonic-support-user-password-auth", true) }
, _db{ db } , _db{ db }
{ {
} }
@@ -317,10 +332,11 @@ namespace lms::api::subsonic
{ {
LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itEntryPoint->first); LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itEntryPoint->first);
if (itEntryPoint->second.checkFunc) if (itEntryPoint->second.authMode == AuthenticationMode::Authenticated)
itEntryPoint->second.checkFunc(); {
requestContext.user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request));
checkUserTypeIsAllowed(requestContext, itEntryPoint->second.allowedUserTypes); checkUserTypeIsAllowed(requestContext.user, itEntryPoint->second.allowedUserTypes);
}
const Response resp{ [&] { const Response resp{ [&] {
LMS_SCOPED_TRACE_DETAILED("Subsonic", "HandleRequest"); LMS_SCOPED_TRACE_DETAILED("Subsonic", "HandleRequest");
@@ -343,11 +359,19 @@ namespace lms::api::subsonic
{ {
LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itStreamHandler->first); LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itStreamHandler->first);
// Media retrieval endpoints are always authenticated
// Optim: no need to reauth user for each continuation
if (!request.continuation())
requestContext.user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request));
itStreamHandler->second(requestContext, request, response); itStreamHandler->second(requestContext, request, response);
LMS_LOG(API_SUBSONIC, DEBUG, "Request " << requestId << " '" << requestPath << "' handled!"); LMS_LOG(API_SUBSONIC, DEBUG, "Request " << requestId << " '" << requestPath << "' handled!");
return; return;
} }
// do not disclose unhandled commands for unauthenticated users
authenticateUser(request);
LMS_LOG(API_SUBSONIC, ERROR, "Unhandled command '" << requestPath << "'"); LMS_LOG(API_SUBSONIC, ERROR, "Unhandled command '" << requestPath << "'");
throw UnknownEntryPointGenericError{}; throw UnknownEntryPointGenericError{};
} }
@@ -391,16 +415,9 @@ namespace lms::api::subsonic
const auto& parameters{ request.getParameterMap() }; const auto& parameters{ request.getParameterMap() };
ClientInfo res; ClientInfo res;
if (hasParameter(parameters, "t"))
throw TokenAuthenticationNotSupportedForLDAPUsersError{};
res.ipAddress = request.clientAddress();
// Mandatory parameters // Mandatory parameters
res.name = getMandatoryParameterAs<std::string>(parameters, "c"); res.name = getMandatoryParameterAs<std::string>(parameters, "c");
res.version = getMandatoryParameterAs<ProtocolVersion>(parameters, "v"); res.version = getMandatoryParameterAs<ProtocolVersion>(parameters, "v");
res.user = getMandatoryParameterAs<std::string>(parameters, "u");
res.password = decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(parameters, "p"));
return res; return res;
} }
@@ -409,25 +426,15 @@ namespace lms::api::subsonic
{ {
const Wt::Http::ParameterMap& parameters{ request.getParameterMap() }; const Wt::Http::ParameterMap& parameters{ request.getParameterMap() };
const ClientInfo clientInfo{ getClientInfo(request) }; const ClientInfo clientInfo{ getClientInfo(request) };
const db::UserId userId{ authenticateUser(request, clientInfo) };
bool enableOpenSubsonic{ !_openSubsonicDisabledClients.contains(clientInfo.name) }; bool enableOpenSubsonic{ !_openSubsonicDisabledClients.contains(clientInfo.name) };
bool enableDefaultCover{ _defaultReleaseCoverClients.contains(clientInfo.name) }; bool enableDefaultCover{ _defaultReleaseCoverClients.contains(clientInfo.name) };
const ResponseFormat format{ getParameterAs<std::string>(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml }; const ResponseFormat format{ getParameterAs<std::string>(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml };
db::User::pointer user;
{
db::Session& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() };
user = db::User::find(session, userId);
if (!user)
throw UserNotAuthorizedError{};
}
return RequestContext{ return RequestContext{
.parameters = parameters, .parameters = parameters,
.dbSession = _db.getTLSSession(), .dbSession = _db.getTLSSession(),
.user = user, .user = db::User::pointer{},
.clientIpAddr = request.clientAddress(),
.clientInfo = clientInfo, .clientInfo = clientInfo,
.serverProtocolVersion = getServerProtocolVersion(clientInfo.name), .serverProtocolVersion = getServerProtocolVersion(clientInfo.name),
.responseFormat = format, .responseFormat = format,
@@ -436,46 +443,52 @@ namespace lms::api::subsonic
}; };
} }
db::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo) db::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request)
{ {
// if the request if a continuation, the user is already authenticated const auto& parameters{ request.getParameterMap() };
if (request.continuation())
if (hasParameter(parameters, "t"))
throw TokenAuthenticationNotSupportedForLDAPUsersError{};
const auto user{ getParameterAs<std::string>(parameters, "u") };
const auto password{ getParameterAs<std::string>(parameters, "p") };
if (!_supportUserPasswordAuthentication && (password || user))
throw ProvidedAuthenticationMechanismNotSupportedError{};
const auto apiKey{ getParameterAs<std::string>(parameters, "apiKey") };
if (user && !password)
throw RequiredParameterMissingError{ "p" };
if (!user && password)
throw RequiredParameterMissingError{ "u" };
if (apiKey && password)
throw MultipleConflictingAuthenticationMechanismsProvidedError{};
if (!apiKey && !password)
throw RequiredParameterMissingError{ "apiKey" };
const auto clientAddress{ boost::asio::ip::address::from_string(request.clientAddress()) };
const std::string authToken{ apiKey ? *apiKey : decodePasswordIfNeeded(*password) };
const auto authResult{ core::Service<auth::IAuthTokenService>::get()->processAuthToken("subsonic", clientAddress, authToken) };
switch (authResult.state)
{ {
db::Session& session{ _db.getTLSSession() }; case auth::IAuthTokenService::AuthTokenProcessResult::State::Granted:
auto transaction{ session.createReadTransaction() }; if (user)
const auto user{ db::User::find(session, clientInfo.user) };
if (!user)
throw UserNotAuthorizedError{};
return user->getId();
}
if (auto* authEnvService{ core::Service<auth::IEnvService>::get() })
{
const auto checkResult{ authEnvService->processRequest(request) };
if (checkResult.state != auth::IEnvService::CheckResult::State::Granted)
throw UserNotAuthorizedError{};
return *checkResult.userId;
}
else if (auto* authPasswordService{ core::Service<auth::IPasswordService>::get() })
{
const auto checkResult{ authPasswordService->checkUserPassword(boost::asio::ip::address::from_string(request.clientAddress()), clientInfo.user, clientInfo.password) };
switch (checkResult.state)
{ {
case auth::IPasswordService::CheckResult::State::Granted: const auto authenticatedUser{ getUserFromUserId(_db.getTLSSession(), authResult.authTokenInfo->userId) };
return *checkResult.userId; if (!authenticatedUser || authenticatedUser->getLoginName() != *user)
break; throw WrongUsernameOrPasswordError{};
case auth::IPasswordService::CheckResult::State::Denied:
throw WrongUsernameOrPasswordError{};
case auth::IPasswordService::CheckResult::State::Throttled:
throw LoginThrottledGenericError{};
} }
return authResult.authTokenInfo->userId;
case auth::IAuthTokenService::AuthTokenProcessResult::State::Denied:
if (apiKey)
throw InvalidAPIkeyError{};
else
throw WrongUsernameOrPasswordError{};
case auth::IAuthTokenService::AuthTokenProcessResult::State::Throttled:
throw LoginThrottledGenericError{};
} }
throw InternalErrorGenericError{ "No service available to authenticate user" }; throw InternalErrorGenericError{ "Cannot authenticate user" };
} }
} // namespace lms::api::subsonic } // namespace lms::api::subsonic
+2 -1
View File
@@ -50,11 +50,12 @@ namespace lms::api::subsonic
static void checkProtocolVersion(ProtocolVersion client, ProtocolVersion server); static void checkProtocolVersion(ProtocolVersion client, ProtocolVersion server);
ClientInfo getClientInfo(const Wt::Http::Request& request); ClientInfo getClientInfo(const Wt::Http::Request& request);
RequestContext buildRequestContext(const Wt::Http::Request& request); RequestContext buildRequestContext(const Wt::Http::Request& request);
db::UserId authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo); db::UserId authenticateUser(const Wt::Http::Request& request);
const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient; const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient;
const std::unordered_set<std::string> _openSubsonicDisabledClients; const std::unordered_set<std::string> _openSubsonicDisabledClients;
const std::unordered_set<std::string> _defaultReleaseCoverClients; const std::unordered_set<std::string> _defaultReleaseCoverClients;
const bool _supportUserPasswordAuthentication;
db::Db& _db; db::Db& _db;
}; };
+44 -22
View File
@@ -33,7 +33,7 @@
namespace lms::api::subsonic namespace lms::api::subsonic
{ {
// Max count expected from all API methods that expose a count // Max count expected from all API methods that expose a count
static inline constexpr std::size_t defaultMaxCountSize{ 1000 }; static inline constexpr std::size_t defaultMaxCountSize{ 1'000 };
enum class ResponseFormat enum class ResponseFormat
{ {
@@ -54,6 +54,9 @@ namespace lms::api::subsonic
ServerMustUpgrade = 30, ServerMustUpgrade = 30,
WrongUsernameOrPassword = 40, WrongUsernameOrPassword = 40,
TokenAuthenticationNotSupportedForLDAPUsers = 41, TokenAuthenticationNotSupportedForLDAPUsers = 41,
ProvidedAuthenticationMechanismNotSupported = 42,
MultipleConflictingAuthenticationMechanismsProvided = 43,
InvalidAPIkey = 44,
UserNotAuthorized = 50, UserNotAuthorized = 50,
RequestedDataNotFound = 70, RequestedDataNotFound = 70,
}; };
@@ -130,6 +133,45 @@ namespace lms::api::subsonic
std::string getMessage() const override { return "Token authentication not supported for LDAP users."; } std::string getMessage() const override { return "Token authentication not supported for LDAP users."; }
}; };
class ProvidedAuthenticationMechanismNotSupportedError : public Error
{
public:
ProvidedAuthenticationMechanismNotSupportedError()
: Error{ Code::ProvidedAuthenticationMechanismNotSupported } {}
private:
std::string getMessage() const override
{
return "Provided authentication mechanism not supported.";
}
};
class MultipleConflictingAuthenticationMechanismsProvidedError : public Error
{
public:
MultipleConflictingAuthenticationMechanismsProvidedError()
: Error{ Code::MultipleConflictingAuthenticationMechanismsProvided } {}
private:
std::string getMessage() const override
{
return "Multiple conflicting authentication mechanisms provided.";
}
};
class InvalidAPIkeyError : public Error
{
public:
InvalidAPIkeyError()
: Error{ Code::InvalidAPIkey } {}
private:
std::string getMessage() const override
{
return "Invalid API key.";
}
};
class UserNotAuthorizedError : public Error class UserNotAuthorizedError : public Error
{ {
public: public:
@@ -153,7 +195,7 @@ namespace lms::api::subsonic
class InternalErrorGenericError : public GenericError class InternalErrorGenericError : public GenericError
{ {
public: public:
InternalErrorGenericError(const std::string& message) InternalErrorGenericError(std::string_view message)
: _message{ message } {} : _message{ message } {}
private: private:
@@ -176,26 +218,6 @@ namespace lms::api::subsonic
std::string getMessage() const override { return "Unknown API method"; } std::string getMessage() const override { return "Unknown API method"; }
}; };
class PasswordTooWeakGenericError : public GenericError
{
std::string getMessage() const override { return "Password too weak"; }
};
class PasswordMustMatchLoginNameGenericError : public GenericError
{
std::string getMessage() const override { return "Password must match login name"; }
};
class DemoUserCannotChangePasswordGenericError : public GenericError
{
std::string getMessage() const override { return "Demo user cannot change its password"; }
};
class UserAlreadyExistsGenericError : public GenericError
{
std::string getMessage() const override { return "User already exists"; }
};
class BadParameterGenericError : public GenericError class BadParameterGenericError : public GenericError
{ {
public: public:
-42
View File
@@ -1,42 +0,0 @@
/*
* Copyright (C) 2023 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Utils.hpp"
#include "core/Service.hpp"
#include "core/String.hpp"
#include "services/auth/IPasswordService.hpp"
#include "SubsonicResponse.hpp"
namespace lms::api::subsonic::utils
{
void checkSetPasswordImplemented()
{
auth::IPasswordService* passwordService{ core::Service<auth::IPasswordService>::get() };
if (!passwordService || !passwordService->canSetPasswords())
throw NotImplementedGenericError{};
}
std::string makeNameFilesystemCompatible(std::string_view name)
{
return core::stringUtils::replaceInString(name, "/", "_");
}
} // namespace lms::api::subsonic::utils
-29
View File
@@ -1,29 +0,0 @@
/*
* Copyright (C) 2023 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <string>
#include <string_view>
namespace lms::api::subsonic::utils
{
void checkSetPasswordImplemented();
std::string makeNameFilesystemCompatible(std::string_view name);
} // namespace lms::api::subsonic::utils
@@ -36,7 +36,6 @@
#include "ParameterParsing.hpp" #include "ParameterParsing.hpp"
#include "SubsonicId.hpp" #include "SubsonicId.hpp"
#include "Utils.hpp"
#include "responses/Album.hpp" #include "responses/Album.hpp"
#include "responses/AlbumInfo.hpp" #include "responses/AlbumInfo.hpp"
#include "responses/Artist.hpp" #include "responses/Artist.hpp"
@@ -54,7 +54,7 @@ namespace lms::api::subsonic
{ {
std::string clientAddress; std::string clientAddress;
std::string clientName; std::string clientName;
std::string userName; UserId user;
MediaLibraryId library; MediaLibraryId library;
std::size_t offset{}; std::size_t offset{};
auto operator<=>(const ScanInfo&) const = default; auto operator<=>(const ScanInfo&) const = default;
@@ -177,9 +177,9 @@ namespace lms::api::subsonic
else else
{ {
ScanTracker<ArtistId>::ScanInfo scanInfo{ ScanTracker<ArtistId>::ScanInfo scanInfo{
.clientAddress = context.clientInfo.ipAddress, .clientAddress = context.clientIpAddr,
.clientName = context.clientInfo.name, .clientName = context.clientInfo.name,
.userName = context.clientInfo.user, .user = context.user->getId(),
.library = mediaLibrary, .library = mediaLibrary,
.offset = artistOffset .offset = artistOffset
}; };
@@ -241,9 +241,9 @@ namespace lms::api::subsonic
else else
{ {
ScanTracker<ReleaseId>::ScanInfo scanInfo{ ScanTracker<ReleaseId>::ScanInfo scanInfo{
.clientAddress = context.clientInfo.ipAddress, .clientAddress = context.clientIpAddr,
.clientName = context.clientInfo.name, .clientName = context.clientInfo.name,
.userName = context.clientInfo.user, .user = context.user->getId(),
.library = mediaLibrary, .library = mediaLibrary,
.offset = albumOffset .offset = albumOffset
}; };
@@ -305,9 +305,9 @@ namespace lms::api::subsonic
else else
{ {
ScanTracker<TrackId>::ScanInfo scanInfo{ ScanTracker<TrackId>::ScanInfo scanInfo{
.clientAddress = context.clientInfo.ipAddress, .clientAddress = context.clientIpAddr,
.clientName = context.clientInfo.name, .clientName = context.clientInfo.name,
.userName = context.clientInfo.user, .user = context.user->getId(),
.library = mediaLibrary, .library = mediaLibrary,
.offset = songOffset .offset = songOffset
}; };
@@ -41,6 +41,12 @@ namespace lms::api::subsonic
songLyricsNode.addArrayValue("versions", 1); songLyricsNode.addArrayValue("versions", 1);
} }
{
Response::Node& apiKeyAuthentication{ response.createArrayNode("openSubsonicExtensions") };
apiKeyAuthentication.setAttribute("name", "apiKeyAuthentication");
apiKeyAuthentication.addArrayValue("versions", 1);
}
return response; return response;
}; };
} // namespace lms::api::subsonic } // namespace lms::api::subsonic
@@ -6,7 +6,6 @@
#include "services/auth/IPasswordService.hpp" #include "services/auth/IPasswordService.hpp"
#include "ParameterParsing.hpp" #include "ParameterParsing.hpp"
#include "Utils.hpp"
#include "responses/User.hpp" #include "responses/User.hpp"
namespace lms::api::subsonic namespace lms::api::subsonic
@@ -52,150 +51,4 @@ namespace lms::api::subsonic
return response; return response;
} }
Response handleCreateUserRequest(RequestContext& context)
{
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
std::string password{ decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password")) };
// Just ignore all the other fields as we don't handle them
db::UserId userId;
{
auto transaction{ context.dbSession.createWriteTransaction() };
User::pointer user{ User::find(context.dbSession, username) };
if (user)
throw UserAlreadyExistsGenericError{};
user = context.dbSession.create<User>(username);
userId = user->getId();
}
auto removeCreatedUser{ [&] {
auto transaction{ context.dbSession.createWriteTransaction() };
User::pointer user{ User::find(context.dbSession, userId) };
if (user)
user.remove();
} };
try
{
core::Service<auth::IPasswordService>::get()->setPassword(userId, password);
}
catch (const auth::PasswordMustMatchLoginNameException&)
{
removeCreatedUser();
throw PasswordMustMatchLoginNameGenericError{};
}
catch (const auth::PasswordTooWeakException&)
{
removeCreatedUser();
throw PasswordTooWeakGenericError{};
}
catch (const auth::Exception& exception)
{
removeCreatedUser();
throw UserNotAuthorizedError{};
}
return Response::createOkResponse(context.serverProtocolVersion);
}
Response handleDeleteUserRequest(RequestContext& context)
{
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
auto transaction{ context.dbSession.createWriteTransaction() };
User::pointer user{ User::find(context.dbSession, username) };
if (!user)
throw RequestedDataNotFoundError{};
// cannot delete ourself
if (user->getId() == context.user->getId())
throw UserNotAuthorizedError{};
user.remove();
return Response::createOkResponse(context.serverProtocolVersion);
}
Response handleUpdateUserRequest(RequestContext& context)
{
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
std::optional<std::string> password{ getParameterAs<std::string>(context.parameters, "password") };
UserId userId;
{
auto transaction{ context.dbSession.createReadTransaction() };
User::pointer user{ User::find(context.dbSession, username) };
if (!user)
throw RequestedDataNotFoundError{};
userId = user->getId();
}
if (password)
{
utils::checkSetPasswordImplemented();
try
{
core::Service<auth::IPasswordService>()->setPassword(userId, decodePasswordIfNeeded(*password));
}
catch (const auth::PasswordMustMatchLoginNameException&)
{
throw PasswordMustMatchLoginNameGenericError{};
}
catch (const auth::PasswordTooWeakException&)
{
throw PasswordTooWeakGenericError{};
}
catch (const auth::Exception&)
{
throw UserNotAuthorizedError{};
}
}
return Response::createOkResponse(context.serverProtocolVersion);
}
Response handleChangePassword(RequestContext& context)
{
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
std::string password{ decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password")) };
try
{
db::UserId userId;
{
auto transaction{ context.dbSession.createReadTransaction() };
checkUserIsMySelfOrAdmin(context, username);
User::pointer user{ User::find(context.dbSession, username) };
if (!user)
throw UserNotAuthorizedError{};
userId = user->getId();
}
core::Service<auth::IPasswordService>::get()->setPassword(userId, password);
}
catch (const auth::PasswordMustMatchLoginNameException&)
{
throw PasswordMustMatchLoginNameGenericError{};
}
catch (const auth::PasswordTooWeakException&)
{
throw PasswordTooWeakGenericError{};
}
catch (const auth::Exception& authException)
{
throw UserNotAuthorizedError{};
}
return Response::createOkResponse(context.serverProtocolVersion);
}
} // namespace lms::api::subsonic } // namespace lms::api::subsonic
@@ -38,7 +38,6 @@
#include "RequestContext.hpp" #include "RequestContext.hpp"
#include "SubsonicId.hpp" #include "SubsonicId.hpp"
#include "Utils.hpp"
#include "responses/Artist.hpp" #include "responses/Artist.hpp"
#include "responses/Contributor.hpp" #include "responses/Contributor.hpp"
#include "responses/ItemGenre.hpp" #include "responses/ItemGenre.hpp"
+40 -13
View File
@@ -45,6 +45,7 @@
#include "services/scanner/IScannerService.hpp" #include "services/scanner/IScannerService.hpp"
#include "services/scrobbling/IScrobblingService.hpp" #include "services/scrobbling/IScrobblingService.hpp"
#include "subsonic/SubsonicResource.hpp" #include "subsonic/SubsonicResource.hpp"
#include "ui/Auth.hpp"
#include "ui/LmsApplication.hpp" #include "ui/LmsApplication.hpp"
#include "ui/LmsApplicationManager.hpp" #include "ui/LmsApplicationManager.hpp"
#include "ui/LmsInitApplication.hpp" #include "ui/LmsInitApplication.hpp"
@@ -79,6 +80,19 @@ namespace lms
throw core::LmsException{ "Invalid config value for 'log-min-severity'" }; throw core::LmsException{ "Invalid config value for 'log-min-severity'" };
} }
ui::AuthenticationBackend getUIAuthenticationBackend()
{
const std::string backend{ core::stringUtils::stringToLower(core::Service<core::IConfig>::get()->getString("authentication-backend", "internal")) };
if (backend == "internal")
return ui::AuthenticationBackend::Internal;
if (backend == "pam")
return ui::AuthenticationBackend::PAM;
if (backend == "http-headers")
return ui::AuthenticationBackend::Env;
throw core::LmsException{ "Invalid config value for 'authentication-backend'" };
}
std::optional<core::tracing::Level> getTracingLevel() std::optional<core::tracing::Level> getTracingLevel()
{ {
std::string_view tracingLevel{ core::Service<core::IConfig>::get()->getString("tracing-level", "disabled") }; std::string_view tracingLevel{ core::Service<core::IConfig>::get()->getString("tracing-level", "disabled") };
@@ -320,24 +334,37 @@ namespace lms
ui::LmsApplicationManager appManager; ui::LmsApplicationManager appManager;
const std::size_t loginThrottlerMaxEntries{ config->getULong("login-throttler-max-entries", 10'000) };
// Service initialization order is important (reverse-order for deinit) // Service initialization order is important (reverse-order for deinit)
core::Service<core::IChildProcessManager> childProcessManagerService{ core::createChildProcessManager(ioContext) }; core::Service<core::IChildProcessManager> childProcessManagerService{ core::createChildProcessManager(ioContext) };
core::Service<auth::IAuthTokenService> authTokenService;
const ui::AuthenticationBackend uiAuthenticationBackend{ getUIAuthenticationBackend() };
core::Service<auth::IAuthTokenService> authTokenService{ auth::createAuthTokenService(database, config->getULong("login-throttler-max-entriees", 10'000)) };
core::Service<auth::IPasswordService> authPasswordService; core::Service<auth::IPasswordService> authPasswordService;
core::Service<auth::IEnvService> authEnvService; core::Service<auth::IEnvService> authEnvService;
const std::string authenticationBackend{ core::stringUtils::stringToLower(config->getString("authentication-backend", "internal")) }; authTokenService->registerDomain("ui", auth::IAuthTokenService::DomainParameters{
if (authenticationBackend == "internal" || authenticationBackend == "pam") .tokenMaxUseCount = 1,
.tokenDuration = std::chrono::weeks{ 8 },
});
authTokenService->registerDomain("subsonic", auth::IAuthTokenService::DomainParameters{
.tokenMaxUseCount = std::nullopt, // no usage limit
.tokenDuration = std::nullopt, // no time limit
});
switch (uiAuthenticationBackend)
{ {
authTokenService.assign(auth::createAuthTokenService(database, config->getULong("login-throttler-max-entriees", 10000))); case ui::AuthenticationBackend::Internal:
authPasswordService.assign(auth::createPasswordService(authenticationBackend, database, config->getULong("login-throttler-max-entriees", 10000), *authTokenService.get())); authPasswordService.assign(auth::createPasswordService("internal", database, loginThrottlerMaxEntries));
break;
case ui::AuthenticationBackend::PAM:
authPasswordService.assign(auth::createPasswordService("PAM", database, loginThrottlerMaxEntries));
break;
case ui::AuthenticationBackend::Env:
authEnvService.assign(auth::createEnvService("http-headers", database));
break;
} }
else if (authenticationBackend == "http-headers")
{
authEnvService.assign(auth::createEnvService(authenticationBackend, database));
}
else
throw core::LmsException{ "Bad value '" + authenticationBackend + "' for 'authentication-backend'" };
image::init(argv[0]); image::init(argv[0]);
core::Service<cover::IArtworkService> artworkService{ cover::createArtworkService(database, server.appRoot() + "/images/unknown-cover.svg", server.appRoot() + "/images/unknown-artist.svg") }; core::Service<cover::IArtworkService> artworkService{ cover::createArtworkService(database, server.appRoot() + "/images/unknown-cover.svg", server.appRoot() + "/images/unknown-artist.svg") };
@@ -370,8 +397,8 @@ namespace lms
// bind UI entry point // bind UI entry point
server.addEntryPoint(Wt::EntryPointType::Application, server.addEntryPoint(Wt::EntryPointType::Application,
[&](const Wt::WEnvironment& env) { [&database, &appManager, uiAuthenticationBackend](const Wt::WEnvironment& env) {
return ui::LmsApplication::create(env, database, appManager); return ui::LmsApplication::create(env, database, appManager, uiAuthenticationBackend);
}); });
proxyScannerEventsToApplication(*scannerService, server); proxyScannerEventsToApplication(*scannerService, server);
+32 -16
View File
@@ -19,6 +19,8 @@
#include "Auth.hpp" #include "Auth.hpp"
#include <Wt/Auth/HashFunction.h>
#include <Wt/WCheckBox.h> #include <Wt/WCheckBox.h>
#include <Wt/WEnvironment.h> #include <Wt/WEnvironment.h>
#include <Wt/WFormModel.h> #include <Wt/WFormModel.h>
@@ -42,14 +44,18 @@ namespace lms::ui
{ {
namespace namespace
{ {
static constexpr core::LiteralString authTokenDomain{ "ui" };
static const std::string authCookieName{ "LmsAuth" }; static const std::string authCookieName{ "LmsAuth" };
static const std::string authCookieSalt{ Wt::Auth::SHA1HashFunction{}.compute(authCookieName, authTokenDomain.c_str()) }; // changing this will invalidate existing tokens
void createAuthToken(db::UserId userId, const Wt::WDateTime& expiry) void createAuthToken(db::UserId userId, const Wt::WDateTime& expiry)
{ {
const std::string secret{ core::Service<auth::IAuthTokenService>::get()->createAuthToken(userId, expiry) }; const std::string authCookie{ Wt::WRandom::generateId(64) };
const std::string hashedAuthCookie{ Wt::Auth::SHA1HashFunction{}.compute(authCookie, authCookieSalt) };
core::Service<auth::IAuthTokenService>::get()->createAuthToken(authTokenDomain, userId, hashedAuthCookie);
LmsApp->setCookie(authCookieName, LmsApp->setCookie(authCookieName,
secret, authCookie,
expiry.toTime_t() - Wt::WDateTime::currentDateTime().toTime_t(), expiry.toTime_t() - Wt::WDateTime::currentDateTime().toTime_t(),
"", "",
"", "",
@@ -64,7 +70,8 @@ namespace lms::ui
static const Field PasswordField; static const Field PasswordField;
static const Field RememberMeField; static const Field RememberMeField;
AuthModel() AuthModel(auth::IPasswordService& passwordService)
: _passwordService{ passwordService }
{ {
addField(LoginNameField); addField(LoginNameField);
addField(PasswordField); addField(PasswordField);
@@ -90,8 +97,7 @@ namespace lms::ui
if (Wt::asNumber(value(RememberMeField))) if (Wt::asNumber(value(RememberMeField)))
{ {
const Wt::WDateTime now{ Wt::WDateTime::currentDateTime() }; const Wt::WDateTime now{ Wt::WDateTime::currentDateTime() };
createAuthToken(_userId, isDemo ? now.addDays(3) : now.addYears(1));
createAuthToken(*_userId, isDemo ? now.addDays(3) : now.addYears(1));
} }
} }
@@ -101,14 +107,14 @@ namespace lms::ui
if (field == PasswordField) if (field == PasswordField)
{ {
const auto checkResult{ core::Service<auth::IPasswordService>::get()->checkUserPassword( const auto checkResult{ _passwordService.checkUserPassword(
boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()), boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()),
valueText(LoginNameField).toUTF8(), valueText(LoginNameField).toUTF8(),
valueText(PasswordField).toUTF8()) }; valueText(PasswordField).toUTF8()) };
switch (checkResult.state) switch (checkResult.state)
{ {
case auth::IPasswordService::CheckResult::State::Granted: case auth::IPasswordService::CheckResult::State::Granted:
_userId = *checkResult.userId; _userId = checkResult.userId;
break; break;
case auth::IPasswordService::CheckResult::State::Denied: case auth::IPasswordService::CheckResult::State::Denied:
error = Wt::WString::tr("Lms.password-bad-login-combination"); error = Wt::WString::tr("Lms.password-bad-login-combination");
@@ -128,10 +134,11 @@ namespace lms::ui
return (validation(field).state() == Wt::ValidationState::Valid); return (validation(field).state() == Wt::ValidationState::Valid);
} }
std::optional<db::UserId> getUserId() const { return _userId; } db::UserId getUserId() const { return _userId; }
private: private:
std::optional<db::UserId> _userId; db::UserId _userId;
auth::IPasswordService& _passwordService;
}; };
const AuthModel::Field AuthModel::LoginNameField{ "login-name" }; const AuthModel::Field AuthModel::LoginNameField{ "login-name" };
@@ -139,21 +146,24 @@ namespace lms::ui
const AuthModel::Field AuthModel::RememberMeField{ "remember-me" }; const AuthModel::Field AuthModel::RememberMeField{ "remember-me" };
} // namespace } // namespace
std::optional<db::UserId> processAuthToken(const Wt::WEnvironment& env) db::UserId processAuthToken(const Wt::WEnvironment& env)
{ {
const std::string* authCookie{ env.getCookie(authCookieName) }; const std::string* authCookie{ env.getCookie(authCookieName) };
if (!authCookie) if (!authCookie)
return std::nullopt; return db::UserId{};
const auto res{ core::Service<auth::IAuthTokenService>::get()->processAuthToken(boost::asio::ip::address::from_string(env.clientAddress()), *authCookie) }; const std::string hashedCookie{ Wt::Auth::SHA1HashFunction{}.compute(*authCookie, authCookieSalt) };
const auto res{ core::Service<auth::IAuthTokenService>::get()->processAuthToken(authTokenDomain, boost::asio::ip::address::from_string(env.clientAddress()), hashedCookie) };
switch (res.state) switch (res.state)
{ {
case auth::IAuthTokenService::AuthTokenProcessResult::State::Denied: case auth::IAuthTokenService::AuthTokenProcessResult::State::Denied:
case auth::IAuthTokenService::AuthTokenProcessResult::State::Throttled: case auth::IAuthTokenService::AuthTokenProcessResult::State::Throttled:
LmsApp->setCookie(authCookieName, std::string{}, 0, "", "", env.urlScheme() == "https"); LmsApp->setCookie(authCookieName, std::string{}, 0, "", "", env.urlScheme() == "https");
return std::nullopt; return db::UserId{};
case auth::IAuthTokenService::AuthTokenProcessResult::State::Granted: case auth::IAuthTokenService::AuthTokenProcessResult::State::Granted:
assert(res.authTokenInfo->maxUseCount && res.authTokenInfo->maxUseCount.value() == 1); // single-use token
createAuthToken(res.authTokenInfo->userId, res.authTokenInfo->expiry); createAuthToken(res.authTokenInfo->userId, res.authTokenInfo->expiry);
break; break;
} }
@@ -161,10 +171,15 @@ namespace lms::ui
return res.authTokenInfo->userId; return res.authTokenInfo->userId;
} }
Auth::Auth() void clearAuthTokens(db::UserId userId)
{
core::Service<auth::IAuthTokenService>::get()->clearAuthTokens(authTokenDomain, userId);
}
PasswordAuth::PasswordAuth(auth::IPasswordService& passwordService)
: Wt::WTemplateFormView{ Wt::WString::tr("Lms.Auth.template") } : Wt::WTemplateFormView{ Wt::WString::tr("Lms.Auth.template") }
{ {
auto model{ std::make_shared<AuthModel>() }; auto model{ std::make_shared<AuthModel>(passwordService) };
auto processAuth = [this, model] { auto processAuth = [this, model] {
updateModel(model.get()); updateModel(model.get());
@@ -172,7 +187,8 @@ namespace lms::ui
if (model->validate()) if (model->validate())
{ {
model->saveData(); model->saveData();
userLoggedIn.emit(*model->getUserId()); assert(model->getUserId().isValid());
userLoggedIn.emit(model->getUserId());
} }
else else
updateView(model.get()); updateView(model.get());
+15 -5
View File
@@ -19,20 +19,30 @@
#pragma once #pragma once
#include <optional>
#include <Wt/WTemplateFormView.h> #include <Wt/WTemplateFormView.h>
#include "database/UserId.hpp" #include "database/UserId.hpp"
namespace lms::auth
{
class IPasswordService;
}
namespace lms::ui namespace lms::ui
{ {
std::optional<db::UserId> processAuthToken(const Wt::WEnvironment& env); enum class AuthenticationBackend
{
Internal,
Env,
PAM,
};
class Auth : public Wt::WTemplateFormView db::UserId processAuthToken(const Wt::WEnvironment& env);
class PasswordAuth : public Wt::WTemplateFormView
{ {
public: public:
Auth(); PasswordAuth(auth::IPasswordService& passwordService);
Wt::Signal<db::UserId> userLoggedIn; Wt::Signal<db::UserId> userLoggedIn;
}; };
+46 -48
View File
@@ -38,6 +38,7 @@
#include "database/TrackList.hpp" #include "database/TrackList.hpp"
#include "database/User.hpp" #include "database/User.hpp"
#include "services/artwork/IArtworkService.hpp" #include "services/artwork/IArtworkService.hpp"
#include "services/auth/IAuthTokenService.hpp"
#include "services/auth/IEnvService.hpp" #include "services/auth/IEnvService.hpp"
#include "services/auth/IPasswordService.hpp" #include "services/auth/IPasswordService.hpp"
#include "services/scrobbling/IScrobblingService.hpp" #include "services/scrobbling/IScrobblingService.hpp"
@@ -184,22 +185,9 @@ namespace lms::ui
} }
} // namespace } // namespace
std::unique_ptr<Wt::WApplication> LmsApplication::create(const Wt::WEnvironment& env, db::Db& db, LmsApplicationManager& appManager) std::unique_ptr<Wt::WApplication> LmsApplication::create(const Wt::WEnvironment& env, db::Db& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend)
{ {
if (auto* authEnvService{ core::Service<auth::IEnvService>::get() }) return std::make_unique<LmsApplication>(env, db, appManager, authBackend);
{
const auto checkResult{ authEnvService->processEnv(env) };
if (checkResult.state != auth::IEnvService::CheckResult::State::Granted)
{
LMS_LOG(UI, ERROR, "Cannot authenticate user from environment!");
// return a blank page
return std::make_unique<Wt::WApplication>(env);
}
return std::make_unique<LmsApplication>(env, db, appManager, checkResult.userId);
}
return std::make_unique<LmsApplication>(env, db, appManager);
} }
LmsApplication* LmsApplication::instance() LmsApplication* LmsApplication::instance()
@@ -249,17 +237,15 @@ namespace lms::ui
return _user->userLoginName; return _user->userLoginName;
} }
LmsApplication::LmsApplication(const Wt::WEnvironment& env, LmsApplication::LmsApplication(const Wt::WEnvironment& env, db::Db& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend)
db::Db& db,
LmsApplicationManager& appManager,
std::optional<db::UserId> userId)
: Wt::WApplication{ env } : Wt::WApplication{ env }
, _db{ db } , _db{ db }
, _appManager{ appManager } , _appManager{ appManager }
, _authBackend{ authBackend }
{ {
try try
{ {
init(userId); init();
} }
catch (LmsApplicationException& e) catch (LmsApplicationException& e)
{ {
@@ -275,7 +261,7 @@ namespace lms::ui
LmsApplication::~LmsApplication() = default; LmsApplication::~LmsApplication() = default;
void LmsApplication::init(std::optional<db::UserId> userId) void LmsApplication::init()
{ {
LMS_SCOPED_TRACE_OVERVIEW("UI", "ApplicationInit"); LMS_SCOPED_TRACE_OVERVIEW("UI", "ApplicationInit");
@@ -291,24 +277,38 @@ namespace lms::ui
// Handle Media Scanner events and other session events // Handle Media Scanner events and other session events
enableUpdates(true); enableUpdates(true);
if (userId) db::UserId userId;
onUserLoggedIn(*userId, false /* strongAuth */); switch (_authBackend)
else if (core::Service<auth::IPasswordService>::exists()) {
case AuthenticationBackend::Env:
{
const auto checkResult{ core::Service<auth::IEnvService>::get()->processEnv(environment()) };
if (checkResult.state != auth::IEnvService::CheckResult::State::Granted)
{
LMS_LOG(UI, ERROR, "Cannot authenticate user from environment!");
throw core::LmsException{ "Cannot authenticate user from environment!" }; // Do not put details here at it may appear on the user rendered html
}
assert(checkResult.userId.isValid());
userId = checkResult.userId;
}
break;
case AuthenticationBackend::Internal:
[[fallthrough]];
case AuthenticationBackend::PAM:
// Try to authenticate using auth token ("remember me" checkbox), may fail
userId = processAuthToken(environment());
break;
}
if (userId.isValid())
onUserLoggedIn(userId, false /* strongAuth */);
else
processPasswordAuth(); processPasswordAuth();
} }
void LmsApplication::processPasswordAuth() void LmsApplication::processPasswordAuth()
{ {
{
std::optional<db::UserId> userId{ processAuthToken(environment()) };
if (userId)
{
LMS_LOG(UI, DEBUG, "User authenticated using Auth token!");
onUserLoggedIn(*userId, false /* strongAuth */);
return;
}
}
// If here is no account in the database, launch the first connection wizard // If here is no account in the database, launch the first connection wizard
bool firstConnection{}; bool firstConnection{};
{ {
@@ -318,17 +318,19 @@ namespace lms::ui
LMS_LOG(UI, DEBUG, "Creating root widget. First connection = " << firstConnection); LMS_LOG(UI, DEBUG, "Creating root widget. First connection = " << firstConnection);
if (firstConnection && core::Service<auth::IPasswordService>::get()->canSetPasswords()) assert(_authBackend == AuthenticationBackend::Internal || _authBackend == AuthenticationBackend::PAM);
auth::IPasswordService& passwordService{ *core::Service<auth::IPasswordService>::get() };
if (firstConnection && _authBackend == AuthenticationBackend::Internal)
{ {
root()->addWidget(std::make_unique<InitWizardView>()); root()->addNew<InitWizardView>(passwordService);
} return;
else
{
Auth* auth{ root()->addNew<Auth>() };
auth->userLoggedIn.connect(this, [this](db::UserId userId) {
onUserLoggedIn(userId, true /* strongAuth */);
});
} }
PasswordAuth* auth{ root()->addNew<PasswordAuth>(passwordService) };
auth->userLoggedIn.connect(this, [this](db::UserId userId) {
onUserLoggedIn(userId, true /* strongAuth */);
});
} }
void LmsApplication::finalize() void LmsApplication::finalize()
@@ -360,11 +362,7 @@ namespace lms::ui
void LmsApplication::logoutUser() void LmsApplication::logoutUser()
{ {
{ core::Service<auth::IAuthTokenService>::get()->clearAuthTokens("ui", getUserId());
auto transaction{ getDbSession().createWriteTransaction() };
getUser().modify()->clearAuthTokens();
}
LMS_LOG(UI, INFO, "User '" << getUserLoginName() << " 'logged out"); LMS_LOG(UI, INFO, "User '" << getUserLoginName() << " 'logged out");
goHomeAndQuit(); goHomeAndQuit();
} }
+7 -3
View File
@@ -30,6 +30,7 @@
#include "database/UserId.hpp" #include "database/UserId.hpp"
#include "services/scanner/ScannerEvents.hpp" #include "services/scanner/ScannerEvents.hpp"
#include "Auth.hpp"
#include "Notification.hpp" #include "Notification.hpp"
#include "admin/ScannerController.hpp" #include "admin/ScannerController.hpp"
@@ -53,10 +54,10 @@ namespace lms::ui
class LmsApplication : public Wt::WApplication class LmsApplication : public Wt::WApplication
{ {
public: public:
LmsApplication(const Wt::WEnvironment& env, db::Db& db, LmsApplicationManager& appManager, std::optional<db::UserId> userId = std::nullopt); LmsApplication(const Wt::WEnvironment& env, db::Db& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend);
~LmsApplication(); ~LmsApplication();
static std::unique_ptr<Wt::WApplication> create(const Wt::WEnvironment& env, db::Db& db, LmsApplicationManager& appManager); static std::unique_ptr<Wt::WApplication> create(const Wt::WEnvironment& env, db::Db& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend);
static LmsApplication* instance(); static LmsApplication* instance();
// Session application data // Session application data
@@ -73,6 +74,8 @@ namespace lms::ui
// Proxified scanner events // Proxified scanner events
scanner::Events& getScannerEvents() { return _scannerEvents; } scanner::Events& getScannerEvents() { return _scannerEvents; }
AuthenticationBackend getAuthBackend() const { return _authBackend; }
// Utils // Utils
void post(std::function<void()> func); void post(std::function<void()> func);
void setTitle(const Wt::WString& title = ""); void setTitle(const Wt::WString& title = "");
@@ -88,7 +91,7 @@ namespace lms::ui
Wt::Signal<>& preQuit() { return _preQuit; } Wt::Signal<>& preQuit() { return _preQuit; }
private: private:
void init(std::optional<db::UserId> userId); void init();
void processPasswordAuth(); void processPasswordAuth();
void handleException(LmsApplicationException& e); void handleException(LmsApplicationException& e);
void goHomeAndQuit(); void goHomeAndQuit();
@@ -106,6 +109,7 @@ namespace lms::ui
db::Db& _db; db::Db& _db;
Wt::Signal<> _preQuit; Wt::Signal<> _preQuit;
LmsApplicationManager& _appManager; LmsApplicationManager& _appManager;
const AuthenticationBackend _authBackend;
scanner::Events _scannerEvents; scanner::Events _scannerEvents;
struct UserAuthInfo struct UserAuthInfo
{ {
+87 -11
View File
@@ -33,6 +33,7 @@
#include "core/Service.hpp" #include "core/Service.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/User.hpp" #include "database/User.hpp"
#include "services/auth/IAuthTokenService.hpp"
#include "services/auth/IPasswordService.hpp" #include "services/auth/IPasswordService.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
@@ -59,6 +60,7 @@ namespace lms::ui
static inline const Field ReplayGainModeField{ "replaygain-mode" }; static inline const Field ReplayGainModeField{ "replaygain-mode" };
static inline const Field ReplayGainPreAmpGainField{ "replaygain-preamp" }; static inline const Field ReplayGainPreAmpGainField{ "replaygain-preamp" };
static inline const Field ReplayGainPreAmpGainIfNoInfoField{ "replaygain-preamp-no-rg-info" }; static inline const Field ReplayGainPreAmpGainIfNoInfoField{ "replaygain-preamp-no-rg-info" };
static inline const Field SubsonicTokenField{ "subsonic-token" };
static inline const Field SubsonicEnableTranscodingByDefault{ "subsonic-enable-transcoding-by-default" }; static inline const Field SubsonicEnableTranscodingByDefault{ "subsonic-enable-transcoding-by-default" };
static inline const Field SubsonicArtistListModeField{ "subsonic-artist-list-mode" }; static inline const Field SubsonicArtistListModeField{ "subsonic-artist-list-mode" };
static inline const Field SubsonicTranscodingOutputFormatField{ "subsonic-transcoding-output-format" }; static inline const Field SubsonicTranscodingOutputFormatField{ "subsonic-transcoding-output-format" };
@@ -76,9 +78,10 @@ namespace lms::ui
using FeedbackBackendModel = ValueStringModel<db::FeedbackBackend>; using FeedbackBackendModel = ValueStringModel<db::FeedbackBackend>;
using ScrobblingBackendModel = ValueStringModel<db::ScrobblingBackend>; using ScrobblingBackendModel = ValueStringModel<db::ScrobblingBackend>;
SettingsModel(auth::IPasswordService* authPasswordService, bool withOldPassword) SettingsModel(auth::IPasswordService* authPasswordService, bool withOldPassword, auth::IAuthTokenService& authTokenService)
: _authPasswordService{ authPasswordService } : _authPasswordService{ authPasswordService }
, _withOldPassword{ withOldPassword } , _withOldPassword{ withOldPassword }
, _authTokenService{ authTokenService }
{ {
initializeModels(); initializeModels();
@@ -89,12 +92,15 @@ namespace lms::ui
addField(ReplayGainModeField); addField(ReplayGainModeField);
addField(ReplayGainPreAmpGainField); addField(ReplayGainPreAmpGainField);
addField(ReplayGainPreAmpGainIfNoInfoField); addField(ReplayGainPreAmpGainIfNoInfoField);
addField(SubsonicTokenField);
addField(SubsonicEnableTranscodingByDefault); addField(SubsonicEnableTranscodingByDefault);
addField(SubsonicTranscodingOutputBitrateField); addField(SubsonicTranscodingOutputBitrateField);
addField(SubsonicTranscodingOutputFormatField); addField(SubsonicTranscodingOutputFormatField);
addField(FeedbackBackendField); addField(FeedbackBackendField);
addField(ScrobblingBackendField); addField(ScrobblingBackendField);
addField(ListenBrainzTokenField); addField(ListenBrainzTokenField);
setValidator(SubsonicTokenField, createUUIDValidator());
setValidator(ListenBrainzTokenField, createUUIDValidator()); setValidator(ListenBrainzTokenField, createUUIDValidator());
if (_authPasswordService) if (_authPasswordService)
@@ -102,11 +108,11 @@ namespace lms::ui
if (_withOldPassword) if (_withOldPassword)
{ {
addField(PasswordOldField); addField(PasswordOldField);
setValidator(PasswordOldField, createPasswordCheckValidator()); setValidator(PasswordOldField, createPasswordCheckValidator(*_authPasswordService));
} }
addField(PasswordField); addField(PasswordField);
setValidator(PasswordField, createPasswordStrengthValidator([] { return auth::PasswordValidationContext{ .loginName = std::string{ LmsApp->getUserLoginName() }, .userType = LmsApp->getUserType() }; })); setValidator(PasswordField, createPasswordStrengthValidator(*authPasswordService, [] { return auth::PasswordValidationContext{ .loginName = std::string{ LmsApp->getUserLoginName() }, .userType = LmsApp->getUserType() }; }));
addField(PasswordConfirmField); addField(PasswordConfirmField);
} }
@@ -173,7 +179,33 @@ namespace lms::ui
LmsApp->getMediaPlayer().setSettings(settings); LmsApp->getMediaPlayer().setSettings(settings);
} }
// Subsonic API
{ {
const std::string token{ Wt::asString(value(SubsonicTokenField)).toUTF8() };
if (token.empty())
{
_authTokenService.clearAuthTokens("subsonic", user->getId());
}
else
{
// Consider there must be only one token
bool hasNonMatchingToken{ false };
bool hasMatchingToken{ false };
_authTokenService.visitAuthTokens("subsonic", user->getId(), [&](const auth::IAuthTokenService::AuthTokenInfo&, std::string_view storedToken) {
if (storedToken == token)
hasMatchingToken = true;
else
hasNonMatchingToken = true;
});
if (!hasMatchingToken || hasNonMatchingToken)
{
_authTokenService.clearAuthTokens("subsonic", user->getId());
_authTokenService.createAuthToken("subsonic", user->getId(), token);
}
}
bool subsonicEnableTranscodingByDefault{ Wt::asNumber(value(SubsonicEnableTranscodingByDefault)) != 0 }; bool subsonicEnableTranscodingByDefault{ Wt::asNumber(value(SubsonicEnableTranscodingByDefault)) != 0 };
user.modify()->setSubsonicEnableTranscodingByDefault(subsonicEnableTranscodingByDefault); user.modify()->setSubsonicEnableTranscodingByDefault(subsonicEnableTranscodingByDefault);
@@ -207,6 +239,7 @@ namespace lms::ui
if (_authPasswordService && !valueText(PasswordField).empty()) if (_authPasswordService && !valueText(PasswordField).empty())
{ {
_authPasswordService->setPassword(user->getId(), valueText(PasswordField).toUTF8()); _authPasswordService->setPassword(user->getId(), valueText(PasswordField).toUTF8());
_authTokenService.clearAuthTokens("ui", user->getId());
} }
} }
@@ -214,7 +247,7 @@ namespace lms::ui
{ {
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
User::pointer user{ LmsApp->getUser() }; const User::pointer user{ LmsApp->getUser() };
{ {
auto artistReleaseSortMethodRow{ _artistReleaseSortMethodModel->getRowFromValue(user->getUIArtistReleaseSortMethod()) }; auto artistReleaseSortMethodRow{ _artistReleaseSortMethodModel->getRowFromValue(user->getUIArtistReleaseSortMethod()) };
@@ -251,7 +284,14 @@ namespace lms::ui
setValue(ReplayGainPreAmpGainIfNoInfoField, settings.replayGain.preAmpGainIfNoInfo); setValue(ReplayGainPreAmpGainIfNoInfoField, settings.replayGain.preAmpGainIfNoInfo);
} }
// Subsonic
{ {
// Consider there is only one auth token
_authTokenService.visitAuthTokens("subsonic", user->getId(), [&](const auth::IAuthTokenService::AuthTokenInfo&, std::string_view storedToken) {
if (Wt::asString(value(SubsonicTokenField)).empty())
setValue(SubsonicTokenField, Wt::WString::fromUTF8(std::string{ storedToken }));
});
setValue(SubsonicEnableTranscodingByDefault, user->getSubsonicEnableTranscodingByDefault()); setValue(SubsonicEnableTranscodingByDefault, user->getSubsonicEnableTranscodingByDefault());
auto subsonicTranscodingOutputBitrateRow{ _transcodingOutputBitrateModel->getRowFromValue(user->getSubsonicDefaultTranscodingOutputBitrate()) }; auto subsonicTranscodingOutputBitrateRow{ _transcodingOutputBitrateModel->getRowFromValue(user->getSubsonicDefaultTranscodingOutputBitrate()) };
@@ -380,6 +420,8 @@ namespace lms::ui
auth::IPasswordService* _authPasswordService{}; auth::IPasswordService* _authPasswordService{};
bool _withOldPassword{}; bool _withOldPassword{};
auth::IAuthTokenService& _authTokenService;
std::shared_ptr<ArtistReleaseSortMethodModel> _artistReleaseSortMethodModel; std::shared_ptr<ArtistReleaseSortMethodModel> _artistReleaseSortMethodModel;
std::shared_ptr<TranscodingModeModel> _transcodingModeModeModel; std::shared_ptr<TranscodingModeModel> _transcodingModeModeModel;
std::shared_ptr<ValueStringModel<Bitrate>> _transcodingOutputBitrateModel; std::shared_ptr<ValueStringModel<Bitrate>> _transcodingOutputBitrateModel;
@@ -403,8 +445,7 @@ namespace lms::ui
refreshView(); refreshView();
} }
void void SettingsView::refreshView()
SettingsView::refreshView()
{ {
if (!wApp->internalPathMatches("/settings")) if (!wApp->internalPathMatches("/settings"))
return; return;
@@ -417,12 +458,14 @@ namespace lms::ui
auto t{ addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.Settings.template")) }; auto t{ addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.Settings.template")) };
auto* authPasswordService{ core::Service<auth::IPasswordService>::get() }; auth::IPasswordService* authPasswordService{};
if (authPasswordService && !authPasswordService->canSetPasswords()) if (LmsApp->getAuthBackend() == AuthenticationBackend::Internal)
authPasswordService = nullptr; {
authPasswordService = core::Service<auth::IPasswordService>::get();
auto model{ std::make_shared<SettingsModel>(authPasswordService, !LmsApp->isUserAuthStrong()) }; assert(authPasswordService->canSetPasswords());
}
auto model{ std::make_shared<SettingsModel>(authPasswordService, !LmsApp->isUserAuthStrong(), *core::Service<auth::IAuthTokenService>::get()) };
if (authPasswordService) if (authPasswordService)
{ {
t->setCondition("if-has-change-password", true); t->setCondition("if-has-change-password", true);
@@ -514,6 +557,31 @@ namespace lms::ui
// Subsonic // Subsonic
{ {
t->setCondition("if-has-subsonic-api", core::Service<core::IConfig>::get()->getBool("api-subsonic", true)); t->setCondition("if-has-subsonic-api", core::Service<core::IConfig>::get()->getBool("api-subsonic", true));
t->setCondition("if-has-subsonic-token-usage", core::Service<core::IConfig>::get()->getBool("api-subsonic-support-user-password-auth", true));
auto subsonicToken{ std::make_unique<Wt::WLineEdit>() };
Wt::WLineEdit* subsonicTokenPtr{ subsonicToken.get() };
subsonicTokenPtr->setEchoMode(Wt::EchoMode::Password);
subsonicTokenPtr->setReadOnly(true);
t->setFormWidget(SettingsModel::SubsonicTokenField, std::move(subsonicToken));
auto subsonicTokenRegenBtn{ std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.Settings.regen-token")) };
subsonicTokenRegenBtn->clicked().connect(this, [subsonicTokenPtr] {
subsonicTokenPtr->setValueText(Wt::WString::fromUTF8(std::string{ core::UUID::generate().getAsString() }));
});
t->bindWidget("subsonic-token-regen-btn", std::move(subsonicTokenRegenBtn));
auto subsonicTokenVisibilityBtn{ std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.template.toggle-visibility-btn"), Wt::TextFormat::XHTML) };
subsonicTokenVisibilityBtn->clicked().connect(this, [subsonicTokenPtr] {
subsonicTokenPtr->setEchoMode(subsonicTokenPtr->echoMode() == Wt::EchoMode::Password ? Wt::EchoMode::Normal : Wt::EchoMode::Password);
});
t->bindWidget("subsonic-token-visibility-btn", std::move(subsonicTokenVisibilityBtn));
auto subsonicTokenDelBtn{ std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.template.trash-btn"), Wt::TextFormat::XHTML) };
subsonicTokenDelBtn->clicked().connect(this, [subsonicTokenPtr] {
subsonicTokenPtr->setValueText("");
});
t->bindWidget("subsonic-token-del-btn", std::move(subsonicTokenDelBtn));
// Enable transcoding by default // Enable transcoding by default
t->setFormWidget(SettingsModel::SubsonicEnableTranscodingByDefault, std::make_unique<Wt::WCheckBox>()); t->setFormWidget(SettingsModel::SubsonicEnableTranscodingByDefault, std::make_unique<Wt::WCheckBox>());
@@ -555,7 +623,15 @@ namespace lms::ui
// Backend settings // Backend settings
{ {
auto listenbrainzToken{ std::make_unique<Wt::WLineEdit>() }; auto listenbrainzToken{ std::make_unique<Wt::WLineEdit>() };
Wt::WLineEdit* listenbrainzTokenPtr{ listenbrainzToken.get() };
listenbrainzTokenPtr->setEchoMode(Wt::EchoMode::Password);
t->setFormWidget(SettingsModel::ListenBrainzTokenField, std::move(listenbrainzToken)); t->setFormWidget(SettingsModel::ListenBrainzTokenField, std::move(listenbrainzToken));
auto listenbrainzTokenVisibilityBtn{ std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.template.toggle-visibility-btn"), Wt::TextFormat::XHTML) };
listenbrainzTokenVisibilityBtn->clicked().connect(this, [listenbrainzTokenPtr] {
listenbrainzTokenPtr->setEchoMode(listenbrainzTokenPtr->echoMode() == Wt::EchoMode::Password ? Wt::EchoMode::Normal : Wt::EchoMode::Password);
});
t->bindWidget("listenbrainz-token-visibility-btn", std::move(listenbrainzTokenVisibilityBtn));
} }
auto updateListenBrainzTokenField{ [=] { auto updateListenBrainzTokenField{ [=] {
+9 -6
View File
@@ -47,15 +47,15 @@ namespace lms::ui
static inline const Field PasswordField{ "password" }; static inline const Field PasswordField{ "password" };
static inline const Field PasswordConfirmField{ "password-confirm" }; static inline const Field PasswordConfirmField{ "password-confirm" };
InitWizardModel() InitWizardModel(auth::IPasswordService& passwordService)
: Wt::WFormModel() : _passwordService{ passwordService }
{ {
addField(AdminLoginField); addField(AdminLoginField);
addField(PasswordField); addField(PasswordField);
addField(PasswordConfirmField); addField(PasswordConfirmField);
setValidator(AdminLoginField, createLoginNameValidator()); setValidator(AdminLoginField, createLoginNameValidator());
setValidator(PasswordField, createPasswordStrengthValidator([this] { return auth::PasswordValidationContext{ valueText(AdminLoginField).toUTF8(), db::UserType::ADMIN }; })); setValidator(PasswordField, createPasswordStrengthValidator(passwordService, [this] { return auth::PasswordValidationContext{ valueText(AdminLoginField).toUTF8(), db::UserType::ADMIN }; }));
validator(PasswordField)->setMandatory(true); validator(PasswordField)->setMandatory(true);
setValidator(PasswordConfirmField, createMandatoryValidator()); setValidator(PasswordConfirmField, createMandatoryValidator());
} }
@@ -71,7 +71,7 @@ namespace lms::ui
db::User::pointer user{ LmsApp->getDbSession().create<db::User>(valueText(AdminLoginField).toUTF8()) }; db::User::pointer user{ LmsApp->getDbSession().create<db::User>(valueText(AdminLoginField).toUTF8()) };
user.modify()->setType(db::UserType::ADMIN); user.modify()->setType(db::UserType::ADMIN);
core::Service<auth::IPasswordService>::get()->setPassword(user->getId(), valueText(PasswordField).toUTF8()); _passwordService.setPassword(user->getId(), valueText(PasswordField).toUTF8());
} }
bool validateField(Field field) bool validateField(Field field)
@@ -97,12 +97,15 @@ namespace lms::ui
return false; return false;
} }
private:
auth::IPasswordService& _passwordService;
}; };
InitWizardView::InitWizardView() InitWizardView::InitWizardView(auth::IPasswordService& passwordService)
: Wt::WTemplateFormView{ Wt::WString::tr("Lms.Admin.InitWizard.template") } : Wt::WTemplateFormView{ Wt::WString::tr("Lms.Admin.InitWizard.template") }
{ {
auto model = std::make_shared<InitWizardModel>(); auto model = std::make_shared<InitWizardModel>(passwordService);
// AdminLogin // AdminLogin
{ {
+6 -2
View File
@@ -21,13 +21,17 @@
#include <Wt/WTemplateFormView.h> #include <Wt/WTemplateFormView.h>
namespace lms::auth
{
class IPasswordService;
}
namespace lms::ui namespace lms::ui
{ {
class InitWizardView : public Wt::WTemplateFormView class InitWizardView : public Wt::WTemplateFormView
{ {
public: public:
InitWizardView(); InitWizardView(auth::IPasswordService& passwordService);
}; };
} // namespace lms::ui } // namespace lms::ui
+18 -7
View File
@@ -34,6 +34,7 @@
#include "core/String.hpp" #include "core/String.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/User.hpp" #include "database/User.hpp"
#include "services/auth/IAuthTokenService.hpp"
#include "services/auth/IPasswordService.hpp" #include "services/auth/IPasswordService.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
@@ -52,9 +53,10 @@ namespace lms::ui
static inline const Field PasswordField{ "password" }; static inline const Field PasswordField{ "password" };
static inline const Field DemoField{ "demo" }; static inline const Field DemoField{ "demo" };
UserModel(std::optional<UserId> userId, auth::IPasswordService* authPasswordService) UserModel(std::optional<UserId> userId, auth::IPasswordService* authPasswordService, auth::IAuthTokenService& authTokenService)
: _userId{ userId } : _userId{ userId }
, _authPasswordService{ authPasswordService } , _authPasswordService{ authPasswordService }
, _authTokenService{ authTokenService }
{ {
if (!_userId) if (!_userId)
{ {
@@ -65,7 +67,7 @@ namespace lms::ui
if (authPasswordService) if (authPasswordService)
{ {
addField(PasswordField); addField(PasswordField);
setValidator(PasswordField, createPasswordStrengthValidator([this] { return auth::PasswordValidationContext{ getLoginName(), getUserType() }; })); setValidator(PasswordField, createPasswordStrengthValidator(*authPasswordService, [this] { return auth::PasswordValidationContext{ getLoginName(), getUserType() }; }));
if (!userId) if (!userId)
validator(PasswordField)->setMandatory(true); validator(PasswordField)->setMandatory(true);
} }
@@ -86,7 +88,10 @@ namespace lms::ui
throw UserNotFoundException{}; throw UserNotFoundException{};
if (_authPasswordService && !valueText(PasswordField).empty()) if (_authPasswordService && !valueText(PasswordField).empty())
{
_authPasswordService->setPassword(user->getId(), valueText(PasswordField).toUTF8()); _authPasswordService->setPassword(user->getId(), valueText(PasswordField).toUTF8());
_authTokenService.clearAuthTokens("ui", user->getId());
}
} }
else else
{ {
@@ -103,6 +108,9 @@ namespace lms::ui
if (_authPasswordService) if (_authPasswordService)
_authPasswordService->setPassword(user->getId(), valueText(PasswordField).toUTF8()); _authPasswordService->setPassword(user->getId(), valueText(PasswordField).toUTF8());
// For demo user, we create the subsonic API auth token now as we have no other mean to create it later
core::Service<auth::IAuthTokenService>::get()->createAuthToken("subsonic", user->getId(), core::UUID::generate().getAsString());
} }
} }
@@ -177,6 +185,7 @@ namespace lms::ui
std::optional<UserId> _userId; std::optional<UserId> _userId;
auth::IPasswordService* _authPasswordService{}; auth::IPasswordService* _authPasswordService{};
auth::IAuthTokenService& _authTokenService;
}; };
UserView::UserView() UserView::UserView()
@@ -199,12 +208,14 @@ namespace lms::ui
Wt::WTemplateFormView* t{ addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.Admin.User.template")) }; Wt::WTemplateFormView* t{ addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.Admin.User.template")) };
auto* authPasswordService{ core::Service<auth::IPasswordService>::get() }; auth::IPasswordService* authPasswordService{};
if (authPasswordService && !authPasswordService->canSetPasswords()) if (LmsApp->getAuthBackend() == AuthenticationBackend::Internal)
authPasswordService = nullptr; {
authPasswordService = core::Service<auth::IPasswordService>::get();
auto model{ std::make_shared<UserModel>(userId, authPasswordService) }; assert(authPasswordService->canSetPasswords());
}
auto model{ std::make_shared<UserModel>(userId, authPasswordService, *core::Service<auth::IAuthTokenService>::get()) };
if (userId) if (userId)
{ {
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
+1 -1
View File
@@ -43,7 +43,7 @@ namespace lms::ui
_container = bindNew<Wt::WContainerWidget>("users"); _container = bindNew<Wt::WContainerWidget>("users");
if (core::Service<auth::IPasswordService>::get() && core::Service<auth::IPasswordService>::get()->canSetPasswords()) if (LmsApp->getAuthBackend() == AuthenticationBackend::Internal)
{ {
setCondition("if-can-create-user", true); setCondition("if-can-create-user", true);
+17 -7
View File
@@ -33,8 +33,9 @@ namespace lms::ui
class PasswordStrengthValidator : public Wt::WValidator class PasswordStrengthValidator : public Wt::WValidator
{ {
public: public:
PasswordStrengthValidator(PasswordValidationContextGetFunc passwordValidationContextGetFunc) PasswordStrengthValidator(const auth::IPasswordService& passwordService, PasswordValidationContextGetFunc passwordValidationContextGetFunc)
: _passwordValidationContextGetFunc{ std::move(passwordValidationContextGetFunc) } : _passwordValidationContextGetFunc{ std::move(passwordValidationContextGetFunc) }
, _passwordService{ passwordService }
{ {
} }
@@ -43,6 +44,7 @@ namespace lms::ui
std::string javaScriptValidate() const override { return {}; } std::string javaScriptValidate() const override { return {}; }
PasswordValidationContextGetFunc _passwordValidationContextGetFunc; PasswordValidationContextGetFunc _passwordValidationContextGetFunc;
const auth::IPasswordService& _passwordService;
}; };
} // namespace } // namespace
@@ -53,7 +55,7 @@ namespace lms::ui
const auth::PasswordValidationContext context{ _passwordValidationContextGetFunc() }; const auth::PasswordValidationContext context{ _passwordValidationContextGetFunc() };
switch (core::Service<auth::IPasswordService>::get()->checkPasswordAcceptability(input.toUTF8(), context)) switch (_passwordService.checkPasswordAcceptability(input.toUTF8(), context))
{ {
case auth::IPasswordService::PasswordAcceptabilityResult::OK: case auth::IPasswordService::PasswordAcceptabilityResult::OK:
return Wt::WValidator::Result{ Wt::ValidationState::Valid }; return Wt::WValidator::Result{ Wt::ValidationState::Valid };
@@ -66,16 +68,24 @@ namespace lms::ui
throw core::LmsException{ "internal error" }; throw core::LmsException{ "internal error" };
} }
std::unique_ptr<Wt::WValidator> createPasswordStrengthValidator(PasswordValidationContextGetFunc passwordValidationContextGetFunc) std::unique_ptr<Wt::WValidator> createPasswordStrengthValidator(const auth::IPasswordService& passwordService, PasswordValidationContextGetFunc passwordValidationContextGetFunc)
{ {
return std::make_unique<PasswordStrengthValidator>(std::move(passwordValidationContextGetFunc)); return std::make_unique<PasswordStrengthValidator>(passwordService, std::move(passwordValidationContextGetFunc));
} }
class PasswordCheckValidator : public Wt::WValidator class PasswordCheckValidator : public Wt::WValidator
{ {
public:
PasswordCheckValidator(auth::IPasswordService& passwordService)
: _passwordService{ passwordService }
{
}
private: private:
Wt::WValidator::Result validate(const Wt::WString& input) const override; Wt::WValidator::Result validate(const Wt::WString& input) const override;
std::string javaScriptValidate() const override { return {}; } std::string javaScriptValidate() const override { return {}; }
auth::IPasswordService& _passwordService;
}; };
Wt::WValidator::Result PasswordCheckValidator::validate(const Wt::WString& input) const Wt::WValidator::Result PasswordCheckValidator::validate(const Wt::WString& input) const
@@ -83,7 +93,7 @@ namespace lms::ui
if (input.empty()) if (input.empty())
return Wt::WValidator::validate(input); return Wt::WValidator::validate(input);
const auto checkResult{ core::Service<auth::IPasswordService>::get()->checkUserPassword( const auto checkResult{ _passwordService.checkUserPassword(
boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()), boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()),
LmsApp->getUserLoginName(), LmsApp->getUserLoginName(),
input.toUTF8()) }; input.toUTF8()) };
@@ -100,9 +110,9 @@ namespace lms::ui
throw core::LmsException{ "InternalError" }; throw core::LmsException{ "InternalError" };
} }
std::unique_ptr<Wt::WValidator> createPasswordCheckValidator() std::unique_ptr<Wt::WValidator> createPasswordCheckValidator(auth::IPasswordService& passwordService)
{ {
return std::make_unique<PasswordCheckValidator>(); return std::make_unique<PasswordCheckValidator>(passwordService);
} }
} // namespace lms::ui } // namespace lms::ui
+7 -2
View File
@@ -25,11 +25,16 @@
#include "services/auth/Types.hpp" #include "services/auth/Types.hpp"
namespace lms::auth
{
class IPasswordService;
}
namespace lms::ui namespace lms::ui
{ {
using PasswordValidationContextGetFunc = std::function<auth::PasswordValidationContext()>; using PasswordValidationContextGetFunc = std::function<auth::PasswordValidationContext()>;
std::unique_ptr<Wt::WValidator> createPasswordStrengthValidator(PasswordValidationContextGetFunc passwordValidationContextGetFunc); std::unique_ptr<Wt::WValidator> createPasswordStrengthValidator(const auth::IPasswordService& passwordService, PasswordValidationContextGetFunc passwordValidationContextGetFunc);
// Check current user password // Check current user password
std::unique_ptr<Wt::WValidator> createPasswordCheckValidator(); std::unique_ptr<Wt::WValidator> createPasswordCheckValidator(auth::IPasswordService& passwordService);
} // namespace lms::ui } // namespace lms::ui