Rename Sim Actions to request

This commit is contained in:
Marek Wolan
2023-10-09 13:24:08 +01:00
parent 2722abe428
commit 5a5710c6ae
16 changed files with 266 additions and 814 deletions

View File

@@ -5,7 +5,7 @@
Actions System Actions System
============== ==============
``SimComponent``s in the simulation are decoupled from the agent training logic. However, they still need a managed means of accepting requests to perform actions. For this, they use ``ActionManager`` and ``Action``. ``SimComponent``s in the simulation are decoupled from the agent training logic. However, they still need a managed means of accepting requests to perform actions. For this, they use ``RequestManager`` and ``Action``.
Just like other aspects of SimComponent, the actions are not managed centrally for the whole simulation, but instead they are dynamically created and updated based on the nodes, links, and other components that currently exist. This was achieved with the following design decisions: Just like other aspects of SimComponent, the actions are not managed centrally for the whole simulation, but instead they are dynamically created and updated based on the nodes, links, and other components that currently exist. This was achieved with the following design decisions:
@@ -16,7 +16,7 @@ Just like other aspects of SimComponent, the actions are not managed centrally f
2. ``context`` - optional extra information that can be used to decide how to process the action. This is formatted as a dictionary. For example, if the action requires authentication, the context can include information about the user that initiated the request to decide if their permissions are sufficient. 2. ``context`` - optional extra information that can be used to decide how to process the action. This is formatted as a dictionary. For example, if the action requires authentication, the context can include information about the user that initiated the request to decide if their permissions are sufficient.
- request - request
The request is a list of strings which help specify who should handle the request. The strings in the request list help ActionManagers traverse the 'ownership tree' of SimComponent. The example given above would be handled in the following way: The request is a list of strings which help specify who should handle the request. The strings in the request list help RequestManagers traverse the 'ownership tree' of SimComponent. The example given above would be handled in the following way:
1. ``Simulation`` receives `['network', 'node', '<node-uuid>', 'service', '<service-uuid>', 'restart']`. 1. ``Simulation`` receives `['network', 'node', '<node-uuid>', 'service', '<service-uuid>', 'restart']`.
The first element of the action is ``network``, therefore it passes the action down to its network. The first element of the action is ``network``, therefore it passes the action down to its network.
@@ -25,22 +25,22 @@ Just like other aspects of SimComponent, the actions are not managed centrally f
3. ``Node`` receives `['service', '<service-uuid>', 'restart']`. 3. ``Node`` receives `['service', '<service-uuid>', 'restart']`.
The first element of the action is ``service``, therefore the node looks at the service uuid and passes the rest of the action to the service with that uuid. The first element of the action is ``service``, therefore the node looks at the service uuid and passes the rest of the action to the service with that uuid.
4. ``Service`` receives ``['restart']``. 4. ``Service`` receives ``['restart']``.
Since ``restart`` is a defined action in the service's own ActionManager, the service performs a restart. Since ``restart`` is a defined action in the service's own RequestManager, the service performs a restart.
Techincal Detail Techincal Detail
================ ================
This system was achieved by implementing two classes, :py:class:`primaite.simulator.core.Action`, and :py:class:`primaite.simulator.core.ActionManager`. This system was achieved by implementing two classes, :py:class:`primaite.simulator.core.Action`, and :py:class:`primaite.simulator.core.RequestManager`.
Action Action
------ ------
The ``Action`` object stores a reference to a method that performs the action, for example a node could have an action that stores a reference to ``self.turn_on()``. Techincally, this can be any callable that accepts `request, context` as it's parameters. In practice, this is often defined using ``lambda`` functions within a component's ``self._init_action_manager()`` method. Optionally, the ``Action`` object can also hold a validator that will permit/deny the action depending on context. The ``Action`` object stores a reference to a method that performs the action, for example a node could have an action that stores a reference to ``self.turn_on()``. Techincally, this can be any callable that accepts `request, context` as it's parameters. In practice, this is often defined using ``lambda`` functions within a component's ``self._init_request_manager()`` method. Optionally, the ``Action`` object can also hold a validator that will permit/deny the action depending on context.
ActionManager RequestManager
------------- -------------
The ``ActionManager`` object stores a mapping between strings and actions. It is responsible for processing the ``request`` and passing it down the ownership tree. Techincally, the ``ActionManager`` is itself a callable that accepts `request, context` tuple, and so it can be chained with other action managers. The ``RequestManager`` object stores a mapping between strings and actions. It is responsible for processing the ``request`` and passing it down the ownership tree. Techincally, the ``RequestManager`` is itself a callable that accepts `request, context` tuple, and so it can be chained with other action managers.
A simple example without chaining can be seen in the :py:class:`primaite.simulator.file_system.file_system.File` class. A simple example without chaining can be seen in the :py:class:`primaite.simulator.file_system.file_system.File` class.
@@ -48,18 +48,18 @@ A simple example without chaining can be seen in the :py:class:`primaite.simulat
class File(FileSystemItemABC): class File(FileSystemItemABC):
... ...
def _init_action_manager(self): def _init_request_manager(self):
... ...
action_manager.add_action("scan", Action(func=lambda request, context: self.scan())) request_manager.add_action("scan", Action(func=lambda request, context: self.scan()))
action_manager.add_action("repair", Action(func=lambda request, context: self.repair())) request_manager.add_action("repair", Action(func=lambda request, context: self.repair()))
action_manager.add_action("restore", Action(func=lambda request, context: self.restore())) request_manager.add_action("restore", Action(func=lambda request, context: self.restore()))
*ellipses (``...``) used to omit code impertinent to this explanation* *ellipses (``...``) used to omit code impertinent to this explanation*
Chaining ActionManagers Chaining RequestManagers
----------------------- -----------------------
Since the method for performing an action needs to accept `request, context` as parameters, and ActionManager itself is a callable that accepts `request, context` as parameters, it possible to use ActionManager as an action. In fact, that is how PrimAITE deals with traversing the ownership tree. Each time an ActionManager accepts a request, it pops the first elements and uses it to decide to which Action it should send the remaining request. However, the Action could have another ActionManager as it's function, therefore the request will be routed again. Each time the request is passed to a new action manager, the first element is popped. Since the method for performing an action needs to accept `request, context` as parameters, and RequestManager itself is a callable that accepts `request, context` as parameters, it possible to use RequestManager as an action. In fact, that is how PrimAITE deals with traversing the ownership tree. Each time an RequestManager accepts a request, it pops the first elements and uses it to decide to which Action it should send the remaining request. However, the Action could have another RequestManager as it's function, therefore the request will be routed again. Each time the request is passed to a new action manager, the first element is popped.
An example of how this works is in the :py:class:`primaite.simulator.network.hardware.base.Node` class. An example of how this works is in the :py:class:`primaite.simulator.network.hardware.base.Node` class.
@@ -67,22 +67,22 @@ An example of how this works is in the :py:class:`primaite.simulator.network.har
class Node(SimComponent): class Node(SimComponent):
... ...
def _init_action_manager(self): def _init_request_manager(self):
... ...
# a regular action which is processed by the Node itself # a regular action which is processed by the Node itself
action_manager.add_action("turn_on", Action(func=lambda request, context: self.turn_on())) request_manager.add_action("turn_on", Action(func=lambda request, context: self.turn_on()))
# if the Node receives a request where the first word is 'service', it will use a dummy manager # if the Node receives a request where the first word is 'service', it will use a dummy manager
# called self._service_action_manager to pass on the reqeust to the relevant service. This dummy # called self._service_request_manager to pass on the reqeust to the relevant service. This dummy
# manager is simply here to map the service UUID that that service's own action manager. This is # manager is simply here to map the service UUID that that service's own action manager. This is
# done because the next string after "service" is always the uuid of that service, so we need an # done because the next string after "service" is always the uuid of that service, so we need an
# actionmanager to pop that string before sending it onto the relevant service's ActionManager. # RequestManager to pop that string before sending it onto the relevant service's RequestManager.
self._service_action_manager = ActionManager() self._service_request_manager = RequestManager()
action_manager.add_action("service", Action(func=self._service_action_manager)) request_manager.add_action("service", Action(func=self._service_request_manager))
... ...
def install_service(self, service): def install_service(self, service):
self.services[service.uuid] = service self.services[service.uuid] = service
... ...
# Here, the service UUID is registered to allow passing actions between the node and the service. # Here, the service UUID is registered to allow passing actions between the node and the service.
self._service_action_manager.add_action(service.uuid, Action(func=service._action_manager)) self._service_request_manager.add_action(service.uuid, Action(func=service._request_manager))

