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