|  | #include "authz_server.h" | 
|  |  | 
|  | #include <chrono> | 
|  | #include <cstddef> | 
|  | #include <cstdint> | 
|  | #include <cstdio> | 
|  | #include <cstdlib> | 
|  | #include <format> | 
|  | #include <fstream> | 
|  | #include <iostream> | 
|  | #include <iterator> | 
|  | #include <memory> | 
|  | #include <string> | 
|  | #include <string_view> | 
|  | #include <utility> | 
|  |  | 
|  | #include "absl/log/log.h" | 
|  | #include "absl/status/status.h" | 
|  | #include "absl/status/statusor.h" | 
|  | #include "absl/strings/str_cat.h" | 
|  | #include "absl/synchronization/mutex.h" | 
|  | #include "grpc/grpc_security_constants.h" | 
|  | #include "grpcpp/ext/proto_server_reflection_plugin.h" | 
|  | #include "grpcpp/security/auth_context.h" | 
|  | #include "grpcpp/security/server_credentials.h" | 
|  | #include "grpcpp/security/tls_certificate_provider.h" | 
|  | #include "grpcpp/security/tls_credentials_options.h" | 
|  | #include "grpcpp/server_builder.h" | 
|  | #include "grpcpp/server_context.h" | 
|  | #include "grpcpp/support/status.h" | 
|  | #include "nlohmann/json.hpp" | 
|  | #include "nlohmann/json_fwd.hpp" | 
|  | #include "jwt-cpp/jwt.h" | 
|  | #include "jwt-cpp/traits/nlohmann-json/defaults.h" | 
|  | #include "config_parser.h" | 
|  | #include "oauth_utils.h" | 
|  | #include "oauth.pb.h" | 
|  | #include "zatar/generate_self_signed_cert.h" | 
|  | #include "bmcweb_authorizer_singleton.h" | 
|  |  | 
|  | namespace milotic::authz { | 
|  | namespace { | 
|  |  | 
|  | using ::milotic::authn::GenerateRandomRsaKey; | 
|  | using ::oauth::ExchangeRequest; | 
|  | using ::oauth::ExchangeResponse; | 
|  | using ::oauth::HiscTokenRequest; | 
|  | using ::oauth::HiscTokenResponse; | 
|  | using ::oauth::MintTokenRequest; | 
|  | using ::oauth::MintTokenResponse; | 
|  | using ::oauth::SignatureAlgorithm; | 
|  | using ::oauth::TokenType; | 
|  |  | 
|  | std::shared_ptr<grpc::ServerCredentials> GetCredentials( | 
|  | const ServerConfiguration& config) { | 
|  | constexpr unsigned int kFileWatcherRefreshIntervalSec = 30; | 
|  | grpc::experimental::TlsServerCredentialsOptions options( | 
|  | std::make_shared<grpc::experimental::FileWatcherCertificateProvider>( | 
|  | config.server_key_path, config.server_certificate_path, | 
|  | config.trust_bundle_path, kFileWatcherRefreshIntervalSec)); | 
|  |  | 
|  | options.set_cert_request_type( | 
|  | GRPC_SSL_REQUEST_AND_REQUIRE_CLIENT_CERTIFICATE_AND_VERIFY); | 
|  | options.watch_identity_key_cert_pairs(); | 
|  | options.watch_root_certs(); | 
|  | options.set_crl_directory(config.crl_directory); | 
|  |  | 
|  | return grpc::experimental::TlsServerCredentials(options); | 
|  | } | 
|  |  | 
|  | class HiscCert { | 
|  | public: | 
|  | explicit HiscCert(uint32_t port_id, uint32_t life_time_in_seconds, | 
|  | std::string_view public_key, | 
|  | HiscTokenSystemDependency& hisc_token_system_dependency) { | 
|  | // save public key to /tmp/hiscXXXXXX file | 
|  | char tmpname[] = "/tmp/hiscXXXXXX"; | 
|  | int fd = hisc_token_system_dependency.LibcMkstemp(tmpname); | 
|  | if (fd == -1) { | 
|  | status_ = absl::InternalError("Failed save key to tmp file"); | 
|  | return; | 
|  | } | 
|  | public_key_file_name_ = tmpname; | 
|  | std::ofstream public_key_file(public_key_file_name_.c_str()); | 
|  | public_key_file << public_key; | 
|  | public_key_file.close(); | 
|  | close(fd); | 
|  | // sign the public key | 
|  | std::string cmd = std::format(kHiscCertGenCmd, port_id, | 
|  | life_time_in_seconds, public_key_file_name_); | 
|  | int ret_code = | 
|  | hisc_token_system_dependency.RunSshKeyGen(cmd, public_key_file_name_); | 
|  | LOG(INFO) << "Execute Command:\n" << cmd << "\n"; | 
|  | if (ret_code != 0) { | 
|  | status_ = | 
|  | absl::InternalError(absl::StrCat("Failed execute command:\n", cmd)); | 
|  | return; | 
|  | } | 
|  |  | 
|  | cert_file_name_ = std::format("{}-cert.pub", public_key_file_name_); | 
|  | // Read the certificate | 
|  | std::ifstream cert_file(cert_file_name_); | 
|  | if (!cert_file.is_open()) { | 
|  | status_ = absl::InternalError( | 
|  | absl::StrCat("Failed opening cert file: ", cert_file_name_)); | 
|  | return; | 
|  | } | 
|  |  | 
|  | cert_.append((std::istreambuf_iterator<char>(cert_file)), | 
|  | std::istreambuf_iterator<char>()); | 
|  | cert_file.close(); | 
|  | status_ = absl::OkStatus(); | 
|  | } | 
|  | virtual ~HiscCert() { | 
|  | if (!public_key_file_name_.empty()) { | 
|  | std::remove(public_key_file_name_.c_str()); | 
|  | } | 
|  | if (!cert_file_name_.empty()) { | 
|  | std::remove(cert_file_name_.c_str()); | 
|  | } | 
|  | } | 
|  | absl::Status status_; | 
|  | std::string cert_; | 
|  |  | 
|  | private: | 
|  | std::string public_key_file_name_; | 
|  | std::string cert_file_name_; | 
|  | }; | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | namespace impl { | 
|  |  | 
|  | AuthorizationServiceImpl::AuthorizationServiceImpl( | 
|  | const ServerConfiguration& server_config) | 
|  | : authz_config_(std::make_unique<AuthzConfiguration>( | 
|  | nlohmann::json(), server_config.offline_node_entities_path, | 
|  | server_config.google_machine_identity_path)), | 
|  | token_count_(0), | 
|  | server_config_(server_config) { | 
|  | // Load Authz Config | 
|  | LoadAuthzConfig(); | 
|  |  | 
|  | // Generate Private Key | 
|  | absl::StatusOr<std::string> rsa_private_key = | 
|  | GenerateRandomRsaKey(server_config.rsa_public_key_path); | 
|  | if (!rsa_private_key.ok()) { | 
|  | LOG(ERROR) << "Failed GenerateRandomRsaKey: " << rsa_private_key.status(); | 
|  | return; | 
|  | } | 
|  | rsa_private_key_ = std::move(*rsa_private_key); | 
|  | } | 
|  |  | 
|  | void AuthorizationServiceImpl::LoadAuthzConfig() { | 
|  | std::ifstream authz_policy_file(server_config_.authz_configuration_path); | 
|  | if (!authz_policy_file.is_open()) { | 
|  | LOG(WARNING) << "Authz policy file at '" | 
|  | << server_config_.authz_configuration_path << "' is missing."; | 
|  | return; | 
|  | } | 
|  | std::string authz_policy_buffer = | 
|  | std::string(std::istreambuf_iterator<char>(authz_policy_file), | 
|  | std::istreambuf_iterator<char>()); | 
|  | nlohmann::json authz_policy_json = nlohmann::json::parse( | 
|  | authz_policy_buffer, nullptr, /*allow_exceptions*/ false); | 
|  | if (authz_policy_json.is_discarded()) { | 
|  | LOG(WARNING) << "Authz policy file at '" | 
|  | << server_config_.authz_configuration_path << "' is invalid." | 
|  | << std::endl | 
|  | << "|authz_policy_buffer|=" << authz_policy_buffer; | 
|  | } | 
|  | authz_config_->ReloadConfig(authz_policy_json, | 
|  | /*platform_config=*/nlohmann::json()); | 
|  | } | 
|  |  | 
|  | grpc::Status AuthorizationServiceImpl::Exchange( | 
|  | grpc::ServerContext* /*context*/, const ExchangeRequest* /*request*/, | 
|  | ExchangeResponse* /*response*/) { | 
|  | return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, | 
|  | "Exchange RPC is not implemented yet"); | 
|  | } | 
|  |  | 
|  | grpc::Status AuthorizationServiceImpl::MintToken( | 
|  | grpc::ServerContext* context, const MintTokenRequest* request, | 
|  | MintTokenResponse* response) { | 
|  | if (GetServerFqdn().empty()) { | 
|  | LOG(WARNING) << "Server fqdn is empty"; | 
|  | return grpc::Status(grpc::StatusCode::INTERNAL, | 
|  | "Internal Server fqdn is empty."); | 
|  | } | 
|  | if (grpc::Status status = AuthorizeMintTokenRpc(*context->auth_context()); | 
|  | !status.ok()) { | 
|  | LOG(WARNING) << "Peer is not authorized: " << status.error_message(); | 
|  | return status; | 
|  | } | 
|  | if (grpc::Status status = CheckMintTokenRequest(*request); !status.ok()) { | 
|  | LOG(WARNING) << "Request is invalid: " << status.error_message(); | 
|  | return status; | 
|  | } | 
|  | PeerSpiffeIdentity peer_identity; | 
|  | if (grpc::Status status = | 
|  | BmcWebAuthorizerSingleton::GetPeerIdentityFromAuthContext( | 
|  | *context->auth_context(), peer_identity); | 
|  | !status.ok()) { | 
|  | return status; | 
|  | } | 
|  | std::string subject = GenerateOAuthSubject(peer_identity); | 
|  |  | 
|  | auto token_builder = | 
|  | jwt::create() | 
|  | .set_type("JWT") | 
|  | .set_issuer("BMC Local Authorization Server") | 
|  | .set_subject(subject) | 
|  | .set_audience(GetServerFqdn()) | 
|  | .set_id(GetAndIncreaseTokenCount()) | 
|  | .set_issued_at(std::chrono::system_clock::now()) | 
|  | .set_expires_at(std::chrono::system_clock::now() + | 
|  | std::chrono::seconds{request->valid_for().seconds()}) | 
|  | .set_payload_claim("scope", | 
|  | "Redfish.Role." + request->redfish_role()); | 
|  | try { | 
|  | std::string token = | 
|  | token_builder.sign(jwt::algorithm::rs256("", rsa_private_key_)); | 
|  | response->set_token(token); | 
|  | } catch (const jwt::error::signature_generation_exception& e) { | 
|  | return grpc::Status( | 
|  | grpc::StatusCode::INTERNAL, | 
|  | absl::StrCat("signature_generation_exception: ", e.what())); | 
|  | } catch (const jwt::error::rsa_exception& e) { | 
|  | return grpc::Status(grpc::StatusCode::INTERNAL, | 
|  | absl::StrCat("rsa_exception: ", e.what())); | 
|  | } catch (...) { | 
|  | LOG(ERROR) << "Unexpected exception was caught."; | 
|  | return grpc::Status(grpc::StatusCode::INTERNAL, | 
|  | absl::StrCat("unexpected exception was caught")); | 
|  | } | 
|  | LOG(INFO) << "Created token for " << subject << "; redfish role is " | 
|  | << request->redfish_role(); | 
|  | return grpc::Status::OK; | 
|  | } | 
|  |  | 
|  | grpc::Status AuthorizationServiceImpl::AuthorizeMintTokenRpc( | 
|  | const ::grpc::AuthContext& context) { | 
|  | PeerSpiffeIdentity identity; | 
|  | if (grpc::Status status = | 
|  | BmcWebAuthorizerSingleton::GetPeerIdentityFromAuthContext(context, | 
|  | identity); | 
|  | !status.ok()) { | 
|  | return status; | 
|  | } | 
|  |  | 
|  | if (!authz_config_->IsPeerResourceOwner(identity)) { | 
|  | return grpc::Status( | 
|  | grpc::StatusCode::PERMISSION_DENIED, | 
|  | absl::StrCat("Peer is not a resource owner! |identity|=", | 
|  | identity.DebugString())); | 
|  | } | 
|  |  | 
|  | return grpc::Status::OK; | 
|  | } | 
|  |  | 
|  | grpc::Status AuthorizationServiceImpl::CheckMintTokenRequest( | 
|  | const MintTokenRequest& request) { | 
|  | if (request.token_type() != TokenType::TOKEN_JWT) { | 
|  | return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, | 
|  | "Only TokenType::TOKEN_JWT is supported."); | 
|  | } | 
|  | if (request.alg() != SignatureAlgorithm::ALG_RS256) { | 
|  | return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, | 
|  | "Only SignatureAlgorithm::ALG_RS256 is supported."); | 
|  | } | 
|  | if (request.valid_for().nanos() != 0) { | 
|  | return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, | 
|  | "Nanos in |valid_for| is not supported."); | 
|  | } | 
|  | // 1 hour | 
|  | constexpr std::int64_t kMinimumDurationSeconds = 60 * 60; | 
|  | // 30 days | 
|  | constexpr std::int64_t kMaximumDurationSeconds = 30 * 24 * 60 * 60; | 
|  |  | 
|  | if (request.valid_for().seconds() > kMaximumDurationSeconds || | 
|  | request.valid_for().seconds() < kMinimumDurationSeconds) { | 
|  | return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, | 
|  | "|valid_for| must be between 1 hour and 30 days."); | 
|  | } | 
|  |  | 
|  | if (request.has_redfish_privileges()) { | 
|  | return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, | 
|  | "Setting |RedfishPrivileges| in |redfish_scope| is " | 
|  | "not supported yet."); | 
|  | } | 
|  |  | 
|  | return grpc::Status::OK; | 
|  | } | 
|  |  | 
|  | std::string AuthorizationServiceImpl::GetServerFqdn() { | 
|  | absl::MutexLock lock(&mutex_); | 
|  | return server_config_.server_fqdn; | 
|  | } | 
|  |  | 
|  | void AuthorizationServiceImpl::SetServerFqdn(const std::string& fqdn) { | 
|  | absl::MutexLock lock(&mutex_); | 
|  | server_config_.server_fqdn = fqdn; | 
|  | } | 
|  |  | 
|  | std::string AuthorizationServiceImpl::GetAndIncreaseTokenCount() { | 
|  | absl::MutexLock lock(&mutex_); | 
|  | std::size_t token_count = token_count_; | 
|  | token_count_++; | 
|  | return std::to_string(token_count); | 
|  | } | 
|  |  | 
|  | grpc::Status AuthorizationServiceImpl::HiscToken( | 
|  | grpc::ServerContext* context, const HiscTokenRequest* request, | 
|  | HiscTokenResponse* response) { | 
|  | if (grpc::Status status = AuthorizeHiscTokenRpc(*context->auth_context()); | 
|  | !status.ok()) { | 
|  | LOG(WARNING) << "Peer is not authorized: " << status.error_message(); | 
|  | return status; | 
|  | } | 
|  | return HiscToken(request, response, bmc_hisc_token_system_dependency_); | 
|  | } | 
|  |  | 
|  | grpc::Status AuthorizationServiceImpl::HiscToken( | 
|  | const HiscTokenRequest* request, HiscTokenResponse* response, | 
|  | HiscTokenSystemDependency& hisc_token_system_dependency) { | 
|  | if (request->token_lifetime_seconds() > kMaxHiscCertLifeTimeInSeconds) { | 
|  | return grpc::Status( | 
|  | grpc::StatusCode::INVALID_ARGUMENT, | 
|  | absl::StrCat("Request certificate lifetime in seconds is ", | 
|  | request->token_lifetime_seconds(), ", longer than ", | 
|  | kMaxHiscCertLifeTimeInSeconds)); | 
|  | } | 
|  | HiscCert hisc_cert(request->hisc_port_id(), request->token_lifetime_seconds(), | 
|  | request->public_key(), hisc_token_system_dependency); | 
|  |  | 
|  | if (!hisc_cert.status_.ok()) { | 
|  | return grpc::Status(grpc::StatusCode::INTERNAL, | 
|  | hisc_cert.status_.ToString()); | 
|  | } | 
|  |  | 
|  | response->set_token(hisc_cert.cert_); | 
|  | return grpc::Status::OK; | 
|  | } | 
|  |  | 
|  | grpc::Status AuthorizationServiceImpl::AuthorizeHiscTokenRpc( | 
|  | const ::grpc::AuthContext& context) { | 
|  | PeerSpiffeIdentity identity; | 
|  | if (grpc::Status status = | 
|  | BmcWebAuthorizerSingleton::GetPeerIdentityFromAuthContext(context, | 
|  | identity); | 
|  | !status.ok()) { | 
|  | return status; | 
|  | } | 
|  |  | 
|  | if (!authz_config_->IsPeerResourceOwner(identity)) { | 
|  | return grpc::Status( | 
|  | grpc::StatusCode::PERMISSION_DENIED, | 
|  | absl::StrCat("Peer is not a resource owner! |identity|=", | 
|  | identity.DebugString())); | 
|  | } | 
|  |  | 
|  | return grpc::Status::OK; | 
|  | } | 
|  |  | 
|  | }  // namespace impl | 
|  |  | 
|  | AuthorizationServer::~AuthorizationServer() { | 
|  | if (server_ != nullptr) { | 
|  | LOG(INFO) << "Server shutting down.."; | 
|  | server_->Shutdown(); | 
|  | } | 
|  | } | 
|  |  | 
|  | AuthorizationServer::AuthorizationServer(const ServerConfiguration& config) | 
|  | : server_config_(config) {} | 
|  |  | 
|  | void AuthorizationServer::StartServer() { | 
|  | std::string server_address(absl::StrCat("[::]:", server_config_.port)); | 
|  | service_ = std::make_unique<impl::AuthorizationServiceImpl>(server_config_); | 
|  | grpc::reflection::InitProtoReflectionServerBuilderPlugin(); | 
|  |  | 
|  | grpc::ServerBuilder builder; | 
|  | builder.AddListeningPort(server_address, GetCredentials(server_config_)); | 
|  | builder.RegisterService(service_.get()); | 
|  |  | 
|  | // Set up the server to start accepting requests. | 
|  | server_ = builder.BuildAndStart(); | 
|  | LOG(INFO) << "Server listening on " << server_address; | 
|  | } | 
|  |  | 
|  | void AuthorizationServer::ReloadAuthzConfig(const std::string& server_fqdn) { | 
|  | LOG(INFO) << "Reloading Server"; | 
|  | service_->LoadAuthzConfig(); | 
|  | service_->SetServerFqdn(server_fqdn); | 
|  | } | 
|  |  | 
|  | void AuthorizationServer::Wait() { server_->Wait(); } | 
|  |  | 
|  | }  // namespace milotic::authz |