View File

@@ -42,15 +42,15 @@ snippet demonstrates usage of the ``ActionPermissionValidator``.
.. code:: python .. code:: python
from primaite.simulator.core import Action, ActionManager, SimComponent from primaite.simulator.core import Action, RequestManager, SimComponent
from primaite.simulator.domain.controller import AccountGroup, GroupMembershipValidator from primaite.simulator.domain.controller import AccountGroup, GroupMembershipValidator
class Smartphone(SimComponent): class Smartphone(SimComponent):
name: str name: str
apps = [] apps = []
def _init_action_manager(self) -> ActionManager: def _init_request_manager(self) -> RequestManager:
am = super()._init_action_manager() am = super()._init_request_manager()
am.add_action( am.add_action(
"reset_factory_settings", "reset_factory_settings",
Action( Action(

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -11,9 +11,9 @@ from primaite import getLogger
_LOGGER = getLogger(__name__) _LOGGER = getLogger(__name__)
class ActionPermissionValidator(BaseModel): class RequestPermissionValidator(BaseModel):
""" """
Base class for action validators. Base class for request validators.
The permissions manager is designed to be generic. So, although in the first instance the permissions The permissions manager is designed to be generic. So, although in the first instance the permissions
are evaluated purely on membership to AccountGroup, this class can support validating permissions based on any are evaluated purely on membership to AccountGroup, this class can support validating permissions based on any
@@ -22,130 +22,127 @@ class ActionPermissionValidator(BaseModel):
@abstractmethod @abstractmethod
def __call__(self, request: List[str], context: Dict) -> bool: def __call__(self, request: List[str], context: Dict) -> bool:
"""Use the request and context paramters to decide whether the action should be permitted.""" """Use the request and context paramters to decide whether the request should be permitted."""
pass pass
class AllowAllValidator(ActionPermissionValidator): class AllowAllValidator(RequestPermissionValidator):
"""Always allows the action.""" """Always allows the request."""
def __call__(self, request: List[str], context: Dict) -> bool: def __call__(self, request: List[str], context: Dict) -> bool:
"""Always allow the action.""" """Always allow the request."""
return True return True
class Action(BaseModel): class RequestType(BaseModel):
""" """
This object stores data related to a single action. This object stores data related to a single request type.
This includes the callable that can execute the action request, and the validator that will decide whether This includes the callable that can execute the request, and the validator that will decide whether
the action can be performed or not. the request can be performed or not.
""" """
func: Callable[[List[str], Dict], None] func: Callable[[List[str], Dict], None]
""" """
``func`` is a function that accepts a request and a context dict. Typically this would be a lambda function ``func`` is a function that accepts a request and a context dict. Typically this would be a lambda function
that invokes a class method of your SimComponent. For example if the component is a node and the action is for that invokes a class method of your SimComponent. For example if the component is a node and the request type is for
turning it off, then the SimComponent should have a turn_off(self) method that does not need to accept any args. turning it off, then the SimComponent should have a turn_off(self) method that does not need to accept any args.
Then, this Action will be given something like ``func = lambda request, context: self.turn_off()``. Then, this request will be given something like ``func = lambda request, context: self.turn_off()``.
``func`` can also be another action manager, since ActionManager is a callable with a signature that matches what is ``func`` can also be another request manager, since RequestManager is a callable with a signature that matches what is
expected by ``func``. expected by ``func``.
""" """
validator: ActionPermissionValidator = AllowAllValidator() validator: RequestPermissionValidator = AllowAllValidator()
""" """
``validator`` is an instance of `ActionPermissionValidator`. This is essentially a callable that ``validator`` is an instance of ``RequestPermissionValidator``. This is essentially a callable that
accepts `request` and `context` and returns a boolean to represent whether the permission is granted to perform accepts `request` and `context` and returns a boolean to represent whether the permission is granted to perform
the action. The default validator will allow the request. The default validator will allow
""" """
# TODO: maybe this can be renamed to something like action selector? class RequestManager(BaseModel):
# Because there are two ways it's used, to select from a list of action verbs, or to select a child object to which to
# forward the request.
class ActionManager(BaseModel):
""" """
ActionManager is used by `SimComponent` instances to keep track of actions. RequestManager is used by `SimComponent` instances to keep track of requests.
Its main purpose is to be a lookup from action name to action function and corresponding validation function. This Its main purpose is to be a lookup from request name to request function and corresponding validation function. This
class is responsible for providing a consistent API for processing actions as well as helpful error messages. class is responsible for providing a consistent API for processing requests as well as helpful error messages.
""" """
actions: Dict[str, Action] = {} request_types: Dict[str, RequestType] = {}
"""maps action verb to an action object.""" """maps request name to an RequestType object."""
def __call__(self, request: Callable[[List[str], Dict], None], context: Dict) -> None: def __call__(self, request: Callable[[List[str], Dict], None], context: Dict) -> None:
""" """
Process an action request. Process an request request.
:param request: A list of strings which specify what action to take. The first string must be one of the allowed :param request: A list of strings describing the request. The first string must be one of the allowed
actions, i.e. it must be a key of self.actions. The subsequent strings in the list are passed as parameters request names, i.e. it must be a key of self.request_types. The subsequent strings in the list are passed as
to the action function. parameters to the request function.
:type request: List[str] :type request: List[str]
:param context: Dictionary of additional information necessary to process or validate the request. :param context: Dictionary of additional information necessary to process or validate the request.
:type context: Dict :type context: Dict
:raises RuntimeError: If the request parameter does not have a valid action identifier as the first item. :raises RuntimeError: If the request parameter does not have a valid request name as the first item.
""" """
action_key = request[0] request_key = request[0]
if action_key not in self.actions: if request_key not in self.request_types:
msg = ( msg = (
f"Action request {request} could not be processed because {action_key} is not a valid action", f"Request {request} could not be processed because {request_key} is not a valid request name",
"within this ActionManager", "within this RequestManager",
) )
_LOGGER.error(msg) _LOGGER.error(msg)
raise RuntimeError(msg) raise RuntimeError(msg)
action = self.actions[action_key] request_type = self.request_types[request_key]
action_options = request[1:] request_options = request[1:]
if not action.validator(action_options, context): if not request_type.validator(request_options, context):
_LOGGER.debug(f"Action request {request} was denied due to insufficient permissions") _LOGGER.debug(f"Request {request} was denied due to insufficient permissions")
return return
action.func(action_options, context) request_type.func(request_options, context)
def add_action(self, name: str, action: Action) -> None: def add_request(self, name: str, request_type: RequestType) -> None:
""" """
Add an action to this action manager. Add a request type to this request manager.
:param name: The string associated to this action. :param name: The string associated to this request.
:type name: str :type name: str
:param action: Action object. :param request_type: Request type object which contains information about how to resolve request.
:type action: Action :type request_type: RequestType
""" """
if name in self.actions: if name in self.request_types:
msg = f"Attempted to register an action but the action name {name} is already taken." msg = f"Attempted to register a request but the request name {name} is already taken."
_LOGGER.error(msg) _LOGGER.error(msg)
raise RuntimeError(msg) raise RuntimeError(msg)
self.actions[name] = action self.request_types[name] = request_type
def remove_action(self, name: str) -> None: def remove_request(self, name: str) -> None:
""" """
Remove an action from this manager. Remove a request from this manager.
:param name: name identifier of the action :param name: name identifier of the request
:type name: str :type name: str
""" """
if name not in self.actions: if name not in self.request_types:
msg = f"Attempted to remove action {name} from action manager, but it was not registered." msg = f"Attempted to remove request {name} from request manager, but it was not registered."
_LOGGER.error(msg) _LOGGER.error(msg)
raise RuntimeError(msg) raise RuntimeError(msg)
self.actions.pop(name) self.request_types.pop(name)
def get_action_tree(self) -> List[List[str]]: def get_request_types_recursively(self) -> List[List[str]]:
"""Recursively generate action tree for this component.""" """Recursively generate request tree for this component."""
actions = [] requests = []
for act_name, act in self.actions.items(): for req_name, req in self.request_types.items():
if isinstance(act.func, ActionManager): if isinstance(req.func, RequestManager):
sub_actions = act.func.get_action_tree() sub_requests = req.func.get_request_types_recursively()
sub_actions = [[act_name] + a for a in sub_actions] sub_requests = [[req_name] + a for a in sub_requests]
actions.extend(sub_actions) requests.extend(sub_requests)
else: else:
actions.append([act_name]) requests.append([req_name])
return actions return requests
class SimComponent(BaseModel): class SimComponent(BaseModel):
@@ -161,30 +158,30 @@ class SimComponent(BaseModel):
if not kwargs.get("uuid"): if not kwargs.get("uuid"):
kwargs["uuid"] = str(uuid4()) kwargs["uuid"] = str(uuid4())
super().__init__(**kwargs) super().__init__(**kwargs)
self._action_manager: ActionManager = self._init_action_manager() self._request_manager: RequestManager = self._init_request_manager()
self._parent: Optional["SimComponent"] = None self._parent: Optional["SimComponent"] = None
def _init_action_manager(self) -> ActionManager: def _init_request_manager(self) -> RequestManager:
""" """
Initialise the action manager for this component. Initialise the request manager for this component.
When using a hierarchy of components, the child classes should call the parent class's _init_action_manager and When using a hierarchy of components, the child classes should call the parent class's _init_request_manager and
add additional actions on top of the existing generic ones. add additional requests on top of the existing generic ones.
Example usage for inherited classes: Example usage for inherited classes:
..code::python ..code::python
class WebBrowser(Application): class WebBrowser(Application):
def _init_action_manager(self) -> ActionManager: def _init_request_manager(self) -> RequestManager:
am = super()._init_action_manager() # all actions generic to any Application get initialised am = super()._init_request_manager() # all requests generic to any Application get initialised
am.add_action(...) # initialise any actions specific to the web browser am.add_request(...) # initialise any requests specific to the web browser
return am return am
:return: Actiona manager object belonging to this sim component. :return: Request manager object belonging to this sim component.
:rtype: ActionManager :rtype: RequestManager
""" """
return ActionManager() return RequestManager()
@abstractmethod @abstractmethod
def describe_state(self) -> Dict: def describe_state(self) -> Dict:
@@ -200,27 +197,27 @@ class SimComponent(BaseModel):
} }
return state return state
def apply_action(self, action: List[str], context: Dict = {}) -> None: def apply_request(self, request: List[str], context: Dict = {}) -> None:
""" """
Apply an action to a simulation component. Action data is passed in as a 'namespaced' list of strings. Apply a request to a simulation component. Request data is passed in as a 'namespaced' list of strings.
If the list only has one element, the action is intended to be applied directly to this object. If the list has If the list only has one element, the request is intended to be applied directly to this object. If the list has
multiple entries, the action is passed to the child of this object specified by the first one or two entries. multiple entries, the request is passed to the child of this object specified by the first one or two entries.
This is essentially a namespace. This is essentially a namespace.
For example, ["turn_on",] is meant to apply an action of 'turn on' to this component. For example, ["turn_on",] is meant to apply a request of 'turn on' to this component.
However, ["services", "email_client", "turn_on"] is meant to 'turn on' this component's email client service. However, ["services", "email_client", "turn_on"] is meant to 'turn on' this component's email client service.
:param action: List describing the action to apply to this object. :param request: List describing the request to apply to this object.
:type action: List[str] :type request: List[str]
:param: context: Dict containing context for actions :param: context: Dict containing context for requests
:type context: Dict :type context: Dict
""" """
if self._action_manager is None: if self._request_manager is None:
return return
self._action_manager(action, context) self._request_manager(request, context)
def apply_timestep(self, timestep: int) -> None: def apply_timestep(self, timestep: int) -> None:
""" """

View File

@@ -1,7 +1,7 @@
from enum import Enum from enum import Enum
from typing import Dict, Final, List, Literal, Tuple from typing import Dict, Final, List, Literal, Tuple
from primaite.simulator.core import Action, ActionManager, ActionPermissionValidator, SimComponent from primaite.simulator.core import RequestManager, RequestPermissionValidator, RequestType, SimComponent
from primaite.simulator.domain.account import Account, AccountType from primaite.simulator.domain.account import Account, AccountType
@@ -43,7 +43,7 @@ class AccountGroup(Enum):
"For full access" "For full access"
class GroupMembershipValidator(ActionPermissionValidator): class GroupMembershipValidator(RequestPermissionValidator):
"""Permit actions based on group membership.""" """Permit actions based on group membership."""
allowed_groups: List[AccountGroup] allowed_groups: List[AccountGroup]
@@ -79,14 +79,14 @@ class DomainController(SimComponent):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
def _init_action_manager(self) -> ActionManager: def _init_request_manager(self) -> RequestManager:
am = super()._init_action_manager() am = super()._init_request_manager()
# Action 'account' matches requests like: # Action 'account' matches requests like:
# ['account', '<account-uuid>', *account_action] # ['account', '<account-uuid>', *account_action]
am.add_action( am.add_request(
"account", "account",
Action( RequestType(
func=lambda request, context: self.accounts[request.pop(0)].apply_action(request, context), func=lambda request, context: self.accounts[request.pop(0)].apply_request(request, context),
validator=GroupMembershipValidator(allowed_groups=[AccountGroup.DOMAIN_ADMIN]), validator=GroupMembershipValidator(allowed_groups=[AccountGroup.DOMAIN_ADMIN]),
), ),
) )

View File

@@ -9,7 +9,7 @@ from typing import Dict, Optional
from prettytable import MARKDOWN, PrettyTable from prettytable import MARKDOWN, PrettyTable
from primaite import getLogger from primaite import getLogger
from primaite.simulator.core import Action, ActionManager, SimComponent from primaite.simulator.core import RequestManager, RequestType, SimComponent
from primaite.simulator.file_system.file_type import FileType, get_file_type_from_extension from primaite.simulator.file_system.file_type import FileType, get_file_type_from_extension
from primaite.simulator.system.core.sys_log import SysLog from primaite.simulator.system.core.sys_log import SysLog
@@ -94,14 +94,14 @@ class FileSystem(SimComponent):
if not self.folders: if not self.folders:
self.create_folder("root") self.create_folder("root")
def _init_action_manager(self) -> ActionManager: def _init_request_manager(self) -> RequestManager:
am = super()._init_action_manager() am = super()._init_request_manager()
self._folder_action_manager = ActionManager() self._folder_request_manager = RequestManager()
am.add_action("folder", Action(func=self._folder_action_manager)) am.add_request("folder", RequestType(func=self._folder_request_manager))
self._file_action_manager = ActionManager() self._file_request_manager = RequestManager()
am.add_action("file", Action(func=self._file_action_manager)) am.add_request("file", RequestType(func=self._file_request_manager))
return am return am
@@ -165,7 +165,7 @@ class FileSystem(SimComponent):
self.folders[folder.uuid] = folder self.folders[folder.uuid] = folder
self._folders_by_name[folder.name] = folder self._folders_by_name[folder.name] = folder
self.sys_log.info(f"Created folder /{folder.name}") self.sys_log.info(f"Created folder /{folder.name}")
self._folder_action_manager.add_action(folder.uuid, Action(func=folder._action_manager)) self._folder_request_manager.add_request(folder.uuid, RequestType(func=folder._request_manager))
return folder return folder
def delete_folder(self, folder_name: str): def delete_folder(self, folder_name: str):
@@ -184,7 +184,7 @@ class FileSystem(SimComponent):
self.folders.pop(folder.uuid) self.folders.pop(folder.uuid)
self._folders_by_name.pop(folder.name) self._folders_by_name.pop(folder.name)
self.sys_log.info(f"Deleted folder /{folder.name} and its contents") self.sys_log.info(f"Deleted folder /{folder.name} and its contents")
self._folder_action_manager.remove_action(folder.uuid) self._folder_request_manager.remove_request(folder.uuid)
else: else:
_LOGGER.debug(f"Cannot delete folder as it does not exist: {folder_name}") _LOGGER.debug(f"Cannot delete folder as it does not exist: {folder_name}")
@@ -226,7 +226,7 @@ class FileSystem(SimComponent):
) )
folder.add_file(file) folder.add_file(file)
self.sys_log.info(f"Created file /{file.path}") self.sys_log.info(f"Created file /{file.path}")
self._file_action_manager.add_action(file.uuid, Action(func=file._action_manager)) self._file_request_manager.add_request(file.uuid, RequestType(func=file._request_manager))
return file return file
def get_file(self, folder_name: str, file_name: str) -> Optional[File]: def get_file(self, folder_name: str, file_name: str) -> Optional[File]:
@@ -254,7 +254,7 @@ class FileSystem(SimComponent):
file = folder.get_file(file_name) file = folder.get_file(file_name)
if file: if file:
folder.remove_file(file) folder.remove_file(file)
self._file_action_manager.remove_action(file.uuid) self._file_request_manager.remove_request(file.uuid)
self.sys_log.info(f"Deleted file /{file.path}") self.sys_log.info(f"Deleted file /{file.path}")
def move_file(self, src_folder_name: str, src_file_name: str, dst_folder_name: str): def move_file(self, src_folder_name: str, src_file_name: str, dst_folder_name: str):
@@ -332,15 +332,15 @@ class Folder(FileSystemItemABC):
is_quarantined: bool = False is_quarantined: bool = False
"Flag that marks the folder as quarantined if true." "Flag that marks the folder as quarantined if true."
def _init_action_manager(self) -> ActionManager: def _init_request_manager(self) -> RequestManager:
am = super()._init_action_manager() am = super()._init_request_manager()
am.add_action("scan", Action(func=lambda request, context: ...)) # TODO implement action am.add_request("scan", RequestType(func=lambda request, context: ...)) # TODO implement request
am.add_action("checkhash", Action(func=lambda request, context: ...)) # TODO implement action am.add_request("checkhash", RequestType(func=lambda request, context: ...)) # TODO implement request
am.add_action("repair", Action(func=lambda request, context: ...)) # TODO implement action am.add_request("repair", RequestType(func=lambda request, context: ...)) # TODO implement request
am.add_action("restore", Action(func=lambda request, context: ...)) # TODO implement action am.add_request("restore", RequestType(func=lambda request, context: ...)) # TODO implement request
am.add_action("delete", Action(func=lambda request, context: ...)) # TODO implement action am.add_request("delete", RequestType(func=lambda request, context: ...)) # TODO implement request
am.add_action("corrupt", Action(func=lambda request, context: ...)) # TODO implement action am.add_request("corrupt", RequestType(func=lambda request, context: ...)) # TODO implement request
return am return am
@@ -509,15 +509,15 @@ class File(FileSystemItemABC):
with open(self.sim_path, mode="a"): with open(self.sim_path, mode="a"):
pass pass
def _init_action_manager(self) -> ActionManager: def _init_request_manager(self) -> RequestManager:
am = super()._init_action_manager() am = super()._init_request_manager()
am.add_action("scan", Action(func=lambda request, context: ...)) # TODO implement action am.add_request("scan", RequestType(func=lambda request, context: ...)) # TODO implement request
am.add_action("checkhash", Action(func=lambda request, context: ...)) # TODO implement action am.add_request("checkhash", RequestType(func=lambda request, context: ...)) # TODO implement request
am.add_action("delete", Action(func=lambda request, context: ...)) # TODO implement action am.add_request("delete", RequestType(func=lambda request, context: ...)) # TODO implement request
am.add_action("repair", Action(func=lambda request, context: ...)) # TODO implement action am.add_request("repair", RequestType(func=lambda request, context: ...)) # TODO implement request
am.add_action("restore", Action(func=lambda request, context: ...)) # TODO implement action am.add_request("restore", RequestType(func=lambda request, context: ...)) # TODO implement request
am.add_action("corrupt", Action(func=lambda request, context: ...)) # TODO implement action am.add_request("corrupt", RequestType(func=lambda request, context: ...)) # TODO implement request
return am return am

