Skip to content

Commit d0126c8

Browse files
api: support authentication methods
Since Tarantool master 2574ff1a configuring authentication type is supported [1-2]. Together with this, Tarantool EE had introduced pap-sha256 authentication method support [3-4]. It can be used only together with SSL transport. To configure, use `auth_type` option in Connection, MeshConnection or ConnectionPool. Newest master (there is no dev build in customer zone yet) supports providing authentication method via IPROTO_ID response. So in this patch we also move ID request to be executed before authentication. 1. tarantool/tarantool#7988 2. tarantool/tarantool#7989 3. tarantool/tarantool-ee#295 4. tarantool/tarantool-ee#322 Closes #269
1 parent bb6b84b commit d0126c8

11 files changed

+288
-42
lines changed

Diff for: CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
- Support [crud module](https://github.com/tarantool/crud) native API (#205).
1212
- Support `ssl_password` and `ssl_password_file` options
1313
to decrypt private SSL key file (#224).
14+
- Support specifying authentication method with `auth_type`
15+
and Tarantool EE `pap-sha256` authentication method (#269).
1416

1517
### Changed
1618

Diff for: tarantool/connection.py

+54-4
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,11 @@
6969
IPROTO_FEATURE_TRANSACTIONS,
7070
IPROTO_FEATURE_ERROR_EXTENSION,
7171
IPROTO_FEATURE_WATCHERS,
72+
IPROTO_AUTH_TYPE,
7273
IPROTO_CHUNK,
74+
AUTH_TYPE_CHAP_SHA1,
75+
AUTH_TYPE_PAP_SHA256,
76+
AUTH_TYPES,
7377
)
7478
from tarantool.error import (
7579
Error,
@@ -574,7 +578,8 @@ def __init__(self, host, port,
574578
ssl_password=DEFAULT_SSL_PASSWORD,
575579
ssl_password_file=DEFAULT_SSL_PASSWORD_FILE,
576580
packer_factory=default_packer_factory,
577-
unpacker_factory=default_unpacker_factory):
581+
unpacker_factory=default_unpacker_factory,
582+
auth_type=None):
578583
"""
579584
:param host: Server hostname or IP address. Use ``None`` for
580585
Unix sockets.
@@ -723,6 +728,14 @@ def __init__(self, host, port,
723728
callable[[:obj:`~tarantool.Connection`], :obj:`~msgpack.Unpacker`],
724729
optional
725730
731+
:param auth_type: Authentication method: ``"chap-sha1"`` (supported in
732+
Tarantool CE and EE) or ``"pap-sha256"`` (supported in Tarantool EE,
733+
``"ssl"`` :paramref:`~tarantool.Connection.transport` must be used).
734+
If `None`, use authentication method provided by server in IPROTO_ID
735+
exchange. If server does not provide an authentication method, use
736+
``"chap-sha1"``.
737+
:type auth_type: :obj:`None` or :obj:`str`, optional
738+
726739
:raise: :exc:`~tarantool.error.ConfigurationError`,
727740
:meth:`~tarantool.Connection.connect` exceptions
728741
@@ -778,6 +791,8 @@ def __init__(self, host, port,
778791
}
779792
self._packer_factory_impl = packer_factory
780793
self._unpacker_factory_impl = unpacker_factory
794+
self._client_auth_type = auth_type
795+
self._server_auth_type = None
781796

782797
if connect_now:
783798
self.connect()
@@ -985,6 +1000,7 @@ def handshake(self):
9851000
if greeting.protocol != "Binary":
9861001
raise NetworkError("Unsupported protocol: " + greeting.protocol)
9871002
self.version_id = greeting.version_id
1003+
self._check_features()
9881004
self.uuid = greeting.uuid
9891005
self._salt = greeting.salt
9901006
if self.user:
@@ -1008,7 +1024,6 @@ def connect(self):
10081024
self.wrap_socket_ssl()
10091025
self.handshake()
10101026
self.load_schema()
1011-
self._check_features()
10121027
except SslError as e:
10131028
raise e
10141029
except Exception as e:
@@ -1390,13 +1405,44 @@ def authenticate(self, user, password):
13901405
if not self._socket:
13911406
return self._opt_reconnect()
13921407

1393-
request = RequestAuthenticate(self, self._salt, self.user,
1394-
self.password)
1408+
request = RequestAuthenticate(self,
1409+
salt=self._salt,
1410+
user=self.user,
1411+
password=self.password,
1412+
auth_type=self._get_auth_type())
13951413
auth_response = self._send_request_wo_reconnect(request)
13961414
if auth_response.return_code == 0:
13971415
self.flush_schema()
13981416
return auth_response
13991417

1418+
def _get_auth_type(self):
1419+
"""
1420+
Get authentication method based on client and server settings.
1421+
1422+
:rtype: :obj:`str`
1423+
1424+
:raise: :exc:`~tarantool.error.DatabaseError`
1425+
1426+
:meta private:
1427+
"""
1428+
1429+
if self._client_auth_type is None:
1430+
if self._server_auth_type is None:
1431+
auth_type = AUTH_TYPE_CHAP_SHA1
1432+
else:
1433+
if self._server_auth_type not in AUTH_TYPES:
1434+
raise ConfigurationError(f'Unknown server authentication type {self._server_auth_type}')
1435+
auth_type = self._server_auth_type
1436+
else:
1437+
if self._client_auth_type not in AUTH_TYPES:
1438+
raise ConfigurationError(f'Unknown client authentication type {self._client_auth_type}')
1439+
auth_type = self._client_auth_type
1440+
1441+
if auth_type == AUTH_TYPE_PAP_SHA256 and self.transport != SSL_TRANSPORT:
1442+
raise ConfigurationError('Use PAP-SHA256 only with ssl transport')
1443+
1444+
return auth_type
1445+
14001446
def _join_v16(self, server_uuid):
14011447
"""
14021448
Execute a JOIN request for Tarantool 1.6 and older.
@@ -2037,11 +2083,13 @@ def _check_features(self):
20372083
response = self._send_request(request)
20382084
server_protocol_version = response.protocol_version
20392085
server_features = response.features
2086+
server_auth_type = response.auth_type
20402087
except DatabaseError as exc:
20412088
ER_UNKNOWN_REQUEST_TYPE = 48
20422089
if exc.code == ER_UNKNOWN_REQUEST_TYPE:
20432090
server_protocol_version = None
20442091
server_features = []
2092+
server_auth_type = None
20452093
else:
20462094
raise exc
20472095

@@ -2054,6 +2102,8 @@ def _check_features(self):
20542102
for val in features_list:
20552103
self._features[val] = True
20562104

2105+
self._server_auth_type = server_auth_type
2106+
20572107
def _packer_factory(self):
20582108
return self._packer_factory_impl(self)
20592109

Diff for: tarantool/connection_pool.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ def __init__(self,
394394
"ssl_ciphers": "str" # optional
395395
"ssl_password": "str", # optional
396396
"ssl_password_file": "str" # optional
397+
"auth_type": "str" # optional
397398
}
398399
399400
Refer to corresponding :class:`~tarantool.Connection`
@@ -498,7 +499,8 @@ def __init__(self,
498499
ssl_ca_file=addr['ssl_ca_file'],
499500
ssl_ciphers=addr['ssl_ciphers'],
500501
ssl_password=addr['ssl_password'],
501-
ssl_password_file=addr['ssl_password_file'])
502+
ssl_password_file=addr['ssl_password_file'],
503+
auth_type=addr['auth_type'])
502504
)
503505

504506
if connect_now:

Diff for: tarantool/const.py

+8
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
#
4141
IPROTO_VERSION = 0x54
4242
IPROTO_FEATURES = 0x55
43+
IPROTO_AUTH_TYPE = 0x5b
4344
IPROTO_CHUNK = 0x80
4445

4546
IPROTO_GREETING_SIZE = 128
@@ -134,3 +135,10 @@
134135
CONNECTOR_IPROTO_VERSION = 3
135136
# List of connector-supported features
136137
CONNECTOR_FEATURES = [IPROTO_FEATURE_ERROR_EXTENSION,]
138+
139+
# Authenticate with CHAP-SHA1 (Tarantool CE and EE)
140+
AUTH_TYPE_CHAP_SHA1 = "chap-sha1"
141+
# Authenticate with PAP-SHA256 (Tarantool EE)
142+
AUTH_TYPE_PAP_SHA256 = "pap-sha256"
143+
# List of supported auth types.
144+
AUTH_TYPES = [AUTH_TYPE_CHAP_SHA1, AUTH_TYPE_PAP_SHA256]

Diff for: tarantool/mesh_connection.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
'ssl_ciphers': DEFAULT_SSL_CIPHERS,
4242
'ssl_password': DEFAULT_SSL_PASSWORD,
4343
'ssl_password_file': DEFAULT_SSL_PASSWORD_FILE,
44+
'auth_type': None,
4445
}
4546

