Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DM-45049: Add support for SSL/TLS in the Qserv Czar frontend #859

Merged
merged 5 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions admin/local/docker/compose/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,8 @@ services:
--xrootd-manager czar_xrootd
--http-frontend-port 4048
--http-frontend-threads 4
--http-ssl-cert-file /config-etc/ssl/czar-cert.pem
--http-ssl-private-key-file /config-etc/ssl/czar-key.pem
--log-cfg-file=/config-etc/log/log-czar-proxy.cnf
--repl-instance-id qserv_proj
--repl-auth-key replauthkey
Expand Down
3 changes: 2 additions & 1 deletion admin/tools/docker/base/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,10 @@ WORKDIR /home/qserv

RUN mkdir -p /qserv/data && \
mkdir /config-etc && \
mkdir /config-etc/ssl && \
mkdir -p /qserv/run/tmp && \
mkdir -p /var/run/xrootd && \
chown qserv:qserv /qserv/data /config-etc /qserv/run/tmp /var/run/xrootd
chown qserv:qserv /qserv/data /config-etc /config-etc/ssl /qserv/run/tmp /var/run/xrootd

RUN alternatives --install /usr/bin/python python /usr/bin/python3.9 1
ENV PYTHONPATH "${PYTHONPATH}:/usr/local/python"
Expand Down
2 changes: 1 addition & 1 deletion src/admin/etc/integration_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ reference-db-uri: mysql://qsmaster@integration-test-reference:3306
reference-db-admin-uri: mysql://root:CHANGEME@integration-test-reference:3306
replication-controller-uri: repl://@repl_controller:25081
qserv-uri: qserv://qsmaster@czar_proxy:4040
qserv-http-uri: http://czar_http:4048
qserv-http-uri: https://czar_http:4048
czar-db-admin-uri: mysql://root:CHANGEME@czar_mariadb:3306
# The folder where the itest sources will be mounted in the container:
qserv-testdata-dir: /tmp/qserv/itest_src
Expand Down
24 changes: 22 additions & 2 deletions src/admin/python/lsst/qserv/admin/cli/entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,13 @@ class CommandInfo:
"--lua-cpath=/usr/local/lua/qserv/lib/czarProxy.so --defaults-file={{proxy_cfg_path}}",
)),
("czar-http", CommandInfo(
"qserv-czar-http http {{czar_cfg_path}} {{http_frontend_port}} {{http_frontend_threads}} ",
"qserv-czar-http "
"http "
"{{czar_cfg_path}} "
"{{http_frontend_port}} "
"{{http_frontend_threads}} "
"{{http_ssl_cert_file}} "
"{{http_ssl_private_key_file}}",
)),
("cmsd-manager", CommandInfo(
"cmsd -c {{cmsd_manager_cfg_path}} -n manager -I v4",
Expand Down Expand Up @@ -563,6 +569,18 @@ def proxy(ctx: click.Context, **kwargs: Any) -> None:
help="The number of threads for the HTTP server of the frontend. The value of http_frontend_threads is passed"
" as a command-line parameter to the application."
)
@click.option(
"--http-ssl-cert-file",
help="A location of a file containing an SSL/TSL certificate.",
default="/config-etc/ssl/czar-cert.pem",
show_default=True,
)
@click.option(
"--http-ssl-private-key-file",
help="A location of a file containing an SSL/TSL private key.",
default="/config-etc/ssl/czar-key.pem",
show_default=True,
)
@click.option(
"--czar-cfg-file",
help="Path to the czar config file.",
Expand Down Expand Up @@ -595,8 +613,10 @@ def czar_http(ctx: click.Context, **kwargs: Any) -> None:
db_uri=targs["db_uri"],
czar_cfg_file=targs["czar_cfg_file"],
czar_cfg_path=targs["czar_cfg_path"],
cmd=targs["cmd"],
log_cfg_file=targs["log_cfg_file"],
http_ssl_cert_file=targs["http_ssl_cert_file"],
http_ssl_private_key_file=targs["http_ssl_private_key_file"],
cmd=targs["cmd"],
)


Expand Down
33 changes: 33 additions & 0 deletions src/admin/python/lsst/qserv/admin/cli/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import logging
import os
import shlex
import socket
import subprocess
import sys
import time
Expand Down Expand Up @@ -657,6 +658,8 @@ def enter_czar_http(
czar_cfg_file: str,
czar_cfg_path: str,
log_cfg_file: str,
http_ssl_cert_file : str,
http_ssl_private_key_file : str,
cmd: str,
) -> None:
"""Entrypoint script for the proxy container.
Expand All @@ -673,6 +676,10 @@ def enter_czar_http(
Location to render the czar config file.
log_cfg_file : `str`
Location of the log4cxx config file.
http_ssl_cert_file : `str`
The path to the SSL certificate file.
http_ssl_private_key_file : `str`
The path to the SSL private key file.
cmd : `str`
The jinja2 template for the command for this function to execute.
"""
Expand Down Expand Up @@ -700,6 +707,32 @@ def enter_czar_http(

_do_smig_block(qmeta_smig_dir, "qmeta", db_uri)

# check if the SSL certificate and private key files exist and create
# them if they don't.
if not os.path.exists(http_ssl_cert_file) or not os.path.exists(http_ssl_private_key_file):
_log.info("Generating self-signed SSL/TLS certificate %s and private key %s for HTTPS",
http_ssl_cert_file, http_ssl_private_key_file)
country = "US"
state = "California"
loc = "Menlo Park"
org = "SLAC National Accelerator Laboratory"
org_unit = "Rubin Observatory"
hostname = socket.gethostbyaddr(socket.gethostname())[0] # FQDN if available
subj = f"/C={country}/ST={state}/L={loc}/O={org}/OU={org_unit}/CN={hostname}"
openssl_cmd = [
"openssl", "req",
"-x509",
"-newkey", "rsa:4096",
"-out", http_ssl_cert_file,
"-keyout", http_ssl_private_key_file,
"-sha256",
"-days", "365",
"-nodes",
"-subj", subj]
ret = subprocess.run(openssl_cmd, env=dict(os.environ,), cwd="/home/qserv")
if ret.returncode != 0:
raise RuntimeError("Failed to create SSL certificate and private key files.")

env = dict(
os.environ,
LD_PRELOAD=ld_preload,
Expand Down
11 changes: 7 additions & 4 deletions src/admin/python/lsst/qserv/admin/itest.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from filecmp import dircmp
import json
import logging
import urllib3
import requests
import os
import re
Expand Down Expand Up @@ -510,7 +511,7 @@ def run_attached_http(self, connection: str, database: str) -> None:

# Submit the query, check and analyze the completion status
svc = str(urljoin(connection, '/query'))
req = requests.post(svc, json={'query': query, 'database': database, 'binary_encoding': 'hex'})
req = requests.post(svc, json={'query': query, 'database': database, 'binary_encoding': 'hex'}, verify=False)
req.raise_for_status()
res = req.json()
if res['success'] == 0:
Expand All @@ -534,7 +535,7 @@ def run_detached_http(self, connection: str, database: str) -> None:

# Submit the query via the async service, check and analyze the completion status
svc = str(urljoin(connection, '/query-async'))
req = requests.post(svc, json={'query': query, 'database': database})
req = requests.post(svc, json={'query': query, 'database': database}, verify=False)
req.raise_for_status()
res = req.json()
if res['success'] == 0:
Expand All @@ -547,7 +548,7 @@ def run_detached_http(self, connection: str, database: str) -> None:
while time.time() < end_time:
# Submit a request to check a status of the query
svc = str(urljoin(connection, f"/query-async/status/{query_id}"))
req = requests.get(svc)
req = requests.get(svc, verify=False)
req.raise_for_status()
res = req.json()
if res['success'] == 0:
Expand All @@ -561,7 +562,7 @@ def run_detached_http(self, connection: str, database: str) -> None:

# Make another request to pull the result set
svc = str(urljoin(connection, f"/query-async/result/{query_id}?binary_encoding=hex"))
req = requests.get(svc)
req = requests.get(svc, verify=False)
req.raise_for_status()
res = req.json()
if res['success'] == 0:
Expand Down Expand Up @@ -747,6 +748,8 @@ def __init__(
self.db_name,
skip_numbers,
)
# Supress the warning about the self-signed certificate
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def run(self) -> None:
"""Run the test queries in a test case.
Expand Down
8 changes: 5 additions & 3 deletions src/czar/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
add_library(czar OBJECT)

target_sources(czar PRIVATE
ChttpModule.cc
Czar.cc
HttpCzarIngestModule.cc
HttpCzarSvc.cc
HttpCzarQueryModule.cc
HttpModule.cc
HttpCzarSvc.cc
HttpMonitorModule.cc
HttpSvc.cc
MessageTable.cc
QhttpModule.cc
)

target_include_directories(czar PRIVATE
Expand All @@ -23,6 +24,7 @@ target_link_libraries(czar PUBLIC
util
log
XrdSsiLib
cpp-httplib
)

function(CZAR_UTILS)
Expand Down Expand Up @@ -51,4 +53,4 @@ endfunction()

czar_utils(
qserv-czar-http
)
)
67 changes: 67 additions & 0 deletions src/czar/ChttpModule.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* LSST Data Management System
*
* This product includes software developed by the
* LSST Project (http://www.lsst.org/).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the LSST License Statement and
* the GNU General Public License along with this program. If not,
* see <http://www.lsstcorp.org/LegalNotices/>.
*/

// Class header
#include "czar/ChttpModule.h"

// System headers
#include <stdexcept>

// Qserv headers
#include "cconfig/CzarConfig.h"
#include "http/Exceptions.h"
#include "http/RequestBodyJSON.h"
#include "http/RequestQuery.h"

using namespace std;

namespace lsst::qserv::czar {

ChttpModule::ChttpModule(string const& context, httplib::Request const& req, httplib::Response& resp)
: http::ChttpModule(cconfig::CzarConfig::instance()->replicationAuthKey(),
cconfig::CzarConfig::instance()->replicationAdminAuthKey(), req, resp),
_context(context) {}

string ChttpModule::context() const { return _context; }

void ChttpModule::enforceCzarName(string const& func) const {
string const czarNameAttrName = "czar";
string czarName;
if (method() == "GET") {
if (!query().has(czarNameAttrName)) {
throw http::Error(func, "No Czar identifier was provided in the request query.");
}
czarName = query().requiredString(czarNameAttrName);
} else {
if (!body().has(czarNameAttrName)) {
throw http::Error(func, "No Czar identifier was provided in the request body.");
}
czarName = body().required<string>(czarNameAttrName);
}
string const expectedCzarName = cconfig::CzarConfig::instance()->name();
if (expectedCzarName != czarName) {
string const msg = "Requested Czar identifier '" + czarName + "' does not match the one '" +
expectedCzarName + "' of the current Czar.";
throw http::Error(func, msg);
}
}

} // namespace lsst::qserv::czar
69 changes: 69 additions & 0 deletions src/czar/ChttpModule.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* LSST Data Management System
*
* This product includes software developed by the
* LSST Project (http://www.lsst.org/).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the LSST License Statement and
* the GNU General Public License along with this program. If not,
* see <http://www.lsstcorp.org/LegalNotices/>.
*/
#ifndef LSST_QSERV_CZAR_CHTTPMODULE_H
#define LSST_QSERV_CZAR_CHTTPMODULE_H

// System headers
#include <string>

// Qserv headers
#include "http/ChttpModule.h"

// Forward declarations
namespace httplib {
class Request;
class Response;
} // namespace httplib

// This header declarations
namespace lsst::qserv::czar {

/**
* Class ChttpModule is an intermediate base class of the Qserv Czar modules.
*/
class ChttpModule : public http::ChttpModule {
public:
ChttpModule() = delete;
ChttpModule(ChttpModule const&) = delete;
ChttpModule& operator=(ChttpModule const&) = delete;

virtual ~ChttpModule() = default;

protected:
ChttpModule(std::string const& context, httplib::Request const& req, httplib::Response& resp);

virtual std::string context() const final;

/**
* Check if Czar identifier is present in a request and if so then the identifier
* is the same as the one of the current Czar. Throw an exception in case of mismatch.
* @param func The name of the calling context (it's used for error reporting).
* @throws std::invalid_argument If the dentifiers didn't match.
*/
void enforceCzarName(std::string const& func) const;

private:
std::string const _context;
};

} // namespace lsst::qserv::czar

#endif // LSST_QSERV_CZAR_CHTTPMODULE_H
15 changes: 6 additions & 9 deletions src/czar/HttpCzarIngestModule.cc
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@
#include "http/BinaryEncoding.h"
#include "http/Exceptions.h"
#include "http/MetaModule.h"
#include "http/RequestBody.h"
#include "qhttp/Request.h"
#include "http/RequestBodyJSON.h"
#include "qhttp/Status.h"

using namespace std;
Expand Down Expand Up @@ -106,18 +105,16 @@ void setProtocolFields(json& data) {
namespace lsst::qserv::czar {

void HttpCzarIngestModule::process(asio::io_service& io_service, string const& context,
shared_ptr<qhttp::Request> const& req,
shared_ptr<qhttp::Response> const& resp, string const& subModuleName,
http::AuthType const authType) {
httplib::Request const& req, httplib::Response& resp,
string const& subModuleName, http::AuthType const authType) {
HttpCzarIngestModule module(io_service, context, req, resp);
module.execute(subModuleName, authType);
}

HttpCzarIngestModule::HttpCzarIngestModule(asio::io_service& io_service, string const& context,
shared_ptr<qhttp::Request> const& req,
shared_ptr<qhttp::Response> const& resp)
: http::ModuleBase(cconfig::CzarConfig::instance()->replicationAuthKey(),
cconfig::CzarConfig::instance()->replicationAdminAuthKey(), req, resp),
httplib::Request const& req, httplib::Response& resp)
: http::ChttpModule(cconfig::CzarConfig::instance()->replicationAuthKey(),
cconfig::CzarConfig::instance()->replicationAdminAuthKey(), req, resp),
_io_service(io_service),
_context(context),
_registryBaseUrl("http://" + cconfig::CzarConfig::instance()->replicationRegistryHost() + ":" +
Expand Down
Loading
Loading