| #include "redfish_session_auth.h" |
| |
| #include <cassert> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/base/thread_annotations.h" |
| #include "absl/cleanup/cleanup.h" |
| #include "absl/log/log.h" |
| #include "absl/status/status.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/escaping.h" |
| #include "absl/strings/match.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/str_format.h" |
| #include "absl/strings/str_join.h" |
| #include "absl/strings/string_view.h" |
| #include "absl/synchronization/mutex.h" |
| #include "absl/time/clock.h" |
| #include "absl/time/time.h" |
| #include <source_location> |
| #include "redfish_query_engine/http/codes.h" |
| #include "nlohmann/json_fwd.hpp" |
| #include "metrics.h" |
| #include "proxy.h" |
| #include "proxy_builder.h" |
| #include "redfish_plugin.h" |
| #include "remote_credentials.h" |
| #include "request_response.h" |
| |
| namespace milotic { |
| |
| static void InsertBasicAuth(ProxyRequest* request, absl::string_view username, |
| absl::string_view password) { |
| std::string encoded; |
| absl::Base64Escape(absl::StrCat(username, ":", password), &encoded); |
| request->headers["Authorization"] = absl::StrCat("Basic ", encoded); |
| } |
| |
| // Mark sessions as expired slightly before the timeout so we don't fail if a |
| // request start just before the session expires |
| constexpr absl::Duration kSessionExpiryMargin = absl::Seconds(10); |
| |
| RedfishPlugin::RequestAction RedfishSessionAuthPlugin::PreprocessRequest( |
| RedfishPlugin::RequestVerb verb, ProxyRequest& request) { |
| if (verb == RequestVerb::kInternal) { |
| if (request.uri == proxy_->GetUrl(SessionStateResourcePath())) { |
| return RequestAction::kHandle; |
| } |
| return RequestAction::kNext; |
| } |
| std::optional<absl::string_view> token; |
| if (!only_use_basic_auth_) { |
| bool force_refresh_token = false; |
| if (auto found_force = request.headers.find("Force-Refresh-Token"); |
| found_force != request.headers.end()) { |
| force_refresh_token = absl::EqualsIgnoreCase(found_force->second, "true"); |
| } |
| token = MaybeGetNewToken(force_refresh_token); |
| } else { |
| // The password file may never have been read so far |
| absl::Status status = credentials_file_manager_.RunLogin( |
| proxy_->GetNetworkEndpoint(), |
| [this](const absl::StatusOr<RemoteCredentials>& credentials) { |
| // Ideally, we should return an error if the credentials were |
| // rejected, but this can only be determined after the request |
| // completes if using basic auth. |
| absl::MutexLock lock(&token_state_mutex_); |
| return LoadCredentials(credentials); |
| }); |
| if (!status.ok()) { |
| LOG(WARNING) << "Failed to load credentials for basic auth: " << status; |
| // Continuing is OK - credentials_ will not have been set |
| } |
| } |
| InsertAuth(token, &request); |
| return RequestAction::kNext; |
| } |
| |
| absl::StatusOr<ProxyResponse> RedfishSessionAuthPlugin::HandleRequest( |
| RedfishPlugin::RequestVerb verb, std::unique_ptr<ProxyRequest> request) { |
| if (verb != RequestVerb::kInternal) { |
| return absl::InternalError("Unexpected request"); |
| } |
| absl::MutexLock lock(&token_state_mutex_); |
| return ProxyResponse(ecclesia::HttpResponseCode::HTTP_CODE_REQUEST_OK, |
| std::string(session_state_)); |
| } |
| |
| void RedfishSessionAuthPlugin::InsertAuth( |
| std::optional<absl::string_view> token, ProxyRequest* request) { |
| if (token.has_value()) { |
| request->headers["X-Auth-Token"] = *token; |
| } else if (credentials_.username) { |
| InsertBasicAuth(request, *credentials_.username, credentials_.password); |
| } |
| } |
| |
| absl::Status RedfishSessionAuthPlugin::Initialize(Proxy* proxy) { |
| proxy_ = proxy; |
| { |
| absl::MutexLock lock(&token_state_mutex_); |
| SetState(SessionState::kNoCredentials); |
| } |
| if (only_use_basic_auth_) { |
| LOG(INFO) << "Using basic auth only. Will not maintain session."; |
| return absl::OkStatus(); |
| } |
| return CheckSession(); |
| } |
| |
| static absl::StatusOr<std::vector<std::string>> GetSessions( |
| const ProxyResponse& resp) { |
| auto code = ecclesia::HttpResponseCodeFromInt(resp.code); |
| if (code != ecclesia::HttpResponseCode::HTTP_CODE_REQUEST_OK) { |
| return absl::InternalError(absl::StrFormat( |
| "Failed to get session: %d %s: %s", code, |
| ecclesia::HttpResponseCodeToReasonPhrase(code), resp.body)); |
| } |
| nlohmann::json parsed = resp.GetBodyJson(); |
| if (parsed.is_discarded()) { |
| return absl::InvalidArgumentError( |
| absl::StrCat("Invalid session response: ", resp.body)); |
| } |
| std::vector<std::string> result; |
| for (nlohmann::json& member : parsed["Members"]) { |
| auto found = member.find("@odata.id"); |
| if (found == member.end()) { |
| return absl::InvalidArgumentError( |
| absl::StrCat("Missing odata.id in members: ", resp.body)); |
| } |
| auto* id = found->get_ptr<std::string*>(); |
| if (id == nullptr) { |
| return absl::InvalidArgumentError( |
| absl::StrCat("odata.id is not string: ", resp.body)); |
| } |
| result.push_back(std::move(*id)); |
| } |
| return result; |
| } |
| |
| absl::Status RedfishSessionAuthPlugin::CheckSession( |
| std::optional<absl::Time> after) { |
| absl::MutexLock lock(&token_state_mutex_); |
| |
| LOG(INFO) << "Scheduling session check at around " |
| << after.value_or(absl::Now()); |
| std::unique_ptr<Proxy::RequestJob> job = proxy_->CreateRequestJob( |
| RedfishPlugin::RequestVerb::kGet, |
| proxy_->CreateRequest("/redfish/v1/SessionService/Sessions")); |
| job->Handle([this](const absl::StatusOr<ProxyResponse>& resp) { |
| absl::StatusOr<std::vector<std::string>> sessions; |
| if (!resp.ok()) { |
| LOG(ERROR) << "Failed to check session"; |
| sessions = resp.status(); |
| } else { |
| sessions = GetSessions(*resp); |
| } |
| if (!sessions.ok()) { |
| { |
| absl::MutexLock lock(&token_state_mutex_); |
| token_.reset(); |
| session_.reset(); |
| SetState(SessionState::kLost); |
| } |
| LOG(ERROR) << sessions.status(); |
| if (!MaybeGetNewToken(/*force=*/true)) { |
| LOG(WARNING) << "Failed to start new session"; |
| } |
| } else { |
| LOG(INFO) << "Active sessions:\n" << absl::StrJoin(*sessions, "\n "); |
| } |
| |
| if (session_check_interval_ == absl::ZeroDuration()) { |
| return; |
| } |
| // We don't really need regular intervals. The time since the last check |
| // is what actually matters. |
| absl::Status status = CheckSession(absl::Now() + session_check_interval_); |
| if (!status.ok()) { |
| LOG(ERROR) << "Failed to schedule next session check: " << status; |
| } |
| }); |
| |
| return proxy_->DispatchRequestToQueue(std::move(job), |
| Proxy::RequestPriority::kCrit, after); |
| } |
| |
| absl::Status RedfishSessionAuthPlugin::LoadCredentials( |
| const absl::StatusOr<RemoteCredentials>& credentials) { |
| if (credentials_file_manager_.HasSources()) { |
| if (!credentials.ok()) { |
| return credentials.status(); |
| } |
| credentials_.password = credentials->password; |
| if (credentials->username.has_value() && |
| credentials_.username != credentials->username) { |
| LOG(WARNING) << "Username overridden by credentials file."; |
| credentials_.username = credentials->username; |
| } |
| if (!credentials_.username) { |
| return absl::FailedPreconditionError("Username not specified"); |
| } |
| LOG_FIRST_N(INFO, 1) << "Loaded credentials"; |
| } else { |
| LOG_FIRST_N(INFO, 1) << "No credentials files specified"; |
| } |
| if (only_use_basic_auth_) { |
| SetState(SessionState::kDisabled); |
| } else if (token_.has_value()) { |
| SetState(SessionState::kOk); |
| } else { |
| SetState(SessionState::kAbsent); |
| } |
| return absl::OkStatus(); |
| } |
| |
| std::optional<absl::string_view> RedfishSessionAuthPlugin::MaybeGetNewToken( |
| bool force) { |
| // There may be a reentrant call to this function in one of two ways, and |
| // there are different behaviors depending on how it is called: |
| // |
| // 1. Called recursively: Don't lock and return no token. This will happen |
| // while setting up a session. |
| // |
| // 2. Called in another thread: Wait for mutex, then get a new token or return |
| // the existing one. This will happen if there are multiple pending requests |
| // before we get a token. Unless the session has a very short (almost 0) |
| // timeout, this usually means that one of the requests will initiate a |
| // session, while the others will wait and then use the token. |
| // |
| // recursion_depth is thread_local so that we can distinguish reentrancy in |
| // the same thread (case 1) from reentrancy across threads (case 2). |
| thread_local int recursion_depth = 0; |
| ++recursion_depth; |
| absl::Cleanup update_recursion_depth = [] { --recursion_depth; }; |
| if (recursion_depth > 1) { |
| return std::nullopt; |
| } |
| |
| absl::MutexLock lock(&token_state_mutex_); |
| if (force || token_timeout_ < Now() || !token_.has_value()) { |
| absl::Status status = credentials_file_manager_.RunLogin( |
| proxy_->GetNetworkEndpoint(), |
| [this](const absl::StatusOr<RemoteCredentials>& credentials) |
| ABSL_EXCLUSIVE_LOCKS_REQUIRED(token_state_mutex_) { |
| absl::Status status = |
| LoadCredentials(credentials); |
| if (!status.ok()) { |
| LOG(ERROR) << "Failed to load credentials: " << status; |
| return status; |
| } |
| LOG(INFO) << "Create new session"; |
| status = DeleteSession(); |
| if (!status.ok()) { |
| LOG(ERROR) << "Failed to delete session: " << status; |
| } |
| status = GetToken(); |
| if (!status.ok()) { |
| LOG(ERROR) << "Failed to get token:" << status; |
| SetState(SessionState::kFailed); |
| return status; |
| } |
| SetState(SessionState::kOk); |
| return absl::OkStatus(); |
| }); |
| if (!status.ok()) { |
| LOG(ERROR) << "Failed to login: " << status; |
| return std::nullopt; |
| } |
| } |
| if (session_timeout_ == absl::ZeroDuration()) { |
| if (absl::Status status = GetSessionService(); !status.ok()) { |
| LOG(ERROR) << "Failed to get session service:" << status; |
| // Note that the session will time out immediately in this case. As a |
| // result, the next attempt to access a Redfish resource will delete this |
| // session and create a new one. In general, GET SessionService is |
| // unlikely to fail if we have successfully set up a session, but if it |
| // does, this behavior ensures a best-effort attempt to maintain a working |
| // session. |
| } |
| } |
| // Redfish tokens will only time out if they are not used for a long time. |
| token_timeout_ = Now() + session_timeout_; |
| return token_; |
| } |
| |
| absl::Status RedfishSessionAuthPlugin::GetSessionService() { |
| using ecclesia::HttpResponseCode; |
| assert(proxy_ != nullptr); |
| |
| std::unique_ptr<ProxyRequest> http_request = |
| proxy_->CreateRequest("/redfish/v1/SessionService"); |
| // Use the token if it is present. Some implementations restrict access to |
| // SessionService once a session is created. |
| if (token_.has_value()) { |
| InsertAuth(token_, http_request.get()); |
| } |
| absl::StatusOr<ProxyResponse> result = proxy_->DispatchRequest( |
| RedfishPlugin::RequestVerb::kGet, std::move(http_request)); |
| if (!result.ok()) { |
| return result.status(); |
| } |
| HttpResponseCode code = ecclesia::HttpResponseCodeFromInt(result->code); |
| if (code != ecclesia::HTTP_CODE_REQUEST_OK) { |
| return absl::UnavailableError(absl::StrFormat( |
| "Failed to get SessionService: %d %s: %s", code, |
| ecclesia::HttpResponseCodeToReasonPhrase(code), result->body)); |
| } |
| nlohmann::json session_service = |
| nlohmann::json::parse(result->body, nullptr, false); |
| if (session_service.is_discarded()) { |
| return absl::UnavailableError(absl::StrCat("Invalid JSON: ", result->body)); |
| } |
| auto found = session_service.find("SessionTimeout"); |
| if (found == session_service.end()) { |
| return absl::UnavailableError( |
| absl::StrCat("No SessionTimeout in response: ", result->body)); |
| } |
| auto* timeout = found->get_ptr<nlohmann::json::number_unsigned_t*>(); |
| if (timeout == nullptr) { |
| return absl::UnavailableError( |
| absl::StrCat("Invalid SessionTimeout: ", found->dump())); |
| } |
| token_state_mutex_.AssertHeld(); |
| session_timeout_ = absl::Seconds(*timeout) - kSessionExpiryMargin; |
| return absl::OkStatus(); |
| } |
| |
| absl::Status RedfishSessionAuthPlugin::GetToken() { |
| assert(proxy_ != nullptr); |
| std::unique_ptr<ProxyRequest> http_request = |
| proxy_->CreateRequest("/redfish/v1/SessionService/Sessions"); |
| if (!credentials_.username.has_value()) { |
| return absl::FailedPreconditionError("Username not set"); |
| } |
| http_request->body = |
| absl::StrFormat(R"json({"UserName": "%s", "Password": "%s"})json", |
| *credentials_.username, credentials_.password); |
| http_request->headers["Content-Type"] = "application/json;charset=utf-8"; |
| http_request->headers["OData-Version"] = "4.0"; |
| absl::StatusOr<ProxyResponse> result = proxy_->DispatchRequest( |
| RedfishPlugin::RequestVerb::kPost, std::move(http_request)); |
| if (!result.ok()) { |
| return result.status(); |
| } |
| auto code = ecclesia::HttpResponseCodeFromInt(result->code); |
| if (code != ecclesia::HTTP_CODE_CREATED) { |
| return absl::FailedPreconditionError(absl::StrCat( |
| "Failed to start session: ", code, " ", |
| ecclesia::HttpResponseCodeToReasonPhrase(code), ": ", result->body)); |
| } |
| |
| LOG(INFO) << "New session:" << result->body; |
| |
| auto found = result->headers.find("X-Auth-Token"); |
| if (found == result->headers.end()) { |
| return absl::FailedPreconditionError("No token in response"); |
| } |
| token_state_mutex_.AssertHeld(); |
| token_ = std::move(found->second); |
| |
| found = result->headers.find("Location"); |
| if (found == result->headers.end()) { |
| LOG(WARNING) << "Session location not found in headers"; |
| } else { |
| session_ = std::move(found->second); |
| } |
| return absl::OkStatus(); |
| } |
| |
| absl::Status RedfishSessionAuthPlugin::DeleteSession() { |
| if (!session_.has_value()) { |
| if (token_.has_value()) { |
| LOG(WARNING) << "Session location unkonwn."; |
| token_.reset(); |
| SetState(SessionState::kAbsent); |
| } |
| LOG(INFO) << "No session to delete."; |
| return absl::OkStatus(); |
| } |
| |
| LOG(INFO) << "Deleting session at " << *session_; |
| std::unique_ptr<ProxyRequest> http_request = proxy_->CreateRequest(*session_); |
| InsertAuth(token_, http_request.get()); |
| |
| absl::StatusOr<ProxyResponse> result = proxy_->DispatchRequest( |
| RedfishPlugin::RequestVerb::kDelete, std::move(http_request)); |
| if (!result.ok()) { |
| return result.status(); |
| } |
| auto code = ecclesia::HttpResponseCodeFromInt(result->code); |
| if (code != ecclesia::HTTP_CODE_NO_CONTENT && |
| code != ecclesia::HTTP_CODE_REQUEST_OK) { |
| return absl::AbortedError(absl::StrFormat( |
| "Failed to delete session: %d %s: %s", code, |
| ecclesia::HttpResponseCodeToReasonPhrase(code), result->body)); |
| } |
| session_.reset(); |
| token_.reset(); |
| SetState(SessionState::kAbsent); |
| return absl::OkStatus(); |
| } |
| |
| void RedfishSessionAuthPlugin::SetState( |
| absl::string_view state, const std::source_location& source_location) { |
| CommonMetrics::Get().session_state.SetState(state, {}, source_location); |
| session_state_ = state; |
| } |
| |
| REGISTER_REDFISH_PLUGIN(redfish_session_auth, RedfishSessionAuthPlugin); |
| |
| } // namespace milotic |