Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: [AAP-38471] - add default values to the user inputs of credentials #1182

Merged
merged 1 commit into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Alex-Izquierdo marked this conversation as resolved.
Show resolved Hide resolved

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,
]
Loading