Skip to content

Commit 2f9263e

Browse files
authored
Merge pull request #38 from synkd/integrate_vault_with_manifester
Enable support for Vault secrets in Manifester
2 parents 54c0c70 + 45588d7 commit 2f9263e

8 files changed

+186
-11
lines changed

Makefile

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
vault-login:
2+
@scripts/vault_login.py --login
3+
4+
vault-logout:
5+
@scripts/vault_login.py --logout
6+
7+
vault-status:
8+
@scripts/vault_login.py --status

manifester/commands.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,13 @@ def delete(allocations, all_, remove_manifest_file):
6060
uuid=allocation.get("uuid")
6161
)
6262
if remove_manifest_file:
63+
manifester_directory = (
64+
Path(os.environ["MANIFESTER_DIRECTORY"]).resolve()
65+
if "MANIFESTER_DIRECTORY" in os.environ
66+
else Path()
67+
)
6368
Path(
64-
f"{os.environ['MANIFESTER_DIRECTORY']}/manifests/{allocation.get('name')}_manifest.zip"
69+
f"{manifester_directory}/manifests/{allocation.get('name')}_manifest.zip"
6570
).unlink()
6671

6772

manifester/helpers.py

+145
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
"""Defines helper functions used by Manifester."""
22
from collections import UserDict
3+
import json
4+
import os
35
from pathlib import Path
46
import random
7+
import re
8+
import subprocess
9+
import sys
510
import time
611

712
from logzero import logger
813
from requests import HTTPError
914
import yaml
1015

16+
from manifester.logger import setup_logzero
1117
from manifester.settings import settings
1218

19+
setup_logzero(level="info")
20+
21+
1322
RESULTS_LIMIT = 10000
1423

1524

