Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/nidmm/system_tests/grpc_server_config_tls.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"address": "[::]",
"port": 31762,
"security": "ni-tls-config",
"feature_toggles": {
"ni-tls-config": true
}
}
140 changes: 137 additions & 3 deletions src/nidmm/system_tests/test_system_nidmm.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import math
import os
import pathlib
import shutil
import sys
import tempfile
import time

import grpc
import hightime
import nitlsconfig
import numpy
import pytest

Expand Down Expand Up @@ -327,13 +329,23 @@ def test_fetch_waveform_into(self, session):
assert not math.isnan(sample)


class TestGrpc(SystemTests):
class TestGrpcSecuredTLS(SystemTests):
@pytest.fixture(scope='class')
def grpc_channel(self):
system_test_utilities.exchange_certificates("localhost")
system_test_utilities.configure_tls_modes(
service="ni-grpc-device",
server_host="localhost",
server_cert_mode="ManagedSelfSigned",
server_client_mode="ManagedSelfSigned",
client_cert_mode="Managed",
client_server_mode="TrustedCertificates"
)

current_directory = os.path.dirname(os.path.abspath(__file__))
config_file_path = os.path.join(current_directory, 'grpc_server_config.json')
config_file_path = os.path.join(current_directory, 'grpc_server_config_tls.json')
with system_test_utilities.GrpcServerProcess(config_file_path) as proc:
channel = grpc.insecure_channel(f"localhost:{proc.server_port}")
channel = nitlsconfig.create_grpc_device_channel('localhost', proc.server_port)
yield channel

@pytest.fixture(scope='class')
Expand Down Expand Up @@ -369,3 +381,125 @@ def test_attach_to_non_existent_session(self, grpc_channel):
assert e.rpc_code == expected_grpc_error
assert e.description == expected_error_message
assert str(e) == f'{expected_grpc_error}: {expected_error_message}'


class TestGrpcUnsecuredTLS:
@pytest.fixture(scope='function')
def session(self, session_creation_kwargs):
with nidmm.Session('FakeDevice', False, True, 'Simulate=1, DriverSetup=Model:4082; BoardType:PXIe', **session_creation_kwargs) as simulated_session:
yield simulated_session

@pytest.fixture(scope='class')
def grpc_channel(self):
system_test_utilities.exchange_certificates("localhost")
system_test_utilities.configure_tls_modes(
service="ni-grpc-device",
server_host="localhost",
server_cert_mode="Disabled",
server_client_mode="Disabled",
client_cert_mode="Disabled",
client_server_mode="Disabled"
)

current_directory = os.path.dirname(os.path.abspath(__file__))
config_file_path = os.path.join(current_directory, 'grpc_server_config_tls.json')
with system_test_utilities.GrpcServerProcess(config_file_path) as proc:
channel = nitlsconfig.create_grpc_device_channel('localhost', proc.server_port)
yield channel

@pytest.fixture(scope='class')
def session_creation_kwargs(self, grpc_channel):
grpc_options = nidmm.GrpcSessionOptions(grpc_channel, '')
return {'grpc_options': grpc_options}

def test_acquisition(self, session):
session.configure_measurement_digits(nidmm.Function.DC_CURRENT, 1, 5.5)
with session.initiate():
session.fetch()
with session.initiate():
session.fetch()


class TestGrpcNoTLS:
@pytest.fixture(scope='function')
def session(self, session_creation_kwargs):
with nidmm.Session('FakeDevice', False, True, 'Simulate=1, DriverSetup=Model:4082; BoardType:PXIe', **session_creation_kwargs) as simulated_session:
yield simulated_session

@pytest.fixture(scope='class')
def grpc_channel(self):
current_directory = os.path.dirname(os.path.abspath(__file__))
config_file_path = os.path.join(current_directory, 'grpc_server_config_no_tls.json')
with system_test_utilities.GrpcServerProcess(config_file_path) as proc:
channel = grpc.insecure_channel(f"localhost:{proc.server_port}")
yield channel

@pytest.fixture(scope='class')
def session_creation_kwargs(self, grpc_channel):
grpc_options = nidmm.GrpcSessionOptions(grpc_channel, '')
return {'grpc_options': grpc_options}

def test_acquisition(self, session):
session.configure_measurement_digits(nidmm.Function.DC_CURRENT, 1, 5.5)
with session.initiate():
session.fetch()
with session.initiate():
session.fetch()


def test_unsecured_client():
system_test_utilities.exchange_certificates("localhost")
system_test_utilities.configure_tls_modes(
service="ni-grpc-device",
server_host="localhost",
server_cert_mode="ManagedSelfSigned",
server_client_mode="ManagedSelfSigned",
client_cert_mode="Disabled",
client_server_mode="Disabled"
)

expected_error_message = 'Failed to connect to server'
expected_grpc_error = grpc.StatusCode.UNAVAILABLE

current_directory = os.path.dirname(os.path.abspath(__file__))
config_file_path = os.path.join(current_directory, 'grpc_server_config_tls.json')

with system_test_utilities.GrpcServerProcess(config_file_path) as proc:
unsecured_client_channel = nitlsconfig.create_grpc_device_channel('localhost', proc.server_port)
grpc_options = nidmm.GrpcSessionOptions(unsecured_client_channel, '')
try:
with nidmm.Session('FakeDevice', False, True, 'Simulate=1, DriverSetup=Model:4082; BoardType:PXIe', grpc_options=grpc_options):
assert False
except nidmm.Error as e:
assert e.rpc_code == expected_grpc_error
assert e.description == expected_error_message
assert str(e) == f'{expected_grpc_error}: {expected_error_message}'


