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

Vault Support (GSI-973) #8

Merged
merged 15 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
2 changes: 1 addition & 1 deletion .devcontainer/.dev_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ token_hashes:
- 7ad83b6b9183c91674eec897935bc154ba9ff9704f8be0840e77f476b5062b6e
vault_token: "dev-token"
vault_url: "http://vault:8200"
vault_path: sms

db_connection_str: mongodb://mongodb:27017
db_prefix: "test_"
db_permissions:
Expand Down
15 changes: 0 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,21 +145,6 @@ The service requires the following configuration parameters:
```


- **`vault_path`** *(string, required)*: Path for the Vault.


Examples:

```json
"sms"
```


```json
"ekss"
```


- **`token_hashes`** *(array, required)*: List of token hashes corresponding to the tokens that can be used to authenticate calls to this service. Hashes are made with SHA-256.

- **Items** *(string)*
Expand Down
10 changes: 0 additions & 10 deletions config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -204,15 +204,6 @@
"title": "Vault Token",
"type": "string"
},
"vault_path": {
"description": "Path for the Vault",
"examples": [
"sms",
"ekss"
],
"title": "Vault Path",
"type": "string"
},
"token_hashes": {
"description": "List of token hashes corresponding to the tokens that can be used to authenticate calls to this service. Hashes are made with SHA-256.",
"examples": [
Expand Down Expand Up @@ -438,7 +429,6 @@
"object_storages",
"vault_url",
"vault_token",
"vault_path",
"token_hashes",
"db_prefix",
"db_connection_str"
Expand Down
1 change: 0 additions & 1 deletion example_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ service_instance_id: '1'
service_name: sms
token_hashes:
- 7ad83b6b9183c91674eec897935bc154ba9ff9704f8be0840e77f476b5062b6e
vault_path: sms
vault_token: dev-token
vault_url: http://vault:8200
workers: 1
42 changes: 38 additions & 4 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -383,22 +383,50 @@ paths:
tags:
- StateManagementService
- sms-s3
/secrets/:
/secrets/{vault_path}:
delete:
description: Delete one or more secrets from the vault
description: Delete all secrets from the specified vault.
operationId: delete_secrets
parameters:
- in: path
name: vault_path
required: true
schema:
description: The path to the vault
examples:
- ekss
- sms
title: Vault Path
type: string
responses:
'204':
description: Successful Response
'422':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
security:
- HTTPBearer: []
summary: Delete all secrets from the vault
summary: Delete all secrets from the specified vault.
tags:
- StateManagementService
- sms-vault
get:
description: Returns a list of secrets in the vault
description: Returns a list of secrets in the specified vault
operationId: get_secrets
parameters:
- in: path
name: vault_path
required: true
schema:
description: The path to the vault
examples:
- ekss
- sms
title: Vault Path
type: string
responses:
'200':
content:
Expand All @@ -409,6 +437,12 @@ paths:
title: Response Get Secrets
type: array
description: Successful Response
'422':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
security:
- HTTPBearer: []
summary: Returns a list of secrets in the vault
Expand Down
21 changes: 14 additions & 7 deletions src/sms/adapters/inbound/fastapi_/routers/secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from typing import Annotated

from fastapi import APIRouter, HTTPException, status
from pydantic import Field

from sms.adapters.inbound.fastapi_ import dummies
from sms.adapters.inbound.fastapi_.http_authorization import (
Expand All @@ -27,9 +28,13 @@

secrets_router = APIRouter()

vault_path_field = Field(
default=..., description="The path to the vault", examples=["ekss", "sms"]
)


@secrets_router.get(
"/",
"/{vault_path}",
operation_id="get_secrets",
summary="Returns a list of secrets in the vault",
status_code=status.HTTP_200_OK,
Expand All @@ -38,26 +43,28 @@
async def get_secrets(
secrets_handler: dummies.SecretsHandlerPortDummy,
_token: Annotated[TokenAuthContext, require_token],
vault_path: Annotated[str, vault_path_field],
):
"""Returns a list of secrets in the vault"""
"""Returns a list of secrets in the specified vault"""
try:
return secrets_handler.get_secrets()
return secrets_handler.get_secrets(vault_path)
except Exception as exc:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) from exc


@secrets_router.delete(
"/",
"/{vault_path}",
operation_id="delete_secrets",
summary="Delete all secrets from the vault",
summary="""Delete all secrets from the specified vault.""",
status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_secrets(
secrets_handler: dummies.SecretsHandlerPortDummy,
_token: Annotated[TokenAuthContext, require_token],
vault_path: Annotated[str, vault_path_field],
):
"""Delete one or more secrets from the vault"""
"""Delete all secrets from the specified vault."""
try:
secrets_handler.delete_secrets()
secrets_handler.delete_secrets(vault_path)
except Exception as exc:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) from exc
27 changes: 11 additions & 16 deletions src/sms/core/secrets_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ class VaultConfig(BaseSettings):
vault_token: str = Field(
default=..., description="Token for the Vault", examples=["dev-token"]
)
vault_path: str = Field(
default=..., description="Path for the Vault", examples=["sms", "ekss"]
)


class SecretsHandler(SecretsHandlerPort):
Expand All @@ -52,31 +49,29 @@ def client(self) -> HvacClient:
"""Return an instance of a vault client"""
return HvacClient(self._config.vault_url, self._config.vault_token)

def get_secrets(self) -> list[str]:
"""Return the IDs of all secrets in the vault."""
def get_secrets(self, vault_path: str) -> list[str]:
"""Return the IDs of all secrets in the specified vault."""
try:
secrets = self.client.secrets.kv.v2.list_secrets(
path=self._config.vault_path
)
secrets = self.client.secrets.kv.v2.list_secrets(path=vault_path)
secret_ids = secrets["data"]["keys"]
return secret_ids
except InvalidPath:
msg = (
"Invalid path error when fetching secrets. The path might be invalid,"
+ " or no secrets may exist."
"Invalid path error when fetching secrets. The path, '%s', might"
+ " be invalid, or no secrets may exist."
)
log.warning(msg)
log.warning(msg, vault_path)
return []

def delete_secrets(self):
"""Delete all secrets from the vault."""
secrets = self.get_secrets()
def delete_secrets(self, vault_path: str):
"""Delete all secrets from the specified vault."""
secrets = self.get_secrets(vault_path)

if not secrets:
log.info("No secrets to delete")
log.info("No secrets to delete from vault path '%s'", vault_path)
return

for secret in secrets:
self.client.secrets.kv.v2.delete_metadata_and_all_versions(
path=f"{self._config.vault_path}/{secret}"
path=f"{vault_path}/{secret}"
)
8 changes: 4 additions & 4 deletions src/sms/ports/inbound/secrets_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ class SecretsHandlerPort(ABC):
"""A class to interact with a HashiCorp Vault."""

@abstractmethod
def get_secrets(self) -> list[str]:
"""Return the IDs of all secrets in the vault."""
def get_secrets(self, vault_path: str) -> list[str]:
"""Returns a list of secrets in the specified vault"""

@abstractmethod
def delete_secrets(self):
"""Delete all secrets from the vault."""
def delete_secrets(self, vault_path: str) -> None:
"""Delete all secrets from the specified vault."""
24 changes: 9 additions & 15 deletions tests/fixtures/dummies.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from dataclasses import dataclass

from hexkit.custom_types import JsonObject
from hvac.exceptions import InvalidPath

from sms.models import Criteria, UpsertionDetails
from sms.ports.inbound.docs_handler import DocsHandlerPort
Expand All @@ -28,27 +27,22 @@
class DummySecretsHandler(SecretsHandlerPort):
"""Dummy SecretsHandler implementation for testing.

It can be set to fail when `get_secrets` is called to mimic an error.
`secrets` is a dictionary that maps vault paths to lists of secrets.
"""

def __init__(
self, secrets: list[str] | None = None, fail_on_get_secrets: bool = False
self,
secrets: dict[str, list[str]] | None = None,
):
self.secrets = secrets if secrets else []
self.fail_on_get_secrets = fail_on_get_secrets

def get_secrets(self) -> list[str]:
"""Get all secrets currently stored.
self.secrets = secrets if secrets else {}

If `fail_on_get_secrets` is set, it will raise an `InvalidPath` error.
"""
if self.fail_on_get_secrets:
raise InvalidPath("Testing failure")
return self.secrets
def get_secrets(self, vault_path: str) -> list[str]:
"""Get all secrets currently stored for the specified vault."""
return self.secrets.get(vault_path, [])

def delete_secrets(self) -> None:
def delete_secrets(self, vault_path: str) -> None:
"""Delete all secrets stored in the vault."""
self.secrets = []
self.secrets.pop(vault_path, None)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would also remove the key from the dict.
Is this intended?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed the whole dummy class and replaced it with Mock in 536198b



@dataclass
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/test_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ token_hashes:
- 7ad83b6b9183c91674eec897935bc154ba9ff9704f8be0840e77f476b5062b6e
vault_token: "dev-token"
vault_url: "http://vault:8200"
vault_path: sms

db_connection_str: mongodb://mongodb:27017
db_prefix: "test_"
db_permissions:
Expand Down
29 changes: 20 additions & 9 deletions tests/fixtures/vault.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,34 +29,46 @@
DEFAULT_URL = "http://0.0.0.0:8200"
DEFAULT_PORT = 8200
DEFAULT_TOKEN = "dev-token"
DEFAULT_VAULT_PATH = "sms"


class VaultFixture:
"""Contains initialized vault client"""
"""Contains initialized vault client.

When a vault secret is stored, the vault path is stored in vaults_used.
"""

def __init__(self, config: VaultConfig):
self.config = config
self.vaults_used: set[str] = set()

def store_secret(self, key: str):
def store_secret(self, *, key: str, vault_path: str = DEFAULT_VAULT_PATH):
"""Store a secret in vault"""
client = hvac.Client(url=self.config.vault_url, token=DEFAULT_TOKEN)

client.secrets.kv.v2.create_or_update_secret(
path=f"{self.config.vault_path}/{key}",
path=f"{vault_path}/{key}",
secret={key: f"secret_for_{key}"},
)
self.vaults_used.add(vault_path)

def delete_all_secrets(self):
def delete_all_secrets(self, vault_path: str):
"""Remove all secrets from the vault to reset for next test"""
client = hvac.Client(url=self.config.vault_url, token=DEFAULT_TOKEN)

with suppress(InvalidPath):
secrets = client.secrets.kv.v2.list_secrets(path=self.config.vault_path)
secrets = client.secrets.kv.v2.list_secrets(path=vault_path)
for key in secrets["data"]["keys"]:
client.secrets.kv.v2.delete_metadata_and_all_versions(
path=f"{self.config.vault_path}/{key}"
path=f"{vault_path}/{key}"
)

def reset(self):
"""Reset the vault for next test"""
for vault_path in self.vaults_used:
self.delete_all_secrets(vault_path)
self.vaults_used.clear()


class VaultContainer(DockerContainer):
"""A hashi corp vault container for testing."""
Expand Down Expand Up @@ -84,7 +96,6 @@ def vault_container_fixture() -> Generator[VaultContainerFixture, None, None]:
port = vault_container.get_exposed_port(DEFAULT_PORT)
vault_container.config = VaultConfig(
vault_url=f"http://{host}:{port}",
vault_path="sms",
vault_token=DEFAULT_TOKEN,
)

Expand All @@ -93,11 +104,11 @@ def vault_container_fixture() -> Generator[VaultContainerFixture, None, None]:
yield vault_container


@pytest.fixture(scope="function")
@pytest.fixture(scope="function", name="vault")
def vault_fixture(
vault_container: VaultContainerFixture,
) -> Generator[VaultFixture, None, None]:
"""Fixture function to produce a VaultFixture"""
vault = VaultFixture(config=vault_container.config)
vault.delete_all_secrets()
vault.reset()
yield vault
Loading