diff --git a/changelogs/fragments/684-fix-mixed-authorized-key-sources.yml b/changelogs/fragments/684-fix-mixed-authorized-key-sources.yml new file mode 100644 index 0000000..91aa4bd --- /dev/null +++ b/changelogs/fragments/684-fix-mixed-authorized-key-sources.yml @@ -0,0 +1,3 @@ +--- +bugfixes: + - authorized_key - fix a bug where providing multiple key sources (URLs or file paths) separated by newlines would fail or only process the first source. The module now correctly resolves each source in a newline-separated list before processing the keys (https://github.com/ansible-collections/ansible.posix/issues/684). diff --git a/plugins/modules/authorized_key.py b/plugins/modules/authorized_key.py index d712a10..cb5f24f 100644 --- a/plugins/modules/authorized_key.py +++ b/plugins/modules/authorized_key.py @@ -568,32 +568,47 @@ def enforce_state(module, params): follow = params.get('follow', False) error_msg = "Error getting key from: %s" - # if the key is a url or file, request it and use it as key source - if key.startswith("http"): - try: - resp, info = fetch_url(module, key) - if info['status'] != 200: - module.fail_json(msg=error_msg % key) - else: - key = resp.read() - except Exception: - module.fail_json(msg=error_msg % key) + # Split the raw input into individual lines first + # This is the key to supporting mixed URLs and raw keys + raw_sources = [s.strip() for s in key.splitlines() if s.strip()] + final_keys = [] - # resp.read gives bytes on python3, convert to native string type - key = to_native(key, errors='surrogate_or_strict') + for source in raw_sources: + if source.startswith('#'): + continue - if key.startswith("file"): - # if the key is an absolute path, check for existense and use it as a key source - key_path = urlparse(key).path - if not os.path.exists(key_path): - module.fail_json(msg="Path to a key file not found: %s" % key_path) - if not os.path.isfile(key_path): - module.fail_json(msg="Path to a key is a directory and must be a file: %s" % key_path) - try: - with open(key_path, 'r') as source_fh: - key = source_fh.read() - except OSError as e: - module.fail_json(msg="Failed to read key file %s : %s" % (key_path, to_native(e))) + validate_certs = params.get('validate_certs', True) + # Identify if this specific line is a URL + if source.startswith(('http://', 'https://')): + try: + resp, info = fetch_url(module, source, validate_certs=validate_certs) + if info['status'] != 200: + module.fail_json(msg=error_msg % source) + + # Fetch the keys from the URL and convert to native string + url_content = to_native(resp.read(), errors='surrogate_or_strict') + if url_content: + final_keys.append(url_content) + except Exception: + module.fail_json(msg=error_msg % source) + + # Identify if this specific line is a local file path + elif source.startswith("file://"): + key_path = urlparse(source).path + if not os.path.exists(key_path): + module.fail_json(msg="Path to a key file not found: %s" % key_path) + try: + with open(key_path, 'r') as source_fh: + final_keys.append(source_fh.read()) + except OSError as e: + module.fail_json(msg="Failed to read key file %s : %s" % (key_path, to_native(e))) + + # If it's not a URL or a File, it's a raw SSH key + else: + final_keys.append(source) + + # Join everything back together for the rest of the module's existing logic + key = "\n".join(final_keys) # extract individual keys into an array, skipping blank lines and comments new_keys = [s for s in key.splitlines() if s and not s.startswith('#')] diff --git a/tests/integration/targets/authorized_key/tasks/multiple_keys.yml b/tests/integration/targets/authorized_key/tasks/multiple_keys.yml index e03abe5..7db10fe 100644 --- a/tests/integration/targets/authorized_key/tasks/multiple_keys.yml +++ b/tests/integration/targets/authorized_key/tasks/multiple_keys.yml @@ -95,3 +95,35 @@ - result.changed == False - multiple_keys_comments.stdout == multiple_key_exclusive.strip() - result.key_options == None + +- name: Create local key source simulating a URL response + ansible.builtin.copy: + dest: "{{ output_dir | expanduser }}/simulated_url.txt" + content: | + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDXJ1HrhQEXOexamplekey1fromurl test1@example.com + ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGexamplekey2fromurl test2@example.com + mode: "0644" + +- name: Add mixed keys (file source + raw key) + ansible.posix.authorized_key: + user: root + key: | + file://{{ output_dir | expanduser }}/simulated_url.txt + ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqmqp9pP9pP9pP9pP9pP9pP9pP9pP9pP9pP9pP test@example.com + state: present + path: "{{ output_dir | expanduser }}/authorized_keys" + exclusive: true + register: mixed_source_result + +- name: Get the file content for mixed source verification + ansible.builtin.command: /bin/cat "{{ output_dir | expanduser }}/authorized_keys" + changed_when: false + register: mixed_file_content + +- name: Assert mixed sources were merged and processed correctly + ansible.builtin.assert: + that: + - mixed_source_result.changed == True + - mixed_file_content.stdout_lines | length == 3 + - "'test1@example.com' in mixed_file_content.stdout" + - "'test@example.com' in mixed_file_content.stdout"