diff options
-rw-r--r-- | http/routing.h | 65 | ||||
-rw-r--r-- | include/sessions.hpp | 19 | ||||
-rw-r--r-- | include/token_authorization_middleware.hpp | 19 | ||||
-rw-r--r-- | redfish-core/include/error_messages.hpp | 11 | ||||
-rw-r--r-- | redfish-core/include/node.hpp | 19 | ||||
-rw-r--r-- | redfish-core/include/privileges.hpp | 16 | ||||
-rw-r--r-- | redfish-core/lib/account_service.hpp | 18 | ||||
-rw-r--r-- | redfish-core/lib/redfish_sessions.hpp | 14 | ||||
-rw-r--r-- | redfish-core/src/error_messages.cpp | 26 |
9 files changed, 190 insertions, 17 deletions
diff --git a/http/routing.h b/http/routing.h index c2a7503f00..cc5c75fc8c 100644 --- a/http/routing.h +++ b/http/routing.h @@ -1,5 +1,6 @@ #pragma once +#include "error_messages.hpp" #include "privileges.hpp" #include "sessions.hpp" @@ -1287,13 +1288,77 @@ class Router << " userRole = " << *userRolePtr; } + bool* remoteUserPtr = nullptr; + auto remoteUserIter = userInfo.find("RemoteUser"); + if (remoteUserIter != userInfo.end()) + { + remoteUserPtr = std::get_if<bool>(&remoteUserIter->second); + } + if (remoteUserPtr == nullptr) + { + BMCWEB_LOG_ERROR + << "RemoteUser property missing or wrong type"; + res.result( + boost::beast::http::status::internal_server_error); + res.end(); + return; + } + bool remoteUser = *remoteUserPtr; + + bool passwordExpired = false; // default for remote user + if (!remoteUser) + { + bool* passwordExpiredPtr = nullptr; + auto passwordExpiredIter = + userInfo.find("UserPasswordExpired"); + if (passwordExpiredIter != userInfo.end()) + { + passwordExpiredPtr = + std::get_if<bool>(&passwordExpiredIter->second); + } + if (passwordExpiredPtr != nullptr) + { + passwordExpired = *passwordExpiredPtr; + } + else + { + BMCWEB_LOG_ERROR + << "UserPasswordExpired property is expected for" + " local user but is missing or wrong type"; + res.result( + boost::beast::http::status::internal_server_error); + res.end(); + return; + } + } + // Get the user privileges from the role redfish::Privileges userPrivileges = redfish::getUserPrivileges(userRole); + // Set isConfigureSelfOnly based on D-Bus results. This + // ignores the results from both pamAuthenticateUser and the + // value from any previous use of this session. + req.session->isConfigureSelfOnly = passwordExpired; + + // Modify privileges if isConfigureSelfOnly. + if (req.session->isConfigureSelfOnly) + { + // Remove all privileges except ConfigureSelf + userPrivileges = userPrivileges.intersection( + redfish::Privileges{"ConfigureSelf"}); + BMCWEB_LOG_DEBUG << "Operation limited to ConfigureSelf"; + } + if (!rules[ruleIndex]->checkPrivileges(userPrivileges)) { res.result(boost::beast::http::status::forbidden); + if (req.session->isConfigureSelfOnly) + { + redfish::messages::passwordChangeRequired( + res, "/redfish/v1/AccountService/Accounts/" + + req.session->username); + } res.end(); return; } diff --git a/include/sessions.hpp b/include/sessions.hpp index 9d24327eab..a7ffe28921 100644 --- a/include/sessions.hpp +++ b/include/sessions.hpp @@ -45,6 +45,15 @@ struct UserSession std::chrono::time_point<std::chrono::steady_clock> lastUpdated; PersistenceType persistence; bool cookieAuth = false; + bool isConfigureSelfOnly = false; + + // There are two sources of truth for isConfigureSelfOnly: + // 1. When pamAuthenticateUser() returns PAM_NEW_AUTHTOK_REQD. + // 2. D-Bus User.Manager.GetUserInfo property UserPasswordExpired. + // These should be in sync, but the underlying condition can change at any + // time. For example, a password can expire or be changed outside of + // bmcweb. The value stored here is updated at the start of each + // operation and used as the truth within bmcweb. /** * @brief Fills object with data from UserSession's JSON representation @@ -196,7 +205,8 @@ class SessionStore public: std::shared_ptr<UserSession> generateUserSession( const std::string_view username, - PersistenceType persistence = PersistenceType::TIMEOUT) + PersistenceType persistence = PersistenceType::TIMEOUT, + bool isConfigureSelfOnly = false) { // TODO(ed) find a secure way to not generate session identifiers if // persistence is set to SINGLE_REQUEST @@ -244,9 +254,10 @@ class SessionStore } } - auto session = std::make_shared<UserSession>(UserSession{ - uniqueId, sessionToken, std::string(username), csrfToken, - std::chrono::steady_clock::now(), persistence}); + auto session = std::make_shared<UserSession>( + UserSession{uniqueId, sessionToken, std::string(username), + csrfToken, std::chrono::steady_clock::now(), + persistence, false, isConfigureSelfOnly}); auto it = authTokens.emplace(std::make_pair(sessionToken, session)); // Only need to write to disk if session isn't about to be destroyed. needWrite = persistence == PersistenceType::TIMEOUT; diff --git a/include/token_authorization_middleware.hpp b/include/token_authorization_middleware.hpp index aaa1325b7a..ccea929f6f 100644 --- a/include/token_authorization_middleware.hpp +++ b/include/token_authorization_middleware.hpp @@ -138,7 +138,9 @@ class Middleware BMCWEB_LOG_DEBUG << "[AuthMiddleware] Authenticating user: " << user; - if (pamAuthenticateUser(user, pass) != PAM_SUCCESS) + int pamrc = pamAuthenticateUser(user, pass); + bool isConfigureSelfOnly = pamrc == PAM_NEW_AUTHTOK_REQD; + if ((pamrc != PAM_SUCCESS) && !isConfigureSelfOnly) { return nullptr; } @@ -150,7 +152,8 @@ class Middleware // This whole flow needs to be revisited anyway, as we can't be // calling directly into pam for every request return persistent_data::SessionStore::getInstance().generateUserSession( - user, crow::persistent_data::PersistenceType::SINGLE_REQUEST); + user, crow::persistent_data::PersistenceType::SINGLE_REQUEST, + isConfigureSelfOnly); } const std::shared_ptr<crow::persistent_data::UserSession> @@ -397,14 +400,20 @@ template <typename... Middlewares> void requestRoutes(Crow<Middlewares...>& app) if (!username.empty() && !password.empty()) { - if (pamAuthenticateUser(username, password) != PAM_SUCCESS) + int pamrc = pamAuthenticateUser(username, password); + bool isConfigureSelfOnly = pamrc == PAM_NEW_AUTHTOK_REQD; + if ((pamrc != PAM_SUCCESS) && !isConfigureSelfOnly) { res.result(boost::beast::http::status::unauthorized); } else { - auto session = persistent_data::SessionStore::getInstance() - .generateUserSession(username); + auto session = + persistent_data::SessionStore::getInstance() + .generateUserSession( + username, + crow::persistent_data::PersistenceType::TIMEOUT, + isConfigureSelfOnly); if (looksLikePhosphorRest) { diff --git a/redfish-core/include/error_messages.hpp b/redfish-core/include/error_messages.hpp index 4d717450a7..6e280c04c6 100644 --- a/redfish-core/include/error_messages.hpp +++ b/redfish-core/include/error_messages.hpp @@ -764,6 +764,17 @@ nlohmann::json queryParameterOutOfRange(const std::string& arg1, void queryParameterOutOfRange(crow::Response& res, const std::string& arg1, const std::string& arg2, const std::string& arg3); +/** + * @brief Formats PasswordChangeRequired message into JSON + * Message body: The password provided for this account must be changed + * before access is granted. PATCH the 'Password' property for this + * account located at the target URI '%1' to complete this process. + * + * @param[in] arg1 Parameter of message that will replace %1 in its body. + * + * @returns Message PasswordChangeRequired formatted to JSON */ +void passwordChangeRequired(crow::Response& res, const std::string& arg1); + } // namespace messages } // namespace redfish diff --git a/redfish-core/include/node.hpp b/redfish-core/include/node.hpp index 9086f1e0ef..a6e1e27ed9 100644 --- a/redfish-core/include/node.hpp +++ b/redfish-core/include/node.hpp @@ -169,8 +169,8 @@ class Node res.end(); } - /* @brief Would the operation be allowed if the user did not have - * the ConfigureSelf Privilege? + /* @brief Would the operation be allowed if the user did not have the + * ConfigureSelf Privilege? Also honors session.isConfigureSelfOnly. * * @param req the request * @@ -181,9 +181,18 @@ class Node const std::string& userRole = req.userRole; BMCWEB_LOG_DEBUG << "isAllowedWithoutConfigureSelf for the role " << req.userRole; - Privileges effectiveUserPrivileges = - redfish::getUserPrivileges(userRole); - effectiveUserPrivileges.resetSinglePrivilege("ConfigureSelf"); + Privileges effectiveUserPrivileges; + if (req.session && req.session->isConfigureSelfOnly) + { + // The session has no privileges because it is limited to + // configureSelfOnly and we are disregarding that privilege. + // Note that some operations do not require any privilege. + } + else + { + effectiveUserPrivileges = redfish::getUserPrivileges(userRole); + effectiveUserPrivileges.resetSinglePrivilege("ConfigureSelf"); + } const auto& requiredPrivilegesIt = entityPrivileges.find(req.method()); return (requiredPrivilegesIt != entityPrivileges.end()) && isOperationAllowedWithPrivileges(requiredPrivilegesIt->second, diff --git a/redfish-core/include/privileges.hpp b/redfish-core/include/privileges.hpp index 1ca57fad36..35f619b77a 100644 --- a/redfish-core/include/privileges.hpp +++ b/redfish-core/include/privileges.hpp @@ -196,7 +196,23 @@ class Privileges return (privilegeBitset & p.privilegeBitset) == p.privilegeBitset; } + /** + * @brief Returns the intersection of two Privilege sets. + * + * @param[in] privilege Privilege set to intersect with. + * + * @return The new Privilege set. + * + */ + Privileges intersection(const Privileges& p) const + { + return Privileges{privilegeBitset & p.privilegeBitset}; + } + private: + Privileges(const std::bitset<maxPrivilegeCount>& p) : privilegeBitset{p} + { + } std::bitset<maxPrivilegeCount> privilegeBitset = 0; }; diff --git a/redfish-core/lib/account_service.hpp b/redfish-core/lib/account_service.hpp index b579994e0b..609c7dbd23 100644 --- a/redfish-core/lib/account_service.hpp +++ b/redfish-core/lib/account_service.hpp @@ -1618,7 +1618,7 @@ class ManagerAccount : public Node *userLocked; asyncResp->res.jsonValue ["Locked@Redfish.AllowableValues"] = { - "false"}; + "false"}; // can only unlock accounts } else if (property.first == "UserPrivilege") { @@ -1647,6 +1647,22 @@ class ManagerAccount : public Node "Roles/" + role}}; } + else if (property.first == "UserPasswordExpired") + { + const bool* userPasswordExpired = + std::get_if<bool>(&property.second); + if (userPasswordExpired == nullptr) + { + BMCWEB_LOG_ERROR << "UserPassword" + "Expired " + "wasn't a bool"; + messages::internalError(asyncResp->res); + return; + } + asyncResp->res + .jsonValue["PasswordChangeRequired"] = + *userPasswordExpired; + } } } } diff --git a/redfish-core/lib/redfish_sessions.hpp b/redfish-core/lib/redfish_sessions.hpp index c3e11a3404..515f5bea63 100644 --- a/redfish-core/lib/redfish_sessions.hpp +++ b/redfish-core/lib/redfish_sessions.hpp @@ -192,7 +192,9 @@ class SessionCollection : public Node return; } - if (pamAuthenticateUser(username, password) != PAM_SUCCESS) + int pamrc = pamAuthenticateUser(username, password); + bool isConfigureSelfOnly = pamrc == PAM_NEW_AUTHTOK_REQD; + if ((pamrc != PAM_SUCCESS) && !isConfigureSelfOnly) { messages::resourceAtUriUnauthorized(res, std::string(req.url), "Invalid username or password"); @@ -204,11 +206,19 @@ class SessionCollection : public Node // User is authenticated - create session std::shared_ptr<crow::persistent_data::UserSession> session = crow::persistent_data::SessionStore::getInstance() - .generateUserSession(username); + .generateUserSession( + username, crow::persistent_data::PersistenceType::TIMEOUT, + isConfigureSelfOnly); res.addHeader("X-Auth-Token", session->sessionToken); res.addHeader("Location", "/redfish/v1/SessionService/Sessions/" + session->uniqueId); res.result(boost::beast::http::status::created); + if (session->isConfigureSelfOnly) + { + messages::passwordChangeRequired( + res, + "/redfish/v1/AccountService/Accounts/" + session->username); + } memberSession.doGet(res, req, {session->uniqueId}); } diff --git a/redfish-core/src/error_messages.cpp b/redfish-core/src/error_messages.cpp index bc5ba77062..4be2687535 100644 --- a/redfish-core/src/error_messages.cpp +++ b/redfish-core/src/error_messages.cpp @@ -1699,6 +1699,32 @@ void queryParameterOutOfRange(crow::Response& res, const std::string& arg1, queryParameterOutOfRange(arg1, arg2, arg3)); } +/** + * @internal + * @brief Formats PasswordChangeRequired message into JSON + * + * See header file for more information + * @endinternal + */ +void passwordChangeRequired(crow::Response& res, const std::string& arg1) +{ + messages::addMessageToJsonRoot( + res.jsonValue, + nlohmann::json{ + {"@odata.type", "/redfish/v1/$metadata#Message.v1_5_0.Message"}, + {"MessageId", "Base.1.5.0.PasswordChangeRequired"}, + {"Message", "The password provided for this account must be " + "changed before access is granted. PATCH the " + "'Password' property for this account located at " + "the target URI '" + + arg1 + "' to complete this process."}, + {"MessageArgs", {arg1}}, + {"Severity", "Critical"}, + {"Resolution", "Change the password for this account using " + "a PATCH to the 'Password' property at the URI " + "provided."}}); +} + } // namespace messages } // namespace redfish |