Island: Simplify Credentials

Storing a sequence of identities and secrets in Credentials objects
added a lot of complication. From now on, a Credentials object consists
of one identity and one secret. See #2072.
This commit is contained in:
Mike Salvatore 2022-07-14 13:10:24 -04:00
parent 8e332e5285
commit 2b245b34cb
3 changed files with 67 additions and 83 deletions

View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Mapping, MutableMapping, Sequence, Tuple from typing import Any, Mapping, MutableMapping, Sequence
from marshmallow import Schema, fields, post_load, pre_dump from marshmallow import Schema, fields, post_load, pre_dump
from marshmallow.exceptions import MarshmallowError from marshmallow.exceptions import MarshmallowError
@ -42,27 +42,15 @@ CREDENTIAL_COMPONENT_TYPE_TO_CLASS_SCHEMA = {
class CredentialsSchema(Schema): class CredentialsSchema(Schema):
# Use fields.List instead of fields.Tuple because marshmallow requires fields.Tuple to have a identity = fields.Mapping()
# fixed length. secret = fields.Mapping()
identities = fields.List(fields.Mapping())
secrets = fields.List(fields.Mapping())
@post_load @post_load
def _make_credentials( def _make_credentials(
self, data: MutableMapping, **kwargs: Mapping[str, Any] self, data: MutableMapping, **kwargs: Mapping[str, Any]
) -> Mapping[str, Sequence[Mapping[str, Any]]]: ) -> Mapping[str, Sequence[Mapping[str, Any]]]:
data["identities"] = tuple( data["identity"] = CredentialsSchema._build_credential_component(data["identity"])
[ data["secret"] = CredentialsSchema._build_credential_component(data["secret"])
CredentialsSchema._build_credential_component(component)
for component in data["identities"]
]
)
data["secrets"] = tuple(
[
CredentialsSchema._build_credential_component(component)
for component in data["secrets"]
]
)
return data return data
@ -89,18 +77,8 @@ class CredentialsSchema(Schema):
) -> Mapping[str, Sequence[Mapping[str, Any]]]: ) -> Mapping[str, Sequence[Mapping[str, Any]]]:
data = {} data = {}
data["identities"] = tuple( data["identity"] = CredentialsSchema._serialize_credential_component(credentials.identity)
[ data["secret"] = CredentialsSchema._serialize_credential_component(credentials.secret)
CredentialsSchema._serialize_credential_component(component)
for component in credentials.identities
]
)
data["secrets"] = tuple(
[
CredentialsSchema._serialize_credential_component(component)
for component in credentials.secrets
]
)
return data return data
@ -117,8 +95,8 @@ class CredentialsSchema(Schema):
@dataclass(frozen=True) @dataclass(frozen=True)
class Credentials(IJSONSerializable): class Credentials(IJSONSerializable):
identities: Tuple[ICredentialComponent] identity: ICredentialComponent
secrets: Tuple[ICredentialComponent] secret: ICredentialComponent
@staticmethod @staticmethod
def from_mapping(credentials: Mapping) -> Credentials: def from_mapping(credentials: Mapping) -> Credentials:

View File

@ -6,21 +6,8 @@ nt_hash = "C1C58F96CDF212B50837BC11A00BE47C"
lm_hash = "299BD128C1101FD6299BD128C1101FD6" lm_hash = "299BD128C1101FD6299BD128C1101FD6"
password_1 = "trytostealthis" password_1 = "trytostealthis"
password_2 = "password" password_2 = "password"
password_3 = "12345678"
PROPAGATION_CREDENTIALS_1 = Credentials( PROPAGATION_CREDENTIALS_1 = Credentials(identity=Username(username), secret=Password(password_1))
identities=(Username(username),), PROPAGATION_CREDENTIALS_2 = Credentials(identity=Username(special_username), secret=LMHash(lm_hash))
secrets=(NTHash(nt_hash), LMHash(lm_hash), Password(password_1)), PROPAGATION_CREDENTIALS_3 = Credentials(identity=Username(username), secret=NTHash(nt_hash))
) PROPAGATION_CREDENTIALS_4 = Credentials(identity=Username(username), secret=Password(password_2))
PROPAGATION_CREDENTIALS_2 = Credentials(
identities=(Username(username), Username(special_username)),
secrets=(Password(password_1), Password(password_2), Password(password_3)),
)
PROPAGATION_CREDENTIALS_3 = Credentials(
identities=(Username(username),),
secrets=(Password(password_1),),
)
PROPAGATION_CREDENTIALS_4 = Credentials(
identities=(Username(username),),
secrets=(Password(password_2),),
)

View File