View File

@@ -6,7 +6,7 @@ from networkx import MultiGraph
from prettytable import MARKDOWN, PrettyTable from prettytable import MARKDOWN, PrettyTable
from primaite import getLogger from primaite import getLogger
from primaite.simulator.core import Action, ActionManager, SimComponent from primaite.simulator.core import RequestManager, RequestType, SimComponent
from primaite.simulator.network.hardware.base import Link, NIC, Node, SwitchPort from primaite.simulator.network.hardware.base import Link, NIC, Node, SwitchPort
from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.computer import Computer
from primaite.simulator.network.hardware.nodes.router import Router from primaite.simulator.network.hardware.nodes.router import Router
@@ -37,18 +37,18 @@ class Network(SimComponent):
Initialise the network. Initialise the network.
Constructs the network and sets up its initial state including Constructs the network and sets up its initial state including
the action manager and an empty MultiGraph for topology representation. the request manager and an empty MultiGraph for topology representation.
""" """
super().__init__(**kwargs) super().__init__(**kwargs)
self._nx_graph = MultiGraph() self._nx_graph = MultiGraph()
def _init_action_manager(self) -> ActionManager: def _init_request_manager(self) -> RequestManager:
am = super()._init_action_manager() am = super()._init_request_manager()
self._node_action_manager = ActionManager() self._node_request_manager = RequestManager()
am.add_action( am.add_request(
"node", "node",
Action(func=self._node_action_manager), RequestType(func=self._node_request_manager),
) )
return am return am
@@ -182,7 +182,7 @@ class Network(SimComponent):
node.parent = self node.parent = self
self._nx_graph.add_node(node.hostname) self._nx_graph.add_node(node.hostname)
_LOGGER.info(f"Added node {node.uuid} to Network {self.uuid}") _LOGGER.info(f"Added node {node.uuid} to Network {self.uuid}")
self._node_action_manager.add_action(name=node.uuid, action=Action(func=node._action_manager)) self._node_request_manager.add_request(name=node.uuid, request_type=RequestType(func=node._request_manager))
def get_node_by_hostname(self, hostname: str) -> Optional[Node]: def get_node_by_hostname(self, hostname: str) -> Optional[Node]:
""" """
@@ -216,7 +216,7 @@ class Network(SimComponent):
break break
node.parent = None node.parent = None
_LOGGER.info(f"Removed node {node.uuid} from network {self.uuid}") _LOGGER.info(f"Removed node {node.uuid} from network {self.uuid}")
self._node_action_manager.remove_action(name=node.uuid) self._node_request_manager.remove_request(name=node.uuid)
def connect(self, endpoint_a: Union[NIC, SwitchPort], endpoint_b: Union[NIC, SwitchPort], **kwargs) -> None: def connect(self, endpoint_a: Union[NIC, SwitchPort], endpoint_b: Union[NIC, SwitchPort], **kwargs) -> None:
""" """

