Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
59 changes: 25 additions & 34 deletions platform_secrets/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,17 @@
from aiohttp.web_urldispatcher import AbstractRoute
from aiohttp_security import check_authorized
from apolo_kube_client import KubeClientSelector
from neuro_auth_client import (
from neuro_admin_client.auth_client import (
AuthClient,
ClientSubTreeViewRoot,
Permission,
User,
check_permissions,
)
from neuro_auth_client.security import AuthScheme, setup_security
from neuro_admin_client.security import AuthScheme, check_permissions, setup_security
from neuro_logging import init_logging, setup_sentry

from platform_secrets import __version__

from .config import Config
from .config_factory import EnvironConfigFactory
from .identity import untrusted_user
from .project_deleter import ProjectDeleter
from .service import (
Secret,
Expand Down Expand Up @@ -99,10 +95,6 @@ def _service(self) -> Service:
def _auth_client(self) -> AuthClient:
return self._app[AUTH_CLIENT_KEY]

async def _get_untrusted_user(self, request: Request) -> User:
identity = await untrusted_user(request)
return User(name=identity.name)

@property
def _secret_cluster_uri(self) -> str:
return f"secret://{self._config.cluster_name}"
Expand All @@ -124,6 +116,9 @@ def _get_secret_read_perm(self, secret: Secret) -> Permission:
def _get_secrets_write_perm(self, project_name: str, org_name: str) -> Permission:
return Permission(self._get_secrets_uri(project_name, org_name), "write")

def _get_secrets_read_perm(self, project_name: str, org_name: str) -> Permission:
return Permission(self._get_secrets_uri(project_name, org_name), "read")

def _convert_secret_to_payload(self, secret: Secret) -> dict[str, str | None]:
return {
"key": secret.key,
Expand All @@ -136,12 +131,8 @@ def _convert_secret_to_payload(self, secret: Secret) -> dict[str, str | None]:
"owner": secret.project_name,
}

def _check_secret_read_perm(
self, secret: Secret, tree: ClientSubTreeViewRoot
) -> bool:
return tree.allows(self._get_secret_read_perm(secret))

async def handle_post(self, request: Request) -> Response:
await check_authorized(request)
payload = await request.json()
payload = secret_request_validator.check(payload)
org_name = payload["org_name"]
Expand All @@ -162,27 +153,23 @@ async def handle_post(self, request: Request) -> Response:
return json_response(resp_payload, status=HTTPCreated.status_code)

async def handle_get_all(self, request: Request) -> Response:
username = await check_authorized(request)
await check_authorized(request)
payload = org_project_validator.check(request.query)
org_name = payload["org_name"]
project_name = payload["project_name"]
tree = await self._auth_client.get_permissions_tree(
username, self._secret_cluster_uri
await check_permissions(
request,
[self._get_secrets_read_perm(project_name, org_name)],
)
secrets = await self._service.get_all_secrets(
org_name=org_name, project_name=project_name
)

secrets = [
secret
for secret in await self._service.get_all_secrets(
org_name=org_name, project_name=project_name
)
if self._check_secret_read_perm(secret, tree)
]
resp_payload = [self._convert_secret_to_payload(secret) for secret in secrets]
resp_payload = secret_list_response_validator.check(resp_payload)
return json_response(resp_payload)

async def handle_get(self, request: Request) -> Response:
username = await check_authorized(request)
await check_authorized(request)
payload = org_project_validator.check(request.query)
org_name = payload["org_name"]
project_name = payload["project_name"]
Expand All @@ -195,14 +182,10 @@ async def handle_get(self, request: Request) -> Response:
project_name=project_name,
)

tree = await self._auth_client.get_permissions_tree(
username, self._secret_cluster_uri
await check_permissions(
request,
[self._get_secret_read_perm(secret)],
)
if not self._check_secret_read_perm(secret, tree):
await check_permissions(
request,
[self._get_secret_read_perm(secret)],
)

try:
secret = await self._service.get_secret(secret)
Expand All @@ -216,6 +199,7 @@ async def handle_get(self, request: Request) -> Response:
return json_response(resp_payload)

async def handle_delete(self, request: Request) -> Response:
await check_authorized(request)
payload = org_project_validator.check(request.query)
org_name = payload["org_name"]
project_name = payload["project_name"]
Expand Down Expand Up @@ -247,6 +231,13 @@ async def handle_exceptions(
except ValueError as e:
payload = {"error": str(e)}
return json_response(payload, status=HTTPBadRequest.status_code)
except RuntimeError as e:
# check_permissions raises RuntimeError when user lacks permissions
# (wraps 403 Forbidden from platform-admin)
error_str = str(e)
if "403" in error_str or "Forbidden" in error_str:
raise aiohttp.web.HTTPForbidden()
raise
except aiohttp.web.HTTPException:
raise
except Exception as e:
Expand Down
23 changes: 13 additions & 10 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ python = ">=3.13,<4.0"
aiohttp = "3.13.3"
yarl = "1.22.0"
multidict = "6.7.0"
neuro-auth-client = "25.8.2"
trafaret = "2.1.1"
neuro-logging = "25.11.0"
apolo-kube-client = "^25.11.10"
apolo-events-client = "25.10.0"
neuro-admin-client = "^25.12.0"

[tool.poetry.scripts]
platform-secrets = "platform_secrets.api:main"
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@


pytest_plugins = [
"tests.integration.conftest_auth",
"tests.integration.conftest_admin",
"tests.integration.conftest_kube",
]

Expand Down Expand Up @@ -109,6 +109,6 @@ def get_service_url(service_name: str, namespace: str = "default") -> str:
pytest.fail(f"Service {service_name} is unavailable.")


@pytest.fixture
@pytest.fixture(scope="session")
def cluster_name() -> str:
return "test-cluster"
Loading