@ -1,3 +1,4 @@
import copy
import json import json
import pytest import pytest
@ -14,7 +15,6 @@ from common.credentials import (
) )
USER1 = "test_user_1" USER1 = "test_user_1"
USER2 = "test_user_2"
PASSWORD = "12435" PASSWORD = "12435"
LM_HASH = "AEBD4DE384C7EC43AAD3B435B51404EE" LM_HASH = "AEBD4DE384C7EC43AAD3B435B51404EE"
NT_HASH = "7A21990FCD3D759941E45C490F143D5F" NT_HASH = "7A21990FCD3D759941E45C490F143D5F"
@ -22,74 +22,93 @@ PUBLIC_KEY = "MY_PUBLIC_KEY"
PRIVATE_KEY = "MY_PRIVATE_KEY" PRIVATE_KEY = "MY_PRIVATE_KEY"
CREDENTIALS_DICT = { CREDENTIALS_DICT = {
"identities": [ "identity": {"credential_type": "USERNAME", "username": USER1},
{"credential_type": "USERNAME", "username": USER1}, "secret": {},
{"credential_type": "USERNAME", "username": USER2},
],
"secrets": [
{"credential_type": "PASSWORD", "password": PASSWORD},
{"credential_type": "LM_HASH", "lm_hash": LM_HASH},
{"credential_type": "NT_HASH", "nt_hash": NT_HASH},
{
"credential_type": "SSH_KEYPAIR",
"public_key": PUBLIC_KEY,
"private_key": PRIVATE_KEY,
},
],
} }
CREDENTIALS_JSON = json.dumps(CREDENTIALS_DICT) IDENTITY = Username(USER1)
IDENTITIES = (Username(USER1), Username(USER2))
SECRETS = ( SECRETS = (
Password(PASSWORD), Password(PASSWORD),
LMHash(LM_HASH), LMHash(LM_HASH),
NTHash(NT_HASH), NTHash(NT_HASH),
SSHKeypair(PRIVATE_KEY, PUBLIC_KEY), SSHKeypair(PRIVATE_KEY, PUBLIC_KEY),
) )
CREDENTIALS_OBJECT = Credentials(IDENTITIES, SECRETS)
SECRETS_DICTS = [
{"credential_type": "PASSWORD", "password": PASSWORD},
{"credential_type": "LM_HASH", "lm_hash": LM_HASH},
{"credential_type": "NT_HASH", "nt_hash": NT_HASH},
{
"credential_type": "SSH_KEYPAIR",
"public_key": PUBLIC_KEY,
"private_key": PRIVATE_KEY,
},
]
def test_credentials_serialization_json(): @pytest.mark.parametrize("secret, expected_secret", zip(SECRETS, SECRETS_DICTS))
serialized_credentials = Credentials.to_json(CREDENTIALS_OBJECT) def test_credentials_serialization_json(secret, expected_secret):
expected_credentials = copy.copy(CREDENTIALS_DICT)
expected_credentials["secret"] = expected_secret
c = Credentials(IDENTITY, secret)
assert json.loads(serialized_credentials) == CREDENTIALS_DICT serialized_credentials = Credentials.to_json(c)
assert json.loads(serialized_credentials) == expected_credentials
def test_credentials_serialization_mapping(): @pytest.mark.parametrize("secret, expected_secret", zip(SECRETS, SECRETS_DICTS))
serialized_credentials = Credentials.to_mapping(CREDENTIALS_OBJECT) def test_credentials_serialization_mapping(secret, expected_secret):
expected_credentials = copy.copy(CREDENTIALS_DICT)
expected_credentials["secret"] = expected_secret
c = Credentials(IDENTITY, secret)
assert serialized_credentials == CREDENTIALS_DICT serialized_credentials = Credentials.to_mapping(c)
assert serialized_credentials == expected_credentials
def test_credentials_deserialization__from_mapping(): @pytest.mark.parametrize("secret, secret_dict", zip(SECRETS, SECRETS_DICTS))
deserialized_credentials = Credentials.from_mapping(CREDENTIALS_DICT) def test_credentials_deserialization__from_mapping(secret, secret_dict):
expected_credentials = Credentials(IDENTITY, secret)
credentials_dict = copy.copy(CREDENTIALS_DICT)
credentials_dict["secret"] = secret_dict
assert deserialized_credentials == CREDENTIALS_OBJECT deserialized_credentials = Credentials.from_mapping(credentials_dict)
assert deserialized_credentials == expected_credentials
def test_credentials_deserialization__from_json(): @pytest.mark.parametrize("secret, secret_dict", zip(SECRETS, SECRETS_DICTS))
deserialized_credentials = Credentials.from_json(CREDENTIALS_JSON) def test_credentials_deserialization__from_json(secret, secret_dict):
expected_credentials = Credentials(IDENTITY, secret)
credentials_dict = copy.copy(CREDENTIALS_DICT)
credentials_dict["secret"] = secret_dict
assert deserialized_credentials == CREDENTIALS_OBJECT deserialized_credentials = Credentials.from_json(json.dumps(credentials_dict))
assert deserialized_credentials == expected_credentials
def test_credentials_deserialization__invalid_credentials(): def test_credentials_deserialization__invalid_credentials():
invalid_data = {"secrets": [], "unknown_key": []} invalid_data = {"secret": SECRETS_DICTS[0], "unknown_key": []}
with pytest.raises(InvalidCredentialsError): with pytest.raises(InvalidCredentialsError):
Credentials.from_mapping(invalid_data) Credentials.from_mapping(invalid_data)
def test_credentials_deserialization__invalid_component_type(): def test_credentials_deserialization__invalid_component_type():
invalid_data = {"secrets": [], "identities": [{"credential_type": "FAKE", "username": "user1"}]} invalid_data = {
"secret": SECRETS_DICTS[0],
"identity": {"credential_type": "FAKE", "username": "user1"},
}
with pytest.raises(InvalidCredentialsError): with pytest.raises(InvalidCredentialsError):
Credentials.from_mapping(invalid_data) Credentials.from_mapping(invalid_data)
def test_credentials_deserialization__invalid_component(): def test_credentials_deserialization__invalid_component():
invalid_data = { invalid_data = {
"secrets": [], "secret": SECRETS_DICTS[0],
"identities": [{"credential_type": "USERNAME", "unknown_field": "user1"}], "identity": {"credential_type": "USERNAME", "unknown_field": "user1"},
} }
with pytest.raises(InvalidCredentialComponentError): with pytest.raises(InvalidCredentialComponentError):
Credentials.from_mapping(invalid_data) Credentials.from_mapping(invalid_data)