Skip to content

Commit

Permalink
add default values to the user inputs of credentials
Browse files Browse the repository at this point in the history
  • Loading branch information
hsong-rh committed Jan 14, 2025
1 parent 7a38086 commit a995d75
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 30 deletions.
4 changes: 4 additions & 0 deletions src/aap_eda/core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ class DuplicateFileTemplateKeyError(Exception):

class DuplicateEnvKeyError(Exception):
pass


class InvalidEnvKeyError(Exception):
pass
16 changes: 16 additions & 0 deletions src/aap_eda/core/utils/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

if typing.TYPE_CHECKING:
from aap_eda.core import models

from aap_eda.core.utils.awx import validate_ssh_private_key

ENCRYPTED_STRING = "$encrypted$"
Expand Down Expand Up @@ -563,3 +564,18 @@ def _add_file_template_keys(context: dict, files: dict):
context["eda"]["filename"][parts[1]] = ""
else:
context["eda"] = {"filename": {parts[1]: ""}}


def add_default_values_to_user_inputs(schema: dict, inputs: dict) -> dict:
for field in schema.get("fields", []):
key = field.get("id")
field_type = field.get("type", "string")
default_value = field.get("default")

if key not in inputs:
if field_type == "string":
inputs[key] = default_value or ""
if field_type == "boolean":
inputs[key] = default_value or False

return inputs
82 changes: 52 additions & 30 deletions src/aap_eda/wsapi/consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@
from aap_eda.core.exceptions import (
DuplicateEnvKeyError,
DuplicateFileTemplateKeyError,
InvalidEnvKeyError,
)
from aap_eda.core.models.activation import ActivationStatus
from aap_eda.core.utils.credentials import get_secret_fields
from aap_eda.core.utils.credentials import (
add_default_values_to_user_inputs,
get_secret_fields,
)
from aap_eda.core.utils.strings import extract_variables, substitute_variables
from aap_eda.tasks import orchestrator

Expand Down Expand Up @@ -118,6 +122,8 @@ async def receive(self, text_data=None, bytes_data=None):
logger.warning(f"Unsupported message received: {data}")
except DatabaseError as err:
logger.error(f"Failed to parse {data} due to DB error: {err}")
except InvalidEnvKeyError as err:
logger.error(f"Failed to parse {data} due to Env error: {err}")

