Skip to content

Commit efd131f

Browse files
4.0 uri scheme (#367)
* implemented uri scheme bolt bolt+ssc bolt+s neo4j neo4j+ssc neo4j+s neo4j+routing (deprecated) * removed bolt+neo4j scheme * added stub tests for routing driver * fixed ssl_context for python 3.5 * config setting secure removed the config secure is named encrypted added tests to check that URI scheme with encryption enabled raises error if config setting encryption and verify_cert is set on driver creation. * updated docs for config setting encrypted * fixed stub tests * changed verify_cert to trust
1 parent aeeb185 commit efd131f

17 files changed

+521
-127
lines changed

docs/source/usage_patterns.rst

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,17 @@ Driver Initialization Work Pattern
4141

4242
.. code-block:: python
4343
44-
from neo4j import GraphDatabase
44+
from neo4j import GraphDatabase, TRUST_SYSTEM_CA_SIGNED_CERTIFICATES
4545
from neo4j.exceptions import ServiceUnavailable
4646
4747
uri = "bolt://localhost:7687"
4848
4949
driver_config = {
5050
"encrypted": False,
51-
"trust": None,
51+
"trust": TRUST_SYSTEM_CA_SIGNED_CERTIFICATES,
5252
"user_agent": "example",
5353
"max_connection_lifetime": 1000,
5454
"max_connection_pool_size": 100,
55-
"connection_acquisition_timeout": 10,
56-
"connection_timeout": 1,
5755
"keep_alive": False,
5856
"max_retry_time": 10,
5957
"resolver": None,

neo4j/__init__.py

Lines changed: 85 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
from neo4j.conf import (
5656
Config,
5757
PoolConfig,
58+
TRUST_ALL_CERTIFICATES,
59+
TRUST_SYSTEM_CA_SIGNED_CERTIFICATES,
5860
)
5961
from neo4j.meta import (
6062
experimental,
@@ -88,29 +90,98 @@ def driver(cls, uri, *, auth=None, acquire_timeout=None, **config):
8890
concurrency.
8991
9092
:param uri:
93+
94+
bolt://host[:port]
95+
Settings: Direct driver with no encryption.
96+
97+
bolt+ssc://host[:port]
98+
Settings: Direct driver with encryption (accepts self signed certificates).
99+
100+
bolt+s://host[:port]
101+
Settings: Direct driver with encryption (accepts only certificates signed by an certificate authority), full certificate checks.
102+
103+
neo4j://host[:port][?routing_context]
104+
Settings: Routing driver with no encryption.
105+
106+
neo4j+ssc://host[:port][?routing_context]
107+
Settings: Routing driver with encryption (accepts self signed certificates).
108+
109+
neo4j+s://host[:port][?routing_context]
110+
Settings: Routing driver with encryption (accepts only certificates signed by an certificate authority), full certificate checks.
111+
91112
:param auth:
92-
:param acquire_timeout:
113+
:param acquire_timeout: seconds
93114
:param config: connection configuration settings
94115
"""
95-
parsed = urlparse(uri)
96-
if parsed.scheme == "bolt":
116+
117+
from neo4j.api import (
118+
parse_neo4j_uri,
119+
DRIVER_BOLT,
120+
DRIVER_NEO4j,
121+
SECURITY_TYPE_NOT_SECURE,
122+
SECURITY_TYPE_SELF_SIGNED_CERTIFICATE,
123+
SECURITY_TYPE_SECURE,
124+
URI_SCHEME_BOLT,
125+
URI_SCHEME_NEO4J,
126+
URI_SCHEME_BOLT_SELF_SIGNED_CERTIFICATE,
127+
URI_SCHEME_BOLT_SECURE,
128+
URI_SCHEME_NEO4J_SELF_SIGNED_CERTIFICATE,
129+
URI_SCHEME_NEO4J_SECURE,
130+
)
131+
from neo4j.conf import (
132+
TRUST_ALL_CERTIFICATES,
133+
TRUST_SYSTEM_CA_SIGNED_CERTIFICATES
134+
)
135+
136+
driver_type, security_type, parsed = parse_neo4j_uri(uri)
137+
138+
if "trust" in config.keys():
139+
if config.get("trust") not in [TRUST_ALL_CERTIFICATES, TRUST_SYSTEM_CA_SIGNED_CERTIFICATES]:
140+
from neo4j.exceptions import ConfigurationError
141+
raise ConfigurationError("The config setting `trust` values are {!r}".format(
142+
[
143+
TRUST_ALL_CERTIFICATES,
144+
TRUST_SYSTEM_CA_SIGNED_CERTIFICATES,
145+
]
146+
))
147+
148+
if security_type in [SECURITY_TYPE_SELF_SIGNED_CERTIFICATE, SECURITY_TYPE_SECURE] and ("encrypted" in config.keys() or "trust" in config.keys()):
149+
from neo4j.exceptions import ConfigurationError
150+
raise ConfigurationError("The config settings 'encrypted' and 'trust' can only be used with the URI schemes {!r}. Use the other URI schemes {!r} for setting encryption settings.".format(
151+
[
152+
URI_SCHEME_BOLT,
153+
URI_SCHEME_NEO4J,
154+
],
155+
[
156+
URI_SCHEME_BOLT_SELF_SIGNED_CERTIFICATE,
157+
URI_SCHEME_BOLT_SECURE,
158+
URI_SCHEME_NEO4J_SELF_SIGNED_CERTIFICATE,
159+
URI_SCHEME_NEO4J_SECURE,
160+
]
161+
))
162+
163+
if security_type == SECURITY_TYPE_SECURE:
164+
config["encrypted"] = True
165+
elif security_type == SECURITY_TYPE_SELF_SIGNED_CERTIFICATE:
166+
config["encrypted"] = True
167+
config["trust"] = TRUST_ALL_CERTIFICATES
168+
169+
if driver_type == DRIVER_BOLT:
97170
return cls.bolt_driver(parsed.netloc, auth=auth, acquire_timeout=acquire_timeout, **config)
98-
elif parsed.scheme == "neo4j" or parsed.scheme == "bolt+routing":
171+
elif driver_type == DRIVER_NEO4j:
99172
rc = cls._parse_routing_context(parsed.query)
100173
return cls.neo4j_driver(parsed.netloc, auth=auth, routing_context=rc, acquire_timeout=acquire_timeout, **config)
101-
else:
102-
raise ValueError("Unknown URI scheme {!r}".format(parsed.scheme))
103174

104175
@classmethod
105176
def bolt_driver(cls, target, *, auth=None, acquire_timeout=None, **config):
106177
""" Create a driver for direct Bolt server access that uses
107178
socket I/O and thread-based concurrency.
108179
"""
109-
from neo4j._exceptions import BoltHandshakeError
180+
from neo4j._exceptions import BoltHandshakeError, BoltSecurityError
110181

111182
try:
112183
return BoltDriver.open(target, auth=auth, acquire_timeout=acquire_timeout, **config)
113-
except BoltHandshakeError as error:
184+
except (BoltHandshakeError, BoltSecurityError) as error:
114185
from neo4j.exceptions import ServiceUnavailable
115186
raise ServiceUnavailable(str(error)) from error
116187

@@ -120,11 +191,11 @@ def neo4j_driver(cls, *targets, auth=None, routing_context=None, acquire_timeout
120191
""" Create a driver for routing-capable Neo4j service access
121192
that uses socket I/O and thread-based concurrency.
122193
"""
123-
from neo4j._exceptions import BoltHandshakeError
194+
from neo4j._exceptions import BoltHandshakeError, BoltSecurityError
124195

125196
try:
126197
return Neo4jDriver.open(*targets, auth=auth, routing_context=routing_context, acquire_timeout=acquire_timeout, **config)
127-
except BoltHandshakeError as error:
198+
except (BoltHandshakeError, BoltSecurityError) as error:
128199
from neo4j.exceptions import ServiceUnavailable
129200
raise ServiceUnavailable(str(error)) from error
130201

@@ -228,8 +299,8 @@ def __exit__(self, exc_type, exc_value, traceback):
228299
self.close()
229300

230301
@property
231-
def secure(self):
232-
return bool(self._pool.config.secure)
302+
def encrypted(self):
303+
return bool(self._pool.config.encrypted)
233304

234305
def session(self, **config):
235306
""" Create a simple session.
@@ -278,8 +349,7 @@ def open(cls, target, *, auth=None, **config):
278349
from neo4j.io import BoltPool
279350
from neo4j.work import WorkspaceConfig
280351
address = cls.parse_target(target)
281-
pool_config, default_workspace_config = Config.consume_chain(config, PoolConfig,
282-
WorkspaceConfig)
352+
pool_config, default_workspace_config = Config.consume_chain(config, PoolConfig, WorkspaceConfig)
283353
pool = BoltPool.open(address, auth=auth, **pool_config)
284354
return cls(pool, default_workspace_config)
285355

@@ -323,8 +393,7 @@ def open(cls, *targets, auth=None, routing_context=None, **config):
323393
from neo4j.io import Neo4jPool
324394
from neo4j.work import WorkspaceConfig
325395
addresses = cls.parse_targets(*targets)
326-
pool_config, default_workspace_config = Config.consume_chain(config, PoolConfig,
327-
WorkspaceConfig)
396+
pool_config, default_workspace_config = Config.consume_chain(config, PoolConfig, WorkspaceConfig)
328397
pool = Neo4jPool.open(*addresses, auth=auth, routing_context=routing_context, **pool_config)
329398
return cls(pool, default_workspace_config)
330399

neo4j/api.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,36 @@
1818
# See the License for the specific language governing permissions and
1919
# limitations under the License.
2020

21+
from urllib.parse import (
22+
urlparse,
23+
)
24+
from.exceptions import (
25+
ConfigurationError,
26+
)
2127

2228
""" Base classes and helpers.
2329
"""
2430

31+
READ_ACCESS = "READ"
32+
WRITE_ACCESS = "WRITE"
33+
34+
DRIVER_BOLT = "DRIVER_BOLT"
35+
DRIVER_NEO4j = "DRIVER_NEO4J"
36+
37+
SECURITY_TYPE_NOT_SECURE = "SECURITY_TYPE_NOT_SECURE"
38+
SECURITY_TYPE_SELF_SIGNED_CERTIFICATE = "SECURITY_TYPE_SELF_SIGNED_CERTIFICATE"
39+
SECURITY_TYPE_SECURE = "SECURITY_TYPE_SECURE"
40+
41+
URI_SCHEME_BOLT = "bolt"
42+
URI_SCHEME_BOLT_SELF_SIGNED_CERTIFICATE = "bolt+ssc"
43+
URI_SCHEME_BOLT_SECURE = "bolt+s"
44+
45+
URI_SCHEME_NEO4J = "neo4j"
46+
URI_SCHEME_NEO4J_SELF_SIGNED_CERTIFICATE = "neo4j+ssc"
47+
URI_SCHEME_NEO4J_SECURE = "neo4j+s"
48+
49+
URI_SCHEME_BOLT_ROUTING = "bolt+routing"
50+
2551

2652
class Auth:
2753
""" Container for auth details.
@@ -161,5 +187,46 @@ def from_bytes(cls, b):
161187
return Version(b[-1], b[-2])
162188

163189

164-
READ_ACCESS = "READ"
165-
WRITE_ACCESS = "WRITE"
190+
def parse_neo4j_uri(uri):
191+
parsed = urlparse(uri)
192+
193+
if parsed.username:
194+
raise ConfigurationError("Username is not supported in the URI")
195+
196+
if parsed.password:
197+
raise ConfigurationError("Password is not supported in the URI")
198+
199+
if parsed.scheme == URI_SCHEME_BOLT_ROUTING:
200+
raise ConfigurationError("Uri scheme {!r} have been renamed. Use {!r}".format(parsed.scheme, URI_SCHEME_NEO4J))
201+
elif parsed.scheme == URI_SCHEME_BOLT:
202+
driver_type = DRIVER_BOLT
203+
security_type = SECURITY_TYPE_NOT_SECURE
204+
elif parsed.scheme == URI_SCHEME_BOLT_SELF_SIGNED_CERTIFICATE:
205+
driver_type = DRIVER_BOLT
206+
security_type = SECURITY_TYPE_SELF_SIGNED_CERTIFICATE
207+
elif parsed.scheme == URI_SCHEME_BOLT_SECURE:
208+
driver_type = DRIVER_BOLT
209+
security_type = SECURITY_TYPE_SECURE
210+
elif parsed.scheme == URI_SCHEME_NEO4J:
211+
driver_type = DRIVER_NEO4j
212+
security_type = SECURITY_TYPE_NOT_SECURE
213+
elif parsed.scheme == URI_SCHEME_NEO4J_SELF_SIGNED_CERTIFICATE:
214+
driver_type = DRIVER_NEO4j
215+
security_type = SECURITY_TYPE_SELF_SIGNED_CERTIFICATE
216+
elif parsed.scheme == URI_SCHEME_NEO4J_SECURE:
217+
driver_type = DRIVER_NEO4j
218+
security_type = SECURITY_TYPE_SECURE
219+
else:
220+
raise ConfigurationError("URI scheme {!r} is not supported. Supported URI schemes are {}. Examples: bolt://host[:port] or neo4j://host[:port][?routing_context]".format(
221+
parsed.scheme,
222+
[
223+
URI_SCHEME_BOLT,
224+
URI_SCHEME_BOLT_SELF_SIGNED_CERTIFICATE,
225+
URI_SCHEME_BOLT_SECURE,
226+
URI_SCHEME_NEO4J,
227+
URI_SCHEME_NEO4J_SELF_SIGNED_CERTIFICATE,
228+
URI_SCHEME_NEO4J_SECURE
229+
]
230+
))
231+
232+
return driver_type, security_type, parsed

neo4j/conf.py

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525

2626
from neo4j.meta import get_user_agent
2727

28+
TRUST_SYSTEM_CA_SIGNED_CERTIFICATES = "TRUST_SYSTEM_CA_SIGNED_CERTIFICATES" # Default
29+
TRUST_ALL_CERTIFICATES = "TRUST_ALL_CERTIFICATES"
30+
2831

2932
def iter_items(iterable):
3033
""" Iterate through all items (key-value pairs) within an iterable
@@ -187,26 +190,61 @@ class PoolConfig(Config):
187190
resolver = None
188191

189192
#:
190-
secure = False
191-
encrypted = DeprecatedAlias("secure")
193+
encrypted = False
192194

193195
#:
194196
user_agent = get_user_agent()
195197

196198
#:
197-
verify_cert = True
199+
trust = TRUST_SYSTEM_CA_SIGNED_CERTIFICATES
198200

199201
def get_ssl_context(self):
200-
if not self.secure:
202+
if not self.encrypted:
201203
return None
202-
# See https://docs.python.org/3.7/library/ssl.html#protocol-versions
203-
from ssl import SSLContext, PROTOCOL_TLS_CLIENT, OP_NO_TLSv1, OP_NO_TLSv1_1, CERT_REQUIRED
204-
ssl_context = SSLContext(PROTOCOL_TLS_CLIENT)
205-
ssl_context.options |= OP_NO_TLSv1
206-
ssl_context.options |= OP_NO_TLSv1_1
207-
if self.verify_cert:
208-
ssl_context.verify_mode = CERT_REQUIRED
209-
ssl_context.set_default_verify_paths()
210-
return ssl_context
211204

205+
import ssl
206+
207+
ssl_context = None
208+
209+
# SSL stands for Secure Sockets Layer and was originally created by Netscape.
210+
# SSLv2 and SSLv3 are the 2 versions of this protocol (SSLv1 was never publicly released).
211+
# After SSLv3, SSL was renamed to TLS.
212+
# TLS stands for Transport Layer Security and started with TLSv1.0 which is an upgraded version of SSLv3.
213+
214+
# SSLv2 - (Disabled)
215+
# SSLv3 - (Disabled)
216+
# TLS 1.0 - Released in 1999, published as RFC 2246. (Disabled)
217+
# TLS 1.1 - Released in 2006, published as RFC 4346. (Disabled)
218+
# TLS 1.2 - Released in 2008, published as RFC 5246.
219+
220+
try:
221+
# python 3.6+
222+
# https://docs.python.org/3.6/library/ssl.html#ssl.PROTOCOL_TLS_CLIENT
223+
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
212224

225+
# For recommended security options see
226+
# https://docs.python.org/3.6/library/ssl.html#protocol-versions
227+
ssl_context.options |= ssl.OP_NO_TLSv1 # Python 3.2
228+
ssl_context.options |= ssl.OP_NO_TLSv1_1 # Python 3.4
229+
230+
except AttributeError:
231+
# python 3.5
232+
# https://docs.python.org/3.5/library/ssl.html#ssl.PROTOCOL_TLS
233+
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS)
234+
235+
# For recommended security options see
236+
# https://docs.python.org/3.5/library/ssl.html#protocol-versions
237+
ssl_context.options |= ssl.OP_NO_SSLv2 # Python 3.2
238+
ssl_context.options |= ssl.OP_NO_SSLv3 # Python 3.2
239+
ssl_context.options |= ssl.OP_NO_TLSv1 # Python 3.2
240+
ssl_context.options |= ssl.OP_NO_TLSv1_1 # Python 3.4
241+
242+
ssl_context.verify_mode = ssl.CERT_REQUIRED # https://docs.python.org/3.5/library/ssl.html#ssl.SSLContext.verify_mode
243+
ssl_context.check_hostname = True # https://docs.python.org/3.5/library/ssl.html#ssl.SSLContext.check_hostname
244+
245+
if self.trust == TRUST_ALL_CERTIFICATES:
246+
ssl_context.check_hostname = False
247+
ssl_context.verify_mode = ssl.CERT_NONE # https://docs.python.org/3.5/library/ssl.html#ssl.CERT_NONE
248+
249+
ssl_context.set_default_verify_paths() # https://docs.python.org/3.5/library/ssl.html#ssl.SSLContext.set_default_verify_paths
250+
return ssl_context

neo4j/io/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ def open(cls, address, *, auth=None, timeout=None, **config):
193193
return connection
194194

195195
@property
196-
def secure(self):
196+
def encrypted(self):
197197
raise NotImplementedError
198198

199199
@property
@@ -792,17 +792,17 @@ def _secure(s, host, ssl_context):
792792
try:
793793
sni_host = host if HAS_SNI and host else None
794794
s = ssl_context.wrap_socket(s, server_hostname=sni_host)
795-
except SSLError as cause:
795+
except (SSLError, OSError) as cause:
796796
s.close()
797-
error = BoltSecurityError(message="Failed to establish secure connection to {!r}".format(cause.args[1]), address=(host, port))
797+
error = BoltSecurityError(message="Failed to establish encrypted connection.", address=(host, local_port))
798798
error.__cause__ = cause
799799
raise error
800800
else:
801801
# Check that the server provides a certificate
802802
der_encoded_server_certificate = s.getpeercert(binary_form=True)
803803
if der_encoded_server_certificate is None:
804804
s.close()
805-
raise BoltProtocolError("When using a secure socket, the server should always provide a certificate", address=(host, port))
805+
raise BoltProtocolError("When using an encrypted socket, the server should always provide a certificate", address=(host, local_port))
806806
return s
807807

808808

neo4j/io/_bolt3.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def __init__(self, unresolved_address, sock, *, auth=None, **config):
118118
raise AuthError("Password cannot be None")
119119

120120
@property
121-
def secure(self):
121+
def encrypted(self):
122122
return isinstance(self.socket, SSLSocket)
123123

124124
@property

0 commit comments

Comments
 (0)