@@ -226,3 +235,139 @@ def __getitem__(self, key):
226235
def __call__(self, *args, **kwargs):
227236
"""Allow MockStub to be used like a function."""
228237
return self
238+
239+
240+
class InvalidVaultURLForOIDC(Exception):
241+
"""Raised if the vault doesn't allow OIDC login."""
242+
243+
244+
class Vault:
245+
"""Helper class for retrieving secrets from HashiCorp Vault."""
246+
247+
HELP_TEXT = (
248+
"The Vault CLI in not installed on this system."
249+
"Please follow https://learn.hashicorp.com/tutorials/vault/getting-started-install to "
250+
"install the Vault CLI."
251+
)
252+
253+
def __init__(self, env_file=".env"):
254+
manifester_directory = Path()
255+
256+
if "MANIFESTER_DIRECTORY" in os.environ:
257+
envar_location = Path(os.environ["MANIFESTER_DIRECTORY"])
258+
if envar_location.is_dir():
259+
manifester_directory = envar_location
260+
self.env_path = manifester_directory.joinpath(env_file)
261+
self.envdata = None
262+
self.vault_enabled = None
263+
264+
def setup(self):
265+
"""Read environment variables from .env."""
266+
if self.env_path.exists():
267+
self.envdata = self.env_path.read_text()
268+
is_enabled = re.findall("^(?:.*\n)*VAULT_ENABLED_FOR_DYNACONF=(.*)", self.envdata)
269+
if is_enabled:
270+
self.vault_enabled = is_enabled[0]
271+
self.export_vault_addr()
272+
273+
def teardown(self):
274+
"""Remove VAULT_ADDR environment variable if present."""
275+
if os.environ.get("VAULT_ADDR") is not None:
276+
del os.environ["VAULT_ADDR"]
277+
278+
def export_vault_addr(self):
279+
"""Set the URL of the Vault server and ensure that the URL is not localhost."""
280+
vaulturl = re.findall("VAULT_URL_FOR_DYNACONF=(.*)", self.envdata)[0]
281+
282+
# Set Vault CLI Env Var
283+
os.environ["VAULT_ADDR"] = vaulturl
284+
285+
# Dynaconf Vault Env Vars
286+
if (
287+
self.vault_enabled
288+
and self.vault_enabled in ["True", "true"]
289+
and "localhost:8200" in vaulturl
290+
):
291+
raise InvalidVaultURLForOIDC(
292+
f"{vaulturl} does not support OIDC login."
293+
"Please set the correct vault URL vault the .env file."
294+
)
295+
296+
def exec_vault_command(self, command: str, **kwargs):
297+
"""Wrap Vault CLI commands for execution.
298+
299+
:param comamnd str: The vault CLI command
300+
:param kwargs dict: Arguments to the subprocess run command to customize the run behavior
301+
"""
302+
COMMAND_NOT_FOUND_EXIT_CODE = 127
303+
vcommand = subprocess.run(command.split(), capture_output=True, **kwargs)
304+
if vcommand.returncode != 0:
305+
verror = str(vcommand.stderr)
306+
if vcommand.returncode == COMMAND_NOT_FOUND_EXIT_CODE:
307+
logger.error(f"Error! {self.HELP_TEXT}")
308+
sys.exit(1)
309+
if vcommand.stderr:
310+
if "Error revoking token" in verror:
311+
logger.info("Token is already revoked")
312+
elif "Error looking up token" in verror:
313+
logger.info("Vault is not logged in")
314+
else:
315+
logger.error(f"Error: {verror}")
316+
return vcommand
317+
318+
def login(self, **kwargs):
319+
"""Authenticate to Vault server and add auth token to .env file."""
320+
if (
321+
self.vault_enabled
322+
and self.vault_enabled in ["True", "true"]
323+
and "VAULT_SECRET_ID_FOR_DYNACONF" not in os.environ
324+
and self.status(**kwargs).returncode != 0
325+
):
326+
logger.info(
327+
"Warning: A browser tab will open for Vault OIDC login. "
328+
"Please close the tab once the sign-in is complete"
329+
)
330+
if (
331+
self.exec_vault_command(command="vault login -method=oidc", **kwargs).returncode
332+
== 0
333+
):
334+
self.exec_vault_command(command="vault token renew -i 10h", **kwargs)
335+
logger.info("Success! Vault OIDC Logged-In and extended for 10 hours!")
336+
# Fetch token
337+
token = self.exec_vault_command("vault token lookup --format json").stdout
338+
token = json.loads(str(token.decode("UTF-8")))["data"]["id"]
339+
# Set new token in .env file
340+
_envdata = re.sub(
341+
".*VAULT_TOKEN_FOR_DYNACONF=.*",
342+
f"VAULT_TOKEN_FOR_DYNACONF={token}",
343+
self.envdata,
344+
)
345+
self.env_path.write_text(_envdata)
346+
logger.info("New OIDC token succesfully added to .env file")
347+
348+
def logout(self):
349+
"""Revoke Vault auth token and remove it from .env file."""
350+
# Teardown - Setting dummy token in env file
351+
_envdata = re.sub(
352+
".*VAULT_TOKEN_FOR_DYNACONF=.*", "# VAULT_TOKEN_FOR_DYNACONF=myroot", self.envdata
353+
)
354+
self.env_path.write_text(_envdata)
355+
vstatus = self.exec_vault_command("vault token revoke -self")
356+
if vstatus.returncode == 0:
357+
logger.info("OIDC token successfully removed from .env file")
358+
359+
def status(self, **kwargs):
360+
"""Check status of Vault auth token."""
361+
vstatus = self.exec_vault_command("vault token lookup", **kwargs)
362+
if vstatus.returncode == 0:
363+
logger.info(str(vstatus.stdout.decode("UTF-8")))
364+
return vstatus
365+
366+
def __enter__(self):
367+
"""Set up Vault context manager."""
368+
self.setup()
369+
return self
370+
371+
def __exit__(self, exc_type, exc_val, exc_tb):
372+
"""Tear down Vault context manager."""
373+
self.teardown()

