Commit 3cb274ed authored by Ales Mrazek's avatar Ales Mrazek

added new configuration option to use "commonName" instead of "emailAddress" for client username

parent 6fe91e83
.. |date| date::
*******
Jetconf
*******
*******************************
JetConf with disable SSL option
*******************************
**ssl** branch of **Jetconf** project allow you to disable SSL in *YAM* configuration file::
:Authors:
Ladislav Lhotka <lhotka@nic.cz>,
Aleš Mrázek <ales.mrazek@nic.cz>,
Pavel Špírek <pavel.spirek@nic.cz>
:Version: 0.3.4
:Date: 15.08.2019
DISABLE_SSL: true
Jetconf is an implementation of the RESTCONF_ protocol written in
Python 3.
and read header SSL ``x-ssl-client-cn`` to establish the user making the request.
Main features:
Http request will be made by user ``example@mail.cz`` which is added to header::
* HTTP/2 over TLS, certificate-based authentication of clients
# get root configuration data
curl --http2-prior-knowledge -H "x-ssl-client-cn: example@mail.cz" -X GET "http://localhost:8443/restconf/data"
* JSON data encoding
This allows you to run Jetconf behind a load balancer like HAproxy, where you can terminate the TLS connection, add necessary headers and forward the http request to Jetconf.::
* Per-user candidate datastores with transactions
# forward SSL headers to jetconf
http-request set-header X-SSL %[ssl_fc]
http-request set-header X-SSL-Client-Verify %[ssl_c_verify]
http-request set-header X-SSL-Client-DN %{+Q}[ssl_c_s_dn]
http-request set-header X-SSL-Client-CN %{+Q}[ssl_c_s_dn(cn)]
http-request set-header X-SSL-Issuer %{+Q}[ssl_c_i_dn]
http-request set-header X-SSL-Client-Not-Before %{+Q}[ssl_c_notbefore]
http-request set-header X-SSL-Client-Not-After %{+Q}[ssl_c_notafter]
* Support for NACM_
Requirements
=============
Jetconf requires **Python 3.6 or newer**::
sudo apt-get install python3
sudo apt-get install python3-pip
These requirements should be installed by running *Instalation*
::
colorlog
h2==3.0.1
pytz
PyYAML
yangson
Installation
============
Jetconf can be installed by PyPI::
$ python3 -m pip install jetconf
Running
=======
Running Jetconf::
$ jetconf -c <path_to_config_file.yaml>
For development purposes, Jetconf can also be started directly
from Git repository with run.py script.::
$ ./run.py -c <path_to_config_file.yaml>
Example configuration (template)
================================
In the data folder, there is an example template for
configuring paths, certificates etc.::
example-config.yaml
In this configuration file, you have to modify all paths to match
your actual file locations.
Links
=====
* `Git repository`_
* `Documentation`_
.. _RESTCONF: https://tools.ietf.org/html/draft-ietf-netconf-restconf-18
.. _NACM: https://datatracker.ietf.org/doc/rfc6536/
.. _Git repository: https://github.com/CZ-NIC/jetconf
.. _Documentation: https://gitlab.labs.nic.cz/labs/jetconf/wikis/home
......@@ -7,15 +7,18 @@ GLOBAL:
LOG_DBG_MODULES: ["usr_conf_data_handlers", "nacm", "data"]
YANG_LIB_DIR: "yang-modules"
DATA_JSON_FILE: "data.json"
CLIENT_CN: true
BACKEND_PACKAGE: "jetconf_jukebox"
HTTP_SERVER:
DOC_ROOT: "/home/user/jetconf-conf/doc-root"
DOC_ROOT: "doc-root"
DOC_DEFAULT_NAME: "index.html"
API_ROOT: "/restconf"
SERVER_NAME: "jetconf-h2"
DISABLE_SSL: true
DBG_DISABLE_CERTS: true
SERVER_SSL_CERT: "server_localhost.crt"
SERVER_SSL_PRIVKEY: "server_localhost.key"
CA_CERT: "ca.pem"
NACM:
ENABLED: true
ALLOWED_USERS: ["example@mail.cz"]
......@@ -22,6 +22,7 @@ class JcConfig:
"YANG_LIB_DIR": yang_mod_dir_env,
"DATA_JSON_FILE": "data.json",
"VALIDATE_TRANSACTIONS": True,
"CLIENT_CN": False,
"BACKEND_PACKAGE": "jetconf_jukebox"
}
......@@ -34,11 +35,11 @@ class JcConfig:
"UPLOAD_SIZE_LIMIT": 1,
"LISTEN_LOCALHOST_ONLY": False,
"PORT": 8443,
"DISABLE_SSL": False,
"DBG_DISABLE_CERT": False,
"SERVER_SSL_CERT": "server.crt",
"SERVER_SSL_PRIVKEY": "server.key",
"DISABLE_SSL": False,
"CA_CERT": "ca.pem",
"DBG_DISABLE_CERTS": False
}
nacm_def = {
......
import re
import sys
import logging
from collections import OrderedDict
from colorlog import debug, getLogger
from enum import Enum
......@@ -21,12 +23,36 @@ class PathFormat(Enum):
XPATH = 1
class CertHelpers:
class ClientHelpers:
@staticmethod
def get_field(cert: SSLCertT, key: str) -> str:
if config.CFG.http["DBG_DISABLE_CERTS"] and (key == "emailAddress"):
def get_username(client_cert: SSLCertT, headers: OrderedDict):
if config.CFG.http["DBG_DISABLE_CERT"]:
return "test-user"
if config.CFG.glob["CLIENT_CN"]:
return ClientHelpers.get_common_name(client_cert, headers)
else:
return ClientHelpers.get_email_address(client_cert, headers)
@staticmethod
def get_email_address(client_cert: SSLCertT, headers: OrderedDict):
if config.CFG.http["DISABLE_SSL"]:
h_dn = HeadersHelper.get_header(headers, "x-ssl-client-dn")
return re.search(r'emailAddress=(.*?)(\/|$)', h_dn).group(1)
else:
return CertHelpers.get_field(client_cert, "emailAddress")
@staticmethod
def get_common_name(client_cert: SSLCertT, headers: OrderedDict):
if config.CFG.http["DISABLE_SSL"]:
return HeadersHelper.get_header(headers, "x-ssl-client-cn")
else:
return CertHelpers.get_field(client_cert, "commonName")
class CertHelpers:
@staticmethod
def get_field(cert: SSLCertT, key: str) -> str:
try:
retval = ([x[0][1] for x in cert["subject"] if x[0][0] == key] or [None])[0]
except (IndexError, KeyError, TypeError):
......@@ -34,6 +60,17 @@ class CertHelpers:
return retval
class HeadersHelper:
@staticmethod
def get_header(headers: OrderedDict, key: str) -> str:
try:
retval = headers.get(key)
except (IndexError, KeyError, TypeError):
retval = None
return retval
class DataHelpers:
# Get the namespace of the first segment in path
# Raises ValueError if the first segment is not in fully-qualified format
......
......@@ -14,7 +14,7 @@ from yangson.instance import NonexistentInstance, InstanceValueError, RootNode,
from yangson.instvalue import ArrayValue
from . import config
from .helpers import CertHelpers, DateTimeHelpers, ErrorHelpers, LogHelpers, SSLCertT
from .helpers import ClientHelpers, DateTimeHelpers, ErrorHelpers, LogHelpers, SSLCertT
from .journal import RpcInfo
from .data import BaseDatastore, ChangeType
from .errors import (
......@@ -48,13 +48,6 @@ ERRTAG_INVVALUE = "invalid-value"
ERRTAG_EXISTS = "data-exists"
def get_username(client_cert: SSLCertT, headers: OrderedDict) -> str:
if config.CFG.http["DISABLE_SSL"]:
return headers.get("x-ssl-client-cn")
else:
return CertHelpers.get_field(client_cert, "emailAddress")
class HttpRequestError(Exception):
pass
......@@ -358,7 +351,7 @@ class HttpHandlersImpl:
return http_resp
def get_api_running(self, headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
username = get_username(client_cert, headers)
username = ClientHelpers.get_username(client_cert, headers)
info("[{}] api_get_running: {}".format(username, headers[":path"]))
api_pth = headers[":path"][len(config.CFG.api_root_running_data):]
......@@ -366,7 +359,7 @@ class HttpHandlersImpl:
return http_resp
def get_api_staging(self, headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
username = get_username(client_cert, headers)
username = ClientHelpers.get_username(client_cert, headers)
info("[{}] api_get_staging: {}".format(username, headers[":path"]))
api_pth = headers[":path"][len(config.CFG.api_root_data):]
......@@ -374,7 +367,7 @@ class HttpHandlersImpl:
return http_resp
def get_api_op(self, headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
username = get_username(client_cert, headers)
username = ClientHelpers.get_username(client_cert, headers)
info("[{}] get_op: {}".format(username, headers[":path"]))
api_pth = headers[":path"][len(config.CFG.api_root_ops):].rstrip("/")
......@@ -423,7 +416,7 @@ class HttpHandlersImpl:
def get_file(self, headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
# Ordinary file on filesystem
username = get_username(client_cert, headers)
username = ClientHelpers.get_username(client_cert, headers)
url_path = headers[":path"].split("?")[0]
url_path_safe = "".join(filter(lambda c: c.isalpha() or c in "/-_.", url_path)).replace("..", "").strip("/")
file_path = os.path.join(config.CFG.http["DOC_ROOT"], url_path_safe)
......@@ -603,7 +596,7 @@ class HttpHandlersImpl:
return http_resp
def post_api(self, headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
username = get_username(client_cert, headers)
username = ClientHelpers.get_username(client_cert, headers)
info("[{}] api_post: {}".format(username, headers[":path"]))
api_pth = headers[":path"][len(config.CFG.api_root_data):]
......@@ -684,7 +677,7 @@ class HttpHandlersImpl:
return http_resp
def put_api(self, headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
username = get_username(client_cert, headers)
username = ClientHelpers.get_username(client_cert, headers)
info("[{}] api_put: {}".format(username, headers[":path"]))
api_pth = headers[":path"][len(config.CFG.api_root_data):]
......@@ -752,7 +745,7 @@ class HttpHandlersImpl:
return http_resp
def delete_api(self, headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
username = get_username(client_cert, headers)
username = ClientHelpers.get_username(client_cert, headers)
info("[{}] api_delete: {}".format(username, headers[":path"]))
api_pth = headers[":path"][len(config.CFG.api_root_data):]
......@@ -760,7 +753,7 @@ class HttpHandlersImpl:
return http_resp
def post_api_op_call(self, headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
username = get_username(client_cert, headers)
username = ClientHelpers.get_username(client_cert, headers)
info("[{}] invoke_op: {}".format(username, headers[":path"]))
api_pth = headers[":path"][len(config.CFG.api_root_ops):]
......
......@@ -279,7 +279,6 @@ class RestServer:
# HTTP server init
if config.CFG.http["DISABLE_SSL"]:
ssl_context = None
warn("SSL Disabled")
else:
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.options |= (ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_COMPRESSION)
......@@ -292,7 +291,7 @@ class RestServer:
info("Python not compiled with ALPN support, using NPN instead.")
ssl_context.set_npn_protocols(["h2"])
if not config.CFG.http["DBG_DISABLE_CERTS"]:
if not config.CFG.http["DBG_DISABLE_CERT"]:
ssl_context.verify_mode = ssl.CERT_REQUIRED
ssl_context.load_verify_locations(cafile=config.CFG.http["CA_CERT"])
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment