Skip to content

Commit fdc29c7

Browse files
committed
[test] Add functional tests
1 parent a5f9048 commit fdc29c7

File tree

4 files changed

+326
-1
lines changed

4 files changed

+326
-1
lines changed
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
# -*- coding: utf-8 -*-
2+
import copy
3+
import os
4+
import subprocess
5+
import tempfile
6+
import requests
7+
from requests.adapters import HTTPAdapter
8+
from urllib3.util.ssl_ import create_urllib3_context
9+
10+
import pytest
11+
12+
from ydb.tests.library.harness.kikimr_config import KikimrConfigGenerator
13+
14+
15+
class MTLSAdapter(HTTPAdapter):
16+
"""Custom HTTPAdapter that uses client certificates for mTLS"""
17+
18+
def __init__(self, cert_file, key_file, ca_file, *args, **kwargs):
19+
self.cert_file = cert_file
20+
self.key_file = key_file
21+
self.ca_file = ca_file
22+
super(MTLSAdapter, self).__init__(*args, **kwargs)
23+
24+
def init_poolmanager(self, *args, **kwargs):
25+
ctx = create_urllib3_context()
26+
ctx.load_verify_locations(cafile=self.ca_file)
27+
ctx.load_cert_chain(certfile=self.cert_file, keyfile=self.key_file)
28+
kwargs['ssl_context'] = ctx
29+
return super(MTLSAdapter, self).init_poolmanager(*args, **kwargs)
30+
31+
32+
def _generate_client_certificate(certs_tmp_dir, client_name, cn_value, ca_crt, ca_key):
33+
client_key = os.path.join(certs_tmp_dir, f'{client_name}.key')
34+
client_crt = os.path.join(certs_tmp_dir, f'{client_name}.crt')
35+
client_csr = os.path.join(certs_tmp_dir, f'{client_name}.csr')
36+
37+
subprocess.run(['openssl', 'genrsa', '-out', client_key, '2048'], check=True, capture_output=True)
38+
subprocess.run(
39+
[
40+
'openssl',
41+
'req',
42+
'-new',
43+
'-key',
44+
client_key,
45+
'-out',
46+
client_csr,
47+
'-subj',
48+
f'/CN={cn_value}/O=YDB/C=RU',
49+
],
50+
check=True,
51+
capture_output=True,
52+
)
53+
subprocess.run(
54+
[
55+
'openssl',
56+
'x509',
57+
'-req',
58+
'-in',
59+
client_csr,
60+
'-CA',
61+
ca_crt,
62+
'-CAkey',
63+
ca_key,
64+
'-CAcreateserial',
65+
'-out',
66+
client_crt,
67+
'-days',
68+
'3650',
69+
'-sha256',
70+
],
71+
check=True,
72+
capture_output=True,
73+
)
74+
return client_key, client_crt
75+
76+
77+
def generate_certificates(certs_tmp_dir):
78+
ca_key = os.path.join(certs_tmp_dir, 'server_ca.key')
79+
ca_crt = os.path.join(certs_tmp_dir, 'server_ca.crt')
80+
81+
subprocess.run(['openssl', 'genrsa', '-out', ca_key, '2048'], check=True, capture_output=True)
82+
83+
subprocess.run(
84+
[
85+
'openssl',
86+
'req',
87+
'-x509',
88+
'-new',
89+
'-nodes',
90+
'-key',
91+
ca_key,
92+
'-sha256',
93+
'-days',
94+
'3650',
95+
'-out',
96+
ca_crt,
97+
'-subj',
98+
'/CN=Monitoring CA/O=YDB/C=RU',
99+
],
100+
check=True,
101+
capture_output=True,
102+
)
103+
104+
# Generate server certificate with localhost in SAN
105+
server_key = os.path.join(certs_tmp_dir, 'server.key')
106+
server_crt = os.path.join(certs_tmp_dir, 'server.crt')
107+
server_csr = os.path.join(certs_tmp_dir, 'server.csr')
108+
server_conf = os.path.join(certs_tmp_dir, 'server.conf')
109+
110+
subprocess.run(['openssl', 'genrsa', '-out', server_key, '2048'], check=True, capture_output=True)
111+
112+
# Create OpenSSL config file with v3_req section and SAN
113+
with open(server_conf, 'w') as f:
114+
f.write('[req]\n')
115+
f.write('distinguished_name = req_distinguished_name\n')
116+
f.write('req_extensions = v3_req\n')
117+
f.write('\n')
118+
f.write('[req_distinguished_name]\n')
119+
f.write('\n')
120+
f.write('[v3_req]\n')
121+
f.write('subjectAltName=DNS:localhost,DNS:test-server,IP:127.0.0.1\n')
122+
123+
# Generate CSR
124+
subprocess.run(
125+
[
126+
'openssl',
127+
'req',
128+
'-new',
129+
'-key',
130+
server_key,
131+
'-out',
132+
server_csr,
133+
'-subj',
134+
'/CN=test-server/O=YDB/C=RU',
135+
'-config',
136+
server_conf,
137+
],
138+
check=True,
139+
capture_output=True,
140+
)
141+
142+
# Generate server certificate signed by CA
143+
subprocess.run(
144+
[
145+
'openssl',
146+
'x509',
147+
'-req',
148+
'-in',
149+
server_csr,
150+
'-CA',
151+
ca_crt,
152+
'-CAkey',
153+
ca_key,
154+
'-CAcreateserial',
155+
'-out',
156+
server_crt,
157+
'-days',
158+
'3650',
159+
'-sha256',
160+
'-extensions',
161+
'v3_req',
162+
'-extfile',
163+
server_conf,
164+
],
165+
check=True,
166+
capture_output=True,
167+
)
168+
169+
admin_client_key, admin_client_crt = _generate_client_certificate(
170+
certs_tmp_dir, 'admin_client', 'administration_allowed', ca_crt, ca_key
171+
)
172+
173+
viewer_client_key, viewer_client_crt = _generate_client_certificate(
174+
certs_tmp_dir, 'viewer_client', 'viewer_client', ca_crt, ca_key
175+
)
176+
177+
return {
178+
'server_cert': server_crt,
179+
'server_key': server_key,
180+
'server_ca': ca_crt,
181+
'admin_client_crt': admin_client_crt,
182+
'admin_client_key': admin_client_key,
183+
'viewer_client_crt': viewer_client_crt,
184+
'viewer_client_key': viewer_client_key,
185+
}
186+
187+
188+
CLUSTER_CONFIG = dict(
189+
enforce_user_token_requirement=True,
190+
default_clusteradmin='root@builtin',
191+
)
192+
193+
194+
@pytest.fixture(scope='module')
195+
def ydb_cluster_configuration():
196+
conf = copy.deepcopy(CLUSTER_CONFIG)
197+
return conf
198+
199+
200+
@pytest.fixture(scope='module')
201+
def certificates():
202+
certs_tmp_dir = tempfile.mkdtemp(prefix='monitoring_certs_')
203+
return generate_certificates(certs_tmp_dir)
204+
205+
206+
@pytest.fixture(scope='module')
207+
def client_certificates(certificates):
208+
"""Extract client certificates from certificates"""
209+
return {
210+
'admin_cert': certificates['admin_client_crt'],
211+
'admin_key': certificates['admin_client_key'],
212+
'viewer_cert': certificates['viewer_client_crt'],
213+
'viewer_key': certificates['viewer_client_key'],
214+
'ca': certificates['server_ca'],
215+
}
216+
217+
218+
@pytest.fixture(scope='module')
219+
def ydb_configurator(ydb_cluster_configuration, certificates):
220+
config_generator = KikimrConfigGenerator(**ydb_cluster_configuration)
221+
222+
config_generator.yaml_config['client_certificate_authorization'] = {
223+
'client_certificate_definitions': [
224+
{
225+
'member_groups': ['AdministrationClientAuth@cert'],
226+
'subject_terms': [{'short_name': 'CN', 'values': ['administration_allowed']}],
227+
},
228+
{
229+
'member_groups': ['ViewerClientAuth@cert'],
230+
'subject_terms': [{'short_name': 'CN', 'values': ['viewer_client']}],
231+
},
232+
]
233+
}
234+
235+
security_config = config_generator.yaml_config['domains_config']['security_config']
236+
237+
if 'administration_allowed_sids' not in security_config:
238+
security_config['administration_allowed_sids'] = []
239+
security_config['administration_allowed_sids'].append('AdministrationClientAuth@cert')
240+
if 'monitoring_allowed_sids' not in security_config:
241+
security_config['monitoring_allowed_sids'] = []
242+
security_config['monitoring_allowed_sids'].append('AdministrationClientAuth@cert')
243+
244+
if 'grpc_config' not in config_generator.yaml_config:
245+
config_generator.yaml_config['grpc_config'] = {}
246+
config_generator.yaml_config['grpc_config']['cert'] = certificates['server_cert']
247+
config_generator.yaml_config['grpc_config']['key'] = certificates['server_key']
248+
config_generator.yaml_config['grpc_config']['ca'] = certificates['server_ca']
249+
250+
config_generator.monitoring_tls_cert_path = certificates['server_cert']
251+
config_generator.monitoring_tls_key_path = certificates['server_key']
252+
config_generator.monitoring_tls_ca_path = certificates['server_ca']
253+
254+
return config_generator
255+
256+
257+
def _test_endpoint_with_certificate(trace_endpoint, client_certificates, cert_type, expected_status):
258+
session = requests.Session()
259+
adapter = MTLSAdapter(
260+
cert_file=client_certificates[f'{cert_type}_cert'],
261+
key_file=client_certificates[f'{cert_type}_key'],
262+
ca_file=client_certificates['ca'],
263+
)
264+
session.mount('https://', adapter)
265+
response = session.get(trace_endpoint, verify=client_certificates['ca'], timeout=5)
266+
assert (
267+
response.status_code == expected_status
268+
), f"Expected /trace with {cert_type} cert to return {expected_status}, got {response.status_code}"
269+
270+
271+
def _test_endpoint_without_any_auth(trace_endpoint):
272+
response = requests.get(trace_endpoint, timeout=5, verify=False)
273+
assert (
274+
response.status_code == 401
275+
), f"Expected /trace without auth to return 401, got {response.status_code}"
276+
277+
278+
def _test_endpoint_with_token(trace_endpoint, token, expected_status):
279+
headers = {'Authorization': token}
280+
response = requests.get(trace_endpoint, headers=headers, timeout=5, verify=False)
281+
assert (
282+
response.status_code == expected_status
283+
), f"Expected /trace with token {token} to return {expected_status}, got {response.status_code}"
284+
285+
286+
def test_mlts_auth(ydb_cluster, client_certificates):
287+
host = ydb_cluster.nodes[1].host
288+
mon_port = ydb_cluster.nodes[1].mon_port
289+
base_url = f'https://{host}:{mon_port}'
290+
trace_endpoint = f'{base_url}/trace'
291+
292+
# Without any authentication
293+
_test_endpoint_without_any_auth(trace_endpoint)
294+
295+
# With viewer certificate
296+
_test_endpoint_with_certificate(trace_endpoint, client_certificates, 'viewer', 403)
297+
298+
# With administration certificate
299+
_test_endpoint_with_certificate(trace_endpoint, client_certificates, 'admin', 200)
300+
301+
302+
def test_token_auth_when_mtls_is_enabled(ydb_cluster, client_certificates):
303+
host = ydb_cluster.nodes[1].host
304+
mon_port = ydb_cluster.nodes[1].mon_port
305+
base_url = f'https://{host}:{mon_port}'
306+
trace_endpoint = f'{base_url}/trace'
307+
308+
# Checks that mTLS is enabled
309+
_test_endpoint_with_certificate(trace_endpoint, client_certificates, 'admin', 200)
310+
311+
# Without any authentication
312+
_test_endpoint_without_any_auth(trace_endpoint)
313+
314+
# With NOT an admin token
315+
_test_endpoint_with_token(trace_endpoint, 'user@builtin', 403)
316+
317+
# With an admin token
318+
_test_endpoint_with_token(trace_endpoint, 'root@builtin', 200)

