Skip to content
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

Updating with logic for expire token #45

Merged
merged 73 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
32ba37d
Updating with logic for expire token
jboucourt Aug 5, 2024
02a9f7d
Updating version with added fix
jboucourt Aug 5, 2024
b40e417
adding pre release with suffix beta
jboucourt Aug 5, 2024
3555bec
Updating tests
jboucourt Aug 5, 2024
38b9721
Refactoring code and fixing bugs
jboucourt Aug 5, 2024
f328e83
Updating
jboucourt Aug 5, 2024
8420b72
Fixing str convertion issue
jboucourt Aug 5, 2024
265f44f
Handling none cases and adding fail message
jboucourt Aug 6, 2024
9b75d72
Updating
jboucourt Aug 6, 2024
27eed33
Updating
jboucourt Aug 6, 2024
960425f
Updating
jboucourt Aug 6, 2024
74ad6c0
Updating
jboucourt Aug 6, 2024
4bff3b9
Updating
jboucourt Aug 6, 2024
8b270ca
Updating
jboucourt Aug 6, 2024
cb75b8d
Updating
jboucourt Aug 6, 2024
db0b201
Updating
jboucourt Aug 6, 2024
3394686
Updating
jboucourt Aug 6, 2024
ce0aa2a
Updating
jboucourt Aug 6, 2024
94819bc
Updating
jboucourt Aug 6, 2024
287e502
Formatting with yapf
jboucourt Aug 6, 2024
59a8dee
Formatting with yapf
jboucourt Aug 6, 2024
c48c550
Formatting with yapf
jboucourt Aug 6, 2024
0c6cb51
Threshold to expire in 10 min to test
jboucourt Aug 6, 2024
f5994d7
Threshold to expire in 10 min to test
jboucourt Aug 6, 2024
1d3e19e
Threshold to expire in 10 min to test
jboucourt Aug 6, 2024
8824d54
Updating with gestalt beta version
jboucourt Aug 7, 2024
a626abc
Updating to test
jboucourt Aug 12, 2024
8c54bfa
Updating to test
jboucourt Aug 12, 2024
ea0b4ca
Updating to test
jboucourt Aug 12, 2024
fbcfb2d
Updating to test
jboucourt Aug 12, 2024
f7a69b5
Updating to test gestalt beta version
jboucourt Aug 12, 2024
2ebd14e
Updating to test gestalt beta version
jboucourt Aug 12, 2024
47d7966
Updating to test gestalt beta version
jboucourt Aug 12, 2024
d4515d8
Updating to test gestalt beta version
jboucourt Aug 12, 2024
aee3fdd
Adding dateutil dependency
jboucourt Aug 12, 2024
c17080c
Adding dateutil dependency
jboucourt Aug 12, 2024
81acd21
Adding dateutil dependency
jboucourt Aug 12, 2024
d1dd465
Updating with beta test
jboucourt Aug 13, 2024
fed95be
Upgrading version
jboucourt Aug 13, 2024
780a95c
Upgrading version
jboucourt Aug 13, 2024
cc48ad2
Updating
jboucourt Aug 13, 2024
63d963f
Updating
jboucourt Aug 13, 2024
076b0ca
Updating
jboucourt Aug 13, 2024
3452d8a
Updating
jboucourt Aug 13, 2024
3698abc
Updating
jboucourt Aug 13, 2024
b5fbd30
Updating
jboucourt Aug 13, 2024
318502e
Updating tests
jboucourt Aug 13, 2024
2023ad1
Updating tests
jboucourt Aug 13, 2024
ac15655
Updating
jboucourt Aug 13, 2024
4ea6986
Updating
jboucourt Aug 13, 2024
c3a839b
Updating
jboucourt Aug 13, 2024
a6a9481
Updating
jboucourt Aug 13, 2024
2a86d9d
Updating
jboucourt Aug 13, 2024
f202635
Updating
jboucourt Aug 13, 2024
69afcf3
Updating version with beta
jboucourt Aug 14, 2024
28b1537
Removing thread and worker.
jboucourt Aug 15, 2024
f131f06
Updating
jboucourt Aug 15, 2024
ff09477
Updating
jboucourt Aug 15, 2024
68a1164
Updating
jboucourt Aug 15, 2024
b49276c
Updating
jboucourt Aug 15, 2024
f99b826
Updating
jboucourt Aug 15, 2024
6a7b61c
Updating
jboucourt Aug 15, 2024
7b9fa5b
Updating
jboucourt Aug 15, 2024
c8719ec
Updating
jboucourt Aug 15, 2024
5a7d8b0
Updating
jboucourt Aug 15, 2024
958c639
Updating
jboucourt Aug 15, 2024
38175eb
Updating
jboucourt Aug 15, 2024
e2f342c
Updating
jboucourt Aug 15, 2024
1849f57
Updating
jboucourt Aug 15, 2024
4aedae0
Updating
jboucourt Aug 15, 2024
3ce7d3a
Updating
jboucourt Aug 15, 2024
7553fdf
Updating
jboucourt Aug 16, 2024
f4ca139
Updating
jboucourt Aug 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)

