diff --git a/monkey/common/credentials/credentials.py b/monkey/common/credentials/credentials.py index e04be27a5..846137af1 100644 --- a/monkey/common/credentials/credentials.py +++ b/monkey/common/credentials/credentials.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json from dataclasses import dataclass from typing import Any, Mapping, MutableMapping, Sequence, Tuple @@ -121,6 +122,17 @@ class Credentials: @staticmethod def from_mapping(credentials: Mapping) -> Credentials: + """ + Construct a Credentials object from a Mapping + + :param credentials: A mapping that represents a Credentials object + :return: A Credentials object + :raises InvalidCredentialsError: If the provided Mapping does not represent a valid + Credentials object + :raises InvalidCredentialComponentError: If any of the contents of `identities` or `secrets` + are not a valid ICredentialComponent + """ + try: deserialized_data = CredentialsSchema().load(credentials) return Credentials(**deserialized_data) @@ -131,6 +143,17 @@ class Credentials: @staticmethod def from_json(credentials: str) -> Credentials: + """ + Construct a Credentials object from a JSON string + + :param credentials: A JSON string that represents a Credentials object + :return: A Credentials object + :raises InvalidCredentialsError: If the provided JSON does not represent a valid + Credentials object + :raises InvalidCredentialComponentError: If any of the contents of `identities` or `secrets` + are not a valid ICredentialComponent + """ + try: deserialized_data = CredentialsSchema().loads(credentials) return Credentials(**deserialized_data) @@ -139,6 +162,41 @@ class Credentials: except MarshmallowError as err: raise InvalidCredentialsError(str(err)) + @staticmethod + def from_json_array(credentials_array_json: str) -> Sequence[Credentials]: + """ + Construct a sequence of Credentials object from a JSON string + + :param credentials: A JSON string that represents an array of Credentials objects + :return: A Sequence of Credentials objects + :raises InvalidCredentialsError: If the provided JSON does not represent a valid + Credentials object + :raises InvalidCredentialComponentError: If any of the contents of `identities` or `secrets` + are not a valid ICredentialComponent + :raises JSONDecodeError: If the provided string is not valid JSON + :raises TypeError: If the provided JSON does not represent an array + """ + + credentials_list = json.loads(credentials_array_json) + return [Credentials.from_mapping(c) for c in credentials_list] + @staticmethod def to_json(credentials: Credentials) -> str: + """ + Serialize a Credentials object to JSON + + :param credentials: A Credentials object + :return: A JSON string representing a Credentials object + """ + return CredentialsSchema().dumps(credentials) + + @staticmethod + def to_json_array(credentials: Sequence[Credentials]) -> str: + """ + Serialize a Sequence of Credentials objects to a JSON array + + :param credentials: A Sequence of Credentials objects + :return: A JSON string representing an array of Credentials objects + """ + return "[" + ",".join([Credentials.to_json(c) for c in credentials]) + "]" diff --git a/monkey/tests/unit_tests/common/credentials/test_credentials.py b/monkey/tests/unit_tests/common/credentials/test_credentials.py index c68e1c813..def11896b 100644 --- a/monkey/tests/unit_tests/common/credentials/test_credentials.py +++ b/monkey/tests/unit_tests/common/credentials/test_credentials.py @@ -87,3 +87,35 @@ def test_credentials_deserialization__invalid_component(): } with pytest.raises(InvalidCredentialComponentError): Credentials.from_mapping(invalid_data) + + +DESERIALIZED_CREDENTIALS_0 = Credentials.from_mapping(CREDENTIALS_DICT) +DESERIALIZED_CREDENTIALS_1 = Credentials( + secrets=(Password(PASSWORD),), identities=(Username("STUPID"),) +) +DESERIALIZED_CREDENTIALS_2 = Credentials(secrets=(LMHash(LM_HASH),), identities=tuple()) +credentials_array_json = ( + f"[{Credentials.to_json(DESERIALIZED_CREDENTIALS_0)}," + f"{Credentials.to_json(DESERIALIZED_CREDENTIALS_1)}," + f"{Credentials.to_json(DESERIALIZED_CREDENTIALS_2)}]" +) + + +def test_from_json_array(): + credentials_sequence = Credentials.from_json_array(credentials_array_json) + + assert len(credentials_sequence) == 3 + assert credentials_sequence[0] == DESERIALIZED_CREDENTIALS_0 + assert credentials_sequence[1] == DESERIALIZED_CREDENTIALS_1 + assert credentials_sequence[2] == DESERIALIZED_CREDENTIALS_2 + + +def test_to_json_array(): + expected_credentials_dict = json.loads(credentials_array_json) + actual_credentials_dict = json.loads( + Credentials.to_json_array( + [DESERIALIZED_CREDENTIALS_0, DESERIALIZED_CREDENTIALS_1, DESERIALIZED_CREDENTIALS_2] + ) + ) + + assert actual_credentials_dict == expected_credentials_dict