| #include "config_generator.h" |
| |
| #include <cstddef> |
| #include <fstream> |
| #include <iterator> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| |
| #include "gmi/machine_identity.pb.h" |
| #include "one/offline_node_entities.pb.h" |
| #include "one/resolved_entities.pb.h" |
| #include "one/public_offline_node_entities.h" |
| #include "absl/log/log.h" |
| #include "absl/strings/str_cat.h" |
| #include "nlohmann/json.hpp" |
| #include "json_utils.h" |
| #include "zatar/certificate_metadata.h" |
| |
| namespace milotic::authz { |
| |
| namespace { |
| |
| using ::milotic::redfish::CertificateMetadataParser; |
| using ::production_msv::node_entities::ReadOfflineNodeEntityInformation; |
| using ::production_msv::node_entities_proto::OfflineNodeEntityInformation; |
| using ::production_msv::node_entities_proto::ResolvedNodeEntity; |
| using ::security_prodid::GoogleMachineIdentityProto; |
| |
| constexpr std::string_view kMachineManagerRedfishRole = "MManager"; |
| constexpr std::string_view kComputeNodeRedfishRole = "ComputeNode"; |
| constexpr std::string_view kEmptyConfiguration = R"( |
| { |
| "base_privilege_registry": "Redfish_1.3.0_PrivilegeRegistry.json", |
| "exchange_mappings": [], |
| "name": "Empty BMC Authorization Configurations", |
| "oem_privilege_mappings": [], |
| "resource_owners": [], |
| "role_privileges_mappings": [] |
| } |
| )"; |
| constexpr std::string_view kHwopsRole = "insecure-hwops-state"; |
| } // namespace |
| |
| nlohmann::json::json_pointer ConfigGenerator::FindLocalNodeInOwners( |
| const nlohmann::json& root, std::string_view hostname) { |
| nlohmann::json::json_pointer resource_owners_ptr = |
| "/resource_owners"_json_pointer; |
| const nlohmann::json::array_t* resource_owners = |
| GetValueAsArray(root, "resource_owners"); |
| if (resource_owners == nullptr) { |
| return resource_owners_ptr / 0; |
| } |
| for (std::size_t i = 0; i < resource_owners->size(); ++i) { |
| const std::string* fqdn = GetValueAsString(resource_owners->at(i), "fqdn"); |
| if (fqdn != nullptr && |
| *fqdn == absl::StrCat(hostname, ".prod.google.com")) { |
| return resource_owners_ptr / i; |
| } |
| } |
| return resource_owners_ptr / resource_owners->size(); |
| } |
| |
| nlohmann::json::json_pointer ConfigGenerator::FindLocalNodeInExchangeMappings( |
| const nlohmann::json& root, std::string_view hostname, |
| std::string_view node_redfish_role) { |
| nlohmann::json::json_pointer exchange_mappings_ptr = |
| "/exchange_mappings"_json_pointer; |
| const nlohmann::json::array_t* exchange_mappings = |
| GetValueAsArray(root, "exchange_mappings"); |
| if (exchange_mappings == nullptr) { |
| return exchange_mappings_ptr / 0; |
| } |
| for (std::size_t i = 0; i < exchange_mappings->size(); ++i) { |
| const std::string* redfish_role = |
| GetValueAsString(exchange_mappings->at(i), "redfish_role"); |
| if (redfish_role != nullptr && *redfish_role == node_redfish_role) { |
| return exchange_mappings_ptr / i; |
| } |
| const nlohmann::json* peer = |
| GetValueAsJson(exchange_mappings->at(i), "peer"); |
| if (peer != nullptr) { |
| const std::string* fqdn = GetValueAsString(*peer, "fqdn"); |
| if (fqdn != nullptr && |
| *fqdn == absl::StrCat(hostname, ".prod.google.com")) { |
| return exchange_mappings_ptr / i; |
| } |
| } |
| } |
| return exchange_mappings_ptr / exchange_mappings->size(); |
| } |
| |
| std::optional<GoogleMachineIdentityProto> ConfigGenerator::GetGmiDataFromGmi( |
| const std::string& gmi_path) { |
| std::ifstream gmi_file(gmi_path); |
| if (!gmi_file.is_open()) { |
| LOG(ERROR) << "GMI file at '" << gmi_path << "' is missing."; |
| return std::nullopt; |
| } |
| GoogleMachineIdentityProto gmi; |
| if (!gmi.ParseFromIstream(&gmi_file)) { |
| LOG(ERROR) << "GMI parsing failed at '" << gmi_path << "'"; |
| return std::nullopt; |
| } |
| return gmi; |
| } |
| |
| // Return the role expected in the certificate. |
| // |
| // When the machine is in HWOPS, GMI reports the actual owner and |
| // "in_hwops_state", while certificates use insecure-hwops-state as the role |
| // rather than the actual owner. go/aristotle-ownership-change#owner |
| std::string_view GetCertNodeOwner(bool in_hwops_state, |
| std::string_view node_owner) { |
| if (in_hwops_state) { |
| return kHwopsRole; |
| } |
| return node_owner; |
| } |
| |
| void ConfigGenerator::UpdateMachineManagerMappingsInRoot( |
| nlohmann::json& root, bool in_hwops_state, |
| const OfflineNodeEntityInformation& one) { |
| std::string mmanager_entity_tag = |
| one.resolved_config().ecclesia_machine_manager_entity(); |
| ResolvedNodeEntity mmanager_entity = |
| one.resolved_config().entities().at(mmanager_entity_tag); |
| |
| nlohmann::json::json_pointer mmanager_exchange_mappings_ptr = |
| FindLocalNodeInExchangeMappings(root, mmanager_entity.hostname(), |
| kMachineManagerRedfishRole); |
| |
| // If a mapping to mmanager already exists, then no-op |
| if (root.contains(mmanager_exchange_mappings_ptr)) { |
| return; |
| } |
| |
| // Define MManager Peer object |
| nlohmann::json::object_t mmanager = nlohmann::json::object(); |
| mmanager["spiffe_id_matcher"]["path"] = absl::StrCat( |
| "/role/", GetCertNodeOwner(in_hwops_state, mmanager_entity.owner())); |
| mmanager["spiffe_id_matcher"]["ca_context"] = |
| CertificateMetadataParser::kCaContextNode; |
| mmanager["spiffe_id_matcher"]["trust_domain_suffix"] = ".prod.google.com"; |
| mmanager["fqdn"] = |
| absl::StrCat(mmanager_entity.hostname(), ".prod.google.com"); |
| root[FindLocalNodeInOwners(root, mmanager_entity.hostname())] = mmanager; |
| |
| // Define MManager Exchange mapping |
| nlohmann::json::object_t mmanager_exchange = nlohmann::json::object(); |
| mmanager_exchange["redfish_role"] = kMachineManagerRedfishRole; |
| mmanager_exchange["peer"] = mmanager; |
| root[mmanager_exchange_mappings_ptr] = mmanager_exchange; |
| } |
| |
| void ConfigGenerator::UpdateComputeNodeMappingsInRoot( |
| nlohmann::json& root, bool in_hwops_state, |
| const OfflineNodeEntityInformation& one) { |
| nlohmann::json::json_pointer compute_node_exchange_mapping_ptr = |
| FindLocalNodeInExchangeMappings( |
| root, one.resolved_config().machine_name(), kComputeNodeRedfishRole); |
| |
| // If a mapping to compute node already exists, then no-op |
| if (root.contains(compute_node_exchange_mapping_ptr)) { |
| return; |
| } |
| |
| // Define Compute Node Peer object |
| // |
| // TODO: this only works on machines on which the compute node hostname == |
| // machine name |
| nlohmann::json::object_t compute_node = nlohmann::json::object(); |
| compute_node["spiffe_id_matcher"]["path"] = absl::StrCat( |
| "/role/", |
| GetCertNodeOwner(in_hwops_state, one.resolved_config().machine_owner())); |
| compute_node["spiffe_id_matcher"]["ca_context"] = |
| CertificateMetadataParser::kCaContextNode; |
| compute_node["spiffe_id_matcher"]["trust_domain_suffix"] = ".prod.google.com"; |
| compute_node["fqdn"] = |
| absl::StrCat(one.resolved_config().machine_name(), ".prod.google.com"); |
| root[FindLocalNodeInOwners(root, one.resolved_config().machine_name())] = |
| compute_node; |
| |
| // Define Compute Node Exchange mapping |
| nlohmann::json::object_t compute_node_exchange = nlohmann::json::object(); |
| compute_node_exchange["redfish_role"] = kComputeNodeRedfishRole; |
| compute_node_exchange["peer"] = compute_node; |
| root[compute_node_exchange_mapping_ptr] = compute_node_exchange; |
| } |
| |
| nlohmann::json ConfigGenerator::ReadConfigAndAddRuntimeSubstitutions( |
| const std::string& base_config_path, bool in_hwops_state, |
| const OfflineNodeEntityInformation& one) { |
| std::ifstream template_file(base_config_path); |
| if (!template_file.is_open()) { |
| LOG(ERROR) << "Authorization configuration file at '" << base_config_path |
| << "' is missing."; |
| return nlohmann::json::object(); |
| } |
| std::string template_content((std::istreambuf_iterator<char>(template_file)), |
| std::istreambuf_iterator<char>()); |
| |
| nlohmann::json root = nlohmann::json::parse(template_content, nullptr, false); |
| if (root.is_discarded()) { |
| LOG(ERROR) << "Authorization configuration is not a JSON! |content|=" |
| << template_content; |
| return nlohmann::json::object(); |
| } |
| |
| UpdateMachineManagerMappingsInRoot(root, in_hwops_state, one); |
| UpdateComputeNodeMappingsInRoot(root, in_hwops_state, one); |
| |
| return root; |
| } |
| |
| nlohmann::json ConfigGenerator::EmptyConfiguration() { |
| return nlohmann::json::parse(kEmptyConfiguration, nullptr, false); |
| } |
| |
| nlohmann::json ConfigGenerator::GenerateConfiguration( |
| const GeneratorOptions& options) { |
| std::optional<GoogleMachineIdentityProto> gmi = |
| GetGmiDataFromGmi(options.gmi_path); |
| if (!gmi) { |
| return nlohmann::json::object(); |
| } |
| absl::StatusOr<OfflineNodeEntityInformation> one = |
| ReadOfflineNodeEntityInformation(options.offline_node_entities_path); |
| if (!one.ok()) { |
| LOG(ERROR) << "Could not read offline node entities information: " |
| << one.status().message(); |
| return nlohmann::json::object(); |
| } |
| return ReadConfigAndAddRuntimeSubstitutions(options.authz_configuration_path, |
| gmi->in_hwops_state(), *one); |
| } |
| } // namespace milotic::authz |