Skip to content
Merged
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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dependencies = [
"openai==1.99.9",
"sqlalchemy>=2.0.42",
"semver<4.0.0",
"jsonpath-ng>=1.6.1",
]


Expand Down
5 changes: 3 additions & 2 deletions src/app/endpoints/authorized.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Handler for REST API call to authorized endpoint."""

import logging
from typing import Any
from typing import Annotated, Any

from fastapi import APIRouter, Depends

from auth.interface import AuthTuple
from auth import get_auth_dependency
from models.responses import AuthorizedResponse, UnauthorizedResponse, ForbiddenResponse

Expand All @@ -31,7 +32,7 @@

@router.post("/authorized", responses=authorized_responses)
async def authorized_endpoint_handler(
auth: Any = Depends(auth_dependency),
auth: Annotated[AuthTuple, Depends(auth_dependency)],
) -> AuthorizedResponse:
"""
Handle request to the /authorized endpoint.
Expand Down
23 changes: 19 additions & 4 deletions src/app/endpoints/config.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
"""Handler for REST API call to retrieve service configuration."""

import logging
from typing import Any
from typing import Annotated, Any

from fastapi import APIRouter, Request
from fastapi import APIRouter, Request, Depends

from models.config import Configuration
from auth.interface import AuthTuple
from auth import get_auth_dependency
from authorization.middleware import authorize
from configuration import configuration
from models.config import Action, Configuration
from utils.endpoints import check_configuration_loaded

logger = logging.getLogger(__name__)
router = APIRouter(tags=["config"])

auth_dependency = get_auth_dependency()