View File

@@ -12,7 +12,7 @@ from prettytable import MARKDOWN, PrettyTable
from primaite import getLogger from primaite import getLogger
from primaite.exceptions import NetworkError from primaite.exceptions import NetworkError
from primaite.simulator import SIM_OUTPUT from primaite.simulator import SIM_OUTPUT
from primaite.simulator.core import Action, ActionManager, SimComponent from primaite.simulator.core import RequestManager, RequestType, SimComponent
from primaite.simulator.domain.account import Account from primaite.simulator.domain.account import Account
from primaite.simulator.file_system.file_system import FileSystem from primaite.simulator.file_system.file_system import FileSystem
from primaite.simulator.network.protocols.arp import ARPEntry, ARPPacket from primaite.simulator.network.protocols.arp import ARPEntry, ARPPacket
@@ -144,11 +144,11 @@ class NIC(SimComponent):
) )
return state return state
def _init_action_manager(self) -> ActionManager: def _init_request_manager(self) -> RequestManager:
am = super()._init_action_manager() am = super()._init_request_manager()
am.add_action("enable", Action(func=lambda request, context: self.enable())) am.add_request("enable", RequestType(func=lambda request, context: self.enable()))
am.add_action("disable", Action(func=lambda request, context: self.disable())) am.add_request("disable", RequestType(func=lambda request, context: self.disable()))
return am return am
@@ -946,31 +946,31 @@ class Node(SimComponent):
self.session_manager.software_manager = self.software_manager self.session_manager.software_manager = self.software_manager
self._install_system_software() self._install_system_software()
def _init_action_manager(self) -> ActionManager: def _init_request_manager(self) -> RequestManager:
# TODO: I see that this code is really confusing and hard to read right now... I think some of these things will # TODO: I see that this code is really confusing and hard to read right now... I think some of these things will
# need a better name and better documentation. # need a better name and better documentation.
am = super()._init_action_manager() am = super()._init_request_manager()
# since there are potentially many services, create an action manager that can map service name # since there are potentially many services, create an request manager that can map service name
self._service_action_manager = ActionManager() self._service_request_manager = RequestManager()
am.add_action("service", Action(func=self._service_action_manager)) am.add_request("service", RequestType(func=self._service_request_manager))
self._nic_action_manager = ActionManager() self._nic_request_manager = RequestManager()
am.add_action("nic", Action(func=self._nic_action_manager)) am.add_request("nic", RequestType(func=self._nic_request_manager))
am.add_action("file_system", Action(func=self.file_system._action_manager)) am.add_request("file_system", RequestType(func=self.file_system._request_manager))
# currently we don't have any applications nor processes, so these will be empty # currently we don't have any applications nor processes, so these will be empty
self._process_action_manager = ActionManager() self._process_request_manager = RequestManager()
am.add_action("process", Action(func=self._process_action_manager)) am.add_request("process", RequestType(func=self._process_request_manager))
self._application_action_manager = ActionManager() self._application_request_manager = RequestManager()
am.add_action("application", Action(func=self._application_action_manager)) am.add_request("application", RequestType(func=self._application_request_manager))
am.add_action("scan", Action(func=lambda request, context: ...)) # TODO implement OS scan am.add_request("scan", RequestType(func=lambda request, context: ...)) # TODO implement OS scan
am.add_action("shutdown", Action(func=lambda request, context: self.power_off())) am.add_request("shutdown", RequestType(func=lambda request, context: self.power_off()))
am.add_action("startup", Action(func=lambda request, context: self.power_on())) am.add_request("startup", RequestType(func=lambda request, context: self.power_on()))
am.add_action("reset", Action(func=lambda request, context: ...)) # TODO implement node reset am.add_request("reset", RequestType(func=lambda request, context: ...)) # TODO implement node reset
am.add_action("logon", Action(func=lambda request, context: ...)) # TODO implement logon action am.add_request("logon", RequestType(func=lambda request, context: ...)) # TODO implement logon request
am.add_action("logoff", Action(func=lambda request, context: ...)) # TODO implement logoff action am.add_request("logoff", RequestType(func=lambda request, context: ...)) # TODO implement logoff request
return am return am
@@ -1071,7 +1071,7 @@ class Node(SimComponent):
self.sys_log.info(f"Connected NIC {nic}") self.sys_log.info(f"Connected NIC {nic}")
if self.operating_state == NodeOperatingState.ON: if self.operating_state == NodeOperatingState.ON:
nic.enable() nic.enable()
self._nic_action_manager.add_action(nic.uuid, Action(func=nic._action_manager)) self._nic_request_manager.add_request(nic.uuid, RequestType(func=nic._request_manager))
else: else:
msg = f"Cannot connect NIC {nic} as it is already connected" msg = f"Cannot connect NIC {nic} as it is already connected"
self.sys_log.logger.error(msg) self.sys_log.logger.error(msg)
@@ -1096,7 +1096,7 @@ class Node(SimComponent):
nic.parent = None nic.parent = None
nic.disable() nic.disable()
self.sys_log.info(f"Disconnected NIC {nic}") self.sys_log.info(f"Disconnected NIC {nic}")
self._nic_action_manager.remove_action(nic.uuid) self._nic_request_manager.remove_request(nic.uuid)
else: else:
msg = f"Cannot disconnect NIC {nic} as it is not connected" msg = f"Cannot disconnect NIC {nic} as it is not connected"
self.sys_log.logger.error(msg) self.sys_log.logger.error(msg)
@@ -1194,7 +1194,7 @@ class Node(SimComponent):
service.install() # Perform any additional setup, such as creating files for this service on the node. service.install() # Perform any additional setup, such as creating files for this service on the node.
self.sys_log.info(f"Installed service {service.name}") self.sys_log.info(f"Installed service {service.name}")
_LOGGER.info(f"Added service {service.uuid} to node {self.uuid}") _LOGGER.info(f"Added service {service.uuid} to node {self.uuid}")
self._service_action_manager.add_action(service.uuid, Action(func=service._action_manager)) self._service_request_manager.add_request(service.uuid, RequestType(func=service._request_manager))
def uninstall_service(self, service: Service) -> None: def uninstall_service(self, service: Service) -> None:
"""Uninstall and completely remove service from this node. """Uninstall and completely remove service from this node.
@@ -1210,7 +1210,7 @@ class Node(SimComponent):
service.parent = None service.parent = None
self.sys_log.info(f"Uninstalled service {service.name}") self.sys_log.info(f"Uninstalled service {service.name}")
_LOGGER.info(f"Removed service {service.uuid} from node {self.uuid}") _LOGGER.info(f"Removed service {service.uuid} from node {self.uuid}")
self._service_action_manager.remove_action(service.uuid) self._service_request_manager.remove_request(service.uuid)
def __contains__(self, item: Any) -> bool: def __contains__(self, item: Any) -> bool:
if isinstance(item, Service): if isinstance(item, Service):