4647

@@ -194,6 +195,7 @@ def update_connection(conn, address):
194195
conn.ssl_ciphers = address['ssl_ciphers']
195196
conn.ssl_password = address['ssl_password']
196197
conn.ssl_password_file = address['ssl_password_file']
198+
conn.auth_type = address['auth_type']
197199

198200

199201
class RoundRobinStrategy(object):
@@ -277,6 +279,7 @@ def __init__(self, host=None, port=None,
277279
ssl_ciphers=DEFAULT_SSL_CIPHERS,
278280
ssl_password=DEFAULT_SSL_PASSWORD,
279281
ssl_password_file=DEFAULT_SSL_PASSWORD_FILE,
282+
auth_type=None,
280283
addrs=None,
281284
strategy_class=RoundRobinStrategy,
282285
cluster_discovery_function=None,
@@ -357,6 +360,11 @@ def __init__(self, host=None, port=None,
357360
Value would be used to add one more server in
358361
:paramref:`~tarantool.MeshConnection.params.addrs` list.
359362
363+
:param auth_type: Refer to
364+
:paramref:`~tarantool.Connection.params.auth_type`.
365+
Value would be used to add one more server in
366+
:paramref:`~tarantool.MeshConnection.params.addrs` list.
367+
360368
:param addrs: Cluster servers addresses list. Refer to
361369
:paramref:`~tarantool.ConnectionPool.params.addrs`.
362370
@@ -437,7 +445,8 @@ def __init__(self, host=None, port=None,
437445
'ssl_ca_file': ssl_ca_file,
438446
'ssl_ciphers': ssl_ciphers,
439447
'ssl_password': ssl_password,
440-
'ssl_password_file': ssl_password_file})
448+
'ssl_password_file': ssl_password_file,
449+
'auth_type': auth_type})
441450

442451
# Verify that at least one address is provided.
443452
if not addrs:
@@ -479,7 +488,8 @@ def __init__(self, host=None, port=None,
479488
ssl_ca_file=addr['ssl_ca_file'],
480489
ssl_ciphers=addr['ssl_ciphers'],
481490
ssl_password=addr['ssl_password'],
482-
ssl_password_file=addr['ssl_password_file'])
491+
ssl_password_file=addr['ssl_password_file'],
492+
auth_type=addr['auth_type'])
483493

484494
def connect(self):
485495
"""
@@ -588,7 +598,8 @@ def _opt_refresh_instances(self):
588598
'ssl_ca_file': self.ssl_ca_file,
589599
'ssl_ciphers': self.ssl_ciphers,
590600
'ssl_password': self.ssl_password,
591-
'ssl_password_file': self.ssl_password_file}
601+
'ssl_password_file': self.ssl_password_file,
602+
'auth_type': self._client_auth_type}
592603
if current_addr not in self.strategy.addrs:
593604
self.close()
594605
addr = self.strategy.getnext()

Diff for: tarantool/request.py

+38-18
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
REQUEST_TYPE_JOIN,
5252
REQUEST_TYPE_SUBSCRIBE,
5353
REQUEST_TYPE_ID,
54+
AUTH_TYPE_CHAP_SHA1,
55+
AUTH_TYPE_PAP_SHA256,
5456
)
5557
from tarantool.response import (
5658
Response,
@@ -223,6 +225,26 @@ def __init__(self, conn, space_no, values):
223225

224226
self._body = request_body
225227

228+
def sha1(values):
229+
"""
230+
Compute hash.
231+
232+
:param values: Values to hash.
233+
:type values: :obj:`tuple`
234+
235+
:rtype: :obj:`str`
236+
237+
:meta private:
238+
"""
239+
240+
sha = hashlib.sha1()
241+
for i in values:
242+
if i is not None:
243+
if isinstance(i, bytes):
244+
sha.update(i)
245+
else:
246+
sha.update(i.encode())
247+
return sha.digest()
226248

227249
class RequestAuthenticate(Request):
228250
"""
@@ -231,7 +253,7 @@ class RequestAuthenticate(Request):
231253