async def handle_workers(self, message: WorkerMessage):
logger.info(f"Start to handle workers: {message}")
Expand Down Expand Up @@ -531,39 +537,41 @@ def get_file_contents_from_credentials(
def get_env_vars_from_credentials(
self, activation: models.Activation
) -> tp.Optional[str]:
vault_password, vault_id = self.get_vault_password_and_id(activation)
env_vars = {}

for eda_credential in activation.eda_credentials.all():
schema_inputs = eda_credential.credential_type.inputs
secret_fields = get_secret_fields(schema_inputs)
injectors = eda_credential.credential_type.injectors
user_inputs = yaml.safe_load(
eda_credential.inputs.get_secret_value()
try:
vault_password, vault_id = self.get_vault_password_and_id(
activation
)
if "env" not in injectors:
continue

if secret_fields:
self.encrypt_user_inputs(
secret_fields=secret_fields,
user_inputs=user_inputs,
password=vault_password,
vault_id=vault_id,
)
env_vars = {}

for key, value in injectors["env"].items():
if key in env_vars:
raise DuplicateEnvKeyError(f"env {key} already exists")
env_vars[key] = (
value
if not isinstance(value, str) or "eda.filename" in value
else substitute_variables(value, user_inputs)
for eda_credential in activation.eda_credentials.all():
injectors = eda_credential.credential_type.injectors
if "env" not in injectors:
continue

schema_inputs = eda_credential.credential_type.inputs
secret_fields = get_secret_fields(schema_inputs)
user_inputs = yaml.safe_load(
eda_credential.inputs.get_secret_value()
)

if not env_vars:
return None
return yaml.dump(env_vars)
add_default_values_to_user_inputs(schema_inputs, user_inputs)

if secret_fields:
self.encrypt_user_inputs(
secret_fields=secret_fields,
user_inputs=user_inputs,
password=vault_password,
vault_id=vault_id,
)

self.substitute_envs(env_vars, injectors, user_inputs)

if not env_vars:
return None

return yaml.dump(env_vars)
except TypeError as err:
raise InvalidEnvKeyError(str(err)) from err

@staticmethod
def encrypt_user_inputs(
Expand All @@ -580,6 +588,20 @@ def encrypt_user_inputs(
vault_id=vault_id,
)

@staticmethod
def substitute_envs(
envs: dict, injectors: dict, user_inputs: dict
) -> None:
for key, value in injectors["env"].items():
if key in envs:
raise DuplicateEnvKeyError(f"env {key} already exists")

envs[key] = (
value
if not isinstance(value, str) or "eda.filename" in value
else substitute_variables(value, user_inputs)
)

@staticmethod
def get_vault_password_and_id(
activation: models.Activation,
Expand Down
64 changes: 64 additions & 0 deletions tests/integration/wsapi/test_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,7 @@ def get_job_instance_event_count():
def _prepare_activation_instance_with_credentials(
default_organization: models.Organization,
credentials: list[models.EdaCredential],
system_vault_credential: models.EdaCredential = None,
):
project, _ = models.Project.objects.get_or_create(
name="test-project",
Expand Down Expand Up @@ -774,6 +775,7 @@ def _prepare_activation_instance_with_credentials(
project=project,
user=user,
decision_environment=decision_environment,
eda_system_vault_credential=system_vault_credential,
organization=default_organization,
)
activation.eda_credentials.add(*credentials)
Expand Down Expand Up @@ -1237,6 +1239,41 @@ async def test_handle_workers_with_file_contents(
assert response[key] == value


@pytest.mark.django_db(transaction=True)
async def test_handle_workers_with_env_vars(
ws_communicator: WebsocketCommunicator,
preseed_credential_types,
default_organization: models.Organization,
):
eda_credential = await _prepare_aap_credential_async(default_organization)
system_credential = await _prepare_system_vault_credential_async(
default_organization
)
rulebook_process_id = await _prepare_activation_instance_with_credentials(
default_organization,
[eda_credential],
system_credential,
)

payload = {
"type": "Worker",
"activation_id": rulebook_process_id,
}
await ws_communicator.send_json_to(payload)

for type in [
"Rulebook",
"ControllerInfo",
"VaultCollection",
"EnvVars",
"EndOfResponse",
]:
response = await ws_communicator.receive_json_from(timeout=TIMEOUT)
assert response["type"] == type
if type == "EnvVars":
assert response["data"].startswith("Q09OVFJPTExFUl9IT1NUOiBodHRwc")


@database_sync_to_async
def _prepare_credential(
credential_type_inputs: dict,
Expand All @@ -1259,6 +1296,33 @@ def _prepare_credential(
)


@database_sync_to_async
def _prepare_aap_credential_async(
organization: models.Organization,
):
return _prepare_aap_credential(organization)


def _prepare_aap_credential(
organization: models.Organization,
) -> models.EdaCredential:
aap_credential_type = models.CredentialType.objects.get(
name=enums.DefaultCredentialType.AAP
)

data = "secret"
return models.EdaCredential.objects.create(
name="eda_aap_credential",
inputs={
"host": "https://controller_url/",
"username": "adam",
"password": data,
},
credential_type=aap_credential_type,
organization=organization,
)


@database_sync_to_async
def _prepare_system_vault_credential_async(
organization: models.Organization,
Expand Down
66 changes: 66 additions & 0 deletions tests/unit/test_credential_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from aap_eda.core.utils.credentials import (
PROTECTED_PASSPHRASE_ERROR,
SUPPORTED_KEYS_IN_INJECTORS,
add_default_values_to_user_inputs,
validate_injectors,
validate_inputs,
validate_schema,
Expand Down Expand Up @@ -632,3 +633,68 @@ def test_validate_registry_host_name(aap_credential_type):
{"host": "invalid@name"},
)
assert "Host format invalid" in errors["inputs.host"]


@pytest.mark.django_db
def test_add_default_values_to_user_inputs():
user_inputs = {"host": "https://eda_controller_url"}
schema = {
"fields": [
{
"id": "host",
"label": "Red Hat Ansible Automation Platform",
"type": "string",
},
{
"id": "username",
"label": "Username",
"type": "string",
"default": "sysadmin",
"help_text": (
"Red Hat Ansible Automation Platform username id"
" to authenticate as.This should not be set if"
" an OAuth token is being used."
),
},
{
"id": "need_password",
"label": "Need Password",
"type": "boolean",
"default": True,
"secret": True,
},
{
"id": "oauth_token",
"label": "OAuth Token",
"type": "string",
"secret": True,
"help_text": (
"An OAuth token to use to authenticate with."
"This should not be set if username/password"
" are being used."
),
},
{
"id": "verify_ssl",
"label": "Verify SSL",
"type": "boolean",
"secret": False,
},
],
"required": ["host"],
}
add_default_values_to_user_inputs(schema, user_inputs)
assert list(user_inputs.keys()) == [
"host",
"username",
"need_password",
"oauth_token",
"verify_ssl",
]
assert list(user_inputs.values()) == [
"https://eda_controller_url",
"sysadmin",
True,
"",
False,
]

0 comments on commit a995d75

Please sign in to comment.