manifester/manifester.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
from manifester.logger import setup_logzero
2222
from manifester.settings import settings
2323

24-
setup_logzero(level=settings.get("log_level", "info"))
25-
2624

2725
class Manifester:
2826
"""Main Manifester class responsible for generating a manifest from the provided settings."""
@@ -35,6 +33,7 @@ def __init__(
3533
proxies=None,
3634
**kwargs,
3735
):
36+
setup_logzero(level=settings.get("log_level", "info"))
3837
if minimal_init:
3938
self.offline_token = settings.get("offline_token")
4039
self.token_request_url = settings.get("url").get("token_request")

manifester/settings.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@
1414

1515
settings_path = MANIFESTER_DIRECTORY.joinpath("manifester_settings.yaml")
1616
validators = [
17-
# Validator("offline_token", must_exist=True),
17+
Validator("offline_token", must_exist=True),
1818
Validator("simple_content_access", default="enabled"),
1919
Validator("username_prefix", len_min=3),
2020
]
2121
settings = Dynaconf(
2222
settings_file=str(settings_path.absolute()),
2323
ENVVAR_PREFIX_FOR_DYNACONF="MANIFESTER",
24+
load_dotenv=True,
2425
validators=validators,
2526
)
26-
27-
settings.validators.validate()
27+
# settings.validators.validate()

manifester_settings.yaml.example

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
#rhsm-manifester settings
2+
inventory_path: "manifester_inventory.yaml"
23
log_level: "info"
34
offline_token: ""
45
proxies: {"https": ""}
6+
url:
7+
token_request: "https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token"
8+
allocations: "https://api.access.redhat.com/management/v1/allocations"
59
username_prefix: "example_username" # replace value with a unique username
6-
inventory_path: "manifester_inventory.yaml"
710
manifest_category:
811
golden_ticket:
912
# An offline token can be generated at https://access.redhat.com/management/api
1013
offline_token: ""
11-
# Value of sat_version setting should be in the form 'sat-6.10'
12-
sat_version: "sat-6.10"
14+
# Value of sat_version setting should be in the form 'sat-6.14'
15+
sat_version: "sat-6.14"
1316
# golden_ticket manifests should not use a quantity higher than 1 for any subscription
1417
# unless doing so is required for a test.
1518
subscription_data:
@@ -25,7 +28,7 @@ manifest_category:
2528
proxies: {"https": ""}
2629
robottelo_automation:
2730
offline_token: ""
28-
sat_version: "sat-6.10"
31+
sat_version: "sat-6.14"
2932
subscription_data:
3033
- name: "Software Collections and Developer Toolset"
3134
quantity: 3

pyproject.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ classifiers = [
4040
]
4141
dependencies = [
4242
"click",
43-
"dynaconf",
43+
"dynaconf[vault]",
4444
"logzero",
4545
"pytest",
4646
"pyyaml",
@@ -156,6 +156,7 @@ ignore = [
156156
"D407", # Section name underlining
157157
"E731", # do not assign a lambda expression, use a def
158158
"PLR0913", # Too many arguments to function call ({c_args} > {max_args})
159+
"PLW1510", # subprocess.run without an explict `check` argument
159160
"RUF012", # Mutable class attributes should be annotated with typing.ClassVar
160161
"D107", # Missing docstring in __init__
161162
]

scripts/vault_login.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env python
2+
"""Enables and Disables an OIDC token to access secrets from HashiCorp Vault."""
3+
import sys
4+
5+
from manifester.helpers import Vault
6+
7+
if __name__ == "__main__":
8+
with Vault() as vclient:
9+
if sys.argv[-1] == "--login":
10+
vclient.login()
11+
elif sys.argv[-1] == "--status":
12+
vclient.status()
13+
else:
14+
vclient.logout()

0 commit comments

Comments
 (0)