ydb/tests/functional/security/ya.make

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ INCLUDE(${ARCADIA_ROOT}/ydb/tests/harness_dep.inc)
55
TEST_SRCS(
66
conftest.py
77
test_grants.py
8-
test_paths_lookup.py
98
test_mon_endpoints_auth.py
9+
test_mon_mtls_auth.py
10+
test_paths_lookup.py
1011
)
1112

1213
SPLIT_FACTOR(20)

ydb/tests/library/harness/kikimr_config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ def __init__(
244244

245245
self.monitoring_tls_cert_path = None
246246
self.monitoring_tls_key_path = None
247+
self.monitoring_tls_ca_path = None
247248

248249
self.__binary_paths = binary_paths
249250
rings_count = 3 if erasure == Erasure.MIRROR_3_DC else 1

ydb/tests/library/harness/kikimr_runner.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,11 @@ def __make_run_command(self):
256256
"--mon-key=%s" % self.__configurator.monitoring_tls_key_path
257257
)
258258

259+
if self.__configurator.monitoring_tls_ca_path is not None:
260+
command.append(
261+
"--mon-ca=%s" % self.__configurator.monitoring_tls_ca_path
262+
)
263+
259264
if os.environ.get("YDB_ALLOCATE_PGWIRE_PORT", "") == "true":
260265
command.append("--pgwire-port=%d" % self.pgwire_port)
261266

0 commit comments

Comments
 (0)