def test_unsecured_server():
system_test_utilities.exchange_certificates("localhost")
system_test_utilities.configure_tls_modes(
service="ni-grpc-device",
server_host="localhost",
server_cert_mode="Disabled",
server_client_mode="Disabled",
client_cert_mode="Managed",
client_server_mode="TrustedCertificates"
)

expected_error_message = 'Failed to connect to server'
expected_grpc_error = grpc.StatusCode.UNAVAILABLE

current_directory = os.path.dirname(os.path.abspath(__file__))
config_file_path = os.path.join(current_directory, 'grpc_server_config_tls.json')

with system_test_utilities.GrpcServerProcess(config_file_path) as proc:
unsecured_server_channel = nitlsconfig.create_grpc_device_channel('localhost', proc.server_port)
grpc_options = nidmm.GrpcSessionOptions(unsecured_server_channel, '')
try:
with nidmm.Session('FakeDevice', False, True, 'Simulate=1, DriverSetup=Model:4082; BoardType:PXIe', grpc_options=grpc_options):
assert False
except nidmm.Error as e:
assert e.rpc_code == expected_grpc_error
assert e.description == expected_error_message
assert str(e) == f'{expected_grpc_error}: {expected_error_message}'
91 changes: 91 additions & 0 deletions src/shared/system_test_utilities.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import json
import os
import pathlib
import pytest
import re
import subprocess
import sys
import threading
import time

Expand Down Expand Up @@ -104,3 +106,92 @@ def impl_test_multi_threading_ivi_synchronized_wrapper_releases_lock(ivi_method_
t2.start()
t2.join()
assert not t2.is_alive()


def exchange_certificates(
server_host: str,
server_user: str | None = None,
client_host: str | None = None,
client_user: str | None = None,
verbosity: int = 2,
):
# 26.5 versions of ni-grpc-device server installers do not properly create the trusted.d directory,
# which causes issues with the certificate exchange process. This has been fixed in the 26.8 version
# of the installer, but it has not yet been released. For now, we're creating it manually; this can
# be removed once nimibot system tests are updated to test against 26.8 versions of the drivers.
trusted_servers_path = pathlib.Path(
r"C:/ProgramData/National Instruments/nitlsconfig/server.d/ni-grpc-device/trusted.d"
if sys.platform == "win32" else
r"/etc/nitlsconfig/server.d/ni-grpc-device/trusted.d"
)
trusted_servers_path.mkdir(parents=True, exist_ok=True)

script_path = (
r"C:/NITests/nitlsconfigtest/exchange_certificates.py"
if sys.platform == "win32" else
r"/opt/NITests/nitlsconfigtest/exchange_certificates.py"
)
if not pathlib.Path(script_path).is_file():
raise FileNotFoundError(f"Certificate exchange script not found: {script_path}")

server_host_arg = f"--server-host={server_host}"
server_user_arg = f"--server-user={server_user}" if server_user else "--local-server"
client_host_arg = f"--client-host={client_host}" if client_host else None
client_user_arg = f"--client-user={client_user}" if client_user else None

verbosity = max(0, min(verbosity, 4))
verbosity_arg = {
0: "-qq",
1: "-q",
3: "-v",
4: "-vv",
}.get(verbosity)

command = [sys.executable, str(pathlib.Path(script_path)), server_host_arg, server_user_arg]
command.extend(arg for arg in (client_host_arg, client_user_arg, verbosity_arg) if arg is not None)
subprocess.run(command, check=True)


def configure_tls_modes(
service: str,
server_host: str,
server_user: str | None = None,
client_host: str | None = None,
client_user: str | None = None,
server_cert_mode: str | None = None,
server_client_mode: str | None = None,
client_cert_mode: str | None = None,
client_server_mode: str | None = None,
):
script_path = (
r"C:/NITests/nitlsconfigtest/configure_tls_modes.py"
if sys.platform == "win32" else
r"/opt/NITests/nitlsconfigtest/configure_tls_modes.py"
)
if not pathlib.Path(script_path).is_file():
raise FileNotFoundError(f"Configure TLS modes script not found: {script_path}")

service_arg = f"--service={service}"
server_host_arg = f"--server-host={server_host}"
server_user_arg = f"--server-user={server_user}" if server_user else "--local-server"
client_host_arg = f"--client-host={client_host}" if client_host else None
client_user_arg = f"--client-user={client_user}" if client_user else None
server_cert_mode_arg = f"--server-certificate-mode={server_cert_mode}" if server_cert_mode else None
server_client_mode_arg = f"--server-client-mode={server_client_mode}" if server_client_mode else None
client_cert_mode_arg = f"--client-certificate-mode={client_cert_mode}" if client_cert_mode else None
client_server_mode_arg = f"--client-server-mode={client_server_mode}" if client_server_mode else None

command = [sys.executable, str(pathlib.Path(script_path)), service_arg, server_host_arg, server_user_arg]
command.extend(
arg
for arg in (
client_host_arg,
client_user_arg,
server_cert_mode_arg,
server_client_mode_arg,
client_cert_mode_arg,
client_server_mode_arg,
)
if arg is not None
)
subprocess.run(command, check=True)
Loading