blob: 091a5b153dc2ae3e65a32ceceba4e7b64dc51828 [file] [log] [blame]
#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