| #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 |