## [3.4.2] - 2024-08-05

### Fixed
- Adding logic to check when the token is about to expire to re-connect. This fix cases for services that are running longer that token's ttl without restarting. Causing requests to get a Permission denied error.


## [3.4.1] - 2024-07-12

### Fixed
Expand Down
82 changes: 37 additions & 45 deletions gestalt/vault.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import os
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from queue import Queue
from threading import Thread
from time import sleep
from typing import Any, Dict, List, Optional, Tuple, Union

import hvac # type: ignore
Expand All @@ -12,6 +10,9 @@
from retry.api import retry_call

from gestalt.provider import Provider
from dateutil.parser import isoparse

EXPIRATION_THRESHOLD_HOURS = 1
adisunw marked this conversation as resolved.
Show resolved Hide resolved


class Vault(Provider):
Expand Down Expand Up @@ -39,7 +40,7 @@ def __init__(
self._scheme: str = scheme
self._run_worker = True
self.dynamic_token_queue: Queue[Tuple[str, str, str]] = Queue()
self.kubes_token_queue: Queue[Tuple[str, str, str]] = Queue()
self.kubes_token: Optional[Tuple[str, str, str, datetime]] = None

self._vault_client: Optional[hvac.Client] = None
self._secret_expiry_times: Dict[str, datetime] = dict()
Expand Down Expand Up @@ -91,31 +92,20 @@ def connect(self) -> None:
)

if token is not None:
print("Kubernetes login successful")
kubes_token = (
"kubernetes",
token["data"]["id"],
token["data"]["ttl"],
token["data"]['expire_time'],
)
self.kubes_token_queue.put(kubes_token)
self.kubes_token = kubes_token
except hvac.exceptions.InvalidPath:
raise RuntimeError(
"Gestalt Error: Kubernetes auth couldn't be performed")
except requests.exceptions.ConnectionError:
raise RuntimeError("Gestalt Error: Couldn't connect to Vault")

dynamic_ttl_renew = Thread(
name="dynamic-token-renew",
target=self.worker,
daemon=True,
args=(self.dynamic_token_queue, ),
) # noqa: F841
kubernetes_ttl_renew = Thread(
name="kubes-token-renew",
target=self.worker,
daemon=True,
args=(self.kubes_token_queue, ),
)
kubernetes_ttl_renew.start()
self._is_connected = True

def stop(self) -> None:
Expand Down Expand Up @@ -151,6 +141,9 @@ def get(
key):
return self._secret_values[key]

# verify if the token still valid, in case not, call connect()
self._validate_token_expiration()

