blob: 39c7d441bdeaea4a8bf3746fad993faa44e8f62e [file] [log] [blame]
#include "bmcweb_authorizer_singleton.h"
#include <array>
#include <cstddef>
#include <cstdint>
#include <fstream>
#include <iostream>
#include <iterator>
#include <map>
#include <string>
#include <string_view>
#include <unordered_set>
#include <utility>
#include <vector>
#include "gmi/machine_identity.pb.h"
#include "one/network_interfaces.pb.h"
#include "one/offline_node_entities.pb.h"
#include "one/resolved_entities.pb.h"
#include "one/public_offline_node_entities.h"
#include "absl/base/no_destructor.h"
#include "absl/log/log.h"
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"
#include "absl/strings/substitute.h"
#include "boost/beast/http/verb.hpp" // NOLINT
#include "authorizer_enums.h"
#include "redfish_v1.pb.h"
#include "grpc/grpc_security_constants.h"
#include "grpcpp/security/auth_context.h"
#include "grpcpp/support/status.h"
#include "grpcpp/support/string_ref.h"
#include "nlohmann/json.hpp"
#include "config_parser.h"
#include "oauth_utils.h"
#include "redfish_authorizer.h"
#include "redfish_privileges.h"
#include "zatar/certificate_metadata.h"
namespace milotic::authz {
namespace {
using ::ecclesia::Operation;
using ::ecclesia::ResourceEntity;
using ::milotic::redfish::CertificateMetadata;
using ::milotic::redfish::CertificateMetadataParser;
using ::production_msv::node_entities::ReadOfflineNodeEntityInformation;
using ::production_msv::node_entities_proto::NetworkInterface;
using ::production_msv::node_entities_proto::OfflineNodeEntityInformation;
using ::production_msv::node_entities_proto::ResolvedNodeEntity;
using ::security_prodid::GoogleMachineIdentityProto;
} // namespace
BmcWebAuthorizerSingleton& BmcWebAuthorizerSingleton::Initialize(
const AuthorizerOptions& options, std::string_view oauth_key_path) {
static absl::NoDestructor<BmcWebAuthorizerSingleton> singleton(
options, oauth_key_path);
return *singleton;
}
void BmcWebAuthorizerSingleton::ReloadRedfishAuthorizer(
const std::string& configuration_path) {
redfish_authorizer_.ReloadConfiguration(configuration_path);
}
BmcWebAuthorizerSingleton& BmcWebAuthorizerSingleton::GetInstance() {
return Initialize(/*options=*/{}, /*oauth_key_path=*/"");
}
BmcWebAuthorizerSingleton::BmcWebAuthorizerSingleton(
const AuthorizerOptions& options, std::string_view oauth_key_path)
: gmi_path_(options.google_machine_identity_path),
oauth_key_path_(oauth_key_path),
offline_node_entity_path_(options.offline_node_entities_path),
redfish_authorizer_(options) {
ReloadConfiguration();
}
void BmcWebAuthorizerSingleton::ReloadGmiPaths(std::string_view gmi_path) {
gmi_path_ = gmi_path;
}
void BmcWebAuthorizerSingleton::ReloadConfiguration() {
LOG(WARNING) << "Reload configuration started..";
redfish_authorizer_.ReloadConfiguration();
std::string primary_fqdn = ReadPrimaryFqdnFromGmi();
SetPrimaryFqdn(primary_fqdn);
SetOauthKey(ReadOauthKeyFromFile());
SetInterfaceFqdns(ReadDomainNameFromOfflineNodeEntity(primary_fqdn));
LOG(WARNING) << "Reload configuration done.";
}
grpc::Status BmcWebAuthorizerSingleton::RecordNewSubscription(
const PeerSpiffeIdentity& peer) {
if (absl::Status status = redfish_authorizer_.RecordNewSubscription(peer);
!status.ok()) {
return grpc::Status(grpc::StatusCode::PERMISSION_DENIED, status.ToString());
}
return grpc::Status::OK;
}
grpc::Status BmcWebAuthorizerSingleton::RecordNewUnsubscription(
const PeerSpiffeIdentity& peer) {
if (absl::Status status = redfish_authorizer_.RecordNewUnsubscription(peer);
!status.ok()) {
return grpc::Status(grpc::StatusCode::PERMISSION_DENIED, status.ToString());
}
return grpc::Status::OK;
}
grpc::Status BmcWebAuthorizerSingleton::Authorize(
std::string_view uri, boost::beast::http::verb verb,
const RequestState& request_state) const {
if (!request_state.peer_authenticated) {
if (request_state.with_trust_bundle) {
return grpc::Status(
grpc::StatusCode::UNAUTHENTICATED,
"Peer must be authenticated if BMC has trust bundle!");
}
if (grpc::Status status = AuthorizeWithoutTrustBundle(uri, verb);
!status.ok()) {
return status;
}
return grpc::Status::OK;
}
return AuthorizeWithTrustBundle(uri, verb, request_state);
}
grpc::Status BmcWebAuthorizerSingleton::AuthorizeWithTrustBundle(
std::string_view uri, boost::beast::http::verb verb,
const RequestState& request_state) const {
Operation operation = BoostVerbToOperation(verb);
RedfishPrivileges peer_privileges =
RedfishPrivileges(request_state.peer_privileges);
if (!redfish_authorizer_.IsPeerAuthorized(uri, operation, peer_privileges)) {
return grpc::Status(grpc::StatusCode::PERMISSION_DENIED,
"Client doesn't have enough permissions: " +
peer_privileges.GetDebugString());
}
return grpc::Status::OK;
}
Operation BmcWebAuthorizerSingleton::BoostVerbToOperation(
boost::beast::http::verb verb) {
switch (verb) {
case boost::beast::http::verb::get:
return Operation::kGet;
case boost::beast::http::verb::post:
return Operation::kPost;
case boost::beast::http::verb::delete_:
return Operation::kDelete;
case boost::beast::http::verb::put:
return Operation::kPut;
case boost::beast::http::verb::patch:
return Operation::kPatch;
case boost::beast::http::verb::head:
return Operation::kHead;
default:
return Operation::kUndefined;
}
}
grpc::Status BmcWebAuthorizerSingleton::AuthorizeWithoutTrustBundle(
std::string_view uri, boost::beast::http::verb verb) const {
constexpr std::array<std::pair<ResourceEntity, boost::beast::http::verb>, 10>
kRecoveryOperations = {
std::pair<ResourceEntity, boost::beast::http::verb>{
ResourceEntity::kServiceRoot, boost::beast::http::verb::get},
{ResourceEntity::kChassisCollection, boost::beast::http::verb::get},
{ResourceEntity::kChassis, boost::beast::http::verb::get},
{ResourceEntity::kActionInfo, boost::beast::http::verb::get},
{ResourceEntity::kChassis, boost::beast::http::verb::post},
{ResourceEntity::kComputerSystem, boost::beast::http::verb::get},
{ResourceEntity::kComputerSystemCollection,
boost::beast::http::verb::get},
{ResourceEntity::kComputerSystem, boost::beast::http::verb::post},
{ResourceEntity::kManagerCollection, boost::beast::http::verb::get},
{ResourceEntity::kManager, boost::beast::http::verb::get},
};
ResourceEntity entity = GetEntityTypeFromRedfishUri(uri);
bool authorized = false;
for (auto const& [allowed_entity, allowed_verb] : kRecoveryOperations) {
if (entity == allowed_entity && verb == allowed_verb) {
authorized = true;
break;
}
}
if (!authorized) {
return grpc::Status(
grpc::StatusCode::PERMISSION_DENIED,
"Without trust bundle, only recovery operations are allowed");
}
return grpc::Status::OK;
}
grpc::Status BmcWebAuthorizerSingleton::GetPeerRoleFromAuthContext(
const grpc::AuthContext& context, std::string& peer_role) const {
PeerSpiffeIdentity peer_identity;
if (grpc::Status status =
GetPeerIdentityFromAuthContext(context, peer_identity);
!status.ok()) {
peer_role.clear();
return status;
}
peer_role = redfish_authorizer_.GetPeerRedfishRole(peer_identity);
if (peer_role.empty()) {
return grpc::Status(
grpc::StatusCode::PERMISSION_DENIED,
absl::StrCat(
"Peer role is not specified in the auth config: peer_identity=",
peer_identity.spiffe_id));
}
return grpc::Status::OK;
}
grpc::Status BmcWebAuthorizerSingleton::GetPrivilegesViaOAuth(
const grpc::AuthContext& context, const std::string& token,
std::unordered_set<std::string>& peer_privileges) const {
PeerSpiffeIdentity peer_identity;
if (grpc::Status status =
GetPeerIdentityFromAuthContext(context, peer_identity);
!status.ok()) {
return status;
}
std::string redfish_role;
constexpr const char* kExpectedIssuer = "BMC Local Authorization Server";
if (grpc::Status status = VerifyAndExtractRoleFromToken(
token, GetOauthKey(), GenerateOAuthSubject(peer_identity),
GetPrimaryFqdn(), kExpectedIssuer, redfish_role);
!status.ok()) {
return status;
}
peer_privileges = redfish_authorizer_.GetRedfishPrivileges(redfish_role);
return grpc::Status::OK;
}
grpc::Status BmcWebAuthorizerSingleton::GetPrivilegesViaMTls(
const grpc::AuthContext& context,
std::unordered_set<std::string>& peer_privileges) const {
PeerSpiffeIdentity peer_identity;
if (grpc::Status status =
GetPeerIdentityFromAuthContext(context, peer_identity);
!status.ok()) {
return status;
}
LOG(INFO) << "Peer identity: " << peer_identity.spiffe_id;
peer_privileges = redfish_authorizer_.GetPeerRedfishPrivileges(peer_identity);
return grpc::Status::OK;
}
std::string BmcWebAuthorizerSingleton::ReadPrimaryFqdnFromGmi() const {
std::ifstream gmi_file(gmi_path_);
if (!gmi_file.is_open()) {
LOG(WARNING) << "GMI file at '" << gmi_path_ << "' is missing." << '\n';
return "";
}
GoogleMachineIdentityProto gmi;
if (!gmi.ParseFromIstream(&gmi_file)) {
std::cerr << "GMI parsing failed at '" << gmi_path_ << "'" << '\n';
return "";
}
return gmi.fqdn();
}
std::string BmcWebAuthorizerSingleton::ReadOauthKeyFromFile() const {
std::ifstream oauth_key_file(oauth_key_path_);
if (!oauth_key_file.is_open()) {
LOG(WARNING) << "OAuth key file at '" << oauth_key_path_ << "' is missing."
<< '\n';
return "";
}
std::string oauth_key = {std::istreambuf_iterator<char>(oauth_key_file),
std::istreambuf_iterator<char>()};
return oauth_key;
}
std::vector<std::string>
BmcWebAuthorizerSingleton::ReadDomainNameFromOfflineNodeEntity(
std::string_view primary_fqdn) const {
std::ifstream offline_node_entity_file(offline_node_entity_path_);
std::vector<std::string> interface_fqdns;
if (!offline_node_entity_file.is_open()) {
LOG(WARNING) << "Offline Node Entity file at '" << offline_node_entity_path_
<< "' is missing." << '\n';
return interface_fqdns;
}
absl::StatusOr<OfflineNodeEntityInformation> one =
ReadOfflineNodeEntityInformation(offline_node_entity_path_);
if (!one.ok()) {
LOG(WARNING) << "Could not read offline node entities information: "
<< one.status().message();
return interface_fqdns;
}
for (const std::pair<const std::string, ResolvedNodeEntity>& entity :
(*one).resolved_config().entities()) {
if (absl::StrCat(entity.second.hostname(), ".prod.google.com") !=
primary_fqdn)
continue;
for (const NetworkInterface& intf :
entity.second.network_interfaces().network_interface()) {
interface_fqdns.push_back(intf.hostname() + ".prod.google.com");
}
break;
}
return interface_fqdns;
}
uint64_t BmcWebAuthorizerSingleton::GetSampleRateLimit(
const std::string& peer_role) const {
return redfish_authorizer_.GetSampleRateLimit(peer_role);
}
ResourceEntity BmcWebAuthorizerSingleton::GetEntityTypeFromRedfishUri(
std::string_view uri) const {
return redfish_authorizer_.GetEntityTypeFromRedfishUri(uri);
}
std::size_t BmcWebAuthorizerSingleton::GetNodeIndexInPatternArray(
std::string_view uri) const {
return redfish_authorizer_.GetNodeIndexInPatternArray(uri);
}
nlohmann::json BmcWebAuthorizerSingleton::GetRedfishPrivilegeRegistry() const {
return redfish_authorizer_.GetRedfishPrivilegeRegistry();
}
bool BmcWebAuthorizerSingleton::IsBasePrivilegeRegistryFound() const {
nlohmann::json config_json = redfish_authorizer_.ParseAuthConfig();
if (config_json.empty()) {
return false;
}
std::string base_privilege_registry_path =
redfish_authorizer_.FindBasePrivilegeRegistryPath(config_json);
return !base_privilege_registry_path.empty();
}
void BmcWebAuthorizerSingleton::SetBasePrivilegesFolder(
std::string_view base_privileges_folder) {
redfish_authorizer_.SetBasePrivilegesFolder(base_privileges_folder);
}
grpc::Status BmcWebAuthorizerSingleton::GetPeerIdentityFromAuthContext(
const grpc::AuthContext& context, PeerSpiffeIdentity& peer_identity) {
if (!context.IsPeerAuthenticated()) {
return grpc::Status(grpc::StatusCode::UNAUTHENTICATED,
"Peer is unauthenticated");
}
if (context.GetPeerIdentityPropertyName() != GRPC_X509_SAN_PROPERTY_NAME) {
return grpc::Status(grpc::StatusCode::PERMISSION_DENIED,
"Peer identity isn't X.509 SAN");
}
for (const grpc::string_ref& val : context.GetPeerIdentity()) {
std::string_view str(val.data(), val.size());
// As per SPIFFE specification, SPIFFE schema and trust domain are case
// insensitive.
// https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#24-spiffe-id-parsing
if (absl::StartsWithIgnoreCase(str, "spiffe://")) {
peer_identity.spiffe_id = str;
} else if (absl::EndsWith(str, ".prod.google.com")) {
peer_identity.fqdn = str;
}
}
return grpc::Status::OK;
}
grpc::Status BmcWebAuthorizerSingleton::CheckTarget(
const grpc::AuthContext& grpc_context,
const std::multimap<grpc::string_ref, grpc::string_ref>& client_metadata,
const ::redfish::v1::Request& request) const {
PeerSpiffeIdentity peer_identity;
if (grpc::Status status =
GetPeerIdentityFromAuthContext(grpc_context, peer_identity);
!status.ok()) {
return status;
}
CertificateMetadata peer_metadata;
if (absl::Status status = metadata_parser_.GetContextAndRoleFromSpiffe(
peer_identity.spiffe_id, peer_metadata.context, peer_metadata.role);
!status.ok()) {
return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, status.ToString());
}
// If the service (BMCWeb) has a legitimate GMI, and peer is not a node (e.g.,
// it's a Borg job), check that the intended recipient of the request matches
// the identity in the certificate. This is to prevent redirect attacks in
// powercycle trust model, where clients never verify server.
std::string primary_fqdn = GetPrimaryFqdn();
if (primary_fqdn.empty()) {
LOG(WARNING) << "Can't parse primary fqdn. GMI is probably broken.";
return grpc::Status::OK;
}
if (peer_metadata.context == CertificateMetadataParser::kCaContextNode) {
return grpc::Status::OK;
}
std::string host;
for (auto const& [key, val] : request.headers()) {
// Both field name of the Authorization header and the token type are case
// insensitive
// References
// https://www.rfc-editor.org/rfc/rfc7230#section-3.2
// https://www.rfc-editor.org/rfc/rfc6749#section-5.1
if (absl::EqualsIgnoreCase(key, "Host")) {
host = val;
}
}
// To keep backward compatibility, extract host from metadata as well if it's
// not in headers.
if (host.empty()) {
auto it = client_metadata.find("target");
if (it != client_metadata.end()) {
host = {it->second.data(), it->second.length()};
}
}
// DNS should be case insensitive
// Reference: https://www.rfc-editor.org/rfc/rfc4343
if (absl::EqualsIgnoreCase(host, primary_fqdn)) {
return grpc::Status::OK;
}
// We could have multiple FQDNs on BMC and they can be different from the
// hostname. Matching any of them should pass the validation
for (const std::string& fqdn : GetInterfaceFqdns()) {
if (absl::EqualsIgnoreCase(host, fqdn)) {
return grpc::Status::OK;
}
}
return grpc::Status(
grpc::StatusCode::PERMISSION_DENIED,
absl::Substitute(
"Unauthorized user: request target doesn't match server; request's "
"host header='$0'; BMC's hostname='$1'; BMC's FQDNs='$2'",
host, primary_fqdn, absl::StrJoin(GetInterfaceFqdns(), " ")));
}
} // namespace milotic::authz