feat: add update
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
from src.infrastructure.vault.utils import read_kv2_secret, create_hvac_client
|
||||
from src.infrastructure.vault.client import VaultClient
|
||||
from src.infrastructure.vault.utils import create_hvac_client, read_kv2_secret
|
||||
from src.infrastructure.vault.keys import JwtKeyStore
|
||||
from src.infrastructure.vault.scheduler import start_jwt_keys_scheduler
|
||||
75
src/infrastructure/vault/client.py
Normal file
75
src/infrastructure/vault/client.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import hvac
|
||||
|
||||
|
||||
def _vault_token_renew_failed(exception: Exception) -> bool:
|
||||
if isinstance(exception, (hvac.exceptions.Forbidden, hvac.exceptions.Unauthorized)):
|
||||
return True
|
||||
message = getattr(exception, 'message', None) or str(exception)
|
||||
if isinstance(message, str):
|
||||
lower = message.lower()
|
||||
return 'permission denied' in lower or 'invalid token' in lower or '403' in lower
|
||||
return False
|
||||
|
||||
|
||||
class VaultClient:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
addr: str,
|
||||
role_id: str,
|
||||
secret_id: str,
|
||||
namespace: str | None,
|
||||
mount_point: str,
|
||||
) -> None:
|
||||
self._mount_point = mount_point
|
||||
self._addr = addr
|
||||
self._role_id = role_id
|
||||
self._secret_id = secret_id
|
||||
self._namespace = namespace
|
||||
self._client = hvac.Client(url=addr, namespace=namespace)
|
||||
self._approle_login()
|
||||
|
||||
def _approle_login(self) -> None:
|
||||
self._client.auth.approle.login(role_id=self._role_id, secret_id=self._secret_id)
|
||||
|
||||
def _renew_or_login(self) -> None:
|
||||
try:
|
||||
self._client.auth.token.renew_self()
|
||||
except Exception:
|
||||
self._approle_login()
|
||||
|
||||
def read_secret(self, path: str) -> dict[str, Any]:
|
||||
for attempt in range(2):
|
||||
try:
|
||||
secret = self._client.secrets.kv.v2.read_secret_version(
|
||||
path=path,
|
||||
mount_point=self._mount_point,
|
||||
)
|
||||
return dict(secret.get('data', {}).get('data', {}))
|
||||
except Exception as exc:
|
||||
if attempt == 0 and _vault_token_renew_failed(exc):
|
||||
self._renew_or_login()
|
||||
continue
|
||||
raise
|
||||
|
||||
def read_secret_optional(self, path: str) -> dict[str, Any]:
|
||||
if not path:
|
||||
return {}
|
||||
try:
|
||||
return self.read_secret(path)
|
||||
except (hvac.exceptions.InvalidPath, hvac.exceptions.Forbidden, hvac.exceptions.Unauthorized):
|
||||
return {}
|
||||
result: dict[str, Any] = {}
|
||||
for path in paths:
|
||||
if not path:
|
||||
continue
|
||||
try:
|
||||
result.update(self.read_secret(path))
|
||||
except (hvac.exceptions.InvalidPath, hvac.exceptions.Forbidden, hvac.exceptions.Unauthorized):
|
||||
continue
|
||||
return result
|
||||
@@ -3,7 +3,7 @@ import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from src.application.domain.dto import JwtPublicKeySet, JwtPublicKey
|
||||
from src.application.domain.exceptions import ApplicationException
|
||||
from src.infrastructure.vault import create_hvac_client, read_kv2_secret
|
||||
from src.infrastructure.vault.client import VaultClient
|
||||
|
||||
|
||||
class JwtKeyStore:
|
||||
@@ -19,21 +19,25 @@ class JwtKeyStore:
|
||||
self,
|
||||
*,
|
||||
vault_addr: str,
|
||||
vault_token: str,
|
||||
vault_role_id: str,
|
||||
vault_secret_id: str,
|
||||
vault_namespace: str | None,
|
||||
mount_point: str,
|
||||
kid_path: str = 'jwt/kid',
|
||||
kids_prefix: str = 'jwt/kids',
|
||||
timeout_seconds: int = 5,
|
||||
refresh_ttl_seconds: int = 60,
|
||||
):
|
||||
if getattr(self, '_initialized', False):
|
||||
return
|
||||
|
||||
self._vault_addr = vault_addr
|
||||
self._vault_token = vault_token
|
||||
self._timeout = timeout_seconds
|
||||
self._vault_client = VaultClient(
|
||||
addr=vault_addr,
|
||||
role_id=vault_role_id,
|
||||
secret_id=vault_secret_id,
|
||||
namespace=vault_namespace,
|
||||
mount_point=mount_point,
|
||||
)
|
||||
|
||||
self._mount = mount_point
|
||||
self._kid_path = kid_path
|
||||
self._kids_prefix = kids_prefix
|
||||
|
||||
@@ -52,29 +56,23 @@ class JwtKeyStore:
|
||||
return cls._instance
|
||||
|
||||
def _read_keyset_sync(self) -> JwtPublicKeySet:
|
||||
client = create_hvac_client(url=self._vault_addr, token=self._vault_token, timeout=self._timeout)
|
||||
|
||||
kids = read_kv2_secret(client=client, mount_point=self._mount, path=self._kid_path)
|
||||
kids = self._vault_client.read_secret(self._kid_path)
|
||||
active_kid = kids.get('active')
|
||||
previous_kid = kids.get('previous')
|
||||
|
||||
if not active_kid:
|
||||
raise RuntimeError('Vault jwt/kid secret missing "active"')
|
||||
|
||||
active = self._read_public_key_sync(client, str(active_kid))
|
||||
active = self._read_public_key_sync(str(active_kid))
|
||||
|
||||
previous = None
|
||||
if previous_kid and previous_kid != active_kid:
|
||||
previous = self._read_public_key_sync(client, str(previous_kid))
|
||||
previous = self._read_public_key_sync(str(previous_kid))
|
||||
|
||||
return JwtPublicKeySet(active=active, previous=previous)
|
||||
|
||||
def _read_public_key_sync(self, client, kid: str) -> JwtPublicKey:
|
||||
data = read_kv2_secret(
|
||||
client=client,
|
||||
mount_point=self._mount,
|
||||
path=f'{self._kids_prefix}/{kid}',
|
||||
)
|
||||
def _read_public_key_sync(self, kid: str) -> JwtPublicKey:
|
||||
data = self._vault_client.read_secret(f'{self._kids_prefix}/{kid}')
|
||||
pub = data.get('public_key')
|
||||
if not pub:
|
||||
raise RuntimeError(f'Vault jwt/kids/{kid} missing public_key')
|
||||
@@ -110,4 +108,4 @@ class JwtKeyStore:
|
||||
if age >= self._refresh_ttl_seconds:
|
||||
return await self.refresh()
|
||||
|
||||
return ks
|
||||
return ks
|
||||
|
||||
Reference in New Issue
Block a user