From 62463db5e26cbe8951a022bc7751f22438c710f3 Mon Sep 17 00:00:00 2001 From: Yogesh Sharma Date: Wed, 26 Aug 2020 08:37:02 -0700 Subject: [PATCH] Add support for user defined HTTP header to Patroni REST API response (#1645) Close #1644 --- docs/ENVIRONMENT.rst | 2 ++ docs/SETTINGS.rst | 22 ++++++++++++++++++++++ patroni/api.py | 6 ++++++ patroni/config.py | 3 ++- tests/test_api.py | 2 ++ 5 files changed, 34 insertions(+), 1 deletion(-) diff --git a/docs/ENVIRONMENT.rst b/docs/ENVIRONMENT.rst index b27c7382..7e19fc0a 100644 --- a/docs/ENVIRONMENT.rst +++ b/docs/ENVIRONMENT.rst @@ -143,6 +143,8 @@ REST API - **PATRONI\_RESTAPI\_KEYFILE**: Specifies the file with the secret key in the PEM format. - **PATRONI\_RESTAPI\_CAFILE**: Specifies the file with the CA_BUNDLE with certificates of trusted CAs to use while verifying client certs. - **PATRONI\_RESTAPI\_VERIFY\_CLIENT**: ``none`` (default), ``optional`` or ``required``. When ``none`` REST API will not check client certificates. When ``required`` client certificates are required for all REST API calls. When ``optional`` client certificates are required for all unsafe REST API endpoints. When ``required`` is used, then client authentication succeeds, if the certificate signature verification succeeds. For ``optional`` the client cert will only be checked for ``PUT``, ``POST``, ``PATCH``, and ``DELETE`` requests. +- **PATRONI\_RESTAPI\_HTTP\_EXTRA\_HEADERS**: (optional) HTTP headers let the REST API server pass additional information with an HTTP response. +- **PATRONI\_RESTAPI\_HTTPS\_EXTRA\_HEADERS**: (optional) HTTPS headers let the REST API server pass additional information with an HTTP response when TLS is enabled. This will also pass additional information set in ``http_extra_headers``. CTL --- diff --git a/docs/SETTINGS.rst b/docs/SETTINGS.rst index 7014d55a..502116ba 100644 --- a/docs/SETTINGS.rst +++ b/docs/SETTINGS.rst @@ -272,6 +272,28 @@ REST API - **keyfile**: (optional): Specifies the file with the secret key in the PEM format. - **cafile**: (optional): Specifies the file with the CA_BUNDLE with certificates of trusted CAs to use while verifying client certs. - **verify\_client**: (optional): ``none`` (default), ``optional`` or ``required``. When ``none`` REST API will not check client certificates. When ``required`` client certificates are required for all REST API calls. When ``optional`` client certificates are required for all unsafe REST API endpoints. When ``required`` is used, then client authentication succeeds, if the certificate signature verification succeeds. For ``optional`` the client cert will only be checked for ``PUT``, ``POST``, ``PATCH``, and ``DELETE`` requests. + - **http\_extra\_headers**: (optional): HTTP headers let the REST API server pass additional information with an HTTP response. + - **https\_extra\_headers**: (optional): HTTPS headers let the REST API server pass additional information with an HTTP response when TLS is enabled. This will also pass additional information set in ``http_extra_headers``. + +Here is an example of both **http_extra_headers** and **https_extra_headers**: + +.. code:: YAML + + restapi: + listen: + connect_address: + authentication: + username: + password: + http_extra_headers: + 'X-Frame-Options': 'SAMEORIGIN' + 'X-XSS-Protection': '1; mode=block' + 'X-Content-Type-Options': 'nosniff' + cafile: + certfile: + keyfile: + https_extra_headers: + 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains' .. _patronictl_settings: diff --git a/patroni/api.py b/patroni/api.py index e206a22b..28e4ce2b 100644 --- a/patroni/api.py +++ b/patroni/api.py @@ -38,6 +38,8 @@ class RestApiHandler(BaseHTTPRequestHandler): headers['Content-Type'] = content_type for name, value in headers.items(): self.send_header(name, value) + for name, value in self.server.patroni.api.http_extra_headers.items(): + self.send_header(name, value) self.end_headers() self.wfile.write(body.encode('utf-8')) @@ -533,6 +535,7 @@ class RestApiServer(ThreadingMixIn, HTTPServer, Thread): self.patroni = patroni self.__listen = None self.__ssl_options = None + self.http_extra_headers = {} self.reload_config(config) self.daemon = True @@ -667,6 +670,9 @@ class RestApiServer(ThreadingMixIn, HTTPServer, Thread): ssl_options = {n: config[n] for n in ('certfile', 'keyfile', 'cafile') if n in config} + self.http_extra_headers = config.get('http_extra_headers') or {} + self.http_extra_headers.update(ssl_options.get('certfile') and config.get('https_extra_headers') or {}) + if isinstance(config.get('verify_client'), six.string_types): ssl_options['verify_client'] = config['verify_client'].lower() diff --git a/patroni/config.py b/patroni/config.py index 8253d18d..f81fa16a 100644 --- a/patroni/config.py +++ b/patroni/config.py @@ -244,7 +244,8 @@ class Config(object): if value: ret[section][param] = value - _set_section_values('restapi', ['listen', 'connect_address', 'certfile', 'keyfile', 'cafile', 'verify_client']) + _set_section_values('restapi', ['listen', 'connect_address', 'certfile', 'keyfile', 'cafile', 'verify_client', + 'http_extra_headers', 'https_extra_headers']) _set_section_values('ctl', ['insecure', 'cacert', 'certfile', 'keyfile']) _set_section_values('postgresql', ['listen', 'connect_address', 'config_dir', 'data_dir', 'pgpass', 'bin_dir']) _set_section_values('log', ['level', 'traceback_level', 'format', 'dateformat', 'max_queue_size', diff --git a/tests/test_api.py b/tests/test_api.py index 867a58d0..5f7dacad 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -120,6 +120,8 @@ class MockPatroni(object): logger = MockLogger() tags = {} version = '0.00' + api = Mock() + api.http_extra_headers = {} noloadbalance = PropertyMock(return_value=False) scheduled_restart = {'schedule': future_restart_time, 'postmaster_start_time': postgresql.postmaster_start_time()}