try:
response = retry_call(
self.vault_client.read,
Expand Down Expand Up @@ -213,33 +206,32 @@ def _set_secrets_ttl(self, requested_data: Dict[str, Any],
secret_expires_dt = last_vault_rotation_dt + timedelta(seconds=ttl)
self._secret_expiry_times[key] = secret_expires_dt

def worker(self, token_queue: Queue) -> None: # type: ignore
"""
Worker function to renew lease on expiry
"""
try:
while self._run_worker:
if not token_queue.empty():
token_type, token_id, token_duration = token = token_queue.get(
)
if token_type == "kubernetes":
self.vault_client.auth.token.renew(token_id)
print("kubernetes token for the app has been renewed")
elif token_type == "dynamic":
self.vault_client.sys.renew_lease(token_id)
print("dynamic token for the app has been renewed")
token_queue.task_done()
token_queue.put_nowait(token)
sleep((token_duration / 3) * 2)
except hvac.exceptions.InvalidPath:
raise RuntimeError(
"Gestalt Error: The lease path or mount is set incorrectly")
except requests.exceptions.ConnectionError:
raise RuntimeError(
"Gestalt Error: Gestalt couldn't connect to Vault")
except Exception as err:
raise RuntimeError(f"Gestalt Error: {err}")

@property
def scheme(self) -> str:
return self._scheme

def _validate_token_expiration(self) -> None:
if self.kubes_token is not None:
expire_time = self.kubes_token[3]
# Use isoparse to correctly parse the datetime string
expire_time = isoparse(expire_time)
Temurson marked this conversation as resolved.
Show resolved Hide resolved

# Ensure the parsed time is in UTC
if expire_time.tzinfo is None:
expire_time = expire_time.replace(tzinfo=timezone.utc)
else:
expire_time = expire_time.astimezone(timezone.utc)

current_time = datetime.now(timezone.utc)
# in hours
delta_time = (expire_time - current_time).total_seconds() / 3600

if delta_time < EXPIRATION_THRESHOLD_HOURS:
Temurson marked this conversation as resolved.
Show resolved Hide resolved
print(f"Re-authenticating with vault")
self.connect()
jboucourt marked this conversation as resolved.
Show resolved Hide resolved
else:
print(f"Token still valid for: {delta_time} hours")
else:
print(
f"Can't reconnect, token information: {self.kubes_token}, not valid"
)
1 change: 1 addition & 0 deletions requirements.test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ retry==0.9.2
types-retry==0.9.9
jsonpath-ng==1.5.3
pytest-asyncio==0.19.0
python-dateutil>=2.8.0
Temurson marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ hvac>=1.0.2,<1.1.0
jsonpath-ng==1.5.3
retry==0.9.2
types-retry==0.9.9
python-dateutil>=2.8.0
types-python-dateutil>=0.1.0
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def readme():

setup(
name="gestalt-cfg",
version="3.4.1",
version="3.4.2",
description="A sensible configuration library for Python",
long_description=readme(),
long_description_content_type="text/markdown",
Expand Down
5 changes: 2 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def request(self, *_, **__):
"rotation_period": 60,
"ttl": 0,
"username": "foo",
"expire_time": "2024-08-15T22:04:49.82981496Z"
},
"wrap_info": None,
"warnings": None,
Expand Down Expand Up @@ -79,9 +80,7 @@ def nested_setup():
def mock_vault_workers():
mock_dynamic_renew = Mock()
mock_k8s_renew = Mock()
with patch("gestalt.vault.Thread",
side_effect=[mock_dynamic_renew, mock_k8s_renew]):
yield (mock_dynamic_renew, mock_k8s_renew)
return (mock_dynamic_renew, mock_k8s_renew)


@pytest.fixture
Expand Down
86 changes: 15 additions & 71 deletions tests/test_gestalt.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,79 +557,23 @@ def test_set_vault_key(nested_setup):
assert secret == "ref+vault://secret/data/testnested#.slack.token"


def test_vault_lazy_connect(mock_vault_workers, mock_vault_k8s_auth):
def test_vault_lazy_connect(mock_vault_k8s_auth):
with patch("gestalt.vault.hvac.Client") as mock_client:
v = Vault(role="test-role", jwt="test-jwt")
v.vault_client.auth.token.lookup_self = Mock(
return_value={
"data": {
"id": "foo",
"ttl": "foo",
"expire_time": "2024-08-15T22:04:49.82981496Z"
}
})
assert not v._is_connected
v.get("foo", "foo", ".foo")
assert v._is_connected
mock_client().auth.token.lookup_self.assert_called()


def test_vault_worker_dynamic(mock_vault_workers, mock_vault_k8s_auth):
mock_dynamic_renew, mock_k8s_renew = mock_vault_workers

mock_sleep = None

def except_once(self, **kwargs):
# side effect used to exit the worker loop after one call
if mock_sleep.call_count == 1:
raise hvac.exceptions.VaultError("some error")

with patch("gestalt.vault.sleep", side_effect=except_once,
autospec=True) as mock_sleep:
with patch("gestalt.vault.hvac.Client") as mock_client:
v = Vault(role="test-role", jwt="test-jwt")
v.connect()

mock_k8s_renew.start.assert_called()

test_token_queue = Queue(maxsize=0)
test_token_queue.put(("dynamic", 1, 100))

with pytest.raises(RuntimeError):
v.worker(test_token_queue)

mock_sleep.assert_called()
mock_client().sys.renew_lease.assert_called()
mock_k8s_renew.start.assert_called_once()

mock_dynamic_renew.stop()
mock_k8s_renew.stop()


def test_vault_worker_k8s(mock_vault_workers):
mock_dynamic_renew, mock_k8s_renew = mock_vault_workers

mock_sleep = None

def except_once(self, **kwargs):
# side effect used to exit the worker loop after one call
if mock_sleep.call_count == 1:
raise hvac.exceptions.VaultError("some error")

with patch("gestalt.vault.sleep", side_effect=except_once,
autospec=True) as mock_sleep:
with patch("gestalt.vault.hvac.Client") as mock_client:
v = Vault(role="test-role", jwt="test-jwt")
v.connect()

mock_k8s_renew.start.assert_called()

test_token_queue = Queue(maxsize=0)
test_token_queue.put(("kubernetes", 1, 100))

with pytest.raises(RuntimeError):
v.worker(test_token_queue)

mock_sleep.assert_called()
mock_client().auth.token.renew.assert_called()
mock_k8s_renew.start.assert_called_once()

mock_dynamic_renew.stop()
mock_k8s_renew.stop()


def test_vault_start_dynamic_lease(mock_vault_workers):
mock_response = {
"lease_id": "1",
Expand All @@ -643,11 +587,11 @@ def test_vault_start_dynamic_lease(mock_vault_workers):
return_value=mock_response)
with mock_vault_client_patch as mock_vault_client_read:
mock_dynamic_token_queue = Mock()
mock_kube_token_queue = Mock()
mock_kube_token = ("kubernetes", "hvs.CAESIEkz-UO8yvfC8v", "2764799")
with patch(
"gestalt.vault.Queue",
side_effect=[mock_dynamic_token_queue, mock_kube_token_queue],
) as mock_queues:
side_effect=[mock_dynamic_token_queue],
) as mock_queue:
v = Vault(role=None, jwt=None)
g = gestalt.Gestalt()
g.add_config_file("./tests/testvault/testmount.json")
Expand All @@ -657,9 +601,9 @@ def test_vault_start_dynamic_lease(mock_vault_workers):

mock_vault_client_read.assert_called()
mock_dynamic_token_queue.put_nowait.assert_called()
assert mock_kube_token == ("kubernetes", "hvs.CAESIEkz-UO8yvfC8v",
"2764799")

mock_vault_client_read.stop()
mock_dynamic_token_queue.stop()
mock_kube_token_queue.stop()
mock_queues.stop()
mock_vault_client_read.stop()
mock_queue.stop()
Loading