diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 2b91fe27a..2817e38cf 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -106,10 +106,6 @@ jobs: - name: Run receptor tests run: make test - - name: Remove sockets for artifacting - if: ${{ failure() }} - run: "find /tmp/receptor-testing -type s -exec /bin/rm {} \\;" - - name: get k8s logs if: ${{ failure() }} run: .github/workflows/artifact-k8s-logs.sh diff --git a/receptorctl/receptorctl/socket_interface.py b/receptorctl/receptorctl/socket_interface.py index b2ad138fc..8c62ea4f3 100644 --- a/receptorctl/receptorctl/socket_interface.py +++ b/receptorctl/receptorctl/socket_interface.py @@ -122,7 +122,8 @@ def connect(self): try: if protocol == "tls": context = ssl.create_default_context( - purpose=ssl.Purpose.SERVER_AUTH, cafile=self._rootcas + purpose=ssl.Purpose.SERVER_AUTH, + cafile=self._rootcas, ) if self._key and self._cert: context.load_cert_chain( diff --git a/receptorctl/tests/conftest.py b/receptorctl/tests/conftest.py index 2fd90de4b..83e29e430 100644 --- a/receptorctl/tests/conftest.py +++ b/receptorctl/tests/conftest.py @@ -1,149 +1,113 @@ +import receptorctl + +import pytest import subprocess import os import shutil import time import json - -import pytest -import receptorctl - +import yaml from click.testing import CliRunner -tmpDir = "/tmp/receptorctltest" +from lib import create_certificate @pytest.fixture(scope="session") -def create_empty_dir(): - def check_dependencies(): - """Check if we have the required dependencies - raise an exception if we don't - """ +def base_tmp_dir(): + receptor_tmp_dir = "/tmp/receptor" + base_tmp_dir = "/tmp/receptorctltest" - # Check if openssl binary is on the path - try: - subprocess.check_output(["openssl", "version"]) - except Exception: - raise Exception( - "openssl binary not found\n" 'Consider run "sudo dnf install openssl"' - ) + # Clean up tmp directory and create a new one + if os.path.exists(base_tmp_dir): + shutil.rmtree(base_tmp_dir) + os.mkdir(base_tmp_dir) - check_dependencies() + yield base_tmp_dir - # Clean up tmp directory and create a new one - if os.path.exists(tmpDir): - shutil.rmtree(tmpDir) - os.mkdir(tmpDir) + # Tear-down + # if os.path.exists(base_tmp_dir): + # shutil.rmtree(base_tmp_dir) + if os.path.exists(receptor_tmp_dir): + shutil.rmtree(receptor_tmp_dir) -@pytest.fixture(scope="session") -def create_certificate(create_empty_dir): - def generate_cert(name, commonName): - keyPath = os.path.join(tmpDir, name + ".key") - crtPath = os.path.join(tmpDir, name + ".crt") - subprocess.check_output(["openssl", "genrsa", "-out", keyPath, "2048"]) - subprocess.check_output( - [ - "openssl", - "req", - "-x509", - "-new", - "-nodes", - "-key", - keyPath, - "-subj", - "/C=/ST=/L=/O=/OU=ReceptorTesting/CN=ca", - "-sha256", - "-out", - crtPath, - ] - ) - return keyPath, crtPath - - def generate_cert_with_ca(name, caKeyPath, caCrtPath, commonName): - keyPath = os.path.join(tmpDir, name + ".key") - crtPath = os.path.join(tmpDir, name + ".crt") - csrPath = os.path.join(tmpDir, name + ".csa") - extPath = os.path.join(tmpDir, name + ".ext") - - # create x509 extension - with open(extPath, "w") as ext: - ext.write("subjectAltName=DNS:" + commonName) - ext.close() - subprocess.check_output(["openssl", "genrsa", "-out", keyPath, "2048"]) - - # create cert request - subprocess.check_output( - [ - "openssl", - "req", - "-new", - "-sha256", - "-key", - keyPath, - "-subj", - "/C=/ST=/L=/O=/OU=ReceptorTesting/CN=" + commonName, - "-out", - csrPath, - ] - ) + subprocess.call(["killall", "receptor"]) - # sign cert request - subprocess.check_output( - [ - "openssl", - "x509", - "-req", - "-extfile", - extPath, - "-in", - csrPath, - "-CA", - caCrtPath, - "-CAkey", - caKeyPath, - "-CAcreateserial", - "-out", - crtPath, - "-sha256", - ] - ) - return keyPath, crtPath +@pytest.fixture(scope="class") +def receptor_mesh(base_tmp_dir): + class ReceptorMeshSetup: + # Relative dir to the receptorctl tests + mesh_definitions_dir = "tests/mesh-definitions" + + def __init__(self): + # Default vars + self.base_tmp_dir = base_tmp_dir + + # Required dependencies + self.__check_dependencies() + + def setup(self, mesh_name: str = "mesh1", socket_file_name: str = "node1.sock"): + self.mesh_name = mesh_name + self.__change_config_files_dir(mesh_name) + self.__create_tmp_dir() + self.__create_certificates() + self.socket_file_name = socket_file_name + + # HACK this should be a dinamic way to select a node socket + self.default_socket_unix = "unix://" + os.path.join( + self.get_mesh_tmp_dir(), socket_file_name + ) - # Create a new CA - caKeyPath, caCrtPath = generate_cert("ca", "ca") - clientKeyPath, clientCrtPath = generate_cert_with_ca( - "client", caKeyPath, caCrtPath, "localhost" - ) - generate_cert_with_ca("server", caKeyPath, caCrtPath, "localhost") + def default_receptor_controller_unix(self): + return receptorctl.ReceptorControl(self.default_socket_unix) - return { - "caKeyPath": caKeyPath, - "caCrtPath": caCrtPath, - "clientKeyPath": clientKeyPath, - "clientCrtPath": clientCrtPath, - } + def __change_config_files_dir(self, mesh_name: str): + self.config_files_dir = "{}/{}".format(self.mesh_definitions_dir, mesh_name) + self.config_files = [] + # Iterate over all the files in the config_files_dir + # and create a list of all files that end with .yaml or .yml + for f in os.listdir(self.config_files_dir): + if f.endswith(".yaml") or f.endswith(".yml"): + self.config_files.append(os.path.join(self.config_files_dir, f)) -@pytest.fixture(scope="session") -def certificate_files(create_certificate): - """Returns a dict with the certificate files - - The dict contains the following keys: - caKeyPath - caCrtPath - clientKeyPath - clientCrtPath - """ - return create_certificate + def __create_certificates(self): + self.certificate_files = create_certificate(self.get_mesh_tmp_dir()) + def get_mesh_name(self): + return self.config_files_dir.split("/")[-1] -@pytest.fixture(scope="session") -def prepare_environment(certificate_files): - pass + def get_mesh_tmp_dir(self): + mesh_tmp_dir = "{}/{}".format(self.base_tmp_dir, self.mesh_name) + return mesh_tmp_dir + def __check_dependencies(self): + """Check if we have the required dependencies + raise an exception if we don't + """ -@pytest.fixture(scope="session") + # Check if openssl binary is on the path + try: + subprocess.check_output(["openssl", "version"]) + except FileNotFoundError: + raise Exception( + "openssl binary not found\n" + 'Consider run "sudo dnf install openssl"' + ) + + def __create_tmp_dir(self): + mesh_tmp_dir_path = self.get_mesh_tmp_dir() + + # Clean up tmp directory and create a new one + if os.path.exists(mesh_tmp_dir_path): + shutil.rmtree(mesh_tmp_dir_path) + os.mkdir(mesh_tmp_dir_path) + + return ReceptorMeshSetup() + + +@pytest.fixture(scope="class") def receptor_bin_path(): """Returns the path to the receptor binary @@ -162,7 +126,17 @@ def receptor_bin_path(): # the path to the binary if it is found. receptor_bin_path_from_test_dir = os.path.join( os.path.dirname(os.path.abspath(__file__)), - "../../tests/artifacts-output", + "../../tests/artifacts-output/", + "receptor", + ) + if os.path.exists(receptor_bin_path_from_test_dir): + return receptor_bin_path_from_test_dir + + # Check if the receptor binary is in '../../' and returns + # the path to the binary if it is found. + receptor_bin_path_from_test_dir = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "../../", "receptor", ) if os.path.exists(receptor_bin_path_from_test_dir): @@ -178,16 +152,6 @@ def receptor_bin_path(): ) -@pytest.fixture(scope="class") -def default_socket_unix(): - return "unix://" + os.path.join(tmpDir, "node1.sock") - - -@pytest.fixture(scope="class") -def default_receptor_controller_unix(default_socket_unix): - return receptorctl.ReceptorControl(default_socket_unix) - - @pytest.fixture(scope="class") def default_socket_tcp(): return "tcp://localhost:11112" @@ -217,48 +181,149 @@ def default_receptor_controller_tcp_tls(default_socket_tcp, certificate_files): @pytest.fixture(scope="class") -def receptor_mesh( - prepare_environment, receptor_bin_path, default_receptor_controller_unix -): +def receptor_nodes(): + class ReceptorNodes: + nodes = [] + log_files = [] - node1 = subprocess.Popen( - [receptor_bin_path, "-c", "tests/mesh-definitions/mesh1/node1.yaml"] - ) - node2 = subprocess.Popen( - [receptor_bin_path, "-c", "tests/mesh-definitions/mesh1/node2.yaml"] - ) - node3 = subprocess.Popen( - [receptor_bin_path, "-c", "tests/mesh-definitions/mesh1/node3.yaml"] - ) + return ReceptorNodes() + + +def receptor_nodes_kill(nodes): + for node in nodes: + node.kill() + + for node in nodes: + node.wait(3) + + +def import_config_from_node(node): + """Receive a node and return the config file as a dict""" + stream = open(node.args[2], "r") + try: + config_unflatten = yaml.safe_load(stream) + except yaml.YAMLError as e: + raise e + stream.close() + + config = {} + for c in config_unflatten: + config.update(c) + return config + + +def receptor_mesh_wait_until_ready(nodes, receptor_controller): time.sleep(0.5) - node1_controller = default_receptor_controller_unix + # Try up to 6 times + tries = 0 while True: - status = node1_controller.simple_command("status") - if status["RoutingTable"] == {"node2": "node2", "node3": "node2"}: + status = receptor_controller.simple_command("status") + # Check if it has three known nodes + if len(status["KnownConnectionCosts"]) == 3: break - time.sleep(0.5) + tries += 1 + if tries > 6: + raise Exception("Receptor Mesh did not start up") + time.sleep(1) + + receptor_controller.close() + + +@pytest.fixture(scope="class") +def certificate_files(receptor_mesh): + return receptor_mesh.certificate_files - node1_controller.close() - # Debug mesh data - print("# Mesh nodes: {}".format(str(status["KnownConnectionCosts"].keys()))) +@pytest.fixture(scope="class") +def default_receptor_controller_unix(receptor_mesh): + return receptor_mesh.default_receptor_controller_unix() + + +def start_nodes(receptor_mesh, receptor_nodes, receptor_bin_path): + for i, config_file in enumerate(receptor_mesh.config_files): + log_file_name = ( + config_file.split("/")[-1].replace(".yaml", ".log").replace(".yml", ".log") + ) + receptor_nodes.log_files.append( + open( + os.path.join(receptor_mesh.get_mesh_tmp_dir(), log_file_name), + "w", + ) + ) + receptor_nodes.nodes.append( + subprocess.Popen( + [receptor_bin_path, "-c", config_file], + stdout=receptor_nodes.log_files[i], + stderr=receptor_nodes.log_files[i], + ) + ) + + +@pytest.fixture(scope="class") +def receptor_mesh_mesh1( + receptor_bin_path, + receptor_nodes, + receptor_mesh, +): + # Set custom config files dir + receptor_mesh.setup("mesh1") + + # Start the receptor nodes processes + start_nodes(receptor_mesh, receptor_nodes, receptor_bin_path) + + receptor_mesh_wait_until_ready( + receptor_nodes.nodes, receptor_mesh.default_receptor_controller_unix() + ) + + yield + + receptor_nodes_kill(receptor_nodes.nodes) + + +@pytest.fixture(scope="class") +def receptor_mesh_access_control( + receptor_bin_path, + receptor_nodes, + receptor_mesh, +): + # Set custom config files dir + receptor_mesh.setup("access_control", "node2.sock") + + # Create PEM key for signed work + key_path = os.path.join(receptor_mesh.get_mesh_tmp_dir(), "signwork_key") + subprocess.check_output( + [ + "ssh-keygen", + "-b", + "2048", + "-t", + "rsa", + "-f", + key_path, + "-q", + "-N", + "", + ] + ) + + # Start the receptor nodes processes + start_nodes(receptor_mesh, receptor_nodes, receptor_bin_path) + + receptor_mesh_wait_until_ready( + receptor_nodes.nodes, receptor_mesh.default_receptor_controller_unix() + ) yield - node1.kill() - node2.kill() - node1.wait() - node2.wait() - node3.kill() - node3.wait() + receptor_nodes_kill(receptor_nodes.nodes) @pytest.fixture(scope="function") -def receptor_control_args(): +def receptor_control_args(receptor_mesh): args = { - "--socket": "/tmp/receptorctltest/node1.sock", + "--socket": f"{receptor_mesh.get_mesh_tmp_dir()}/{receptor_mesh.socket_file_name}", "--config": None, "--tls": None, "--rootcas": None, diff --git a/receptorctl/tests/lib.py b/receptorctl/tests/lib.py new file mode 100644 index 000000000..7398e9042 --- /dev/null +++ b/receptorctl/tests/lib.py @@ -0,0 +1,95 @@ +import os +import subprocess + + +def __init__(): + pass + + +def create_certificate(tmp_dir: str): + def generate_cert(name, commonName): + keyPath = os.path.join(tmp_dir, name + ".key") + crtPath = os.path.join(tmp_dir, name + ".crt") + subprocess.check_output(["openssl", "genrsa", "-out", keyPath, "2048"]) + subprocess.check_output( + [ + "openssl", + "req", + "-x509", + "-new", + "-nodes", + "-key", + keyPath, + "-subj", + "/C=/ST=/L=/O=/OU=ReceptorTesting/CN=ca", + "-sha256", + "-out", + crtPath, + ] + ) + return keyPath, crtPath + + def generate_cert_with_ca(name, caKeyPath, caCrtPath, commonName): + keyPath = os.path.join(tmp_dir, name + ".key") + crtPath = os.path.join(tmp_dir, name + ".crt") + csrPath = os.path.join(tmp_dir, name + ".csa") + extPath = os.path.join(tmp_dir, name + ".ext") + + # create x509 extension + with open(extPath, "w") as ext: + ext.write("subjectAltName=DNS:" + commonName) + ext.close() + subprocess.check_output(["openssl", "genrsa", "-out", keyPath, "2048"]) + + # create cert request + subprocess.check_output( + [ + "openssl", + "req", + "-new", + "-sha256", + "-key", + keyPath, + "-subj", + "/C=/ST=/L=/O=/OU=ReceptorTesting/CN=" + commonName, + "-out", + csrPath, + ] + ) + + # sign cert request + subprocess.check_output( + [ + "openssl", + "x509", + "-req", + "-extfile", + extPath, + "-in", + csrPath, + "-CA", + caCrtPath, + "-CAkey", + caKeyPath, + "-CAcreateserial", + "-out", + crtPath, + "-sha256", + ] + ) + + return keyPath, crtPath + + # Create a new CA + caKeyPath, caCrtPath = generate_cert("ca", "ca") + clientKeyPath, clientCrtPath = generate_cert_with_ca( + "client", caKeyPath, caCrtPath, "localhost" + ) + generate_cert_with_ca("server", caKeyPath, caCrtPath, "localhost") + + return { + "caKeyPath": caKeyPath, + "caCrtPath": caCrtPath, + "clientKeyPath": clientKeyPath, + "clientCrtPath": clientCrtPath, + } diff --git a/receptorctl/tests/mesh-definitions/access_control/node1.yaml b/receptorctl/tests/mesh-definitions/access_control/node1.yaml new file mode 100644 index 000000000..daa7d98f8 --- /dev/null +++ b/receptorctl/tests/mesh-definitions/access_control/node1.yaml @@ -0,0 +1,28 @@ +- node: + id: node1 + +- log-level: debug + +- tcp-listener: + port: 12111 + +- control-service: + filename: /tmp/receptorctltest/access_control/node1.sock + +- work-signing: + privatekey: /tmp/receptorctltest/access_control/signwork_key + tokenexpiration: 10h30m + +- work-verification: + publickey: /tmp/receptorctltest/access_control/signwork_key.pub + +- work-command: + worktype: signed-echo + command: bash + params: "-c \"for w in {1..4}; do echo ${line^^}; sleep 1; done\"" + verifysignature: true + +- work-command: + workType: unsigned-echo + command: bash + params: "-c \"for w in {1..4}; do echo ${line^^}; sleep 1; done\"" diff --git a/receptorctl/tests/mesh-definitions/access_control/node2.yaml b/receptorctl/tests/mesh-definitions/access_control/node2.yaml new file mode 100644 index 000000000..fbe87d529 --- /dev/null +++ b/receptorctl/tests/mesh-definitions/access_control/node2.yaml @@ -0,0 +1,13 @@ +- node: + id: node2 + +- log-level: debug + +- tcp-peer: + address: localhost:12111 + +- tcp-listener: + port: 12121 + +- control-service: + filename: /tmp/receptorctltest/access_control/node2.sock diff --git a/receptorctl/tests/mesh-definitions/access_control/node3.yaml b/receptorctl/tests/mesh-definitions/access_control/node3.yaml new file mode 100644 index 000000000..58eee859a --- /dev/null +++ b/receptorctl/tests/mesh-definitions/access_control/node3.yaml @@ -0,0 +1,10 @@ +- node: + id: node3 + +- log-level: debug + +- tcp-peer: + address: localhost:12121 + +- control-service: + filename: /tmp/receptorctltest/access_control/node3.sock diff --git a/receptorctl/tests/mesh-definitions/mesh1/node1.yaml b/receptorctl/tests/mesh-definitions/mesh1/node1.yaml index 4fd4621e5..73d63a54d 100644 --- a/receptorctl/tests/mesh-definitions/mesh1/node1.yaml +++ b/receptorctl/tests/mesh-definitions/mesh1/node1.yaml @@ -5,7 +5,7 @@ port: 11111 - control-service: - filename: /tmp/receptorctltest/node1.sock + filename: /tmp/receptorctltest/mesh1/node1.sock - tcp-server: port: 11112 @@ -14,10 +14,10 @@ - tls-server: name: tlsserver - key: /tmp/receptorctltest/server.key - cert: /tmp/receptorctltest/server.crt + key: /tmp/receptorctltest/mesh1/server.key + cert: /tmp/receptorctltest/mesh1/server.crt requireclientcert: true - clientcas: /tmp/receptorctltest/ca.crt + clientcas: /tmp/receptorctltest/mesh1/ca.crt - control-service: service: ctltls diff --git a/receptorctl/tests/mesh-definitions/mesh1/node2.yaml b/receptorctl/tests/mesh-definitions/mesh1/node2.yaml index 52cb4dff8..f1173d792 100644 --- a/receptorctl/tests/mesh-definitions/mesh1/node2.yaml +++ b/receptorctl/tests/mesh-definitions/mesh1/node2.yaml @@ -8,4 +8,4 @@ port: 11121 - control-service: - filename: /tmp/receptorctltest/node2.sock + filename: /tmp/receptorctltest/mesh1/node2.sock diff --git a/receptorctl/tests/mesh-definitions/mesh1/node3.yaml b/receptorctl/tests/mesh-definitions/mesh1/node3.yaml index 1af76c119..68df56a6f 100644 --- a/receptorctl/tests/mesh-definitions/mesh1/node3.yaml +++ b/receptorctl/tests/mesh-definitions/mesh1/node3.yaml @@ -5,4 +5,4 @@ address: localhost:11121 - control-service: - filename: /tmp/receptorctltest/node3.sock + filename: /tmp/receptorctltest/mesh1/node3.sock diff --git a/receptorctl/tests/test_cli.py b/receptorctl/tests/test_cli.py index 7a5f583e8..5cafe057b 100644 --- a/receptorctl/tests/test_cli.py +++ b/receptorctl/tests/test_cli.py @@ -6,7 +6,7 @@ import pytest -@pytest.mark.usefixtures("receptor_mesh") +@pytest.mark.usefixtures("receptor_mesh_mesh1") class TestCommands: def test_cmd_status(self, invoke_as_json): result, json_output = invoke_as_json(commands.status, []) diff --git a/receptorctl/tests/test_connection.py b/receptorctl/tests/test_connection.py index e68b602ed..a07c8a858 100644 --- a/receptorctl/tests/test_connection.py +++ b/receptorctl/tests/test_connection.py @@ -1,7 +1,7 @@ import pytest -@pytest.mark.usefixtures("receptor_mesh") +@pytest.mark.usefixtures("receptor_mesh_mesh1") class TestReceptorCtlConnection: def test_connect_to_service(self, default_receptor_controller_unix): node1_controller = default_receptor_controller_unix diff --git a/receptorctl/tests/test_mesh.py b/receptorctl/tests/test_mesh.py new file mode 100644 index 000000000..e58f005c8 --- /dev/null +++ b/receptorctl/tests/test_mesh.py @@ -0,0 +1,61 @@ +from receptorctl import cli as commands + +# The goal is to write tests following the click documentation: +# https://click.palletsprojects.com/en/8.0.x/testing/ + +import pytest +import time + + +@pytest.mark.usefixtures("receptor_mesh_access_control") +class TestMeshFirewall: + def test_work_unsigned(self, invoke, receptor_nodes): + """Run a unsigned work-command + + Steps: + 1. Create node1 with a unsigned work-command + 2. Create node2 + 3. Run from node2 a unsigned work-command to node1 + 4. Expect to be accepted + """ + + # Run an unsigned command + result = invoke( + commands.work, + "submit unsigned-echo --node node1 --no-payload".split(), + ) + work_unit_id = result.stdout.split("Unit ID: ")[-1].replace("\n", "") + + time.sleep(5) + assert result.exit_code == 0 + + # Release unsigned work + result = invoke(commands.work, f"release {work_unit_id}".split()) + + assert result.exit_code == 0 + + # DISABLE UNTIL THE FIX BEING IMPLEMENTED + # + # def test_work_signed_expect_block(self, invoke, receptor_nodes): + # """Run a signed work-command without the right key + # and expect to be blocked. + + # Steps: + # 1. Create node1 with a signed work-command + # 2. Create node2 + # 3. Run from node2 a signed work-command to node1 + # 4. Expect to be blocked + # """ + # # Run an unsigned command + # result = invoke( + # commands.work, "submit signed-echo --node node1 --no-payload".split() + # ) + # work_unit_id = result.stdout.split("Unit ID: ")[-1].replace("\n", "") + + # time.sleep(5) + # assert work_unit_id, "Work unit ID should not be empty" + # assert result.exit_code != 0, "Work signed run should fail, but it worked" + + # # Release unsigned work + # result = invoke(commands.work, f"release {work_unit_id}".split()) + # assert result.exit_code == 0, "Work release failed"