From 7c57aebd46efd892b53d8b27d51202d82cf8f038 Mon Sep 17 00:00:00 2001 From: Rebeccah Date: Wed, 24 Mar 2021 11:44:40 -0400 Subject: [PATCH 1/4] if there is a credential associated with an EE, create a JSON structure and write it to a file, then use that file to pull from protected registries from quay and edit the credential type for registries so that they combine the password and token fields into one field --- awx/main/models/credential/__init__.py | 10 ++-------- awx/main/tasks.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index f37bfee884c5..77ee3fe10664 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -1096,14 +1096,8 @@ def create(self): 'type': 'string', }, { - 'id': 'password', - 'label': ugettext_noop('Password'), - 'type': 'string', - 'secret': True, - }, - { - 'id': 'token', - 'label': ugettext_noop('Access Token'), + 'id': 'password/token', + 'label': ugettext_noop('Password/Token'), 'type': 'string', 'secret': True, 'help_text': ugettext_noop('A token to use to authenticate with. ' 'This should not be set if username/password are being used.'), diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 0acaee3b9ce4..31d029daf0ec 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -851,6 +851,17 @@ def build_execution_environment_params(self, instance): "container_options": ['--user=root'], } + if instance.execution_environment.credential: + with open('/tmp/auth.json', 'w') as authfile: + host = instance.execution_environment.credential.get_input('host') + username = instance.execution_environment.credential.get_input('username') + password = instance.execution_environment.credential.get_input('password') + token = "{}:{}".format(username, password) + auth_data = {'auths': {host: {'auth': b64encode(token.encode('ascii')).decode()}}} + authfile.write(json.dumps(auth_data, indent=4)) + authfile.close() + params["container_options"].append(f'--authfile={authfile.name}') + pull = instance.execution_environment.pull if pull: params['container_options'].append(f'--pull={pull}') From e61d0c5cb793b36e8bad4ea2d9614450266c1c48 Mon Sep 17 00:00:00 2001 From: Rebeccah Date: Fri, 26 Mar 2021 10:27:08 -0400 Subject: [PATCH 2/4] credential validation for execution envs to allow only registry credentials to be associated with them, also adding security precautions for authfile and password, also combined token & password into one term to align with Quay, and added handling to account for users not filling in credential data and add a has_inputs function to simplify checking if the host, username, and password are present in the credential --- awx/api/serializers.py | 5 +++++ awx/main/models/credential/__init__.py | 12 +++++++++--- awx/main/tasks.py | 24 +++++++++++++++--------- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index cae3724ca3f4..8ac06b67931f 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1412,6 +1412,11 @@ def get_related(self, obj): res['credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.credential.pk}) return res + def validate_credential(self, value): + if value and value.kind != 'registry': + raise serializers.ValidationError(_('Only Container Registry credentials can be associated with an Execution Environment')) + return value + def validate(self, attrs): # prevent changing organization of ee. Unsetting (change to null) is allowed if self.instance: diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index 77ee3fe10664..bd1f608d627b 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -295,6 +295,12 @@ def has_input(self, field_name): return True return field_name in self.inputs and self.inputs[field_name] not in ('', None) + def has_inputs(self, field_names=()): + for name in field_names: + if name not in self.inputs: + return False + return True + def _get_dynamic_input(self, field_name): for input_source in self.input_sources.all(): if input_source.input_field_name == field_name: @@ -1096,11 +1102,11 @@ def create(self): 'type': 'string', }, { - 'id': 'password/token', - 'label': ugettext_noop('Password/Token'), + 'id': 'password', + 'label': ugettext_noop('Password'), 'type': 'string', 'secret': True, - 'help_text': ugettext_noop('A token to use to authenticate with. ' 'This should not be set if username/password are being used.'), + 'help_text': ugettext_noop('A password or token used to authenticate with'), }, ], 'required': ['host'], diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 31d029daf0ec..012eb9387d8c 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -852,15 +852,21 @@ def build_execution_environment_params(self, instance): } if instance.execution_environment.credential: - with open('/tmp/auth.json', 'w') as authfile: - host = instance.execution_environment.credential.get_input('host') - username = instance.execution_environment.credential.get_input('username') - password = instance.execution_environment.credential.get_input('password') - token = "{}:{}".format(username, password) - auth_data = {'auths': {host: {'auth': b64encode(token.encode('ascii')).decode()}}} - authfile.write(json.dumps(auth_data, indent=4)) - authfile.close() - params["container_options"].append(f'--authfile={authfile.name}') + cred = instance.execution_environment.credential + if cred.has_inputs(field_names=('host', 'username', 'password')): + path = self.build_private_data_dir(instance) + with open(path + '/auth.json', 'w') as authfile: + host = cred.get_input('host') + username = cred.get_input('username') + password = cred.get_input('password') + token = "{}:{}".format(username, password) + auth_data = {'auths': {host: {'auth': b64encode(token.encode('ascii')).decode()}}} + authfile.write(json.dumps(auth_data, indent=4)) + authfile.close() + os.chmod(authfile.name, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) + params["container_options"].append(f'--authfile={authfile.name}') + else: + logger.exception('Please recheck that your host, username, and password fields are all filled.') pull = instance.execution_environment.pull if pull: From 4a62932ecde72cbcc92aae77ab030ceabf633573 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Wed, 31 Mar 2021 10:25:31 -0400 Subject: [PATCH 3/4] Store auth.json is pdd_wrapper directory --- awx/main/models/credential/__init__.py | 7 +++++-- awx/main/tasks.py | 18 +++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index bd1f608d627b..4f10c3ed2c18 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -297,8 +297,11 @@ def has_input(self, field_name): def has_inputs(self, field_names=()): for name in field_names: - if name not in self.inputs: - return False + if name in self.inputs: + if self.inputs[name] in ('', None): + return False + else: + raise ValueError('{} is not an input field'.format(name)) return True def _get_dynamic_input(self, field_name): diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 012eb9387d8c..fd01a6140e93 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -834,7 +834,7 @@ def get_path_to(self, *args): """ return os.path.abspath(os.path.join(os.path.dirname(__file__), *args)) - def build_execution_environment_params(self, instance): + def build_execution_environment_params(self, instance, private_data_dir): if settings.IS_K8S: return {} @@ -854,7 +854,7 @@ def build_execution_environment_params(self, instance): if instance.execution_environment.credential: cred = instance.execution_environment.credential if cred.has_inputs(field_names=('host', 'username', 'password')): - path = self.build_private_data_dir(instance) + path = os.path.split(private_data_dir)[0] with open(path + '/auth.json', 'w') as authfile: host = cred.get_input('host') username = cred.get_input('username') @@ -866,7 +866,7 @@ def build_execution_environment_params(self, instance): os.chmod(authfile.name, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) params["container_options"].append(f'--authfile={authfile.name}') else: - logger.exception('Please recheck that your host, username, and password fields are all filled.') + raise RuntimeError('Please recheck that your host, username, and password fields are all filled.') pull = instance.execution_environment.pull if pull: @@ -1726,11 +1726,11 @@ def should_use_resource_profiling(self, job): """ return settings.AWX_RESOURCE_PROFILING_ENABLED - def build_execution_environment_params(self, instance): + def build_execution_environment_params(self, instance, private_data_dir): if settings.IS_K8S: return {} - params = super(RunJob, self).build_execution_environment_params(instance) + params = super(RunJob, self).build_execution_environment_params(instance, private_data_dir) # If this has an insights agent and it is not already mounted then show it insights_dir = os.path.dirname(settings.INSIGHTS_SYSTEM_ID_FILE) if instance.use_fact_cache and os.path.exists(insights_dir): @@ -2341,11 +2341,11 @@ def post_run_hook(self, instance, status): if status == 'successful' and instance.launch_type != 'sync': self._update_dependent_inventories(instance, dependent_inventory_sources) - def build_execution_environment_params(self, instance): + def build_execution_environment_params(self, instance, private_data_dir): if settings.IS_K8S: return {} - params = super(RunProjectUpdate, self).build_execution_environment_params(instance) + params = super(RunProjectUpdate, self).build_execution_environment_params(instance, private_data_dir) project_path = instance.get_project_path(check_if_exists=False) cache_path = instance.get_cache_path() params.setdefault('container_volume_mounts', []) @@ -2848,7 +2848,7 @@ class RunSystemJob(BaseTask): event_model = SystemJobEvent event_data_key = 'system_job_id' - def build_execution_environment_params(self, system_job): + def build_execution_environment_params(self, system_job, private_data_dir): return {} def build_args(self, system_job, private_data_dir, passwords): @@ -2964,7 +2964,7 @@ def __init__(self, task=None, runner_params=None): self.unit_id = None if self.task and not self.task.instance.is_container_group_task: - execution_environment_params = self.task.build_execution_environment_params(self.task.instance) + execution_environment_params = self.task.build_execution_environment_params(self.task.instance, runner_params['private_data_dir']) self.runner_params['settings'].update(execution_environment_params) def run(self): From 0d2ab5f61ee4076543683bf90dffcf6e9b255ad8 Mon Sep 17 00:00:00 2001 From: Rebeccah Date: Mon, 5 Apr 2021 10:45:09 -0400 Subject: [PATCH 4/4] add in OR to the UI label for editing a registry credential --- awx/main/models/credential/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index 4f10c3ed2c18..01e71302bf6f 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -1106,7 +1106,7 @@ def create(self): }, { 'id': 'password', - 'label': ugettext_noop('Password'), + 'label': ugettext_noop('Password or Token'), 'type': 'string', 'secret': True, 'help_text': ugettext_noop('A password or token used to authenticate with'),