HFT Authorization
Add the sampling rate based authorization. The following rules are implemented:
1. any subscription client must be authorized, with a maximum number of sampling per second allowed
2. if the subscription client hasn't reached to the upper limit, the subscription will be allowed, the current total sampling rate for this client will increase by the delta sampling rate made in this subscription
3. sampling rate check is done in the RPC level in a "all or none" manner. This means if a subscription is rejected because of too many sampling in a RPC, none of the subscriptions in that RPC is made
4. in all other cases, the client will be rejected
Comprehensive tests are added to make sure the sampling count code is correct.
Authorization code is modified accordingly to provide necessary APIs.
#tlbmc
#tlbmc-hft
PiperOrigin-RevId: 761251595
Change-Id: If8b5c140d000624356f855c79c530db2125f299a
diff --git a/redfish_authorization/bmcweb_authorizer_singleton.cc b/redfish_authorization/bmcweb_authorizer_singleton.cc
index eded210..39c7d44 100644
--- a/redfish_authorization/bmcweb_authorizer_singleton.cc
+++ b/redfish_authorization/bmcweb_authorizer_singleton.cc
@@ -2,7 +2,7 @@
#include <array>
#include <cstddef>
-#include <vector>
+#include <cstdint>
#include <fstream>
#include <iostream>
#include <iterator>
@@ -11,12 +11,17 @@
#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
@@ -32,9 +37,6 @@
#include "redfish_authorizer.h"
#include "redfish_privileges.h"
#include "zatar/certificate_metadata.h"
-#include "one/offline_node_entities.pb.h"
-#include "one/resolved_entities.pb.h"
-#include "one/public_offline_node_entities.h"
namespace milotic::authz {
@@ -45,10 +47,10 @@
using ::milotic::redfish::CertificateMetadata;
using ::milotic::redfish::CertificateMetadataParser;
using ::production_msv::node_entities::ReadOfflineNodeEntityInformation;
-using ::production_msv::node_entities_proto::OfflineNodeEntityInformation;
using ::production_msv::node_entities_proto::NetworkInterface;
-using ::security_prodid::GoogleMachineIdentityProto;
+using ::production_msv::node_entities_proto::OfflineNodeEntityInformation;
using ::production_msv::node_entities_proto::ResolvedNodeEntity;
+using ::security_prodid::GoogleMachineIdentityProto;
} // namespace
@@ -60,6 +62,11 @@
return *singleton;
}
+void BmcWebAuthorizerSingleton::ReloadRedfishAuthorizer(
+ const std::string& configuration_path) {
+ redfish_authorizer_.ReloadConfiguration(configuration_path);
+}
+
BmcWebAuthorizerSingleton& BmcWebAuthorizerSingleton::GetInstance() {
return Initialize(/*options=*/{}, /*oauth_key_path=*/"");
}
@@ -193,6 +200,26 @@
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 {
@@ -257,38 +284,39 @@
}
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 (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;
- }
+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(
@@ -401,19 +429,16 @@
// DNS should be case insensitive
// Reference: https://www.rfc-editor.org/rfc/rfc4343
- if (absl::EqualsIgnoreCase(host, primary_fqdn))
- {
- return grpc::Status::OK;
+ 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;
- }
+ for (const std::string& fqdn : GetInterfaceFqdns()) {
+ if (absl::EqualsIgnoreCase(host, fqdn)) {
+ return grpc::Status::OK;
+ }
}
return grpc::Status(
diff --git a/redfish_authorization/bmcweb_authorizer_singleton.h b/redfish_authorization/bmcweb_authorizer_singleton.h
index eb4853a..202b9c0 100644
--- a/redfish_authorization/bmcweb_authorizer_singleton.h
+++ b/redfish_authorization/bmcweb_authorizer_singleton.h
@@ -2,6 +2,7 @@
#define THIRD_PARTY_MILOTIC_INTERNAL_CC_AUTHZ_BMCWEB_AUTHORIZER_SINGLETON_H_
#include <cstddef>
+#include <cstdint>
#include <map>
#include <string>
#include <string_view>
@@ -91,6 +92,17 @@
static grpc::Status GetPeerIdentityFromAuthContext(
const grpc::AuthContext& context, PeerSpiffeIdentity& peer_identity);
+ // Returns the peer role from the auth context.
+ // On error, peer_role will be empty.
+ grpc::Status GetPeerRoleFromAuthContext(const grpc::AuthContext& context,
+ std::string& peer_role) const;
+
+ // Returns the sample rate limit for the peer role.
+ uint64_t GetSampleRateLimit(const std::string& peer_role) const;
+
+ // Reloads the redfish authorizer with the given configuration path.
+ void ReloadRedfishAuthorizer(const std::string& configuration_path);
+
static ecclesia::Operation BoostVerbToOperation(
boost::beast::http::verb verb);
@@ -115,8 +127,8 @@
protected:
std::string ReadPrimaryFqdnFromGmi() const;
std::string ReadOauthKeyFromFile() const;
- std::vector<std::string>
- ReadDomainNameFromOfflineNodeEntity(std::string_view primary_fqdn) const;
+ std::vector<std::string> ReadDomainNameFromOfflineNodeEntity(
+ std::string_view primary_fqdn) const;
virtual grpc::Status AuthorizeWithoutTrustBundle(
std::string_view uri, boost::beast::http::verb verb) const;
@@ -133,10 +145,9 @@
absl::MutexLock lock(&mutex_);
return oauth_key_;
}
- std::vector<std::string> GetInterfaceFqdns() const
- {
- absl::MutexLock lock(&mutex_);
- return interface_fqdns_;
+ std::vector<std::string> GetInterfaceFqdns() const {
+ absl::MutexLock lock(&mutex_);
+ return interface_fqdns_;
}
void SetPrimaryFqdn(std::string_view fqdn) {
absl::MutexLock lock(&mutex_);
@@ -146,10 +157,9 @@
absl::MutexLock lock(&mutex_);
oauth_key_ = oauth_key;
}
- void SetInterfaceFqdns(std::vector<std::string>&& fqdns)
- {
- absl::MutexLock lock(&mutex_);
- interface_fqdns_ = std::move(fqdns);
+ void SetInterfaceFqdns(std::vector<std::string>&& fqdns) {
+ absl::MutexLock lock(&mutex_);
+ interface_fqdns_ = std::move(fqdns);
}
private:
diff --git a/redfish_authorization/config_parser.cc b/redfish_authorization/config_parser.cc
index 0bbaa52..07ea3b9 100644
--- a/redfish_authorization/config_parser.cc
+++ b/redfish_authorization/config_parser.cc
@@ -3,6 +3,7 @@
#include <algorithm>
#include <array>
#include <cstddef>
+#include <cstdint>
#include <fstream>
#include <optional>
#include <string>
@@ -189,6 +190,39 @@
return it->redfish_role;
}
+std::string AuthzConfiguration::GetPeerRedfishRole(
+ const PeerSpiffeIdentity& peer) const {
+ std::optional<SpiffeIdentityMatcher> peer_matcher =
+ CreateIdentityMatcherFromPeerSpiffeIdentity(peer);
+ if (peer_matcher == std::nullopt) {
+ LOG(WARNING) << "Peer spiffe id" << peer.spiffe_id << "is malformed";
+ return "";
+ }
+
+ SpiffeUser peer_to_spiffe_user{.identity_matcher = *peer_matcher,
+ .redfish_role = ""};
+
+ absl::StatusOr<std::string> role =
+ GetRedfishRoleOfSpiffeMatcher(peer_to_spiffe_user);
+
+ if (!role.ok()) {
+ LOG(WARNING) << role.status();
+ return "";
+ }
+
+ return *role;
+}
+
+uint64_t AuthzConfiguration::GetSampleRateLimit(
+ const std::string& peer_role) const {
+ absl::MutexLock lock(&mutex_);
+ auto it = role_to_sample_rate_limit_.find(peer_role);
+ if (it != role_to_sample_rate_limit_.end()) {
+ return it->second;
+ }
+ return 0;
+}
+
std::unordered_set<std::string> AuthzConfiguration::GetPeerRedfishPrivileges(
const PeerSpiffeIdentity& peer) const {
std::optional<SpiffeIdentityMatcher> peer_matcher =
@@ -274,6 +308,34 @@
return matchers;
}
+void AuthzConfiguration::ReloadSampleRateLimit(const nlohmann::json& config) {
+ if (config.is_discarded()) {
+ return;
+ }
+ const nlohmann::json::array_t* role_sample_rate_limits =
+ GetValueAsArray(config, "role_sample_rate_limits");
+ if (role_sample_rate_limits == nullptr) {
+ return;
+ }
+ std::unordered_map<std::string, uint64_t> new_role_to_sample_rate_limit;
+ for (const nlohmann::json& role_sample_rate_limit :
+ *role_sample_rate_limits) {
+ const std::string* role =
+ GetValueAsString(role_sample_rate_limit, "redfish_role");
+ if (role == nullptr) {
+ continue;
+ }
+ const uint64_t* sample_rate_limit =
+ GetValueAsUint(role_sample_rate_limit, "sample_rate_limit");
+ if (sample_rate_limit == nullptr) {
+ continue;
+ }
+ new_role_to_sample_rate_limit[*role] = *sample_rate_limit;
+ }
+ absl::MutexLock lock(&mutex_);
+ role_to_sample_rate_limit_ = std::move(new_role_to_sample_rate_limit);
+}
+
void AuthzConfiguration::ReloadResourceOwners(const nlohmann::json& config) {
if (config.is_discarded()) {
return;
@@ -517,6 +579,7 @@
ReloadResourceOwners(config);
ReloadRedfishUsers(config);
ReloadRoleToPrivileges(config, platform_config);
+ ReloadSampleRateLimit(config);
}
std::unordered_set<std::string> AuthzConfiguration::GetOemPrivileges() const {
diff --git a/redfish_authorization/config_parser.h b/redfish_authorization/config_parser.h
index 593e10f..0c1e2e0 100644
--- a/redfish_authorization/config_parser.h
+++ b/redfish_authorization/config_parser.h
@@ -2,6 +2,7 @@
#define THIRD_PARTY_MILOTIC_EXTERNAL_CC_AUTHZ_CONFIG_PARSER_H_
#include <array>
+#include <cstdint>
#include <optional>
#include <string>
#include <tuple>
@@ -171,6 +172,10 @@
absl::string_view offline_node_entities_path = "",
absl::string_view gmi_path = "");
+ std::string GetPeerRedfishRole(const PeerSpiffeIdentity& peer) const;
+
+ uint64_t GetSampleRateLimit(const std::string& peer_role) const;
+
bool IsPeerResourceOwner(const PeerSpiffeIdentity& peer) const;
// Returns the Redfish privileges of the given user identified by SPIFFE.
@@ -277,6 +282,7 @@
const nlohmann::json& platform_config);
void ReloadOfflineNodeEntities();
void ReloadGmi();
+ void ReloadSampleRateLimit(const nlohmann::json& config);
std::optional<AuthzConfiguration::SpiffeIdentityMatcher>
ParseSpiffeIdentityMatcher(const nlohmann::json& config);
@@ -305,6 +311,8 @@
ABSL_GUARDED_BY(mutex_);
std::unordered_map<std::string, std::unordered_set<std::string>>
role_to_privileges_ ABSL_GUARDED_BY(mutex_);
+ std::unordered_map<std::string, uint64_t> role_to_sample_rate_limit_
+ ABSL_GUARDED_BY(mutex_);
};
} // namespace milotic::authz
diff --git a/redfish_authorization/redfish_authorizer.cc b/redfish_authorization/redfish_authorizer.cc
index e16b1c9..84e0808 100644
--- a/redfish_authorization/redfish_authorizer.cc
+++ b/redfish_authorization/redfish_authorizer.cc
@@ -175,8 +175,23 @@
return base_privilege_registry_path;
}
+std::string RedfishAuthorizer::GetPeerRedfishRole(
+ const PeerSpiffeIdentity& peer) const {
+ return authz_configuration_.GetPeerRedfishRole(peer);
+}
+
+uint64_t RedfishAuthorizer::GetSampleRateLimit(
+ const std::string& peer_role) const {
+ return authz_configuration_.GetSampleRateLimit(peer_role);
+}
+
void RedfishAuthorizer::ReloadConfiguration() {
- nlohmann::json configuration_json = ParseAuthConfig();
+ ReloadConfiguration(options_.configuration_path);
+}
+
+void RedfishAuthorizer::ReloadConfiguration(
+ const std::string& configuration_path) {
+ nlohmann::json configuration_json = ParseJsonConfig(configuration_path);
if (configuration_json.empty()) {
return;
diff --git a/redfish_authorization/redfish_authorizer.h b/redfish_authorization/redfish_authorizer.h
index d15374e..4b5f61e 100644
--- a/redfish_authorization/redfish_authorizer.h
+++ b/redfish_authorization/redfish_authorizer.h
@@ -91,8 +91,19 @@
// Reloads the authorization configuration.
void ReloadConfiguration();
+ // Reloads the authorization configuration from the given path.
+ // Useful when you want to reload the configuration from a different path.
+ void ReloadConfiguration(const std::string& configuration_path);
+
void SetBasePrivilegesFolder(std::string_view base_privileges_folder);
+ // Returns the peer's redfish role. Returns an empty string if the peer is not
+ // authorized.
+ std::string GetPeerRedfishRole(const PeerSpiffeIdentity& peer) const;
+
+ // Returns the sample rate limit for the peer role.
+ uint64_t GetSampleRateLimit(const std::string& peer_role) const;
+
std::unordered_set<std::string> GetPeerRedfishPrivileges(
const PeerSpiffeIdentity& peer) const {
return authz_configuration_.GetPeerRedfishPrivileges(peer);