From c2a19af6fa259d9cf4ac4525075b8b8900936ac3 Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Fri, 2 Aug 2024 09:20:00 +0100 Subject: [PATCH] #2735 - added documentation for users, usermanager and usersessionmanager. Added the ability to add additional users from config and documented this. also tested additional users from config. --- .../nodes/common/common_node_attributes.rst | 27 +++ .../network/base_hardware.rst | 206 +++++++++++++++++- src/primaite/game/game.py | 8 +- .../assets/configs/basic_node_with_users.yaml | 34 +++ .../test_users_creation_from_config.py | 26 +++ 5 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 tests/assets/configs/basic_node_with_users.yaml create mode 100644 tests/integration_tests/network/test_users_creation_from_config.py diff --git a/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst b/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst index e648e4a1..7cf11eb4 100644 --- a/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst +++ b/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst @@ -53,3 +53,30 @@ The number of time steps required to occur in order for the node to cycle from ` Optional. Default value is ``3``. The number of time steps required to occur in order for the node to cycle from ``ON`` to ``SHUTTING_DOWN`` and then finally ``OFF``. + +``users`` +--------- + +The list of pre-existing users that are additional to the default admin user (``username=admin``, ``password=admin``). +Additional users are configured as an array nd must contain a ``username``, ``password``, and can contain an optional +boolean ``is_admin``. + +Example of adding two additional users to a node: + +.. code-block:: yaml + + simulation: + network: + nodes: + - hostname: client_1 + type: computer + ip_address: 192.168.10.11 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + users: + - username: jane.doe + password: '1234' + is_admin: true + - username: john.doe + password: password_1 + is_admin: false diff --git a/docs/source/simulation_components/network/base_hardware.rst b/docs/source/simulation_components/network/base_hardware.rst index 9e42b1de..ce1e5c74 100644 --- a/docs/source/simulation_components/network/base_hardware.rst +++ b/docs/source/simulation_components/network/base_hardware.rst @@ -97,8 +97,8 @@ Node Behaviours/Functions - **receive_frame()**: Handles the processing of incoming network frames. - **apply_timestep()**: Advances the state of the node according to the simulation timestep. - **power_on()**: Initiates the node, enabling all connected Network Interfaces and starting all Services and - Applications, taking into account the `start_up_duration`. -- **power_off()**: Stops the node's operations, adhering to the `shut_down_duration`. + Applications, taking into account the ``start_up_duration``. +- **power_off()**: Stops the node's operations, adhering to the ``shut_down_duration``. - **ping()**: Sends ICMP echo requests to a specified IP address to test connectivity. - **has_enabled_network_interface()**: Checks if the node has any network interfaces enabled, facilitating network communication. @@ -109,3 +109,205 @@ Node Behaviours/Functions The Node class handles installation of system software, network connectivity, frame processing, system logging, and power states. It establishes baseline functionality while allowing subclassing to model specific node types like hosts, routers, firewalls etc. The flexible architecture enables composing complex network topologies. + +User, UserManager, and UserSessionManager +========================================= + +The ``base.py`` module also includes essential classes for managing users and their sessions within the PrimAITE +simulation. These are the ``User``, ``UserManager``, and ``UserSessionManager`` classes. The base ``Node`` class comes +with ``UserManager``, and ``UserSessionManager`` classes pre-installed. + +User Class +---------- + +The ``User`` class represents a user in the system. It includes attributes such as ``username``, ``password``, +``disabled``, and ``is_admin`` to define the user's credentials and status. + +Example Usage +^^^^^^^^^^^^^ + +Creating a user: + .. code-block:: python + + user = User(username="john_doe", password="12345") + +UserManager Class +----------------- + +The ``UserManager`` class handles user management tasks such as creating users, authenticating them, changing passwords, +and enabling or disabling user accounts. It maintains a dictionary of users and provides methods to manage them +effectively. + +Example Usage +^^^^^^^^^^^^^ + +Creating a ``UserManager`` instance and adding a user: + .. code-block:: python + + user_manager = UserManager() + user_manager.add_user(username="john_doe", password="12345") + +Authenticating a user: + .. code-block:: python + + user = user_manager.authenticate_user(username="john_doe", password="12345") + +UserSessionManager Class +------------------------ + +The ``UserSessionManager`` class manages user sessions, including local and remote sessions. It handles session creation, +timeouts, and provides methods for logging users in and out. + +Example Usage +^^^^^^^^^^^^^ + +Creating a ``UserSessionManager`` instance and logging a user in locally: + .. code-block:: python + + session_manager = UserSessionManager() + session_id = session_manager.local_login(username="john_doe", password="12345") + +Logging a user out: + .. code-block:: python + + session_manager.local_logout() + +Practical Examples +------------------ + +Below are unit tests which act as practical examples illustrating how to use the ``User``, ``UserManager``, and +``UserSessionManager`` classes within the context of a client-server network simulation. + +Setting up a Client-Server Network +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + from typing import Tuple + from uuid import uuid4 + + import pytest + + from primaite.simulator.network.container import Network + from primaite.simulator.network.hardware.nodes.host.computer import Computer + from primaite.simulator.network.hardware.nodes.host.server import Server + + @pytest.fixture(scope="function") + def client_server_network() -> Tuple[Computer, Server, Network]: + network = Network() + + client = Computer( + hostname="client", + ip_address="192.168.1.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + start_up_duration=0, + ) + client.power_on() + + server = Server( + hostname="server", + ip_address="192.168.1.3", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + start_up_duration=0, + ) + server.power_on() + + network.connect(client.network_interface[1], server.network_interface[1]) + + return client, server, network + +Local Login Success +^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + def test_local_login_success(client_server_network): + client, server, network = client_server_network + + assert not client.user_session_manager.local_user_logged_in + + client.user_session_manager.local_login(username="admin", password="admin") + + assert client.user_session_manager.local_user_logged_in + +Local Login Failure +^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + def test_local_login_failure(client_server_network): + client, server, network = client_server_network + + assert not client.user_session_manager.local_user_logged_in + + client.user_session_manager.local_login(username="jane.doe", password="12345") + + assert not client.user_session_manager.local_user_logged_in + +Adding a New User and Successful Local Login +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + def test_new_user_local_login_success(client_server_network): + client, server, network = client_server_network + + assert not client.user_session_manager.local_user_logged_in + + client.user_manager.add_user(username="jane.doe", password="12345") + + client.user_session_manager.local_login(username="jane.doe", password="12345") + + assert client.user_session_manager.local_user_logged_in + +Clearing Previous Login on New Local Login +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + def test_new_local_login_clears_previous_login(client_server_network): + client, server, network = client_server_network + + assert not client.user_session_manager.local_user_logged_in + + current_session_id = client.user_session_manager.local_login(username="admin", password="admin") + + assert client.user_session_manager.local_user_logged_in + + assert client.user_session_manager.local_session.user.username == "admin" + + client.user_manager.add_user(username="jane.doe", password="12345") + + new_session_id = client.user_session_manager.local_login(username="jane.doe", password="12345") + + assert client.user_session_manager.local_user_logged_in + + assert client.user_session_manager.local_session.user.username == "jane.doe" + + assert new_session_id != current_session_id + +Persistent Login for the Same User +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + def test_new_local_login_attempt_same_uses_persists(client_server_network): + client, server, network = client_server_network + + assert not client.user_session_manager.local_user_logged_in + + current_session_id = client.user_session_manager.local_login(username="admin", password="admin") + + assert client.user_session_manager.local_user_logged_in + + assert client.user_session_manager.local_session.user.username == "admin" + + new_session_id = client.user_session_manager.local_login(username="admin", password="admin") + + assert client.user_session_manager.local_user_logged_in + + assert client.user_session_manager.local_session.user.username == "admin" + + assert new_session_id == current_session_id diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 5ef8c14c..68abf9f2 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -18,7 +18,7 @@ from primaite.game.agent.scripted_agents.tap001 import TAP001 from primaite.game.science import graph_has_cycle, topological_sort from primaite.simulator import SIM_OUTPUT from primaite.simulator.network.airspace import AirSpaceFrequency -from primaite.simulator.network.hardware.base import NodeOperatingState +from primaite.simulator.network.hardware.base import NodeOperatingState, UserManager from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC from primaite.simulator.network.hardware.nodes.host.server import Printer, Server @@ -267,6 +267,7 @@ class PrimaiteGame: for node_cfg in nodes_cfg: n_type = node_cfg["type"] + new_node = None if n_type == "computer": new_node = Computer( hostname=node_cfg["hostname"], @@ -316,6 +317,11 @@ class PrimaiteGame: msg = f"invalid node type {n_type} in config" _LOGGER.error(msg) raise ValueError(msg) + + if "users" in node_cfg and new_node.software_manager.software.get("UserManager"): + user_manager: UserManager = new_node.software_manager.software["UserManager"] # noqa + for user_cfg in node_cfg["users"]: + user_manager.add_user(**user_cfg, bypass_can_perform_action=True) if "services" in node_cfg: for service_cfg in node_cfg["services"]: new_service = None diff --git a/tests/assets/configs/basic_node_with_users.yaml b/tests/assets/configs/basic_node_with_users.yaml new file mode 100644 index 00000000..064519dd --- /dev/null +++ b/tests/assets/configs/basic_node_with_users.yaml @@ -0,0 +1,34 @@ +io_settings: + save_step_metadata: false + save_pcap_logs: true + save_sys_logs: true + sys_log_level: WARNING + agent_log_level: INFO + save_agent_logs: true + write_agent_log_to_terminal: True + + +game: + max_episode_length: 256 + ports: + - ARP + protocols: + - ICMP + - UDP + + +simulation: + network: + nodes: + - hostname: client_1 + type: computer + ip_address: 192.168.10.11 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + users: + - username: jane.doe + password: '1234' + is_admin: true + - username: john.doe + password: password_1 + is_admin: false diff --git a/tests/integration_tests/network/test_users_creation_from_config.py b/tests/integration_tests/network/test_users_creation_from_config.py new file mode 100644 index 00000000..8cd3b037 --- /dev/null +++ b/tests/integration_tests/network/test_users_creation_from_config.py @@ -0,0 +1,26 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +import yaml + +from primaite.game.game import PrimaiteGame +from primaite.simulator.network.hardware.base import UserManager +from tests import TEST_ASSETS_ROOT + + +def test_users_from_config(): + config_path = TEST_ASSETS_ROOT / "configs" / "basic_node_with_users.yaml" + + with open(config_path, "r") as f: + config_dict = yaml.safe_load(f) + network = PrimaiteGame.from_config(cfg=config_dict).simulation.network + + client_1 = network.get_node_by_hostname("client_1") + + user_manager: UserManager = client_1.software_manager.software["UserManager"] + + assert len(user_manager.users) == 3 + + assert user_manager.users["jane.doe"].password == "1234" + assert user_manager.users["jane.doe"].is_admin + + assert user_manager.users["john.doe"].password == "password_1" + assert not user_manager.users["john.doe"].is_admin