Skip to content

Commit

Permalink
modules: add _password args as alternative to password_files (#402)
Browse files Browse the repository at this point in the history
this PR adds a `_password` parameter to go along with
every currently existing `password_file` module parameter.
As discussed in #352, this makes passing in passwords to tasks
much more convenient, as users no longer need to create and delete
password files manually.

To ensure no sensitive password files remain on the system, they
are created in a temporary directory that is handled through a context manager.
  • Loading branch information
maxhoesel authored Apr 8, 2024
1 parent ec8d82d commit 1d6fa88
Show file tree
Hide file tree
Showing 11 changed files with 448 additions and 359 deletions.
11 changes: 10 additions & 1 deletion plugins/doc_fragments/ca_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,16 @@ class ModuleDocFragment:
type: str
aliases:
- admin_name
admin_password:
description: >
The password to encrypt or decrypt the private key.
Will be passed to step-cli through a temporary file.
Mutually exclusive with I(admin_password_file)
type: str
admin_password_file:
description: The path to the file containing the password to encrypt or decrypt the private key.
description: >
The path to the file containing the password to encrypt or decrypt the private key.
Must already be present on the remote host.
Mutually exclusive with I(admin_password)
type: path
'''
12 changes: 5 additions & 7 deletions plugins/module_utils/cli_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,23 +90,21 @@ def build(self, module: AnsibleModule, tmpdir: Path) -> List[str]:
# Create temporary files for any parameters that need to point to files, such as password-file
# Since these files may contain sensitive data, we first create the fd with locked-down permissions,
# then write the actual content
for module_arg in self.module_tmpfile_args:
for module_arg in [arg for arg in self.module_tmpfile_args if module_params[arg]]:
path = tmpdir / module_arg
path.touch(0o700, exist_ok=False)
with open(path, "w", encoding="utf-8") as f:
f.write(module_params[module_arg])
args.extend([self.module_tmpfile_args[module_arg], path.as_posix()])

# transform the values in module_params into valid step-coi arguments using module_args_params mapping
for param_name in self.module_param_args:
for param_name in [arg for arg in self.module_param_args if module_params[arg]]:
if param_name not in module_params:
raise CliError(f"Could not build command parameters: "
f"param '{param_name}' not in module argspec, this is most likely a bug")

param_type = module.argument_spec[param_name].get("type", "str")
if not module_params[param_name]:
# param not set
pass
elif param_type == "bool" and bool(module_params[param_name]):
if param_type == "bool" and bool(module_params[param_name]):
args.append(self.module_param_args[param_name])
elif param_type == "list":
for item in cast(List, module_params[param_name]):
Expand Down Expand Up @@ -147,7 +145,7 @@ def run(self, module: AnsibleModule) -> CliCommandResult:
CliError if the module args don't match with the provided params
"""
# use a context manager to ensure that our sensitive temporary files are *always* deleted
with tempfile.TemporaryDirectory("ansible-smallstep") as tmpdir:
with tempfile.TemporaryDirectory("-ansible-smallstep") as tmpdir:
cmd = [self.executable.path] + self.args.build(module, Path(tmpdir))

if module.check_mode and not self.run_in_check_mode:
Expand Down
17 changes: 12 additions & 5 deletions plugins/module_utils/params/ca_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,29 @@ class AdminParams(ParamsHelper):
admin_key=dict(type="path"),
admin_provisioner=dict(type="str", aliases=["admin_issuer"]),
admin_subject=dict(type="str", aliases=["admin_name"]),
admin_password=dict(type="str", no_log=True),
admin_password_file=dict(type="path", no_log=False)
)

@classmethod
def cli_args(cls) -> CliCommandArgs:
return CliCommandArgs([], {key: f"--{key.replace('_', '-')}" for key in cls.argument_spec})
return CliCommandArgs([], {
"admin_cert": "--admin-cert",
"admin_key": "--admin-key",
"admin_provisioner": "--admin-provisioner",
"admin_subject": "--admin-subject",
"admin_password_file": "--admin-password-file",
}, {
"admin_password": "--admin-password-file"
})

# pylint: disable=useless-parent-delegation
def __init__(self, module: AnsibleModule) -> None:
super().__init__(module)

def check(self):
try:
validation.check_required_together(["admin_cert", "admin_key"], self.module.params)
except ValueError:
self.module.fail_json(msg="admin_cert and admin_key must be specified together")
validation.check_required_together(["admin_cert", "admin_key"], self.module.params)
validation.check_mutually_exclusive(["admin_password", "admin_password_file"], self.module.params)

def is_defined(self):
return bool(self.module.params["admin_cert"]) # type: ignore
30 changes: 23 additions & 7 deletions plugins/modules/step_ca_certificate.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,16 @@
- issuer
description: The provisioner name to use. Required if I(state=present).
type: str
provisioner_password:
description: >
The password to decrypt the one-time token generating key.
Will be passed to step-cli through a temporary file.
Mutually exclusive with I(provisioner_password_file)
type: str
provisioner_password_file:
description: The path to the file containing the password to decrypt the one-time token generating key.
description: >
The path to the file containing the password to decrypt the one-time token generating key.
Mutually exclusive with I(provisioner_password)
type: path
revoke_on_delete:
description: If I(state=absent), attempt to revoke the certificate before deleting it
Expand Down Expand Up @@ -264,7 +272,7 @@
from typing import cast, Dict, Any

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.validation import check_required_if
from ansible.module_utils.common.validation import check_required_if, check_mutually_exclusive

from ..module_utils.params.ca_connection import CaConnectionParams
from ..module_utils.cli_wrapper import CliCommand, CliCommandArgs, StepCliExecutable
Expand Down Expand Up @@ -297,7 +305,8 @@ def create_certificate(executable: StepCliExecutable, module: AnsibleModule, for
if force:
args.append("--force")

create_args = CaConnectionParams.cli_args().join(CliCommandArgs(args, cert_cliarg_map))
create_args = CaConnectionParams.cli_args().join(CliCommandArgs(
args, cert_cliarg_map, {"provisioner_password": "--provisioner-password-file"}))
create_cmd = CliCommand(executable, create_args)
create_cmd.run(module)
return {"changed": True}
Expand Down Expand Up @@ -408,6 +417,7 @@ def run_module():
not_after=dict(type="str"),
not_before=dict(type="str"),
provisioner=dict(type="str", aliases=["issuer"]),
provisioner_password=dict(type="str", no_log=True),
provisioner_password_file=dict(type="path", no_log=False),
revoke_on_delete=dict(type="bool", default=True),
revoke_reason=dict(type="str"),
Expand All @@ -431,11 +441,17 @@ def run_module():
**CaConnectionParams.argument_spec,
**argument_spec,
}, supports_check_mode=True)
CaConnectionParams(module).check()
module_params = cast(Dict, module.params)
check_required_if([
["state", "present", ["name", "provisioner"], True],
], module_params)

try:
CaConnectionParams(module).check()
check_required_if([
["state", "present", ["name", "provisioner"], True],
], module_params)
check_mutually_exclusive(["provisioner_password", "provisioner_password_file"], module_params)
except TypeError as e:
module.fail_json(f"Parameter validation failed: {e}")

executable = StepCliExecutable(module, module_params["step_cli_executable"])

crt_exists = Path(module_params["crt_file"]).exists()
Expand Down
30 changes: 25 additions & 5 deletions plugins/modules/step_ca_provisioner.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,16 @@
version_added: 0.20.0
aliases:
- tenant_id
password:
description: >
The password to encrypt or decrypt the private key.
Will be passed to step-cli through a temporary file.
Mutually exclusive with I(password_file)
type: str
password_file:
description: The path to the file containing the password to encrypt or decrypt the private key.
description: >
The path to the file containing the password to encrypt or decrypt the private key.
Mutually exclusive with I(password)
type: path
public_key:
description: >
Expand Down Expand Up @@ -440,6 +448,7 @@
from typing import cast, Dict, Any

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.validation import check_mutually_exclusive

from ..module_utils.params.ca_admin import AdminParams
from ..module_utils.cli_wrapper import CliCommand, CliCommandArgs, StepCliExecutable
Expand Down Expand Up @@ -502,12 +511,16 @@
"x509_default_dur": "--x509-default-dur",
"x5c_root": "--x5c-root",
}
CREATE_UPDATE_TMPFILE_ARGS = {
"password": "--password-file"
}


def add_provisioner(name: str, provisioner_type: str, executable: StepCliExecutable, module: AnsibleModule):
args = AdminParams.cli_args().join(CliCommandArgs(
["ca", "provisioner", "add", name, "--type", provisioner_type],
{**CREATE_UPDATE_CLIARGS, **CONNECTION_CLIARG_MAP}
{**CREATE_UPDATE_CLIARGS, **CONNECTION_CLIARG_MAP},
CREATE_UPDATE_TMPFILE_ARGS
))
cmd = CliCommand(executable, args)
cmd.run(module)
Expand All @@ -517,7 +530,8 @@ def add_provisioner(name: str, provisioner_type: str, executable: StepCliExecuta
def update_provisioner(name: str, executable: StepCliExecutable, module: AnsibleModule):
args = AdminParams.cli_args().join(CliCommandArgs(
["ca", "provisioner", "update", name],
{**CREATE_UPDATE_CLIARGS, **CONNECTION_CLIARG_MAP}
{**CREATE_UPDATE_CLIARGS, **CONNECTION_CLIARG_MAP},
CREATE_UPDATE_TMPFILE_ARGS
))
cmd = CliCommand(executable, args)
cmd.run(module)
Expand Down Expand Up @@ -564,6 +578,7 @@ def run_module():
oidc_groups=dict(type="list", elements="str", aliases=["group", "oidc_group"]),
oidc_listen_address=dict(type="str", aliases=["listen_address", "oidc_client_address"]),
oidc_tenant_id=dict(type="str", aliases=["tenant_id"]),
password=dict(type="str", no_log=True),
password_file=dict(type="path", no_log=False),
public_key=dict(type="path", aliases=["jwk_public_key", "k8ssa_public_key", "k8s_pem_keys_file"]),
require_eab=dict(type="bool"),
Expand Down Expand Up @@ -598,9 +613,14 @@ def run_module():
**AdminParams.argument_spec,
**argument_spec
}, supports_check_mode=True)
admin_params = AdminParams(module)
admin_params.check()
module_params = cast(Dict, module.params)
admin_params = AdminParams(module)

try:
admin_params.check()
check_mutually_exclusive(["password", "password_file"], module_params)
except TypeError as e:
module.fail_json(f"Parameter validation failed: {e}")

executable = StepCliExecutable(module, module_params["step_cli_executable"])

Expand Down
24 changes: 21 additions & 3 deletions plugins/modules/step_ca_renew.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,16 @@
output_file:
description: The new certificate file path. Defaults to overwriting the crt-file positional argument.
type: path
password:
description: >
The password to encrypt or decrypt the private key.
Will be passed to step-cli through a temporary file.
Mutually exclusive with I(password_file)
type: str
password_file:
description: The path to the file containing the password to encrypt or decrypt the private key.
description: >
The path to the file containing the password to encrypt or decrypt the private key.
Mutually exclusive with I(password)
type: path
pid:
description: >
Expand Down Expand Up @@ -76,6 +84,7 @@
from typing import Dict, cast, Any

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.validation import check_mutually_exclusive

from ..module_utils.cli_wrapper import CliCommand, CliCommandArgs, StepCliExecutable
from ..module_utils.params.ca_connection import CaConnectionParams
Expand All @@ -90,6 +99,7 @@ def run_module():
exec=dict(type="str"),
key_file=dict(type="path", required=True),
output_file=dict(type="path"),
password=dict(type="str", no_log=True),
password_file=dict(type="path", no_log=False),
pid=dict(type="int"),
pid_file=dict(type="path"),
Expand All @@ -101,9 +111,14 @@ def run_module():
**CaConnectionParams.argument_spec,
**argument_spec
}, supports_check_mode=True)
CaConnectionParams(module).check()
module_params = cast(Dict, module.params)

try:
CaConnectionParams(module).check()
check_mutually_exclusive(["password", "password_file"], module_params)
except TypeError as e:
module.fail_json(f"Parameter validation failed: {e}")

executable = StepCliExecutable(module, module_params["step_cli_executable"])

# Regular args
Expand All @@ -112,7 +127,10 @@ def run_module():
renew_cliarg_map = {arg: f"--{arg.replace('_', '-')}" for arg in renew_cliargs}

renew_args = CaConnectionParams.cli_args().join(CliCommandArgs(
["ca", "renew", module_params["crt_file"], module_params["key_file"]], renew_cliarg_map))
["ca", "renew", module_params["crt_file"], module_params["key_file"]],
renew_cliarg_map,
{"password": "--password-file"}
))
renew_cmd = CliCommand(executable, renew_args)
renew_res = renew_cmd.run(module)
if "Your certificate has been saved in" in renew_res.stderr:
Expand Down
33 changes: 20 additions & 13 deletions plugins/modules/step_ca_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,16 @@
- issuer
description: The provisioner name to use.
type: str
provisioner_password:
description: >
The password to encrypt or decrypt the one-time token generating key.
Will be passed to step-cli through a temporary file.
Mutually exclusive with I(password_file)
type: str
provisioner_password_file:
description: The path to the file containing the password to decrypt the one-time token generating key.
description: >
The path to the file containing the password to decrypt the one-time token generating key.
Mutually exclusive with I(provisioner_password_file)
type: path
return_token:
description: >
Expand Down Expand Up @@ -137,8 +145,7 @@
"""
from typing import cast, Dict, Any

from ansible.module_utils.common.validation import check_required_one_of
from ansible.module_utils.common.validation import check_mutually_exclusive
from ansible.module_utils.common.validation import check_required_one_of, check_mutually_exclusive
from ansible.module_utils.basic import AnsibleModule

from ..module_utils.cli_wrapper import CliCommandArgs, StepCliExecutable, CliCommand
Expand All @@ -161,6 +168,7 @@ def run_module():
output_file=dict(type="path"),
principal=dict(type="list", elements="str"),
provisioner=dict(type="str", aliases=["issuer"]),
provisioner_password=dict(type="str", no_log=True),
provisioner_password_file=dict(type="path", no_log=False),
return_token=dict(type="bool"),
revoke=dict(type="bool"),
Expand All @@ -183,15 +191,11 @@ def run_module():
module_params = cast(Dict, module.params)

try:
check_mutually_exclusive(["return_token", "output_file"], module.params)
except TypeError:
result["msg"] = "return_token and output_file cannot be specified at the same time"
module.fail_json(**result)
try:
check_required_one_of(["return_token", "output_file"], module.params)
except TypeError:
result["msg"] = "At least one of return_token and output_file must be specified"
module.fail_json(**result)
check_mutually_exclusive(["return_token", "output_file"], module_params)
check_required_one_of(["return_token", "output_file"], module_params)
check_mutually_exclusive(["provisioner_password", "provisioner_password_file"], module_params)
except TypeError as e:
module.fail_json(f"Parameter validation failed: {e}")

executable = StepCliExecutable(module, module_params["step_cli_executable"])

Expand All @@ -204,7 +208,10 @@ def run_module():
token_cliarg_map = {arg: f"--{arg.replace('_', '-')}" for arg in token_cliargs}

token_args = CaConnectionParams.cli_args().join(CliCommandArgs(
["ca", "token", module_params["name"]], token_cliarg_map))
["ca", "token", module_params["name"]],
token_cliarg_map,
{"provisioner_password": "--provisioner-password-file"}
))
token_cmd = CliCommand(executable, token_args)
token_res = token_cmd.run(module)

Expand Down
Loading

0 comments on commit 1d6fa88

Please sign in to comment.