diff options
author | Kowalski, Kamil <kamil.kowalski@intel.com> | 2018-01-31 15:24:59 +0300 |
---|---|---|
committer | Ed Tanous <ed.tanous@intel.com> | 2018-02-06 02:40:03 +0300 |
commit | 2b7981f6e53f76c662d427ced8bd9cffc5dde695 (patch) | |
tree | 22412d4e43c4a963e01067487c6f9d1175d5409e | |
parent | 109799e512a6abcad3303dffa0505fcf2a521a1a (diff) | |
download | bmcweb-2b7981f6e53f76c662d427ced8bd9cffc5dde695.tar.xz |
Session and SessionCollection
New Redfish-Core nodes added (removed from redfish_v1.hpp) - Session
and SessionCollection. Tested manually on x86 VM and Wolfpass Platform.
Behavior almost identical to what was before - differences:
- SessionCollection - now only returns TIMEOUT presistence sessions, not SINGLE
- Aquiring sessions from session storage now applies timeouts
Change-Id: I68bf4fa7fa1c8371216a7d4daa30bbfb653cfa72
Signed-off-by: Kowalski, Kamil <kamil.kowalski@intel.com>
-rw-r--r-- | include/persistent_data_middleware.hpp | 173 | ||||
-rw-r--r-- | include/redfish_v1.hpp | 114 | ||||
-rw-r--r-- | include/session_storage_singleton.hpp | 10 | ||||
-rw-r--r-- | include/sessions.hpp | 181 | ||||
-rw-r--r-- | include/token_authorization_middleware.hpp | 77 | ||||
-rw-r--r-- | redfish-core/include/redfish.hpp | 4 | ||||
-rw-r--r-- | redfish-core/lib/redfish_sessions.hpp | 226 | ||||
-rw-r--r-- | src/webserver_main.cpp | 4 |
8 files changed, 466 insertions, 323 deletions
diff --git a/include/persistent_data_middleware.hpp b/include/persistent_data_middleware.hpp index b52e2258d5..9d6195c578 100644 --- a/include/persistent_data_middleware.hpp +++ b/include/persistent_data_middleware.hpp @@ -4,6 +4,7 @@ #include <pam_authenticate.hpp> #include <webassets.hpp> #include <random> +#include "session_storage_singleton.hpp" #include <crow/app.h> #include <crow/http_request.h> #include <crow/http_response.h> @@ -16,182 +17,23 @@ namespace crow { namespace PersistentData { -enum class PersistenceType { - TIMEOUT, // User session times out after a predetermined amount of time - SINGLE_REQUEST // User times out once this request is completed. -}; - -struct UserSession { - std::string unique_id; - std::string session_token; - std::string username; - std::string csrf_token; - std::chrono::time_point<std::chrono::steady_clock> last_updated; - PersistenceType persistence; -}; - -void to_json(nlohmann::json& j, const UserSession& p) { - if (p.persistence != PersistenceType::SINGLE_REQUEST) { - j = nlohmann::json{{"unique_id", p.unique_id}, - {"session_token", p.session_token}, - {"username", p.username}, - {"csrf_token", p.csrf_token}}; - } -} - -void from_json(const nlohmann::json& j, UserSession& p) { - try { - p.unique_id = j.at("unique_id").get<std::string>(); - p.session_token = j.at("session_token").get<std::string>(); - p.username = j.at("username").get<std::string>(); - p.csrf_token = j.at("csrf_token").get<std::string>(); - // For now, sessions that were persisted through a reboot get their timer - // reset. This could probably be overcome with a better understanding of - // wall clock time and steady timer time, possibly persisting values with - // wall clock time instead of steady timer, but the tradeoffs of all the - // corner cases involved are non-trivial, so this is done temporarily - p.last_updated = std::chrono::steady_clock::now(); - } catch (std::out_of_range) { - // do nothing. Session API incompatibility, leave sessions empty - } -} - -class Middleware; - -class SessionStore { - public: - const UserSession& generate_user_session( - const std::string& username, - PersistenceType persistence = PersistenceType::TIMEOUT) { - // TODO(ed) find a secure way to not generate session identifiers if - // persistence is set to SINGLE_REQUEST - static constexpr std::array<char, 62> alphanum = { - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', - 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', - 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', - 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', - 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}; - - // entropy: 30 characters, 62 possibilities. log2(62^30) = 178 bits of - // entropy. OWASP recommends at least 60 - // https://www.owasp.org/index.php/Session_Management_Cheat_Sheet#Session_ID_Entropy - std::string session_token; - session_token.resize(20, '0'); - std::uniform_int_distribution<int> dist(0, alphanum.size() - 1); - for (int i = 0; i < session_token.size(); ++i) { - session_token[i] = alphanum[dist(rd)]; - } - // Only need csrf tokens for cookie based auth, token doesn't matter - std::string csrf_token; - csrf_token.resize(20, '0'); - for (int i = 0; i < csrf_token.size(); ++i) { - csrf_token[i] = alphanum[dist(rd)]; - } - - std::string unique_id; - unique_id.resize(10, '0'); - for (int i = 0; i < unique_id.size(); ++i) { - unique_id[i] = alphanum[dist(rd)]; - } - - const auto session_it = auth_tokens.emplace( - session_token, - std::move(UserSession{unique_id, session_token, username, csrf_token, - std::chrono::steady_clock::now(), persistence})); - const UserSession& user = (session_it).first->second; - // Only need to write to disk if session isn't about to be destroyed. - need_write_ = persistence == PersistenceType::TIMEOUT; - return user; - } - - const UserSession* login_session_by_token(const std::string& token) { - apply_session_timeouts(); - auto session_it = auth_tokens.find(token); - if (session_it == auth_tokens.end()) { - return nullptr; - } - UserSession& foo = session_it->second; - foo.last_updated = std::chrono::steady_clock::now(); - return &foo; - } - - const UserSession* get_session_by_uid(const std::string& uid) { - apply_session_timeouts(); - // TODO(Ed) this is inefficient - auto session_it = auth_tokens.begin(); - while (session_it != auth_tokens.end()) { - if (session_it->second.unique_id == uid) { - return &session_it->second; - } - session_it++; - } - return nullptr; - } - - void remove_session(const UserSession* session) { - auth_tokens.erase(session->session_token); - need_write_ = true; - } - - std::vector<const std::string*> get_unique_ids() { - std::vector<const std::string*> ret; - ret.reserve(auth_tokens.size()); - for (auto& session : auth_tokens) { - ret.push_back(&session.second.unique_id); - } - return ret; - } - - bool needs_write() { return need_write_; } - - // Persistent data middleware needs to be able to serialize our auth_tokens - // structure, which is private - friend Middleware; - - private: - void apply_session_timeouts() { - std::chrono::minutes timeout(60); - auto time_now = std::chrono::steady_clock::now(); - if (time_now - last_timeout_update > std::chrono::minutes(1)) { - last_timeout_update = time_now; - auto auth_tokens_it = auth_tokens.begin(); - while (auth_tokens_it != auth_tokens.end()) { - if (time_now - auth_tokens_it->second.last_updated >= timeout) { - auth_tokens_it = auth_tokens.erase(auth_tokens_it); - need_write_ = true; - } else { - auth_tokens_it++; - } - } - } - } - std::chrono::time_point<std::chrono::steady_clock> last_timeout_update; - boost::container::flat_map<std::string, UserSession> auth_tokens; - std::random_device rd; - bool need_write_{false}; -}; - class Middleware { // todo(ed) should read this from a fixed location somewhere, not CWD static constexpr const char* filename = "bmcweb_persistent_data.json"; int json_revision = 1; public: - struct context { - SessionStore* sessions; - }; + struct context {}; Middleware() { read_data(); } ~Middleware() { - if (sessions.needs_write()) { + if (PersistentData::session_store->needs_write()) { write_data(); } } - void before_handle(crow::request& req, response& res, context& ctx) { - ctx.sessions = &sessions; - } + void before_handle(crow::request& req, response& res, context& ctx) {} void after_handle(request& req, response& res, context& ctx) {} @@ -206,8 +48,8 @@ class Middleware { auto data = nlohmann::json::parse(persistent_file, nullptr, false); if (!data.is_discarded()) { file_revision = data.value("revision", 0); - sessions.auth_tokens = - data.value("sessions", decltype(sessions.auth_tokens)()); + PersistentData::session_store->auth_tokens = + data.value("sessions", decltype(session_store->auth_tokens)()); system_uuid = data.value("system_uuid", ""); } } @@ -229,13 +71,12 @@ class Middleware { void write_data() { std::ofstream persistent_file(filename); nlohmann::json data; - data["sessions"] = sessions.auth_tokens; + data["sessions"] = PersistentData::session_store->auth_tokens; data["system_uuid"] = system_uuid; data["revision"] = json_revision; persistent_file << data; } - SessionStore sessions; std::string system_uuid; }; diff --git a/include/redfish_v1.hpp b/include/redfish_v1.hpp index 1a5ee74eee..3aecae09fe 100644 --- a/include/redfish_v1.hpp +++ b/include/redfish_v1.hpp @@ -168,9 +168,8 @@ void request_routes(Crow<Middlewares...>& app) { for (int user_index = 0; user_index < users.size(); user_index++) { member_array.push_back( - {{"@odata.id", - "/redfish/v1/AccountService/Accounts/" + - std::to_string(user_index)}}); + {{"@odata.id", "/redfish/v1/AccountService/Accounts/" + + std::to_string(user_index)}}); } res.json_value["Members"] = member_array; } @@ -222,110 +221,6 @@ void request_routes(Crow<Middlewares...>& app) { res.end(); }); - CROW_ROUTE(app, "/redfish/v1/SessionService/Sessions/") - .methods("POST"_method, "GET"_method)([&](const crow::request& req, - crow::response& res) { - auto& session_store = - app.template get_middleware<PersistentData::Middleware>().sessions; - if (req.method == "POST"_method) { - // call with exceptions disabled - auto login_credentials = - nlohmann::json::parse(req.body, nullptr, false); - if (login_credentials.is_discarded()) { - res.code = 400; - res.end(); - return; - } - // check for username/password in the root object - auto user_it = login_credentials.find("UserName"); - auto pass_it = login_credentials.find("Password"); - if (user_it == login_credentials.end() || - pass_it == login_credentials.end()) { - res.code = 400; - res.end(); - return; - } - - std::string username = user_it->get<const std::string>(); - std::string password = pass_it->get<const std::string>(); - if (username.empty() || password.empty()) { - res.code = 400; - res.end(); - return; - } - - if (!pam_authenticate_user(username, password)) { - res.code = 401; - res.end(); - return; - } - auto session = session_store.generate_user_session(username); - res.code = 200; - res.add_header("X-Auth-Token", session.session_token); - res.json_value = { - {"@odata.context", "/redfish/v1/$metadata#Session"}, - {"@odata.id", - "/redfish/v1/SessionService/Sessions/" + session.unique_id}, - {"@odata.type", "#Session.v1_0_3.Session"}, - {"Id", session.unique_id}, - {"Name", "User Session"}, - {"Description", "Manager User Session"}, - {"UserName", username}}; - } else { // assume get - std::vector<const std::string*> session_ids = - session_store.get_unique_ids(); - res.json_value = { - {"@odata.context", - "/redfish/v1/$metadata#SessionCollection.SessionCollection"}, - {"@odata.id", "/redfish/v1/SessionService/Sessions"}, - {"@odata.type", "#SessionCollection.SessionCollection"}, - {"Name", "Session Collection"}, - {"Description", "Session Collection"}, - {"Members@odata.count", session_ids.size()} - - }; - nlohmann::json member_array = nlohmann::json::array(); - for (auto session_uid : session_ids) { - member_array.push_back( - {{"@odata.id", - "/redfish/v1/SessionService/Sessions/" + *session_uid}}); - } - res.json_value["Members"] = member_array; - } - res.end(); - }); - - CROW_ROUTE(app, "/redfish/v1/SessionService/Sessions/<str>/") - .methods("GET"_method, "DELETE"_method)([&]( - const crow::request& req, crow::response& res, - const std::string& session_id) { - auto& session_store = - app.template get_middleware<PersistentData::Middleware>().sessions; - // TODO(Ed) this is inefficient - auto session = session_store.get_session_by_uid(session_id); - - if (session == nullptr) { - res.code = 404; - res.end(); - return; - } - if (req.method == "DELETE"_method) { - session_store.remove_session(session); - res.code = 200; - } else { // assume get - res.json_value = { - {"@odata.context", "/redfish/v1/$metadata#Session.Session"}, - {"@odata.id", - "/redfish/v1/SessionService/Sessions/" + session->unique_id}, - {"@odata.type", "#Session.v1_0_3.Session"}, - {"Id", session->unique_id}, - {"Name", "User Session"}, - {"Description", "Manager User Session"}, - {"UserName", session->username}}; - } - res.end(); - }); - CROW_ROUTE(app, "/redfish/v1/Managers/") .methods("GET"_method)( [&](const crow::request& req, crow::response& res) { @@ -365,9 +260,8 @@ void request_routes(Crow<Middlewares...>& app) { {"Id", "openbmc"}, {"Name", "OpenBmc Manager"}, {"Description", "Baseboard Management Controller"}, - {"UUID", - app.template get_middleware<PersistentData::Middleware>() - .system_uuid}, + {"UUID", app.template get_middleware<PersistentData::Middleware>() + .system_uuid}, {"Model", "OpenBmc"}, // TODO(ed), get model {"DateTime", time_buffer.data()}, {"Status", diff --git a/include/session_storage_singleton.hpp b/include/session_storage_singleton.hpp new file mode 100644 index 0000000000..6ff8a0e303 --- /dev/null +++ b/include/session_storage_singleton.hpp @@ -0,0 +1,10 @@ +#pragma once +#include "sessions.hpp" + +namespace crow { +namespace PersistentData { + +static std::shared_ptr<SessionStore> session_store; + +} // namespace PersistentData +} // namespace crow diff --git a/include/sessions.hpp b/include/sessions.hpp new file mode 100644 index 0000000000..6d4ab4da57 --- /dev/null +++ b/include/sessions.hpp @@ -0,0 +1,181 @@ +#pragma once + +#include <nlohmann/json.hpp> +#include <pam_authenticate.hpp> +#include <webassets.hpp> +#include <random> +#include <crow/app.h> +#include <crow/http_request.h> +#include <crow/http_response.h> +#include <boost/container/flat_map.hpp> +#include <boost/uuid/uuid.hpp> +#include <boost/uuid/uuid_generators.hpp> +#include <boost/uuid/uuid_io.hpp> + +namespace crow { + +namespace PersistentData { + +enum class PersistenceType { + TIMEOUT, // User session times out after a predetermined amount of time + SINGLE_REQUEST // User times out once this request is completed. +}; + +struct UserSession { + std::string unique_id; + std::string session_token; + std::string username; + std::string csrf_token; + std::chrono::time_point<std::chrono::steady_clock> last_updated; + PersistenceType persistence; +}; + +void to_json(nlohmann::json& j, const UserSession& p) { + if (p.persistence != PersistenceType::SINGLE_REQUEST) { + j = nlohmann::json{{"unique_id", p.unique_id}, + {"session_token", p.session_token}, + {"username", p.username}, + {"csrf_token", p.csrf_token}}; + } +} + +void from_json(const nlohmann::json& j, UserSession& p) { + try { + p.unique_id = j.at("unique_id").get<std::string>(); + p.session_token = j.at("session_token").get<std::string>(); + p.username = j.at("username").get<std::string>(); + p.csrf_token = j.at("csrf_token").get<std::string>(); + // For now, sessions that were persisted through a reboot get their timer + // reset. This could probably be overcome with a better understanding of + // wall clock time and steady timer time, possibly persisting values with + // wall clock time instead of steady timer, but the tradeoffs of all the + // corner cases involved are non-trivial, so this is done temporarily + p.last_updated = std::chrono::steady_clock::now(); + } catch (std::out_of_range) { + // do nothing. Session API incompatibility, leave sessions empty + } +} + +class Middleware; + +class SessionStore { + public: + const UserSession& generate_user_session( + const std::string& username, + PersistenceType persistence = PersistenceType::TIMEOUT) { + // TODO(ed) find a secure way to not generate session identifiers if + // persistence is set to SINGLE_REQUEST + static constexpr std::array<char, 62> alphanum = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', + 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', + 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', + 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}; + + // entropy: 30 characters, 62 possibilities. log2(62^30) = 178 bits of + // entropy. OWASP recommends at least 60 + // https://www.owasp.org/index.php/Session_Management_Cheat_Sheet#Session_ID_Entropy + std::string session_token; + session_token.resize(20, '0'); + std::uniform_int_distribution<int> dist(0, alphanum.size() - 1); + for (int i = 0; i < session_token.size(); ++i) { + session_token[i] = alphanum[dist(rd)]; + } + // Only need csrf tokens for cookie based auth, token doesn't matter + std::string csrf_token; + csrf_token.resize(20, '0'); + for (int i = 0; i < csrf_token.size(); ++i) { + csrf_token[i] = alphanum[dist(rd)]; + } + + std::string unique_id; + unique_id.resize(10, '0'); + for (int i = 0; i < unique_id.size(); ++i) { + unique_id[i] = alphanum[dist(rd)]; + } + + const auto session_it = auth_tokens.emplace( + session_token, + std::move(UserSession{unique_id, session_token, username, csrf_token, + std::chrono::steady_clock::now(), persistence})); + const UserSession& user = (session_it).first->second; + // Only need to write to disk if session isn't about to be destroyed. + need_write_ = persistence == PersistenceType::TIMEOUT; + return user; + } + + const UserSession* login_session_by_token(const std::string& token) { + apply_session_timeouts(); + auto session_it = auth_tokens.find(token); + if (session_it == auth_tokens.end()) { + return nullptr; + } + UserSession& foo = session_it->second; + foo.last_updated = std::chrono::steady_clock::now(); + return &foo; + } + + const UserSession* get_session_by_uid(const std::string& uid) { + apply_session_timeouts(); + // TODO(Ed) this is inefficient + auto session_it = auth_tokens.begin(); + while (session_it != auth_tokens.end()) { + if (session_it->second.unique_id == uid) { + return &session_it->second; + } + session_it++; + } + return nullptr; + } + + void remove_session(const UserSession* session) { + auth_tokens.erase(session->session_token); + need_write_ = true; + } + + std::vector<const std::string*> get_unique_ids( + bool getAll = true, + const PersistenceType& type = PersistenceType::SINGLE_REQUEST) { + apply_session_timeouts(); + + std::vector<const std::string*> ret; + ret.reserve(auth_tokens.size()); + for (auto& session : auth_tokens) { + if (getAll || type == session.second.persistence) { + ret.push_back(&session.second.unique_id); + } + } + return ret; + } + + bool needs_write() { return need_write_; } + + // Persistent data middleware needs to be able to serialize our auth_tokens + // structure, which is private + friend Middleware; + + private: + void apply_session_timeouts() { + std::chrono::minutes timeout(60); + auto time_now = std::chrono::steady_clock::now(); + if (time_now - last_timeout_update > std::chrono::minutes(1)) { + last_timeout_update = time_now; + auto auth_tokens_it = auth_tokens.begin(); + while (auth_tokens_it != auth_tokens.end()) { + if (time_now - auth_tokens_it->second.last_updated >= timeout) { + auth_tokens_it = auth_tokens.erase(auth_tokens_it); + need_write_ = true; + } else { + auth_tokens_it++; + } + } + } + } + std::chrono::time_point<std::chrono::steady_clock> last_timeout_update; + boost::container::flat_map<std::string, UserSession> auth_tokens; + std::random_device rd; + bool need_write_{false}; +}; + +} // namespaec PersistentData +} // namespace crow diff --git a/include/token_authorization_middleware.hpp b/include/token_authorization_middleware.hpp index 809902022f..7497b43f9f 100644 --- a/include/token_authorization_middleware.hpp +++ b/include/token_authorization_middleware.hpp @@ -20,23 +20,20 @@ class Middleware { struct context { const crow::PersistentData::UserSession* session; }; - template <typename AllContext> - void before_handle(crow::request& req, response& res, context& ctx, - AllContext& allctx) { - auto& sessions = - allctx.template get<crow::PersistentData::Middleware>().sessions; + + void before_handle(crow::request& req, response& res, context& ctx) { std::string auth_header = req.get_header_value("Authorization"); if (auth_header != "") { // Reject any kind of auth other than basic or token if (boost::starts_with(auth_header, "Basic ")) { - ctx.session = perform_basic_auth(auth_header, sessions); + ctx.session = perform_basic_auth(auth_header); } else if (boost::starts_with(auth_header, "Token ")) { - ctx.session = perform_token_auth(auth_header, sessions); + ctx.session = perform_token_auth(auth_header); } } else if (req.headers.count("X-Auth-Token") == 1) { - ctx.session = perform_xtoken_auth(req, sessions); + ctx.session = perform_xtoken_auth(req); } else if (req.headers.count("Cookie") == 1) { - ctx.session = perform_cookie_auth(req, sessions); + ctx.session = perform_cookie_auth(req); } if (ctx.session == nullptr && !is_on_whitelist(req)) { @@ -61,17 +58,13 @@ class Middleware { if (ctx.session != nullptr && ctx.session->persistence == crow::PersistentData::PersistenceType::SINGLE_REQUEST) { - auto& session_store = - allctx.template get<crow::PersistentData::Middleware>().sessions; - - session_store->remove_session(ctx.session); + PersistentData::session_store->remove_session(ctx.session); } } private: const crow::PersistentData::UserSession* perform_basic_auth( - const std::string& auth_header, - crow::PersistentData::SessionStore* sessions) const { + const std::string& auth_header) const { CROW_LOG_DEBUG << "[AuthMiddleware] Basic authentication"; std::string auth_data; @@ -103,33 +96,30 @@ class Middleware { // needed. // This whole flow needs to be revisited anyway, as we can't be // calling directly into pam for every request - return &(sessions->generate_user_session( + return &(PersistentData::session_store->generate_user_session( user, crow::PersistentData::PersistenceType::SINGLE_REQUEST)); } const crow::PersistentData::UserSession* perform_token_auth( - const std::string& auth_header, - crow::PersistentData::SessionStore* sessions) const { + const std::string& auth_header) const { CROW_LOG_DEBUG << "[AuthMiddleware] Token authentication"; std::string token = auth_header.substr(strlen("Token ")); - auto session = sessions->login_session_by_token(token); + auto session = PersistentData::session_store->login_session_by_token(token); return session; } const crow::PersistentData::UserSession* perform_xtoken_auth( - const crow::request& req, - crow::PersistentData::SessionStore* sessions) const { + const crow::request& req) const { CROW_LOG_DEBUG << "[AuthMiddleware] X-Auth-Token authentication"; auto& token = req.get_header_value("X-Auth-Token"); - auto session = sessions->login_session_by_token(token); + auto session = PersistentData::session_store->login_session_by_token(token); return session; } const crow::PersistentData::UserSession* perform_cookie_auth( - const crow::request& req, - crow::PersistentData::SessionStore* sessions) const { + const crow::request& req) const { CROW_LOG_DEBUG << "[AuthMiddleware] Cookie authentication"; auto& cookie_value = req.get_header_value("Cookie"); @@ -147,7 +137,7 @@ class Middleware { cookie_value.substr(start_index, end_index - start_index); const crow::PersistentData::UserSession* session = - sessions->login_session_by_token(auth_key); + PersistentData::session_store->login_session_by_token(auth_key); if (session == nullptr) { return nullptr; } @@ -266,10 +256,8 @@ void request_routes(Crow<Middlewares...>& app) { if (!pam_authenticate_user(username, password)) { res.code = res.code = static_cast<int>(HttpRespCode::UNAUTHORIZED); } else { - auto& context = - app.template get_context<PersistentData::Middleware>(req); - auto& session_store = context.sessions; - auto& session = session_store->generate_user_session(username); + auto& session = + PersistentData::session_store->generate_user_session(username); if (looks_like_ibm) { // IBM requires a very specific login structure, and doesn't @@ -279,9 +267,8 @@ void request_routes(Crow<Middlewares...>& app) { {"message", "200 OK"}, {"status", "ok"}}; res.add_header("Set-Cookie", "XSRF-TOKEN=" + session.csrf_token); - res.add_header( - "Set-Cookie", - "SESSION=" + session.session_token + "; Secure; HttpOnly"); + res.add_header("Set-Cookie", "SESSION=" + session.session_token + + "; Secure; HttpOnly"); res.write(ret.dump()); } else { @@ -300,21 +287,19 @@ void request_routes(Crow<Middlewares...>& app) { }); CROW_ROUTE(app, "/logout") - .methods( - "POST"_method)([&](const crow::request& req, crow::response& res) { - auto& session_store = - app.template get_context<PersistentData::Middleware>(req).sessions; - auto& session = - app.template get_context<TokenAuthorization::Middleware>(req) - .session; - if (session != nullptr) { - session_store->remove_session(session); - } - res.code = static_cast<int>(HttpRespCode::OK); - res.end(); - return; + .methods("POST"_method)( + [&](const crow::request& req, crow::response& res) { + auto& session = + app.template get_context<TokenAuthorization::Middleware>(req) + .session; + if (session != nullptr) { + PersistentData::session_store->remove_session(session); + } + res.code = static_cast<int>(HttpRespCode::OK); + res.end(); + return; - }); + }); } } // namespaec TokenAuthorization } // namespace crow diff --git a/redfish-core/include/redfish.hpp b/redfish-core/include/redfish.hpp index c4b765c982..c26d90beec 100644 --- a/redfish-core/include/redfish.hpp +++ b/redfish-core/include/redfish.hpp @@ -15,6 +15,7 @@ */ #pragma once +#include "../lib/redfish_sessions.hpp" #include "../lib/service_root.hpp" namespace redfish { @@ -34,10 +35,13 @@ class RedfishService { RedfishService(CrowApp& app) { auto privilegeProvider = PrivilegeProvider(); serviceRootPtr = std::make_unique<ServiceRoot>(app, privilegeProvider); + sessionsCollectionPtr = + std::make_unique<SessionCollection>(app, privilegeProvider); } private: std::unique_ptr<ServiceRoot> serviceRootPtr; + std::unique_ptr<SessionCollection> sessionsCollectionPtr; }; } // namespace redfish diff --git a/redfish-core/lib/redfish_sessions.hpp b/redfish-core/lib/redfish_sessions.hpp new file mode 100644 index 0000000000..0835fa18a8 --- /dev/null +++ b/redfish-core/lib/redfish_sessions.hpp @@ -0,0 +1,226 @@ +/* +// Copyright (c) 2018 Intel Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +*/ +#pragma once +#include <tuple> +#include "node.hpp" +#include "session_storage_singleton.hpp" + +namespace redfish { + +class SessionCollection; + +class Sessions : public Node { + public: + template <typename CrowApp, typename PrivilegeProvider> + Sessions(CrowApp& app, PrivilegeProvider& provider) + : Node(app, provider, "#Session.v1_0_2.Session", + "/redfish/v1/SessionService/Sessions/<str>", std::string()) { + nodeJson["@odata.type"] = Node::odataType; + nodeJson["@odata.context"] = "/redfish/v1/$metadata#Session.Session"; + nodeJson["Name"] = "User Session"; + nodeJson["Description"] = "Manager User Session"; + } + + private: + void doGet(crow::response& res, const crow::request& req, + const std::vector<std::string>& params) override { + auto session = + crow::PersistentData::session_store->get_session_by_uid(params[0]); + + if (session == nullptr) { + res.code = static_cast<int>(HttpRespCode::NOT_FOUND); + res.end(); + return; + } + + nodeJson["Id"] = session->unique_id; + nodeJson["UserName"] = session->username; + nodeJson["@odata.id"] = + "/redfish/v1/SessionService/Sessions/" + session->unique_id; + + res.json_value = nodeJson; + res.end(); + } + + void doDelete(crow::response& res, const crow::request& req, + const std::vector<std::string>& params) override { + // Need only 1 param which should be id of session to be deleted + if (params.size() != 1) { + res.code = static_cast<int>(HttpRespCode::BAD_REQUEST); + res.end(); + return; + } + + auto session = + crow::PersistentData::session_store->get_session_by_uid(params[0]); + + if (session == nullptr) { + res.code = static_cast<int>(HttpRespCode::NOT_FOUND); + res.end(); + return; + } + + crow::PersistentData::session_store->remove_session(session); + res.code = static_cast<int>(HttpRespCode::OK); + res.end(); + } + + /** + * This allows SessionCollection to reuse this class' doGet method, to + * maintain consistency of returned data, as Collection's doPost should return + * data for created member which should match member's doGet result in 100% + */ + friend SessionCollection; + + nlohmann::json nodeJson; +}; + +class SessionCollection : public Node { + public: + template <typename CrowApp, typename PrivilegeProvider> + SessionCollection(CrowApp& app, PrivilegeProvider& provider) + : Node(app, provider, "#SessionCollection.SessionCollection", + "/redfish/v1/SessionService/Sessions/"), + memberSession(app, provider) { + nodeJson["@odata.type"] = Node::odataType; + nodeJson["@odata.id"] = Node::odataId; + nodeJson["@odata.context"] = + "/redfish/v1/$metadata#SessionCollection.SessionCollection"; + nodeJson["Name"] = "Session Collection"; + nodeJson["Description"] = "Session Collection"; + nodeJson["Members@odata.count"] = 0; + nodeJson["Members"] = nlohmann::json::array(); + } + + private: + void doGet(crow::response& res, const crow::request& req, + const std::vector<std::string>& params) override { + std::vector<const std::string*> session_ids = + crow::PersistentData::session_store->get_unique_ids( + false, crow::PersistentData::PersistenceType::TIMEOUT); + + nodeJson["Members@odata.count"] = session_ids.size(); + nodeJson["Members"] = nlohmann::json::array(); + for (const auto& uid : session_ids) { + nodeJson["Members"].push_back( + {{"@odata.id", "/redfish/v1/SessionService/Sessions/" + *uid}}); + } + + res.json_value = nodeJson; + res.end(); + } + + void doPost(crow::response& res, const crow::request& req, + const std::vector<std::string>& params) override { + std::string username; + bool userAuthSuccessful = authenticateUser(req, &res.code, &username); + + if (!userAuthSuccessful) { + res.end(); + return; + } + + // User is authenticated - create session for him + auto session = + crow::PersistentData::session_store->generate_user_session(username); + res.add_header("X-Auth-Token", session.session_token); + + // Return data for created session + memberSession.doGet(res, req, {session.unique_id}); + + // No need for res.end(), as it is called by doGet() + } + + /** + * @brief Verifies data provided in request and tries to authenticate user + * + * @param[in] req Crow request containing authentication data + * @param[out] httpRespCode HTTP Code that should be returned in response + * @param[out] user Retrieved username - not filled on failure + * + * @return true if authentication was successful, false otherwise + */ + bool authenticateUser(const crow::request& req, int* httpRespCode, + std::string* user) { + // We need only UserName and Password - nothing more, nothing less + static constexpr const unsigned int numberOfRequiredFieldsInReq = 2; + + // call with exceptions disabled + auto login_credentials = nlohmann::json::parse(req.body, nullptr, false); + if (login_credentials.is_discarded()) { + *httpRespCode = static_cast<int>(HttpRespCode::BAD_REQUEST); + + return false; + } + + // Check that there are only as many fields as there should be + if (login_credentials.size() != numberOfRequiredFieldsInReq) { + *httpRespCode = static_cast<int>(HttpRespCode::BAD_REQUEST); + + return false; + } + + // Find fields that we need - UserName and Password + auto user_it = login_credentials.find("UserName"); + auto pass_it = login_credentials.find("Password"); + if (user_it == login_credentials.end() || + pass_it == login_credentials.end()) { + *httpRespCode = static_cast<int>(HttpRespCode::BAD_REQUEST); + + return false; + } + + // Check that given data is of valid type (string) + if (!user_it->is_string() || !pass_it->is_string()) { + *httpRespCode = static_cast<int>(HttpRespCode::BAD_REQUEST); + + return false; + } + + // Extract username and password + std::string username = user_it->get<const std::string>(); + std::string password = pass_it->get<const std::string>(); + + // Verify that required fields are not empty + if (username.empty() || password.empty()) { + *httpRespCode = static_cast<int>(HttpRespCode::BAD_REQUEST); + + return false; + } + + // Finally - try to authenticate user + if (!pam_authenticate_user(username, password)) { + *httpRespCode = static_cast<int>(HttpRespCode::UNAUTHORIZED); + + return false; + } + + // User authenticated successfully + *httpRespCode = static_cast<int>(HttpRespCode::OK); + *user = username; + + return true; + } + + /** + * Member session to ensure consistency between collection's doPost and + * member's doGet, as they should return 100% matching data + */ + Sessions memberSession; + nlohmann::json nodeJson; +}; + +} // namespace redfish diff --git a/src/webserver_main.cpp b/src/webserver_main.cpp index 834a5df41e..f504cc7ed8 100644 --- a/src/webserver_main.cpp +++ b/src/webserver_main.cpp @@ -12,10 +12,10 @@ #include <webassets.hpp> #include <memory> #include <string> +#include "redfish.hpp" #include <crow/app.h> #include <boost/asio.hpp> #include <systemd/sd-daemon.h> -#include "redfish.hpp" constexpr int defaultPort = 18080; @@ -41,6 +41,8 @@ void setup_socket(crow::Crow<Middlewares...>& app) { int main(int argc, char** argv) { auto io = std::make_shared<boost::asio::io_service>(); + crow::PersistentData::session_store = + std::make_shared<crow::PersistentData::SessionStore>(); crow::App<crow::PersistentData::Middleware, crow::TokenAuthorization::Middleware, crow::SecurityHeadersMiddleware> |