-
Notifications
You must be signed in to change notification settings - Fork 9
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
Enable support for Vault secrets in Manifester #38
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
vault-login: | ||
@scripts/vault_login.py --login | ||
|
||
vault-logout: | ||
@scripts/vault_login.py --logout | ||
|
||
vault-status: | ||
@scripts/vault_login.py --status |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,24 @@ | ||
"""Defines helper functions used by Manifester.""" | ||
from collections import UserDict | ||
import json | ||
import os | ||
from pathlib import Path | ||
import random | ||
import re | ||
import subprocess | ||
import sys | ||
import time | ||
|
||
from logzero import logger | ||
from requests import HTTPError | ||
import yaml | ||
|
||
from manifester.logger import setup_logzero | ||
from manifester.settings import settings | ||
|
||
setup_logzero(level="info") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If switching to using the settings module, you can just use the configured log level |
||
|
||
|
||
RESULTS_LIMIT = 10000 | ||
|
||
|
||
|
@@ -226,3 +235,139 @@ def __getitem__(self, key): | |
def __call__(self, *args, **kwargs): | ||
"""Allow MockStub to be used like a function.""" | ||
return self | ||
|
||
|
||
class InvalidVaultURLForOIDC(Exception): | ||
"""Raised if the vault doesn't allow OIDC login.""" | ||
|
||
|
||
class Vault: | ||
"""Helper class for retrieving secrets from HashiCorp Vault.""" | ||
|
||
HELP_TEXT = ( | ||
"The Vault CLI in not installed on this system." | ||
"Please follow https://learn.hashicorp.com/tutorials/vault/getting-started-install to " | ||
"install the Vault CLI." | ||
) | ||
|
||
def __init__(self, env_file=".env"): | ||
manifester_directory = Path() | ||
|
||
if "MANIFESTER_DIRECTORY" in os.environ: | ||
envar_location = Path(os.environ["MANIFESTER_DIRECTORY"]) | ||
if envar_location.is_dir(): | ||
manifester_directory = envar_location | ||
self.env_path = manifester_directory.joinpath(env_file) | ||
Comment on lines
+254
to
+260
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These would likely be best done in a common place, like the settings module There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @JacobCallahan This actually is done in the settings module, but I revisited this today and remembered why I had repeated it here: There may be another way to work around this race condition, but I have not figured one out yet. Do you have any thoughts on an alternative, or alternatively, do you think my admittedly inelegant solution is acceptable? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm not ideal, but if it needs to be repeated anyway for the vault login then it's up to you whether that repetition happens here or in the vault script. I have no objections either way now. |
||
self.envdata = None | ||
self.vault_enabled = None | ||
|
||
def setup(self): | ||
"""Read environment variables from .env.""" | ||
if self.env_path.exists(): | ||
self.envdata = self.env_path.read_text() | ||
is_enabled = re.findall("^(?:.*\n)*VAULT_ENABLED_FOR_DYNACONF=(.*)", self.envdata) | ||
if is_enabled: | ||
self.vault_enabled = is_enabled[0] | ||
self.export_vault_addr() | ||
|
||
def teardown(self): | ||
"""Remove VAULT_ADDR environment variable if present.""" | ||
if os.environ.get("VAULT_ADDR") is not None: | ||
del os.environ["VAULT_ADDR"] | ||
|
||
def export_vault_addr(self): | ||
"""Set the URL of the Vault server and ensure that the URL is not localhost.""" | ||
vaulturl = re.findall("VAULT_URL_FOR_DYNACONF=(.*)", self.envdata)[0] | ||
|
||
# Set Vault CLI Env Var | ||
os.environ["VAULT_ADDR"] = vaulturl | ||
|
||
# Dynaconf Vault Env Vars | ||
if ( | ||
self.vault_enabled | ||
and self.vault_enabled in ["True", "true"] | ||
and "localhost:8200" in vaulturl | ||
): | ||
raise InvalidVaultURLForOIDC( | ||
f"{vaulturl} does not support OIDC login." | ||
"Please set the correct vault URL vault the .env file." | ||
) | ||
|
||
def exec_vault_command(self, command: str, **kwargs): | ||
"""Wrap Vault CLI commands for execution. | ||
|
||
:param comamnd str: The vault CLI command | ||
:param kwargs dict: Arguments to the subprocess run command to customize the run behavior | ||
""" | ||
COMMAND_NOT_FOUND_EXIT_CODE = 127 | ||
vcommand = subprocess.run(command.split(), capture_output=True, **kwargs) | ||
if vcommand.returncode != 0: | ||
verror = str(vcommand.stderr) | ||
if vcommand.returncode == COMMAND_NOT_FOUND_EXIT_CODE: | ||
logger.error(f"Error! {self.HELP_TEXT}") | ||
sys.exit(1) | ||
if vcommand.stderr: | ||
if "Error revoking token" in verror: | ||
logger.info("Token is already revoked") | ||
elif "Error looking up token" in verror: | ||
logger.info("Vault is not logged in") | ||
else: | ||
logger.error(f"Error: {verror}") | ||
return vcommand | ||
|
||
def login(self, **kwargs): | ||
"""Authenticate to Vault server and add auth token to .env file.""" | ||
if ( | ||
self.vault_enabled | ||
and self.vault_enabled in ["True", "true"] | ||
and "VAULT_SECRET_ID_FOR_DYNACONF" not in os.environ | ||
and self.status(**kwargs).returncode != 0 | ||
): | ||
logger.info( | ||
"Warning: A browser tab will open for Vault OIDC login. " | ||
"Please close the tab once the sign-in is complete" | ||
) | ||
if ( | ||
self.exec_vault_command(command="vault login -method=oidc", **kwargs).returncode | ||
== 0 | ||
): | ||
self.exec_vault_command(command="vault token renew -i 10h", **kwargs) | ||
logger.info("Success! Vault OIDC Logged-In and extended for 10 hours!") | ||
# Fetch token | ||
token = self.exec_vault_command("vault token lookup --format json").stdout | ||
token = json.loads(str(token.decode("UTF-8")))["data"]["id"] | ||
# Set new token in .env file | ||
_envdata = re.sub( | ||
".*VAULT_TOKEN_FOR_DYNACONF=.*", | ||
f"VAULT_TOKEN_FOR_DYNACONF={token}", | ||
self.envdata, | ||
) | ||
self.env_path.write_text(_envdata) | ||
logger.info("New OIDC token succesfully added to .env file") | ||
|
||
def logout(self): | ||
"""Revoke Vault auth token and remove it from .env file.""" | ||
# Teardown - Setting dymmy token in env file | ||
_envdata = re.sub( | ||
".*VAULT_TOKEN_FOR_DYNACONF=.*", "# VAULT_TOKEN_FOR_DYNACONF=myroot", self.envdata | ||
) | ||
self.env_path.write_text(_envdata) | ||
vstatus = self.exec_vault_command("vault token revoke -self") | ||
if vstatus.returncode == 0: | ||
logger.info("OIDC token successfully removed from .env file") | ||
|
||
def status(self, **kwargs): | ||
"""Check status of Vault auth token.""" | ||
vstatus = self.exec_vault_command("vault token lookup", **kwargs) | ||
if vstatus.returncode == 0: | ||
logger.info(str(vstatus.stdout.decode("UTF-8"))) | ||
return vstatus | ||
|
||
def __enter__(self): | ||
"""Set up Vault context manager.""" | ||
self.setup() | ||
return self | ||
|
||
def __exit__(self, exc_type, exc_val, exc_tb): | ||
"""Tear down Vault context manager.""" | ||
self.teardown() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,7 @@ | |
import os | ||
from pathlib import Path | ||
|
||
from dynaconf import Dynaconf, Validator | ||
from dynaconf import LazySettings, Validator | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why this change? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change was part of my initial debugging. In trying to determine why my Vault integration was failing, I tried matching the Robottelo implementation as closely as possible. I can revert this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah they're equivalent statements since https://github.com/dynaconf/dynaconf/blob/master/dynaconf/__init__.py#L28 |
||
|
||
settings_file = "manifester_settings.yaml" | ||
MANIFESTER_DIRECTORY = Path() | ||
|
@@ -14,14 +14,14 @@ | |
|
||
settings_path = MANIFESTER_DIRECTORY.joinpath("manifester_settings.yaml") | ||
validators = [ | ||
# Validator("offline_token", must_exist=True), | ||
Validator("offline_token", must_exist=True), | ||
Validator("simple_content_access", default="enabled"), | ||
Validator("username_prefix", len_min=3), | ||
] | ||
settings = Dynaconf( | ||
settings = LazySettings( | ||
settings_file=str(settings_path.absolute()), | ||
ENVVAR_PREFIX_FOR_DYNACONF="MANIFESTER", | ||
load_dotenv=True, | ||
validators=validators, | ||
) | ||
|
||
settings.validators.validate() |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at https://hvac.readthedocs.io/en/stable/usage/auth_methods/jwt-oidc.html#oidc-authorization-url-request, it should be possible to use pure Python implementation, using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm definitely interested in this as a possibility. I'll miss the next Automation Brainstorming meeting, but maybe we can discuss this at the following one. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
#!/usr/bin/env python | ||
# This Enables and Disables individuals OIDC token to access secrets from vault | ||
import sys | ||
|
||
from manifester.helpers import Vault | ||
|
||
if __name__ == '__main__': | ||
with Vault() as vclient: | ||
if sys.argv[-1] == '--login': | ||
vclient.login() | ||
elif sys.argv[-1] == '--status': | ||
vclient.status() | ||
else: | ||
vclient.logout() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might want to slap on a resolve here