Write new secret manager using existing RSA logic
This commit is contained in:
1
tests/integration/admin/__init__.py
Normal file
1
tests/integration/admin/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
||||
67
tests/integration/admin/base.py
Normal file
67
tests/integration/admin/base.py
Normal file
@ -0,0 +1,67 @@
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
import httpx
|
||||
|
||||
from sshecret.backend import Client
|
||||
|
||||
from sshecret.crypto import generate_private_key, generate_public_key_string
|
||||
|
||||
from ..types import AdminServer
|
||||
|
||||
|
||||
def make_test_key() -> str:
|
||||
"""Generate a test key."""
|
||||
private_key = generate_private_key()
|
||||
return generate_public_key_string(private_key.public_key())
|
||||
|
||||
|
||||
class BaseAdminTests:
|
||||
"""Base admin test class."""
|
||||
|
||||
@asynccontextmanager
|
||||
async def http_client(
|
||||
self, admin_server: AdminServer, authenticate: bool = True
|
||||
) -> AsyncIterator[httpx.AsyncClient]:
|
||||
"""Run a client towards the admin rest api."""
|
||||
admin_url, credentials = admin_server
|
||||
username, password = credentials
|
||||
headers: dict[str, str] | None = None
|
||||
if authenticate:
|
||||
async with httpx.AsyncClient(base_url=admin_url) as client:
|
||||
|
||||
response = await client.post(
|
||||
"api/v1/token", data={"username": username, "password": password}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "access_token" in data
|
||||
token = data["access_token"]
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
async with httpx.AsyncClient(base_url=admin_url, headers=headers) as client:
|
||||
yield client
|
||||
|
||||
async def create_client(
|
||||
self,
|
||||
admin_server: AdminServer,
|
||||
name: str,
|
||||
public_key: str | None = None,
|
||||
) -> Client:
|
||||
"""Create a client."""
|
||||
if not public_key:
|
||||
public_key = make_test_key()
|
||||
|
||||
new_client = {
|
||||
"name": name,
|
||||
"public_key": public_key,
|
||||
"sources": ["192.0.2.0/24"],
|
||||
}
|
||||
|
||||
async with self.http_client(admin_server, True) as http_client:
|
||||
response = await http_client.post("api/v1/clients/", json=new_client)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
client = Client.model_validate(data)
|
||||
|
||||
return client
|
||||
@ -1,76 +1,12 @@
|
||||
"""Tests of the admin interface."""
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
import allure
|
||||
import pytest
|
||||
|
||||
import httpx
|
||||
|
||||
from allure_commons.types import Severity
|
||||
|
||||
from sshecret.backend import Client
|
||||
|
||||
from sshecret.crypto import generate_private_key, generate_public_key_string
|
||||
|
||||
from .types import AdminServer
|
||||
|
||||
|
||||
def make_test_key() -> str:
|
||||
"""Generate a test key."""
|
||||
private_key = generate_private_key()
|
||||
return generate_public_key_string(private_key.public_key())
|
||||
|
||||
|
||||
class BaseAdminTests:
|
||||
"""Base admin test class."""
|
||||
|
||||
@asynccontextmanager
|
||||
async def http_client(
|
||||
self, admin_server: AdminServer, authenticate: bool = True
|
||||
) -> AsyncIterator[httpx.AsyncClient]:
|
||||
"""Run a client towards the admin rest api."""
|
||||
admin_url, credentials = admin_server
|
||||
username, password = credentials
|
||||
headers: dict[str, str] | None = None
|
||||
if authenticate:
|
||||
async with httpx.AsyncClient(base_url=admin_url) as client:
|
||||
|
||||
response = await client.post(
|
||||
"api/v1/token", data={"username": username, "password": password}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "access_token" in data
|
||||
token = data["access_token"]
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
async with httpx.AsyncClient(base_url=admin_url, headers=headers) as client:
|
||||
yield client
|
||||
|
||||
async def create_client(
|
||||
self,
|
||||
admin_server: AdminServer,
|
||||
name: str,
|
||||
public_key: str | None = None,
|
||||
) -> Client:
|
||||
"""Create a client."""
|
||||
if not public_key:
|
||||
public_key = make_test_key()
|
||||
|
||||
new_client = {
|
||||
"name": name,
|
||||
"public_key": public_key,
|
||||
"sources": ["192.0.2.0/24"],
|
||||
}
|
||||
|
||||
async with self.http_client(admin_server, True) as http_client:
|
||||
response = await http_client.post("api/v1/clients/", json=new_client)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
client = Client.model_validate(data)
|
||||
|
||||
return client
|
||||
from ..types import AdminServer
|
||||
from .base import BaseAdminTests
|
||||
|
||||
|
||||
@allure.title("Admin API")
|
||||
@ -196,7 +132,9 @@ class TestAdminApiSecrets(BaseAdminTests):
|
||||
assert "testclient" in data["clients"]
|
||||
|
||||
@allure.title("Test adding a secret with automatic value")
|
||||
@allure.description("Test that we can add a secret where we let the system come up with the value of a given length.")
|
||||
@allure.description(
|
||||
"Test that we can add a secret where we let the system come up with the value of a given length."
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_secret_auto(self, admin_server: AdminServer) -> None:
|
||||
"""Test adding a secret with an auto-generated value."""
|
||||
634
tests/integration/admin/test_secret_manager.py
Normal file
634
tests/integration/admin/test_secret_manager.py
Normal file
@ -0,0 +1,634 @@
|
||||
"""Test secret manager.
|
||||
|
||||
This package tests the rewritten secret manager system.
|
||||
|
||||
This is technically an integration test, as it requires the other subsystems to
|
||||
run, but it uses the internal API rather than the exposed routes.
|
||||
"""
|
||||
|
||||
import allure
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from sshecret_admin.core.settings import AdminServerSettings
|
||||
from sshecret_admin.services.models import SecretGroup
|
||||
from sshecret_admin.services.secret_manager import (
|
||||
password_manager_context,
|
||||
AsyncSecretContext,
|
||||
InvalidSecretNameError,
|
||||
InvalidGroupNameError,
|
||||
)
|
||||
from sshecret_admin.auth.models import Base, PasswordDB
|
||||
from sshecret_admin.services.master_password import setup_master_password
|
||||
|
||||
# -------- global parameter sets start here -------- #
|
||||
|
||||
|
||||
# -------- Fixtures start here -------- #
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(autouse=True)
|
||||
async def create_admin_db(admin_server_settings: AdminServerSettings) -> None:
|
||||
"""Create the database."""
|
||||
engine = create_engine(admin_server_settings.admin_db)
|
||||
Base.metadata.create_all(engine)
|
||||
encr_master_password = setup_master_password(
|
||||
settings=admin_server_settings, regenerate=True
|
||||
)
|
||||
|
||||
with Session(engine) as session:
|
||||
pwdb = PasswordDB(id=1, encrypted_password=encr_master_password)
|
||||
session.add(pwdb)
|
||||
session.commit()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture()
|
||||
async def secrets_manager(admin_server_settings: AdminServerSettings):
|
||||
"""Test that the context manager can be created."""
|
||||
async with password_manager_context(
|
||||
admin_server_settings, "TEST", "127.0.0.1"
|
||||
) as manager:
|
||||
yield manager
|
||||
|
||||
|
||||
# -------- Tests start here -------- #
|
||||
|
||||
|
||||
@allure.title("Adding entries")
|
||||
@pytest.mark.parametrize("name,secret", [("testentry", "testsecret")])
|
||||
class TestSecretsAddEntry:
|
||||
"""Tests for the add_entry method."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_entry(
|
||||
self,
|
||||
secrets_manager: AsyncSecretContext,
|
||||
name: str,
|
||||
secret: str,
|
||||
) -> None:
|
||||
"""Test add entry.
|
||||
|
||||
This tests add_entry and get_secret
|
||||
"""
|
||||
await secrets_manager.add_entry(name, secret)
|
||||
stored_secret = await secrets_manager.get_secret(name)
|
||||
assert stored_secret == secret
|
||||
|
||||
async def test_add_entry_duplicate(
|
||||
self,
|
||||
secrets_manager: AsyncSecretContext,
|
||||
name: str,
|
||||
secret: str,
|
||||
) -> None:
|
||||
"""Test adding an entry twice."""
|
||||
await secrets_manager.add_entry(name, secret)
|
||||
stored_secret = await secrets_manager.get_secret(name)
|
||||
assert stored_secret == secret
|
||||
|
||||
with pytest.raises(InvalidSecretNameError):
|
||||
await secrets_manager.add_entry(name, secret)
|
||||
|
||||
async def test_add_entry_with_group(
|
||||
self,
|
||||
secrets_manager: AsyncSecretContext,
|
||||
name: str,
|
||||
secret: str,
|
||||
) -> None:
|
||||
"""Test adding a secret with a group."""
|
||||
group = "testgroup"
|
||||
await secrets_manager.add_group(group)
|
||||
await secrets_manager.add_entry(name, secret, group_path=group)
|
||||
result = await secrets_manager.get_entry_group(name)
|
||||
assert result == group
|
||||
|
||||
async def test_add_entry_with_nonexisting_group(
|
||||
self,
|
||||
secrets_manager: AsyncSecretContext,
|
||||
name: str,
|
||||
secret: str,
|
||||
) -> None:
|
||||
"""Test adding a secret where the group does not exist."""
|
||||
group = "testgroup"
|
||||
with pytest.raises(InvalidGroupNameError):
|
||||
await secrets_manager.add_entry(name, secret, group_path=group)
|
||||
|
||||
async def test_add_entry_with_deep_path(
|
||||
self,
|
||||
secrets_manager: AsyncSecretContext,
|
||||
name: str,
|
||||
secret: str,
|
||||
) -> None:
|
||||
"""Test adding a secret to a nested group with a path specification."""
|
||||
await secrets_manager.add_group("root")
|
||||
await secrets_manager.add_group("nested", parent_group="root")
|
||||
await secrets_manager.add_entry(name, secret, group_path="/root/nested")
|
||||
group = await secrets_manager.get_secret_group("/root/nested")
|
||||
assert group is not None
|
||||
assert name in group.entries
|
||||
|
||||
async def test_overwrite_secret(
|
||||
self, secrets_manager: AsyncSecretContext, name: str, secret: str
|
||||
) -> None:
|
||||
"""Test overwriting a secret."""
|
||||
await secrets_manager.add_entry(name, secret)
|
||||
stored_secret = await secrets_manager.get_secret(name)
|
||||
assert stored_secret == secret
|
||||
|
||||
new_secret = "newsecret"
|
||||
await secrets_manager.add_entry(name, new_secret, overwrite=True)
|
||||
|
||||
stored_secret = await secrets_manager.get_secret(name)
|
||||
assert stored_secret == new_secret
|
||||
|
||||
|
||||
@allure.title("Creating groups")
|
||||
class TestSecretGroupCreation:
|
||||
"""Test secret groups."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"group_name", ["testgroup", "long group name with spaces", "blåbærgrød"]
|
||||
)
|
||||
@allure.title("Add a group name {group_name}")
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_group(
|
||||
self,
|
||||
secrets_manager: AsyncSecretContext,
|
||||
group_name: str,
|
||||
) -> None:
|
||||
"""Get adding a group."""
|
||||
await secrets_manager.add_group(group_name)
|
||||
groups = await secrets_manager.get_secret_groups()
|
||||
assert len(groups) == 1
|
||||
assert groups[0].name == group_name
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_group_with_parent(
|
||||
self, secrets_manager: AsyncSecretContext
|
||||
) -> None:
|
||||
"""Test add a group with a parent group."""
|
||||
parent_name = "parent"
|
||||
child_name = "child"
|
||||
await secrets_manager.add_group(parent_name)
|
||||
await secrets_manager.add_group(child_name, parent_group=parent_name)
|
||||
parent_group = await secrets_manager.get_secret_group(f"/parent")
|
||||
assert parent_group is not None
|
||||
assert len(parent_group.children) == 1
|
||||
assert parent_group.children[0].name == child_name
|
||||
|
||||
child_group = await secrets_manager.get_secret_group("/parent/child")
|
||||
assert child_group is not None
|
||||
assert child_group.name == child_name
|
||||
assert child_group.parent_group is not None
|
||||
assert child_group.parent_group.name == parent_name
|
||||
assert len(child_group.children) == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_group_as_path(self, secrets_manager: AsyncSecretContext) -> None:
|
||||
"""Add a nested group with path annotation."""
|
||||
parent_name = "parent"
|
||||
child_path = "/parent/child"
|
||||
await secrets_manager.add_group(parent_name)
|
||||
await secrets_manager.add_group(child_path)
|
||||
parent_group = await secrets_manager.get_secret_group(f"/parent")
|
||||
assert parent_group is not None
|
||||
assert len(parent_group.children) == 1
|
||||
assert parent_group.children[0].name == "child"
|
||||
|
||||
child_group = await secrets_manager.get_secret_group(child_path)
|
||||
assert child_group is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_overlapping_names(self, secrets_manager: AsyncSecretContext) -> None:
|
||||
"""Test having overlapping names in different groups."""
|
||||
await secrets_manager.add_group("root")
|
||||
with pytest.raises(InvalidGroupNameError):
|
||||
await secrets_manager.add_group("/root")
|
||||
|
||||
await secrets_manager.add_group("/root/root")
|
||||
|
||||
group = await secrets_manager.get_secret_group("/root/root")
|
||||
assert group is not None
|
||||
assert group.name == "root"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_group_with_nonexisting_parent(
|
||||
self, secrets_manager: AsyncSecretContext
|
||||
) -> None:
|
||||
"""Test adding a group with a nonexisting parent."""
|
||||
with pytest.raises(InvalidGroupNameError):
|
||||
await secrets_manager.add_group("orphan", parent_group="unknown")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_duplicate_group(
|
||||
self, secrets_manager: AsyncSecretContext
|
||||
) -> None:
|
||||
"""Test adding the same group twice."""
|
||||
await secrets_manager.add_group("snowflake")
|
||||
with pytest.raises(InvalidGroupNameError):
|
||||
await secrets_manager.add_group("snowflake")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"group_name,description", [("testgroup", "test description")]
|
||||
)
|
||||
@allure.title("Add a group name {group_name} with description {description}")
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_group_with_description(
|
||||
self,
|
||||
secrets_manager: AsyncSecretContext,
|
||||
group_name: str,
|
||||
description: str,
|
||||
) -> None:
|
||||
"""Test adding a group with description."""
|
||||
await secrets_manager.add_group(group_name, description)
|
||||
result = await secrets_manager.get_secret_group(group_name)
|
||||
assert result is not None
|
||||
assert result.description == description
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"groups",
|
||||
[
|
||||
[
|
||||
("root", None, "root"),
|
||||
("level1", "root", "/root/level1"),
|
||||
("level2", "level1", "/root/level1/level2"),
|
||||
],
|
||||
[("flat1", None, "flat1"), ("flat2", None, "flat2")],
|
||||
[
|
||||
("stub", None, "stub"),
|
||||
("root", None, "root"),
|
||||
("nested", "root", "/root/nested"),
|
||||
],
|
||||
],
|
||||
)
|
||||
@allure.title("Listing groups")
|
||||
class TestSecretGroupListing:
|
||||
"""Tests for listing groups."""
|
||||
|
||||
@pytest_asyncio.fixture(autouse=True)
|
||||
async def create_groups(
|
||||
self,
|
||||
secrets_manager: AsyncSecretContext,
|
||||
groups: list[tuple[str, str | None, str]],
|
||||
) -> None:
|
||||
"""Pre-create groups."""
|
||||
for name, parent_group, _path in groups:
|
||||
await secrets_manager.add_group(name, parent_group=parent_group)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_secret_groups_list(
|
||||
self,
|
||||
secrets_manager: AsyncSecretContext,
|
||||
groups: list[tuple[str, str | None, str]],
|
||||
) -> None:
|
||||
"""Test the flat get_secret_groups_list."""
|
||||
# Create three levels of content
|
||||
group_list = await secrets_manager.get_secret_group_list()
|
||||
assert len(group_list) == len(groups)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_secret_groups(
|
||||
self,
|
||||
secrets_manager: AsyncSecretContext,
|
||||
groups: list[tuple[str, str | None, str]],
|
||||
) -> None:
|
||||
"""Test the tree-oriented get_secretsgroups."""
|
||||
group_map = dict([(group[0], group[1]) for group in sorted(groups)])
|
||||
group_tree = await secrets_manager.get_secret_groups()
|
||||
root_groups = [key for key, value in group_map.items() if value is None]
|
||||
assert len(group_tree) == len(root_groups)
|
||||
reconstructed_groups: list[tuple[str, str | None]] = []
|
||||
|
||||
def crawl_tree(item: SecretGroup) -> list[tuple[str, str | None]]:
|
||||
"""Crawl a tree recursively."""
|
||||
parent_group_name = None
|
||||
if item.parent_group:
|
||||
parent_group_name = item.parent_group.name
|
||||
items: list[tuple[str, str | None]] = [(item.name, parent_group_name)]
|
||||
for child in item.children:
|
||||
items.extend(crawl_tree(child))
|
||||
|
||||
return items
|
||||
|
||||
for item in group_tree:
|
||||
reconstructed_groups.extend(crawl_tree(item))
|
||||
|
||||
assert dict(sorted(reconstructed_groups)) == group_map
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_secret_groups_with_secrets(
|
||||
self,
|
||||
secrets_manager: AsyncSecretContext,
|
||||
groups: list[tuple[str, str | None, str]],
|
||||
) -> None:
|
||||
"""Test fetching groups where there are secrets in all groups."""
|
||||
# We will create exactly two secrets in each group.
|
||||
for group_name, _parent, path in groups:
|
||||
await secrets_manager.add_entry(
|
||||
f"{group_name}_1", f"{group_name}_secret_1", group_path=path
|
||||
)
|
||||
await secrets_manager.add_entry(
|
||||
f"{group_name}_2", f"{group_name}_secret_2", group_path=path
|
||||
)
|
||||
|
||||
group_list = await secrets_manager.get_secret_group_list()
|
||||
for group in group_list:
|
||||
assert len(group.entries) == 2
|
||||
assert group.entries[0] == f"{group.name}_1"
|
||||
assert group.entries[1] == f"{group.name}_2"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"query,expected,groups,children",
|
||||
[
|
||||
("MATCH", 1, [("MATCH", None), ("SOMETHINGELSE", None)], 0),
|
||||
("MATCH", 1, [("root", None), ("MATCH", "root"), ("SOMETHINGELSE", "root")], 0),
|
||||
("MATCH", 3, [("MATCH1", None), ("MATCH2", None), ("MATCH3", None)], 0),
|
||||
("MATCH", 1, [("root", None), ("MATCH", "root"), ("CHILD", "MATCH")], 1),
|
||||
(
|
||||
"NOMATCH",
|
||||
0,
|
||||
[("foo", None), ("bar", None), ("foobar", "foo"), ("barfoo", "bar")],
|
||||
0,
|
||||
),
|
||||
],
|
||||
)
|
||||
@allure.title("Searching in groups using patterns")
|
||||
class TestGroupSearchPattern:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_group_list_pattern(
|
||||
self,
|
||||
secrets_manager: AsyncSecretContext,
|
||||
query: str,
|
||||
expected: int,
|
||||
groups: list[tuple[str, str | None]],
|
||||
children: int,
|
||||
) -> None:
|
||||
"""Test matching a pattern."""
|
||||
for name, parent_group in groups:
|
||||
await secrets_manager.add_group(name, parent_group=parent_group)
|
||||
|
||||
result = await secrets_manager.get_secret_group_list(pattern=query, regex=False)
|
||||
assert len(result) == expected
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_group_tree_pattern(
|
||||
self,
|
||||
secrets_manager: AsyncSecretContext,
|
||||
query: str,
|
||||
expected: int,
|
||||
groups: list[tuple[str, str | None]],
|
||||
children: int,
|
||||
) -> None:
|
||||
"""Test matching a pattern with a tree result."""
|
||||
for name, parent_group in groups:
|
||||
await secrets_manager.add_group(name, parent_group=parent_group)
|
||||
|
||||
result = await secrets_manager.get_secret_groups(pattern=query, regex=False)
|
||||
assert len(result) == expected
|
||||
if expected == 1 and children > 0:
|
||||
assert len(result[0].children) == children
|
||||
|
||||
|
||||
@allure.title("Modifying groups")
|
||||
class TestGroupModification:
|
||||
"""Test modifying groups."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"group_name,parent,description",
|
||||
[("test", None, "test description"), ("test", "root", "test_description")],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_group_description(
|
||||
self,
|
||||
secrets_manager: AsyncSecretContext,
|
||||
group_name: str,
|
||||
parent: str | None,
|
||||
description: str,
|
||||
) -> None:
|
||||
"""Test setting a description on a group."""
|
||||
if parent:
|
||||
await secrets_manager.add_group(parent)
|
||||
await secrets_manager.add_group(group_name, parent_group=parent)
|
||||
path = group_name
|
||||
if parent:
|
||||
path = f"/{parent}/{group_name}"
|
||||
group = await secrets_manager.get_secret_group(path)
|
||||
assert group is not None
|
||||
assert group.description is None
|
||||
|
||||
await secrets_manager.set_group_description(path, description)
|
||||
group = await secrets_manager.get_secret_group(path)
|
||||
assert group is not None
|
||||
assert group.description == description
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"groups,target_group, expected_path",
|
||||
[
|
||||
(
|
||||
[("root", None), ("test", None)],
|
||||
("test", "root"),
|
||||
"/root/test",
|
||||
),
|
||||
(
|
||||
[("root", None), ("test", "root")],
|
||||
("/root/test", None),
|
||||
"test",
|
||||
),
|
||||
([("test", None)], ("test", None), "test"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_move_group(
|
||||
self,
|
||||
secrets_manager: AsyncSecretContext,
|
||||
groups: list[tuple[str, str | None]],
|
||||
target_group: tuple[str, str | None],
|
||||
expected_path: str,
|
||||
) -> None:
|
||||
"""Test moving groups around."""
|
||||
for group_name, parent_name in groups:
|
||||
await secrets_manager.add_group(group_name, parent_group=parent_name)
|
||||
|
||||
group_name, target = target_group
|
||||
await secrets_manager.move_group(group_name, target)
|
||||
group = await secrets_manager.get_secret_group(expected_path)
|
||||
assert group is not None
|
||||
|
||||
|
||||
@allure.title("Deleting items")
|
||||
class TestSecretManagerDeletions:
|
||||
"""Test secret manager deletions."""
|
||||
|
||||
@pytest_asyncio.fixture(autouse=True)
|
||||
async def create_test_data(self, secrets_manager: AsyncSecretContext) -> None:
|
||||
"""Create some test data."""
|
||||
groups = [
|
||||
("root", None, "root"),
|
||||
("level1", "root", "/root/level1"),
|
||||
("level2", "level1", "/root/level1/level2"),
|
||||
]
|
||||
for n in range(2):
|
||||
await secrets_manager.add_entry(f"ungrouped_{n}", "secret")
|
||||
for group_name, parent_name, path in groups:
|
||||
await secrets_manager.add_group(group_name, parent_group=parent_name)
|
||||
for n in range(2):
|
||||
await secrets_manager.add_entry(
|
||||
f"{group_name}_{n}", "secret", group_path=path
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name,group_name",
|
||||
[("root_1", "root"), ("level1_0", "/root/level1"), ("ungrouped_1", None)],
|
||||
)
|
||||
@allure.title("Delete secret {name in group {group_name}}")
|
||||
@pytest.mark.asyncio
|
||||
async def test_secret_deletion(
|
||||
self, secrets_manager: AsyncSecretContext, name: str, group_name: str | None
|
||||
) -> None:
|
||||
"""Test secret deletion."""
|
||||
if group_name:
|
||||
group = await secrets_manager.get_secret_group(group_name)
|
||||
assert group is not None
|
||||
assert name in group.entries
|
||||
|
||||
secret = await secrets_manager.get_secret(name)
|
||||
assert secret is not None
|
||||
|
||||
await secrets_manager.delete_entry(name)
|
||||
|
||||
secret = await secrets_manager.get_secret(name)
|
||||
assert secret is None
|
||||
|
||||
if group_name:
|
||||
group = await secrets_manager.get_secret_group(group_name)
|
||||
assert group is not None
|
||||
assert name not in group.entries
|
||||
|
||||
@pytest.mark.parametrize("name", ["NONEXISTING"])
|
||||
@allure.title("Deleting non-existing entry {name}")
|
||||
@pytest.mark.asyncio
|
||||
async def test_nonexisting_entry(
|
||||
self, secrets_manager: AsyncSecretContext, name: str
|
||||
) -> None:
|
||||
"""Test deleting something that doesn't exist."""
|
||||
secret = await secrets_manager.get_secret(name)
|
||||
assert secret is None
|
||||
# Deleting something that is already deleted returns None
|
||||
await secrets_manager.delete_entry(name)
|
||||
|
||||
@pytest.mark.parametrize("path", ["/root/level1"])
|
||||
@allure.title("Deleting group {path}")
|
||||
async def test_group_delete(
|
||||
self, secrets_manager: AsyncSecretContext, path: str
|
||||
) -> None:
|
||||
"""Test deleting a group."""
|
||||
group = await secrets_manager.get_secret_group(path)
|
||||
assert group is not None
|
||||
entries = list(group.entries)
|
||||
await secrets_manager.delete_group(path)
|
||||
group = await secrets_manager.get_secret_group(path)
|
||||
assert group is None
|
||||
|
||||
for name in entries:
|
||||
new_grouping = await secrets_manager.get_entry_group(name)
|
||||
assert new_grouping is None
|
||||
|
||||
|
||||
@allure.title("Other tests")
|
||||
class TestSecretManagerOther:
|
||||
"""Uncategorized tests to standardize module."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_secret_nonexisting(
|
||||
self, secrets_manager: AsyncSecretContext
|
||||
) -> None:
|
||||
"""Test get_secret with invalid name."""
|
||||
result = await secrets_manager.get_secret("NOMATCH")
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"group_name,num_grouped,num_ungrouped",
|
||||
[("GROUP", 3, 3), ("GROUP", 3, 0), ("GROUP", 0, 0)],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_ungrouped_secrets(
|
||||
self,
|
||||
secrets_manager: AsyncSecretContext,
|
||||
group_name: str,
|
||||
num_grouped: int,
|
||||
num_ungrouped: int,
|
||||
) -> None:
|
||||
"""Test get_ungrouped_secrets."""
|
||||
await secrets_manager.add_group(group_name)
|
||||
for n in range(num_ungrouped):
|
||||
await secrets_manager.add_entry(f"ungrouped_{n}", "secret")
|
||||
|
||||
for n in range(num_grouped):
|
||||
await secrets_manager.add_entry(
|
||||
f"grouped_{n}", "secret", group_path="GROUP"
|
||||
)
|
||||
|
||||
ungrouped = await secrets_manager.get_ungrouped_secrets()
|
||||
assert len(ungrouped) == num_ungrouped
|
||||
matching = [entry for entry in ungrouped if entry.startswith("ungrouped_")]
|
||||
assert len(matching) == num_ungrouped
|
||||
|
||||
grouped_secrets = await secrets_manager.get_available_secrets(
|
||||
group_path=group_name
|
||||
)
|
||||
assert len(grouped_secrets) == num_grouped
|
||||
all_secrets = await secrets_manager.get_available_secrets()
|
||||
assert len(all_secrets) == (num_ungrouped + num_grouped)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"entries", [[("test1", "secret1"), ("test2", "secret2")], []]
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_available_secrets(
|
||||
self, secrets_manager: AsyncSecretContext, entries: list[tuple[str, str]]
|
||||
) -> None:
|
||||
"""Test the get_available_secrets method."""
|
||||
for name, secret in entries:
|
||||
await secrets_manager.add_entry(name, secret)
|
||||
|
||||
entry_names = [entry[0] for entry in entries]
|
||||
response = await secrets_manager.get_available_secrets()
|
||||
assert len(response) == len(entries)
|
||||
|
||||
assert sorted(response) == sorted(entry_names)
|
||||
|
||||
async def test_get_secret_groups_none(
|
||||
self, secrets_manager: AsyncSecretContext
|
||||
) -> None:
|
||||
"""Test get_secret_groups with no groups created."""
|
||||
result = await secrets_manager.get_secret_groups()
|
||||
assert len(result) == 0
|
||||
result_flat = await secrets_manager.get_secret_group_list()
|
||||
assert len(result_flat) == 0
|
||||
|
||||
@allure.title("Search for a group using regular expression")
|
||||
async def test_group_regex_search(
|
||||
self, secrets_manager: AsyncSecretContext
|
||||
) -> None:
|
||||
"""Search for entries with regular expressions."""
|
||||
groups = [
|
||||
"test1",
|
||||
"test2",
|
||||
"other",
|
||||
"somethingelse",
|
||||
]
|
||||
|
||||
for group in groups:
|
||||
await secrets_manager.add_group(group)
|
||||
|
||||
results = await secrets_manager.get_secret_group_list(
|
||||
pattern="^test", regex=True
|
||||
)
|
||||
assert len(results) == 2
|
||||
for group in results:
|
||||
assert group.name.startswith("test")
|
||||
@ -79,6 +79,32 @@ async def run_backend_server(test_ports: TestPorts):
|
||||
await server_task
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(
|
||||
scope=TEST_SCOPE, name="admin_server_settings", loop_scope=LOOP_SCOPE
|
||||
)
|
||||
async def get_admin_server_settings(
|
||||
test_ports: TestPorts, backend_server: tuple[str, str]
|
||||
):
|
||||
"""Get admin server settings."""
|
||||
backend_url, backend_token = backend_server
|
||||
port = test_ports.admin
|
||||
secret_key = secrets.token_urlsafe(32)
|
||||
with in_tempdir() as admin_work_path:
|
||||
admin_db = admin_work_path / "ssh_admin.db"
|
||||
admin_settings = AdminServerSettings.model_validate(
|
||||
{
|
||||
"sshecret_backend_url": backend_url,
|
||||
"backend_token": backend_token,
|
||||
"secret_key": secret_key,
|
||||
"listen_address": "0.0.0.0",
|
||||
"port": port,
|
||||
"database": str(admin_db.absolute()),
|
||||
"password_manager_directory": str(admin_work_path.absolute()),
|
||||
}
|
||||
)
|
||||
yield admin_settings
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope=TEST_SCOPE, name="admin_server", loop_scope=LOOP_SCOPE)
|
||||
async def run_admin_server(test_ports: TestPorts, backend_server: tuple[str, str]):
|
||||
"""Run admin server."""
|
||||
@ -98,7 +124,7 @@ async def run_admin_server(test_ports: TestPorts, backend_server: tuple[str, str
|
||||
"password_manager_directory": str(admin_work_path.absolute()),
|
||||
}
|
||||
)
|
||||
admin_app = create_admin_app(admin_settings)
|
||||
admin_app = create_admin_app(admin_settings, create_db=True)
|
||||
config = uvicorn.Config(app=admin_app, port=port, loop="asyncio")
|
||||
server = uvicorn.Server(config=config)
|
||||
server_task = asyncio.create_task(server.serve())
|
||||
|
||||
Reference in New Issue
Block a user