232254
request_type = REQUEST_TYPE_AUTHENTICATE
233255

234-
def __init__(self, conn, salt, user, password):
256+
def __init__(self, conn, salt, user, password, auth_type=AUTH_TYPE_CHAP_SHA1):
235257
"""
236258
:param conn: Request sender.
237259
:type conn: :class:`~tarantool.Connection`
@@ -246,27 +268,25 @@ def __init__(self, conn, salt, user, password):
246268
:param password: User password for authentication on the
247269
Tarantool server.
248270
:type password: :obj:`str`
271+
272+
:param auth_type: Refer to :paramref:`~tarantool.Connection.auth_type`.
273+
:type auth_type: :obj:`str`, optional
249274
"""
250275

251276
super(RequestAuthenticate, self).__init__(conn)
252277

253-
def sha1(values):
254-
sha = hashlib.sha1()
255-
for i in values:
256-
if i is not None:
257-
if isinstance(i, bytes):
258-
sha.update(i)
259-
else:
260-
sha.update(i.encode())
261-
return sha.digest()
262-
263-
hash1 = sha1((password,))
264-
hash2 = sha1((hash1,))
265-
scramble = sha1((salt, hash2))
266-
scramble = strxor(hash1, scramble)
267-
request_body = self._dumps({IPROTO_USER_NAME: user,
268-
IPROTO_TUPLE: ("chap-sha1", scramble)})
269-
self._body = request_body
278+
if auth_type == AUTH_TYPE_CHAP_SHA1:
279+
hash1 = sha1((password,))
280+
hash2 = sha1((hash1,))
281+
scramble = sha1((salt, hash2))
282+
scramble = strxor(hash1, scramble)
283+
elif auth_type == AUTH_TYPE_PAP_SHA256:
284+
scramble = password
285+
else:
286+
raise ValueError(f'Unexpected auth_type {auth_type}')
287+
288+
self._body = self._dumps({IPROTO_USER_NAME: user,
289+
IPROTO_TUPLE: (auth_type, scramble)})
270290

271291
def header(self, length):
272292
"""

Diff for: tarantool/response.py

+12
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
IPROTO_SQL_INFO_AUTOINCREMENT_IDS,
2222
IPROTO_VERSION,
2323
IPROTO_FEATURES,
24+
IPROTO_AUTH_TYPE,
2425
)
2526
from tarantool.types import decode_box_error
2627
from tarantool.error import (
@@ -386,3 +387,14 @@ def features(self):
386387
return []
387388
return self._body.get(IPROTO_FEATURES)
388389

390+
@property
391+
def auth_type(self):
392+
"""
393+
Server expected authentication method.
394+
395+
:rtype: :obj:`str` or :obj:`None`
396+
"""
397+
398+
if self._return_code != 0:
399+
return None
400+
return self._body.get(IPROTO_AUTH_TYPE)

Diff for: test/suites/box.lua

+2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ local os = require('os')
44

55
local admin_listen = os.getenv("ADMIN")
66
local primary_listen = os.getenv("LISTEN")
7+
local auth_type = os.getenv("AUTH_TYPE")
78

89
require('console').listen(admin_listen)
910
box.cfg{
1011
listen = primary_listen,
1112
memtx_memory = 0.1 * 1024^3, -- 0.1 GiB
1213
pid_file = "box.pid",
14+
auth_type = (auth_type:len() > 0) and auth_type or nil,
1315
}

Diff for: test/suites/lib/skip.py

+15
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,18 @@ def skip_or_run_ssl_password_test_call(self):
221221

222222
return skip_or_run_test_tarantool_call(self, '2.11.0',
223223
'does not support SSL passwords')
224+
225+
def skip_or_run_auth_type_test_call(self):
226+
"""Function to skip or run tests related to configuring
227+
authentication method.
228+
229+
Tarantool supports auth_type only in current master since
230+
commit 2574ff1a (after 2.11.0-entrypoint).
231+
See https://github.com/tarantool/tarantool/issues/7988
232+
https://github.com/tarantool/tarantool/issues/7989
233+
https://github.com/tarantool/tarantool-ee/issues/295
234+
https://github.com/tarantool/tarantool-ee/issues/322
235+
"""
236+
237+
return skip_or_run_test_tarantool_call(self, '2.11.0',
238+
'does not support auth type')

0 commit comments

Comments
 (0)