diff --git a/approot/messages.xml b/approot/messages.xml
index 9c63aee0..babaaad2 100644
--- a/approot/messages.xml
+++ b/approot/messages.xml
@@ -17,6 +17,9 @@
Login throttled, please try again later
Confirm password
New password
+
Is auth managed externally?
+
Password must be empty for users with external authentication
+
Password must not be empty for users without external authentication
Old password
Password too weak
Passwords don't match
diff --git a/cmake/modules/FindPAM.cmake b/cmake/modules/FindPAM.cmake
new file mode 100644
index 00000000..08c86def
--- /dev/null
+++ b/cmake/modules/FindPAM.cmake
@@ -0,0 +1,73 @@
+# From http://code.google.com/p/pam-face-authentication/source/browse/branches/pam_face_authentication/cmake/modules/FindPAM.cmake?r=336
+
+# - Try to find the PAM libraries
+# Once done this will define
+#
+# PAM_FOUND - system has pam
+# PAM_INCLUDE_DIR - the pam include directory
+# PAM_LIBRARIES - libpam library
+
+if (PAM_INCLUDE_DIR AND PAM_LIBRARY)
+ # Already in cache, be silent
+ set(PAM_FIND_QUIETLY TRUE)
+endif (PAM_INCLUDE_DIR AND PAM_LIBRARY)
+
+find_path(PAM_INCLUDE_DIR NAMES security/pam_appl.h pam/pam_appl.h)
+find_library(PAM_LIBRARY pam)
+find_library(DL_LIBRARY dl)
+
+if (PAM_INCLUDE_DIR AND PAM_LIBRARY)
+ set(PAM_FOUND TRUE)
+ if (DL_LIBRARY)
+ set(PAM_LIBRARIES ${PAM_LIBRARY} ${DL_LIBRARY})
+ else (DL_LIBRARY)
+ set(PAM_LIBRARIES ${PAM_LIBRARY})
+ endif (DL_LIBRARY)
+
+ if (EXISTS ${PAM_INCLUDE_DIR}/pam/pam_appl.h)
+ # darwin claims to be something special
+ set(HAVE_PAM_PAM_APPL_H 1)
+ endif (EXISTS ${PAM_INCLUDE_DIR}/pam/pam_appl.h)
+
+ if (NOT DEFINED PAM_MESSAGE_CONST)
+ include(CheckCXXSourceCompiles)
+ # XXX does this work with plain c?
+ check_cxx_source_compiles("
+#if ${HAVE_PAM_PAM_APPL_H}+0
+# include
+#else
+# include
+#endif
+static int PAM_conv(
+ int num_msg,
+ const struct pam_message **msg, /* this is the culprit */
+ struct pam_response **resp,
+ void *ctx)
+{
+ return 0;
+}
+int main(void)
+{
+ struct pam_conv PAM_conversation = {
+ &PAM_conv, /* this bombs out if the above does not match */
+ 0
+ };
+ return 0;
+}
+" PAM_MESSAGE_CONST)
+ endif (NOT DEFINED PAM_MESSAGE_CONST)
+ set(PAM_MESSAGE_CONST ${PAM_MESSAGE_CONST} CACHE BOOL "PAM expects a conversation function with const pam_message")
+
+endif (PAM_INCLUDE_DIR AND PAM_LIBRARY)
+
+if (PAM_FOUND)
+ if (NOT PAM_FIND_QUIETLY)
+ message(STATUS "Found PAM: ${PAM_LIBRARIES}")
+ endif (NOT PAM_FIND_QUIETLY)
+else (PAM_FOUND)
+ if (PAM_FIND_REQUIRED)
+ message(FATAL_ERROR "PAM was not found")
+ endif(PAM_FIND_REQUIRED)
+endif (PAM_FOUND)
+
+mark_as_advanced(PAM_INCLUDE_DIR PAM_LIBRARY DL_LIBRARY PAM_MESSAGE_CONST)
diff --git a/docroot/css/lms.css b/docroot/css/lms.css
index 3fecc982..6f1ca489 100644
--- a/docroot/css/lms.css
+++ b/docroot/css/lms.css
@@ -483,4 +483,7 @@ a.Lms-releasename:hover, a.Lms-releasename:focus {
margin-bottom: 8px;
}
+.hack {
+ margin-left: 20px;
+}
diff --git a/src/libs/auth/CMakeLists.txt b/src/libs/auth/CMakeLists.txt
index 9be20d0c..20e1cf37 100644
--- a/src/libs/auth/CMakeLists.txt
+++ b/src/libs/auth/CMakeLists.txt
@@ -22,6 +22,7 @@ target_link_libraries(lmsauth PUBLIC
pthread
boost_system
wt
+ ${PAM_LIBRARIES}
)
install(TARGETS lmsauth DESTINATION lib)
diff --git a/src/libs/auth/impl/AuthTokenService.cpp b/src/libs/auth/impl/AuthTokenService.cpp
index ba37b35c..ff21dbeb 100644
--- a/src/libs/auth/impl/AuthTokenService.cpp
+++ b/src/libs/auth/impl/AuthTokenService.cpp
@@ -17,8 +17,6 @@
* along with LMS. If not, see .
*/
-/* This file contains some classes in order to get info from file using the libavconv */
-
#include "AuthTokenService.hpp"
#include
diff --git a/src/libs/auth/impl/AuthTokenService.hpp b/src/libs/auth/impl/AuthTokenService.hpp
index 36a9721a..00b0b5a8 100644
--- a/src/libs/auth/impl/AuthTokenService.hpp
+++ b/src/libs/auth/impl/AuthTokenService.hpp
@@ -17,8 +17,6 @@
* along with LMS. If not, see .
*/
-/* This file contains some classes in order to get info from file using the libavconv */
-
#pragma once
#include "auth/IAuthTokenService.hpp"
diff --git a/src/libs/auth/impl/PasswordService.cpp b/src/libs/auth/impl/PasswordService.cpp
index 9c17de2b..3a0fac63 100644
--- a/src/libs/auth/impl/PasswordService.cpp
+++ b/src/libs/auth/impl/PasswordService.cpp
@@ -17,8 +17,6 @@
* along with LMS. If not, see .
*/
-/* This file contains some classes in order to get info from file using the libavconv */
-
#include "PasswordService.hpp"
#include
@@ -29,6 +27,8 @@
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
+#include
+
namespace Auth {
std::unique_ptr createPasswordService(std::size_t maxThrottlerEntries)
@@ -41,10 +41,101 @@ PasswordService::PasswordService(std::size_t maxThrottlerEntries)
{
}
+#ifdef USEPAM
+static void
+delete_resp(int num_msg, pam_response *response)
+{
+ if (response == nullptr)
+ return;
+ for (int i = 0; i < num_msg; i++) {
+ if (response[i].resp) {
+ /* clear before freeing -- might be a password */
+ bzero(response[i].resp, strlen(response[i].resp));
+ free(response[i].resp);
+ response[i].resp = nullptr;
+ }
+ }
+}
+
+struct pam_conv_data
+{
+ const char *username;
+ const char *password;
+};
+
+static
+int lms_conv(int num_msg, const pam_message** msgs, pam_response** resps, void* appdata_ptr)
+{
+ if(num_msg < 1)
+ return PAM_CONV_ERR;
+ if (!resps || !msgs || !appdata_ptr)
+ return PAM_CONV_ERR;
+
+ pam_conv_data *data = static_cast( appdata_ptr );
+ pam_response *response = new (std::nothrow) pam_response[num_msg];
+ if(!response)
+ return PAM_CONV_ERR;
+
+ for(int i = 0; i < num_msg; ++i)
+ {
+ response[i].resp_retcode = 0;
+ response[i].resp = 0;
+ switch (msgs[i]->msg_style) {
+ case PAM_PROMPT_ECHO_ON:
+ /* on memory allocation failure, auth fails */
+ response[i].resp = strdup(data->username);
+ break;
+ case PAM_PROMPT_ECHO_OFF:
+ response[i].resp = strdup(data->password);
+ break;
+ case PAM_ERROR_MSG:
+ case PAM_TEXT_INFO:
+ default:
+ delete_resp(i, response);
+ return PAM_CONV_ERR;
+ }
+ }
+
+ *resps = response;
+ return PAM_SUCCESS;
+}
+#endif
+
+static bool
+pamCheckUserPassword(const std::string& loginName, const std::string& password)
+{
+#ifdef USEPAM
+ pam_conv_data authdata{loginName.c_str(), password.c_str()};
+ pam_conv conv = { lms_conv, &authdata };
+ pam_handle_t *pamh;
+ bool authenticated{false};
+
+ /* Initialize PAM framework */
+ int err = pam_start("lms", loginName.c_str(), &conv, &pamh);
+ if (err != PAM_SUCCESS) {
+ return false;
+ }
+
+ err = pam_authenticate(pamh, 0);
+ if(err == PAM_SUCCESS)
+ {
+ /* Make sure account and password are still valid */
+ err = pam_acct_mgmt(pamh, PAM_SILENT);
+ authenticated = err == PAM_SUCCESS;
+ }
+
+ (void) pam_end(pamh, 0);
+ return authenticated;
+#else
+ return false;
+#endif
+}
+
static
bool
checkUserPassword(Database::Session& session, const std::string& loginName, const std::string& password)
{
+ bool hasExternalAuth;
Database::User::PasswordHash passwordHash;
{
auto transaction {session.createSharedTransaction()};
@@ -53,11 +144,19 @@ checkUserPassword(Database::Session& session, const std::string& loginName, cons
if (!user)
return false;
+ hasExternalAuth = user->hasExternalAuth();
passwordHash = user->getPasswordHash();
}
- const Wt::Auth::BCryptHashFunction hashFunc {6};
- return hashFunc.verify(password, passwordHash.salt, passwordHash.hash);
+ if (hasExternalAuth)
+ {
+ return pamCheckUserPassword(loginName, password);
+ }
+ else
+ {
+ const Wt::Auth::BCryptHashFunction hashFunc {6};
+ return hashFunc.verify(password, passwordHash.salt, passwordHash.hash);
+ }
}
diff --git a/src/libs/database/include/database/User.hpp b/src/libs/database/include/database/User.hpp
index 24410c47..99b4eeb5 100644
--- a/src/libs/database/include/database/User.hpp
+++ b/src/libs/database/include/database/User.hpp
@@ -165,6 +165,7 @@ class User : public Wt::Dbo::Dbo
void setSubsonicTranscodeBitrate(Bitrate bitrate);
void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; }
void setRadio(bool val) { _radio = val; }
+ void setExternalAuth(bool val) { _externalAuth = val; }
void setRepeatAll(bool val) { _repeatAll = val; }
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
void clearAuthTokens();
@@ -179,6 +180,7 @@ class User : public Wt::Dbo::Dbo
std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; }
bool isRepeatAllSet() const { return _repeatAll; }
bool isRadioSet() const { return _radio; }
+ bool hasExternalAuth() const { return _externalAuth; }
UITheme getUITheme() const { return _uiTheme; }
SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; }
@@ -218,6 +220,7 @@ class User : public Wt::Dbo::Dbo
Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos");
Wt::Dbo::field(a, _repeatAll, "repeat_all");
Wt::Dbo::field(a, _radio, "radio");
+ Wt::Dbo::field(a, _externalAuth, "external_auth");
Wt::Dbo::hasMany(a, _tracklists, Wt::Dbo::ManyToOne, "user");
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_artist_starred", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _starredReleases, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade);
@@ -246,6 +249,8 @@ class User : public Wt::Dbo::Dbo
int _curPlayingTrackPos {}; // Current track position in queue
bool _repeatAll {};
bool _radio {};
+
+ bool _externalAuth {false};
Wt::Dbo::collection> _tracklists;
Wt::Dbo::collection> _starredArtists;
diff --git a/src/lms/ui/admin/UserView.cpp b/src/lms/ui/admin/UserView.cpp
index 3bb52e6d..961bd906 100644
--- a/src/lms/ui/admin/UserView.cpp
+++ b/src/lms/ui/admin/UserView.cpp
@@ -51,6 +51,7 @@ class UserModel : public Wt::WFormModel
static inline const Field LoginField {"login"};
static inline const Field PasswordField {"password"};
static inline const Field DemoField {"demo"};
+ static inline const Field ExternalAuthField{"external-auth"};
UserModel(std::optional userId)
: _userId {userId}
@@ -63,6 +64,7 @@ class UserModel : public Wt::WFormModel
addField(PasswordField);
addField(DemoField);
+ addField(ExternalAuthField);
if (!_userId)
setValidator(PasswordField, createMandatoryValidator());
@@ -90,6 +92,8 @@ class UserModel : public Wt::WFormModel
user.modify()->setPasswordHash(*passwordHash);
user.modify()->clearAuthTokens();
}
+
+ user.modify()->setExternalAuth(static_cast(Wt::asNumber(value(ExternalAuthField))));
}
else
{
@@ -98,6 +102,11 @@ class UserModel : public Wt::WFormModel
if (Wt::asNumber(value(DemoField)))
user.modify()->setType(Database::User::Type::DEMO);
+
+ if (Wt::asNumber(value(ExternalAuthField)))
+ user.modify()->setExternalAuth(true);
+ else
+ user.modify()->setExternalAuth(false);
}
}
@@ -116,6 +125,19 @@ class UserModel : public Wt::WFormModel
else if (user == LmsApp->getUser())
throw UserNotAllowedException {};
}
+
+ bool getExternalAuth() const
+ {
+ if (_userId)
+ {
+ auto transaction {LmsApp->getDbSession().createSharedTransaction()};
+
+ const Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *_userId)};
+ return user->hasExternalAuth();
+ }
+ else
+ return Wt::asNumber(value(ExternalAuthField));
+ }
std::string getLoginName() const
{
@@ -144,7 +166,14 @@ class UserModel : public Wt::WFormModel
}
else if (field == PasswordField)
{
- if (!valueText(PasswordField).empty())
+ if (Wt::asNumber(value(ExternalAuthField)))
+ {
+ if (!valueText(PasswordField).empty())
+ {
+ error = Wt::WString::tr("Lms.password_must_be_empty_for_ext");
+ }
+ }
+ else if (!valueText(PasswordField).empty())
{
if (Wt::asNumber(value(DemoField)))
{
@@ -157,8 +186,13 @@ class UserModel : public Wt::WFormModel
// Evaluate the strength of the password for non demo accounts
if (!ServiceProvider<::Auth::IPasswordService>::get()->evaluatePasswordStrength(getLoginName(), valueText(PasswordField).toUTF8()))
error = Wt::WString::tr("Lms.password-too-weak");
+
}
}
+ else
+ {
+ error = Wt::WString::tr("Lms.password-must-not-be-empty");
+ }
}
else if (field == DemoField)
{
@@ -167,6 +201,11 @@ class UserModel : public Wt::WFormModel
if (Wt::asNumber(value(DemoField)) && Database::User::getDemo(LmsApp->getDbSession()))
error = Wt::WString::tr("Lms.Admin.User.demo-account-already-exists");
}
+// else if (field == ExternalAuthField)
+// {
+// if (!valueText(PasswordField).empty())
+// error = Wt::WString::tr("Lms.password_must_be_empty_for_ext");
+// }
if (error.empty())
return Wt::WFormModel::validateField(field);
@@ -215,6 +254,12 @@ UserView::refreshView()
t->setCondition("if-has-last-login", true);
t->bindString("last-login", user->getLastLogin().toString(), Wt::TextFormat::Plain);
+
+
+ auto extCheckBox = std::make_unique();
+ extCheckBox->setChecked(user->hasExternalAuth());
+ t->setFormWidget(UserModel::ExternalAuthField, std::move(extCheckBox));
+ t->setCondition("if-external-auth", true);
}
else
{
@@ -222,6 +267,11 @@ UserView::refreshView()
t->setCondition("if-has-login", true);
t->setFormWidget(UserModel::LoginField, std::make_unique());
t->bindString("title", Wt::WString::tr("Lms.Admin.User.user-create"));
+
+ auto extCheckBox = std::make_unique();
+ extCheckBox->setChecked(true);
+ t->setCondition(UserModel::ExternalAuthField, false);
+ t->setFormWidget("external-auth", std::move(extCheckBox));
}
// Password
@@ -233,7 +283,7 @@ UserView::refreshView()
t->setFormWidget(UserModel::DemoField, std::make_unique());
if (!userId && ServiceProvider::get()->getBool("demo", false))
t->setCondition("if-demo", true);
-
+
Wt::WPushButton* saveBtn = t->bindNew("save-btn", Wt::WString::tr(userId ? "Lms.save" : "Lms.create"));
saveBtn->clicked().connect([=]()
{
diff --git a/src/lms/ui/common/Validators.cpp b/src/lms/ui/common/Validators.cpp
index 40bf8b91..a1050b8c 100644
--- a/src/lms/ui/common/Validators.cpp
+++ b/src/lms/ui/common/Validators.cpp
@@ -39,7 +39,7 @@ std::shared_ptr
createMandatoryValidator()
{
auto v = std::make_shared();
- v->setMandatory(true);
+ //sv->setMandatory(true);
return v;
}