Skip to content

Commit

Permalink
feat: support decryption from ansible vaulted strings (#651)
Browse files Browse the repository at this point in the history
Add optional cli arguments (as as ansible-playbook cli):
--vault-password-file
--vault-id
--ask-vault-pass
Will decrypt vaulted strings in extra vars, source plugin arguments, and
action arguments.

AAP-7807: Add support for ansible-vault for ansible-rulebook
https://issues.redhat.com/browse/AAP-7807
  • Loading branch information
bzwei authored Feb 26, 2024
1 parent 204a480 commit 67004a0
Show file tree
Hide file tree
Showing 27 changed files with 525 additions and 56 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
## [Unreleased]
### Changed
### Added
- Support for vaulted variables

### Fixed


Expand All @@ -21,7 +23,6 @@
- skip-audit-events to disable sending audit events to server
- restrict drools async connection to localhost


### Changed
- Generic print as well as printing of events use new banner style

Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ RUN bash -c "if [ $DEVEL_COLLECTION_LIBRARY -ne 0 ]; then \
RUN pip install .

RUN chmod -R 0775 $APP_DIR
RUN chmod -x $APP_DIR/tests/e2e/files/passwords/*.*
9 changes: 9 additions & 0 deletions ansible_rulebook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,13 @@

"""Top-level package for Ansible Events."""

import yaml

__version__ = "1.0.5"


def construct_vault_encrypted_unicode(loader, node):
return loader.construct_scalar(node)


yaml.SafeLoader.add_constructor("!vault", construct_vault_encrypted_unicode)
39 changes: 27 additions & 12 deletions ansible_rulebook/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
from ansible_rulebook.engine import run_rulesets, start_source
from ansible_rulebook.job_template_runner import job_template_runner
from ansible_rulebook.rule_types import RuleSet, RuleSetQueue
from ansible_rulebook.util import substitute_variables
from ansible_rulebook.validators import Validate
from ansible_rulebook.vault import has_vaulted_str
from ansible_rulebook.websocket import (
request_workload,
send_event_log_to_websocket,
Expand Down Expand Up @@ -75,9 +77,7 @@ async def run(parsed_args: argparse.Namespace) -> None:
else:
startup_args = StartupArgs()
startup_args.variables = load_vars(parsed_args)
startup_args.rulesets = load_rulebook(
parsed_args, startup_args.variables
)
startup_args.rulesets = load_rulebook(parsed_args, startup_args)
if parsed_args.hot_reload is True and os.path.exists(
parsed_args.rulebook
):
Expand All @@ -94,6 +94,7 @@ async def run(parsed_args: argparse.Namespace) -> None:
startup_args.controller_ssl_verify = parsed_args.controller_ssl_verify

validate_actions(startup_args)
validate_variables(startup_args)

if startup_args.check_controller_connection:
await validate_controller_params(startup_args)
Expand Down Expand Up @@ -179,7 +180,8 @@ def load_vars(parsed_args) -> Dict[str, str]:

# TODO(cutwater): Maybe move to util.py
def load_rulebook(
parsed_args: argparse.Namespace, variables: Optional[Dict] = None
parsed_args: argparse.Namespace,
startup_args: Optional[StartupArgs] = None,
) -> List[RuleSet]:
if not parsed_args.rulebook:
logger.debug("Loading no rules")
Expand All @@ -188,21 +190,24 @@ def load_rulebook(
logger.debug(
"Loading rules from the file system %s", parsed_args.rulebook
)
with open(parsed_args.rulebook) as f:
data = yaml.safe_load(f.read())
with open(parsed_args.rulebook, "rb") as f:
raw_data = f.read()
if startup_args:
startup_args.check_vault = has_vaulted_str(raw_data)
data = yaml.safe_load(raw_data)
Validate.rulebook(data)
if variables is None:
variables = {}
variables = startup_args.variables if startup_args else {}
rulesets = rules_parser.parse_rule_sets(data, variables)
elif has_rulebook(*split_collection_name(parsed_args.rulebook)):
logger.debug(
"Loading rules from a collection %s", parsed_args.rulebook
)
rulesets = rules_parser.parse_rule_sets(
collection_load_rulebook(
*split_collection_name(parsed_args.rulebook)
)
vaulted, rulesets = collection_load_rulebook(
*split_collection_name(parsed_args.rulebook)
)
rulesets = rules_parser.parse_rule_sets(rulesets)
if startup_args:
startup_args.check_vault = vaulted
else:
raise RulebookNotFoundException(
f"Could not find rulebook {parsed_args.rulebook}"
Expand Down Expand Up @@ -236,6 +241,12 @@ def spawn_sources(
return tasks, ruleset_queues


def validate_variables(startup_args: StartupArgs) -> None:
for _key, value in startup_args.variables.items():
# attempt to substitute, raise an error on failure
substitute_variables(value, {})


def validate_actions(startup_args: StartupArgs) -> None:
for ruleset in startup_args.rulesets:
for rule in ruleset.rules:
Expand Down Expand Up @@ -266,6 +277,10 @@ def validate_actions(startup_args: StartupArgs) -> None:
f"Rule {rule.name} has an action {action.action} "
"which needs controller url and token to be defined"
)
if startup_args.check_vault:
for _key, value in action.action_args.items():
# attempt to substitute, raise an error on failure
substitute_variables(value, {})


async def validate_controller_params(startup_args: StartupArgs) -> None:
Expand Down
43 changes: 43 additions & 0 deletions ansible_rulebook/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import ansible_rulebook.util as util
from ansible_rulebook.messages import DEFAULT_SHUTDOWN_DELAY
from ansible_rulebook.vault import Vault

# ensure a valid JVM is available and configures JAVA_HOME if necessary
# must be done before importing any other modules
Expand Down Expand Up @@ -219,6 +220,23 @@ def get_parser() -> argparse.ArgumentParser:
default=settings.skip_audit_events,
help="Don't send audit events to the server",
)
parser.add_argument(
"--vault-password-file",
help="The file containing one ansible vault password",
default=os.environ.get("EDA_VAULT_PASSWORD_FILE", ""),
)
parser.add_argument(
"--vault-id",
help="label@filename pointing to an ansible vault password file",
action="append",
default=[],
)
parser.add_argument(
"--ask-vault-pass",
help="Ask vault password interactively",
action="store_true",
default=False,
)
return parser


Expand Down Expand Up @@ -281,6 +299,29 @@ def update_settings(args: argparse.Namespace) -> None:
settings.websocket_access_token = args.websocket_access_token
settings.websocket_refresh_token = args.websocket_refresh_token
settings.skip_audit_events = args.skip_audit_events
parse_vault_passwords(args)


def parse_vault_passwords(args: argparse.Namespace) -> None:
if (
not args.vault_password_file
and not args.vault_id
and not args.ask_vault_pass
):
return

secret_files = []

if args.vault_password_file:
secret_files.append(args.vault_password_file)

secret_files += args.vault_id

settings.vault = Vault(
password_file=args.vault_password_file,
vault_ids=args.vault_id,
ask_pass=args.ask_vault_pass,
)


def main(args: List[str] = None) -> int:
Expand Down Expand Up @@ -318,6 +359,8 @@ def main(args: List[str] = None) -> int:
except Exception as err:
logger.error("Terminating %s", str(err))
return 1
finally:
settings.vault.close()
return 0


Expand Down
11 changes: 8 additions & 3 deletions ansible_rulebook/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import yaml

from ansible_rulebook import terminal
from ansible_rulebook.exception import RulebookNotFoundException
from ansible_rulebook.vault import has_vaulted_str

ANSIBLE_GALAXY = shutil.which("ansible-galaxy")
EDA_PATH_PREFIX = "extensions/eda"
Expand Down Expand Up @@ -130,12 +132,15 @@ def load_rulebook(collection, rulebook):
".yml",
)
if not location:
return False
with open(location) as f:
raise RulebookNotFoundException(f"Cannot find collection {collection}")

with open(location, "rb") as f:
terminal.Display.instance().banner(
"collection", f"Loading rulebook from {location}"
)
return yaml.safe_load(f.read())
raw_data = f.read()
vaulted = has_vaulted_str(raw_data)
return vaulted, yaml.safe_load(raw_data)


def has_source(collection, source):
Expand Down
1 change: 1 addition & 0 deletions ansible_rulebook/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ class StartupArgs:
project_data_file: str = field(default="")
inventory: str = field(default="")
check_controller_connection: bool = field(default=False)
check_vault: bool = field(default=True)
3 changes: 3 additions & 0 deletions ansible_rulebook/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

import uuid

from ansible_rulebook.vault import Vault


class _Settings:
def __init__(self):
Expand All @@ -28,6 +30,7 @@ def __init__(self):
self.websocket_access_token = None
self.websocket_refresh_token = None
self.skip_audit_events = False
self.vault = Vault()


settings = _Settings()
Loading

0 comments on commit 67004a0

Please sign in to comment.