get_config_responses: dict[int | str, dict[str, Any]] = {
200: {
Expand Down Expand Up @@ -56,7 +61,11 @@


@router.get("/config", responses=get_config_responses)
def config_endpoint_handler(_request: Request) -> Configuration:
@authorize(Action.GET_CONFIG)
async def config_endpoint_handler(
auth: Annotated[AuthTuple, Depends(auth_dependency)],
request: Request,
) -> Configuration:
"""
Handle requests to the /config endpoint.

Expand All @@ -66,6 +75,12 @@ def config_endpoint_handler(_request: Request) -> Configuration:
Returns:
Configuration: The loaded service configuration object.
"""
# Used only for authorization
_ = auth
Copy link
Contributor

Choose a reason for hiding this comment

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

why it's used like this? Can't you just rename the parameter to _auth and _request?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, see my commit message remark under "Technical notes"


# Nothing interesting in the request
_ = request

# ensure that configuration is loaded
check_configuration_loaded(configuration)

Expand Down
35 changes: 27 additions & 8 deletions src/app/endpoints/conversations.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@

from llama_stack_client import APIConnectionError, NotFoundError

from fastapi import APIRouter, HTTPException, status, Depends
from fastapi import APIRouter, HTTPException, Request, status, Depends

from client import AsyncLlamaStackClientHolder
from configuration import configuration
from app.database import get_session
from auth import get_auth_dependency
from authorization.middleware import authorize
from models.config import Action
from models.database.conversations import UserConversation
from models.responses import (
ConversationResponse,
ConversationDeleteResponse,
ConversationsListResponse,
ConversationDetails,
)
from models.database.conversations import UserConversation
from auth import get_auth_dependency
from app.database import get_session
from utils.endpoints import check_configuration_loaded, validate_conversation_ownership
from utils.suid import check_suid

Expand Down Expand Up @@ -146,7 +148,9 @@ def simplify_session_data(session_data: dict) -> list[dict[str, Any]]:


@router.get("/conversations", responses=conversations_list_responses)
def get_conversations_list_endpoint_handler(
@authorize(Action.LIST_CONVERSATIONS)
async def get_conversations_list_endpoint_handler(
request: Request,
auth: Any = Depends(auth_dependency),
) -> ConversationsListResponse:
"""Handle request to retrieve all conversations for the authenticated user."""
Expand All @@ -158,11 +162,16 @@ def get_conversations_list_endpoint_handler(

with get_session() as session:
try:
# Get all conversations for this user
user_conversations = (
session.query(UserConversation).filter_by(user_id=user_id).all()
query = session.query(UserConversation)

filtered_query = (
query
if Action.LIST_OTHERS_CONVERSATIONS in request.state.authorized_actions
else query.filter_by(user_id=user_id)
)

user_conversations = filtered_query.all()

Comment on lines +167 to +174
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard against missing request.state.authorized_actions

If the decorator/middleware doesn’t populate authorized_actions (misconfiguration or legacy auth), this will raise AttributeError. Default to least privilege.

-            filtered_query = (
-                query
-                if Action.LIST_OTHERS_CONVERSATIONS in request.state.authorized_actions
-                else query.filter_by(user_id=user_id)
-            )
+            allowed = getattr(request.state, "authorized_actions", set())
+            filtered_query = (
+                query
+                if Action.LIST_OTHERS_CONVERSATIONS in allowed
+                else query.filter_by(user_id=user_id)
+            )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
filtered_query = (
query
if Action.LIST_OTHERS_CONVERSATIONS in request.state.authorized_actions
else query.filter_by(user_id=user_id)
)
user_conversations = filtered_query.all()
allowed = getattr(request.state, "authorized_actions", set())
filtered_query = (
query
if Action.LIST_OTHERS_CONVERSATIONS in allowed
else query.filter_by(user_id=user_id)
)
user_conversations = filtered_query.all()
🤖 Prompt for AI Agents
In src/app/endpoints/conversations.py around lines 167 to 174, the code assumes
request.state.authorized_actions exists and will raise AttributeError if
middleware/decorator didn't set it; change the check to safely read
authorized_actions with a default least-privilege value (e.g., actions =
getattr(request.state, "authorized_actions", set()) or actions or set()), then
use that actions set in the membership test so that missing or None
authorized_actions defaults to no privileges and the query is filtered by
user_id.

# Return conversation summaries with metadata
conversations = [
ConversationDetails(
Expand Down Expand Up @@ -200,7 +209,9 @@ def get_conversations_list_endpoint_handler(


@router.get("/conversations/{conversation_id}", responses=conversation_responses)
@authorize(Action.GET_CONVERSATION)
async def get_conversation_endpoint_handler(
request: Request,
conversation_id: str,
auth: Any = Depends(auth_dependency),
) -> ConversationResponse:
Expand Down Expand Up @@ -239,6 +250,9 @@ async def get_conversation_endpoint_handler(
validate_conversation_ownership(
user_id=user_id,
conversation_id=conversation_id,
others_allowed=(
Action.READ_OTHERS_CONVERSATIONS in request.state.authorized_actions
),
)
Comment on lines +253 to 256
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Same defensive default when checking READ_OTHERS_CONVERSATIONS

-        others_allowed=(
-            Action.READ_OTHERS_CONVERSATIONS in request.state.authorized_actions
-        ),
+        others_allowed=(
+            Action.READ_OTHERS_CONVERSATIONS
+            in getattr(request.state, "authorized_actions", set())
+        ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
others_allowed=(
Action.READ_OTHERS_CONVERSATIONS in request.state.authorized_actions
),
)
others_allowed=(
Action.READ_OTHERS_CONVERSATIONS
in getattr(request.state, "authorized_actions", set())
),
🤖 Prompt for AI Agents
In src/app/endpoints/conversations.py around lines 253 to 256, the membership
check for Action.READ_OTHERS_CONVERSATIONS assumes
request.state.authorized_actions is always present and iterable; make it
defensive by using a safe default (e.g., getattr(request.state,
"authorized_actions", set()) or (request.state.authorized_actions or set()))
when performing the "in" check so it won't raise if authorized_actions is None
or missing.


agent_id = conversation_id
Expand Down Expand Up @@ -309,7 +323,9 @@ async def get_conversation_endpoint_handler(
@router.delete(
"/conversations/{conversation_id}", responses=conversation_delete_responses
)
@authorize(Action.DELETE_CONVERSATION)
async def delete_conversation_endpoint_handler(
request: Request,
conversation_id: str,
auth: Any = Depends(auth_dependency),
) -> ConversationDeleteResponse:
Expand Down Expand Up @@ -342,6 +358,9 @@ async def delete_conversation_endpoint_handler(
validate_conversation_ownership(
user_id=user_id,
conversation_id=conversation_id,
others_allowed=(
Action.DELETE_OTHERS_CONVERSATIONS in request.state.authorized_actions
),
)
Comment on lines +361 to 364
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Same defensive default when checking DELETE_OTHERS_CONVERSATIONS

-        others_allowed=(
-            Action.DELETE_OTHERS_CONVERSATIONS in request.state.authorized_actions
-        ),
+        others_allowed=(
+            Action.DELETE_OTHERS_CONVERSATIONS
+            in getattr(request.state, "authorized_actions", set())
+        ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
others_allowed=(
Action.DELETE_OTHERS_CONVERSATIONS in request.state.authorized_actions
),
)
others_allowed=(
Action.DELETE_OTHERS_CONVERSATIONS
in getattr(request.state, "authorized_actions", set())
),
)
🤖 Prompt for AI Agents
In src/app/endpoints/conversations.py around lines 361 to 364, the membership
check for DELETE_OTHERS_CONVERSATIONS should defensively handle missing or None
authorized_actions; change the expression to check membership against a safe
default (e.g., getattr(request.state, "authorized_actions", set()) or an empty
iterable) so the code won't raise if authorized_actions is absent or null.


agent_id = conversation_id
Expand Down
9 changes: 6 additions & 3 deletions src/app/endpoints/feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@
from pathlib import Path
import json
from datetime import datetime, UTC
from fastapi import APIRouter, Request, HTTPException, Depends, status
from fastapi import APIRouter, HTTPException, Depends, Request, status

from auth import get_auth_dependency
from auth.interface import AuthTuple
from authorization.middleware import authorize
from configuration import configuration
from models.config import Action
from models.requests import FeedbackRequest
from models.responses import (
ErrorResponse,
FeedbackResponse,
StatusResponse,
UnauthorizedResponse,
ForbiddenResponse,
)
from models.requests import FeedbackRequest
from utils.suid import get_suid

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -79,7 +81,8 @@ async def assert_feedback_enabled(_request: Request) -> None:


@router.post("", responses=feedback_response)
def feedback_endpoint_handler(
@authorize(Action.FEEDBACK)
async def feedback_endpoint_handler(
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

feedback_request: FeedbackRequest,
auth: Annotated[AuthTuple, Depends(auth_dependency)],
_ensure_feedback_enabled: Any = Depends(assert_feedback_enabled),
Expand Down
27 changes: 23 additions & 4 deletions src/app/endpoints/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
"""

import logging
from typing import Any
from typing import Annotated, Any

from llama_stack.providers.datatypes import HealthStatus

from fastapi import APIRouter, status, Response
from fastapi import APIRouter, status, Response, Depends
from client import AsyncLlamaStackClientHolder
from auth.interface import AuthTuple
from auth import get_auth_dependency
from authorization.middleware import authorize
from models.config import Action
from models.responses import (
LivenessResponse,
ReadinessResponse,
Expand All @@ -21,6 +25,8 @@
logger = logging.getLogger(__name__)
router = APIRouter(tags=["health"])

auth_dependency = get_auth_dependency()


async def get_providers_health_statuses() -> list[ProviderHealthStatus]:
"""
Expand Down Expand Up @@ -72,14 +78,21 @@ async def get_providers_health_statuses() -> list[ProviderHealthStatus]:


@router.get("/readiness", responses=get_readiness_responses)
async def readiness_probe_get_method(response: Response) -> ReadinessResponse:
@authorize(Action.INFO)
async def readiness_probe_get_method(
auth: Annotated[AuthTuple, Depends(auth_dependency)],
response: Response,
) -> ReadinessResponse:
"""
Handle the readiness probe endpoint, returning service readiness.

If any provider reports an error status, responds with HTTP 503
and details of unhealthy providers; otherwise, indicates the
service is ready.
"""
# Used only for authorization
_ = auth

provider_statuses = await get_providers_health_statuses()

# Check if any provider is unhealthy (not counting not_implemented as unhealthy)
Expand Down Expand Up @@ -112,11 +125,17 @@ async def readiness_probe_get_method(response: Response) -> ReadinessResponse:


@router.get("/liveness", responses=get_liveness_responses)
def liveness_probe_get_method() -> LivenessResponse:
@authorize(Action.INFO)
async def liveness_probe_get_method(
auth: Annotated[AuthTuple, Depends(auth_dependency)],
) -> LivenessResponse:
"""
Return the liveness status of the service.

Returns:
LivenessResponse: Indicates that the service is alive.
"""
# Used only for authorization
_ = auth

return LivenessResponse(alive=True)
23 changes: 20 additions & 3 deletions src/app/endpoints/info.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
"""Handler for REST API call to provide info."""

import logging
from typing import Any
from typing import Annotated, Any

from fastapi import APIRouter, Request
from fastapi import Depends

from auth.interface import AuthTuple
from auth import get_auth_dependency
from authorization.middleware import authorize
from configuration import configuration
from version import __version__
from models.config import Action
from models.responses import InfoResponse
from version import __version__

logger = logging.getLogger(__name__)
router = APIRouter(tags=["info"])

auth_dependency = get_auth_dependency()


get_info_responses: dict[int | str, dict[str, Any]] = {
200: {
Expand All @@ -22,7 +29,11 @@


@router.get("/info", responses=get_info_responses)
def info_endpoint_handler(_request: Request) -> InfoResponse:
@authorize(Action.INFO)
async def info_endpoint_handler(
auth: Annotated[AuthTuple, Depends(auth_dependency)],
request: Request,
) -> InfoResponse:
"""
Handle request to the /info endpoint.

Expand All @@ -32,4 +43,10 @@ def info_endpoint_handler(_request: Request) -> InfoResponse:
Returns:
InfoResponse: An object containing the service's name and version.
"""
# Used only for authorization
Copy link
Contributor

Choose a reason for hiding this comment

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

dtto

Copy link
Contributor Author

Choose a reason for hiding this comment

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

_ = auth

# Nothing interesting in the request
_ = request

return InfoResponse(name=configuration.configuration.name, version=__version__)
21 changes: 19 additions & 2 deletions src/app/endpoints/metrics.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
"""Handler for REST API call to provide metrics."""

from typing import Annotated
from fastapi.responses import PlainTextResponse
from fastapi import APIRouter, Request
from fastapi import APIRouter, Request, Depends
from prometheus_client import (
generate_latest,
CONTENT_TYPE_LATEST,
)

from auth.interface import AuthTuple
from auth import get_auth_dependency
from authorization.middleware import authorize
from models.config import Action
from metrics.utils import setup_model_metrics

router = APIRouter(tags=["metrics"])

auth_dependency = get_auth_dependency()


@router.get("/metrics", response_class=PlainTextResponse)
async def metrics_endpoint_handler(_request: Request) -> PlainTextResponse:
@authorize(Action.GET_METRICS)
async def metrics_endpoint_handler(
auth: Annotated[AuthTuple, Depends(auth_dependency)],
request: Request,
) -> PlainTextResponse:
"""
Handle request to the /metrics endpoint.

Expand All @@ -24,6 +35,12 @@ async def metrics_endpoint_handler(_request: Request) -> PlainTextResponse:
set up, then responds with the current metrics snapshot in
Prometheus format.
"""
# Used only for authorization
_ = auth

# Nothing interesting in the request
_ = request

# Setup the model metrics if not already done. This is a one-time setup
# and will not be run again on subsequent calls to this endpoint
await setup_model_metrics()
Expand Down
Loading