blob: 5ceed39b7f67ae73c1a9d9c944b165ac7f043d2b [file] [log] [blame]
#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