View File

@@ -7,7 +7,7 @@ from typing import Dict, List, Optional, Tuple, Union
from prettytable import MARKDOWN, PrettyTable from prettytable import MARKDOWN, PrettyTable
from primaite.simulator.core import Action, ActionManager, SimComponent from primaite.simulator.core import RequestManager, RequestType, SimComponent
from primaite.simulator.network.hardware.base import ARPCache, ICMP, NIC, Node from primaite.simulator.network.hardware.base import ARPCache, ICMP, NIC, Node
from primaite.simulator.network.transmission.data_link_layer import EthernetHeader, Frame from primaite.simulator.network.transmission.data_link_layer import EthernetHeader, Frame
from primaite.simulator.network.transmission.network_layer import ICMPPacket, ICMPType, IPPacket, IPProtocol from primaite.simulator.network.transmission.network_layer import ICMPPacket, ICMPType, IPPacket, IPProtocol
@@ -43,7 +43,7 @@ class ACLRule(SimComponent):
def __str__(self) -> str: def __str__(self) -> str:
rule_strings = [] rule_strings = []
for key, value in self.model_dump(exclude={"uuid", "action_manager"}).items(): for key, value in self.model_dump(exclude={"uuid", "request_manager"}).items():
if value is None: if value is None:
value = "ANY" value = "ANY"
if isinstance(value, Enum): if isinstance(value, Enum):
@@ -87,8 +87,8 @@ class AccessControlList(SimComponent):
super().__init__(**kwargs) super().__init__(**kwargs)
self._acl = [None] * (self.max_acl_rules - 1) self._acl = [None] * (self.max_acl_rules - 1)
def _init_action_manager(self) -> ActionManager: def _init_request_manager(self) -> RequestManager:
am = super()._init_action_manager() am = super()._init_request_manager()
# When the request reaches this action, it should now contain solely positional args for the 'add_rule' action. # When the request reaches this action, it should now contain solely positional args for the 'add_rule' action.
# POSITIONAL ARGUMENTS: # POSITIONAL ARGUMENTS:
@@ -99,9 +99,9 @@ class AccessControlList(SimComponent):
# 4: destination ip address (str castable to IPV4Address (e.g. '10.10.1.2')) # 4: destination ip address (str castable to IPV4Address (e.g. '10.10.1.2'))
# 5: destination port (str name of a Port (e.g. "HTTP")) # 5: destination port (str name of a Port (e.g. "HTTP"))
# 6: position (int) # 6: position (int)
am.add_action( am.add_request(
"add_rule", "add_rule",
Action( RequestType(
func=lambda request, context: self.add_rule( func=lambda request, context: self.add_rule(
ACLAction[request[0]], ACLAction[request[0]],
IPProtocol[request[1]], IPProtocol[request[1]],
@@ -114,7 +114,7 @@ class AccessControlList(SimComponent):
), ),
) )
am.add_action("remove_rule", Action(func=lambda request, context: self.remove_rule(int(request[0])))) am.add_request("remove_rule", RequestType(func=lambda request, context: self.remove_rule(int(request[0]))))
return am return am
def describe_state(self) -> Dict: def describe_state(self) -> Dict:
@@ -626,9 +626,9 @@ class Router(Node):
self.arp.nics = self.nics self.arp.nics = self.nics
self.icmp.arp = self.arp self.icmp.arp = self.arp
def _init_action_manager(self) -> ActionManager: def _init_request_manager(self) -> RequestManager:
am = super()._init_action_manager() am = super()._init_request_manager()
am.add_action("acl", Action(func=self.acl._action_manager)) am.add_request("acl", RequestType(func=self.acl._request_manager))
return am return am
def _get_port_of_nic(self, target_nic: NIC) -> Optional[int]: def _get_port_of_nic(self, target_nic: NIC) -> Optional[int]:

View File

@@ -1,6 +1,6 @@
from typing import Dict from typing import Dict
from primaite.simulator.core import Action, ActionManager, AllowAllValidator, SimComponent from primaite.simulator.core import RequestManager, RequestType, SimComponent
from primaite.simulator.domain.controller import DomainController from primaite.simulator.domain.controller import DomainController
from primaite.simulator.network.container import Network from primaite.simulator.network.container import Network
@@ -21,12 +21,12 @@ class Simulation(SimComponent):
super().__init__(**kwargs) super().__init__(**kwargs)
def _init_action_manager(self) -> ActionManager: def _init_request_manager(self) -> RequestManager:
am = super()._init_action_manager() am = super()._init_request_manager()
# pass through network actions to the network objects # pass through network requests to the network objects
am.add_action("network", Action(func=self.network._action_manager)) am.add_request("network", RequestType(func=self.network._request_manager))
# pass through domain actions to the domain object # pass through domain requests to the domain object
am.add_action("domain", Action(func=self.domain._action_manager)) am.add_request("domain", RequestType(func=self.domain._request_manager))
return am return am
def describe_state(self) -> Dict: def describe_state(self) -> Dict:

View File

@@ -2,7 +2,7 @@ from enum import Enum
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from primaite import getLogger from primaite import getLogger
from primaite.simulator.core import Action, ActionManager from primaite.simulator.core import RequestManager, RequestType
from primaite.simulator.system.software import IOSoftware from primaite.simulator.system.software import IOSoftware
_LOGGER = getLogger(__name__) _LOGGER = getLogger(__name__)
@@ -39,15 +39,15 @@ class Service(IOSoftware):
_restart_countdown: Optional[int] = None _restart_countdown: Optional[int] = None
"If currently restarting, how many timesteps remain until the restart is finished." "If currently restarting, how many timesteps remain until the restart is finished."
def _init_action_manager(self) -> ActionManager: def _init_request_manager(self) -> RequestManager:
am = super()._init_action_manager() am = super()._init_request_manager()
am.add_action("stop", Action(func=lambda request, context: self.stop())) am.add_request("stop", RequestType(func=lambda request, context: self.stop()))
am.add_action("start", Action(func=lambda request, context: self.start())) am.add_request("start", RequestType(func=lambda request, context: self.start()))
am.add_action("pause", Action(func=lambda request, context: self.pause())) am.add_request("pause", RequestType(func=lambda request, context: self.pause()))
am.add_action("resume", Action(func=lambda request, context: self.resume())) am.add_request("resume", RequestType(func=lambda request, context: self.resume()))
am.add_action("restart", Action(func=lambda request, context: self.restart())) am.add_request("restart", RequestType(func=lambda request, context: self.restart()))
am.add_action("disable", Action(func=lambda request, context: self.disable())) am.add_request("disable", RequestType(func=lambda request, context: self.disable()))
am.add_action("enable", Action(func=lambda request, context: self.enable())) am.add_request("enable", RequestType(func=lambda request, context: self.enable()))
return am return am
def describe_state(self) -> Dict: def describe_state(self) -> Dict:

View File

@@ -2,7 +2,7 @@ from abc import abstractmethod
from enum import Enum from enum import Enum
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from primaite.simulator.core import Action, ActionManager, SimComponent from primaite.simulator.core import RequestManager, RequestType, SimComponent
from primaite.simulator.file_system.file_system import FileSystem, Folder from primaite.simulator.file_system.file_system import FileSystem, Folder
from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.network.transmission.transport_layer import Port
from primaite.simulator.system.core.sys_log import SysLog from primaite.simulator.system.core.sys_log import SysLog
@@ -85,15 +85,15 @@ class Software(SimComponent):
folder: Optional[Folder] = None folder: Optional[Folder] = None
"The folder on the file system the Software uses." "The folder on the file system the Software uses."
def _init_action_manager(self) -> ActionManager: def _init_request_manager(self) -> RequestManager:
am = super()._init_action_manager() am = super()._init_request_manager()
am.add_action( am.add_request(
"compromise", "compromise",
Action( RequestType(
func=lambda request, context: self.set_health_state(SoftwareHealthState.COMPROMISED), func=lambda request, context: self.set_health_state(SoftwareHealthState.COMPROMISED),
), ),
) )
am.add_action("scan", Action(func=lambda request, context: self.scan())) am.add_request("scan", RequestType(func=lambda request, context: self.scan()))
return am return am
@abstractmethod @abstractmethod

View File

@@ -1,6 +1,6 @@
import pytest import pytest
from primaite.simulator.core import Action from primaite.simulator.core import RequestType
from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.computer import Computer
from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.network.hardware.nodes.server import Server
from primaite.simulator.network.hardware.nodes.switch import Switch from primaite.simulator.network.hardware.nodes.switch import Switch
@@ -32,7 +32,7 @@ def test_passing_actions_down(monkeypatch) -> None:
sim.network.connect(s1.switch_ports[3], srv.ethernet_port[1]) sim.network.connect(s1.switch_ports[3], srv.ethernet_port[1])
# call this method to make sure no errors occur. # call this method to make sure no errors occur.
sim._action_manager.get_action_tree() sim._request_manager.get_request_types_recursively()
# patch the action to do something which we can check the result of. # patch the action to do something which we can check the result of.
action_invoked = False action_invoked = False
@@ -42,13 +42,13 @@ def test_passing_actions_down(monkeypatch) -> None:
action_invoked = True action_invoked = True
monkeypatch.setitem( monkeypatch.setitem(
downloads_folder._action_manager.actions, "repair", Action(func=lambda request, context: succeed()) downloads_folder._request_manager.request_types, "repair", RequestType(func=lambda request, context: succeed())
) )
assert not action_invoked assert not action_invoked
# call the patched method # call the patched method
sim.apply_action( sim.apply_request(
["network", "node", pc1.uuid, "file_system", "folder", pc1.file_system.get_folder("downloads").uuid, "repair"] ["network", "node", pc1.uuid, "file_system", "folder", pc1.file_system.get_folder("downloads").uuid, "repair"]
) )

View File

@@ -3,7 +3,7 @@ from typing import Dict, List, Literal
import pytest import pytest
from primaite.simulator.core import Action, ActionManager, AllowAllValidator, SimComponent from primaite.simulator.core import AllowAllValidator, RequestManager, RequestType, SimComponent
from primaite.simulator.domain.controller import AccountGroup, GroupMembershipValidator from primaite.simulator.domain.controller import AccountGroup, GroupMembershipValidator
@@ -29,11 +29,11 @@ def test_group_action_validation() -> None:
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self._action_manager = ActionManager() self._request_manager = RequestManager()
self._action_manager.add_action( self._request_manager.add_request(
"create_folder", "create_folder",
Action( RequestType(
func=lambda request, context: self.create_folder(request[0]), func=lambda request, context: self.create_folder(request[0]),
validator=GroupMembershipValidator([AccountGroup.LOCAL_ADMIN, AccountGroup.DOMAIN_ADMIN]), validator=GroupMembershipValidator([AccountGroup.LOCAL_ADMIN, AccountGroup.DOMAIN_ADMIN]),
), ),
@@ -52,13 +52,13 @@ def test_group_action_validation() -> None:
# check that the folder is created when a local admin tried to do it # check that the folder is created when a local admin tried to do it
permitted_context = {"request_source": {"agent": "BLUE", "account": "User1", "groups": ["LOCAL_ADMIN"]}} permitted_context = {"request_source": {"agent": "BLUE", "account": "User1", "groups": ["LOCAL_ADMIN"]}}
my_node = Node(uuid="0000-0000-1234", name="pc") my_node = Node(uuid="0000-0000-1234", name="pc")
my_node.apply_action(["create_folder", "memes"], context=permitted_context) my_node.apply_request(["create_folder", "memes"], context=permitted_context)
assert len(my_node.folders) == 1 assert len(my_node.folders) == 1
assert my_node.folders[0].name == "memes" assert my_node.folders[0].name == "memes"
# check that the number of folders is still 1 even after attempting to create a second one without permissions # check that the number of folders is still 1 even after attempting to create a second one without permissions
invalid_context = {"request_source": {"agent": "BLUE", "account": "User1", "groups": ["LOCAL_USER", "DOMAIN_USER"]}} invalid_context = {"request_source": {"agent": "BLUE", "account": "User1", "groups": ["LOCAL_USER", "DOMAIN_USER"]}}
my_node.apply_action(["create_folder", "memes2"], context=invalid_context) my_node.apply_request(["create_folder", "memes2"], context=invalid_context)
assert len(my_node.folders) == 1 assert len(my_node.folders) == 1
assert my_node.folders[0].name == "memes" assert my_node.folders[0].name == "memes"
@@ -79,32 +79,32 @@ def test_hierarchical_action_with_validation() -> None:
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.action_manager = ActionManager() self.request_manager = RequestManager()
self.action_manager.add_action( self.request_manager.add_request(
"turn_on", "turn_on",
Action( RequestType(
func=lambda request, context: self.turn_on(), func=lambda request, context: self.turn_on(),
validator=AllowAllValidator(), validator=AllowAllValidator(),
), ),
) )
self.action_manager.add_action( self.request_manager.add_request(
"turn_off", "turn_off",
Action( RequestType(
func=lambda request, context: self.turn_off(), func=lambda request, context: self.turn_off(),
validator=AllowAllValidator(), validator=AllowAllValidator(),
), ),
) )
self.action_manager.add_action( self.request_manager.add_request(
"disable", "disable",
Action( RequestType(
func=lambda request, context: self.disable(), func=lambda request, context: self.disable(),
validator=GroupMembershipValidator([AccountGroup.LOCAL_ADMIN, AccountGroup.DOMAIN_ADMIN]), validator=GroupMembershipValidator([AccountGroup.LOCAL_ADMIN, AccountGroup.DOMAIN_ADMIN]),
), ),
) )
self.action_manager.add_action( self.request_manager.add_request(
"enable", "enable",
Action( RequestType(
func=lambda request, context: self.enable(), func=lambda request, context: self.enable(),
validator=GroupMembershipValidator([AccountGroup.LOCAL_ADMIN, AccountGroup.DOMAIN_ADMIN]), validator=GroupMembershipValidator([AccountGroup.LOCAL_ADMIN, AccountGroup.DOMAIN_ADMIN]),
), ),
@@ -135,11 +135,11 @@ def test_hierarchical_action_with_validation() -> None:
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.action_manager = ActionManager() self.request_manager = RequestManager()
self.action_manager.add_action( self.request_manager.add_request(
"apps", "apps",
Action( RequestType(
func=lambda request, context: self.send_action_to_app(request.pop(0), request, context), func=lambda request, context: self.send_action_to_app(request.pop(0), request, context),
validator=AllowAllValidator(), validator=AllowAllValidator(),
), ),
@@ -155,7 +155,7 @@ def test_hierarchical_action_with_validation() -> None:
def send_action_to_app(self, app_name: str, options: List[str], context: Dict): def send_action_to_app(self, app_name: str, options: List[str], context: Dict):
for app in self.apps: for app in self.apps:
if app_name == app.name: if app_name == app.name:
app.apply_action(options, context) app.apply_request(options, context)
break break
else: else:
msg = f"Node has no app with name {app_name}" msg = f"Node has no app with name {app_name}"
@@ -178,15 +178,15 @@ def test_hierarchical_action_with_validation() -> None:
} }
# check that a non-admin can't disable this app # check that a non-admin can't disable this app
my_node.apply_action(["apps", "Chrome", "disable"], non_admin_context) my_node.apply_request(["apps", "Chrome", "disable"], non_admin_context)
assert my_node.apps[0].name == "Chrome" # if failure occurs on this line, the test itself is broken assert my_node.apps[0].name == "Chrome" # if failure occurs on this line, the test itself is broken
assert my_node.apps[0].state == "off" assert my_node.apps[0].state == "off"
# check that a non-admin can turn this app on # check that a non-admin can turn this app on
my_node.apply_action(["apps", "Firefox", "turn_on"], non_admin_context) my_node.apply_request(["apps", "Firefox", "turn_on"], non_admin_context)
assert my_node.apps[1].name == "Firefox" # if failure occurs on this line, the test itself is broken assert my_node.apps[1].name == "Firefox" # if failure occurs on this line, the test itself is broken
assert my_node.apps[1].state == "on" assert my_node.apps[1].state == "on"
# check that an admin can disable this app # check that an admin can disable this app
my_node.apply_action(["apps", "Chrome", "disable"], admin_context) my_node.apply_request(["apps", "Chrome", "disable"], admin_context)
assert my_node.apps[0].state == "disabled" assert my_node.apps[0].state == "disabled"

View File

@@ -13,6 +13,6 @@ def test_account_deserialise():
"""Test that an account can be deserialised. The test fails if pydantic throws an error.""" """Test that an account can be deserialised. The test fails if pydantic throws an error."""
acct_json = ( acct_json = (
'{"uuid":"dfb2bcaa-d3a1-48fd-af3f-c943354622b4","num_logons":0,"num_logoffs":0,"num_group_changes":0,' '{"uuid":"dfb2bcaa-d3a1-48fd-af3f-c943354622b4","num_logons":0,"num_logoffs":0,"num_group_changes":0,'
'"username":"Jake","password":"JakePass1!","account_type":2,"status":2,"action_manager":null}' '"username":"Jake","password":"JakePass1!","account_type":2,"status":2,"request_manager":null}'
) )
acct = Account.model_validate_json(acct_json) acct = Account.model_validate_json(acct_json)