Skip to content
Closed
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
31 changes: 0 additions & 31 deletions .env.test

This file was deleted.

2 changes: 1 addition & 1 deletion .env.test.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ API_BASE_URL=http://localhost:8000

# Postgres
POSTGRES_SERVER=localhost
POSTGRES_DB=kaapi_guardrails_testing
POSTGRES_PORT=5432
POSTGRES_DB=kaapi-guardrails
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres

Expand Down
6 changes: 3 additions & 3 deletions backend/app/alembic/versions/001_added_request_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@

# revision identifiers, used by Alembic.
revision: str = '001'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
down_revision: str | None = None
branch_labels = None
depends_on = None


def upgrade() -> None:
Expand Down
6 changes: 3 additions & 3 deletions backend/app/alembic/versions/002_added_validator_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@

# revision identifiers, used by Alembic.
revision: str = '002'
down_revision: Union[str, Sequence[str], None] = '001'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
down_revision: str = '001'
branch_labels = None
depends_on = None


def upgrade() -> None:
Expand Down
50 changes: 50 additions & 0 deletions backend/app/alembic/versions/003_added_validator_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Added validator_config table

Revision ID: 003
Revises: 002
Create Date: 2026-02-05 09:42:54.128852

"""
from typing import Sequence, Union

from alembic import op
from sqlalchemy.dialects import postgresql
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision: str = '003'
down_revision: str = '002'
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table('validator_config',
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('organization_id', sa.Integer(), nullable=False),
sa.Column('project_id', sa.Integer(), nullable=False),
sa.Column('type', sa.String(), nullable=False),
sa.Column('stage', sa.String(), nullable=False),
sa.Column('on_fail_action', sa.String(), nullable=False),
sa.Column(
"config",
postgresql.JSONB(astext_type=sa.Text()),
nullable=False,
server_default=sa.text("'{}'::jsonb"),
),
sa.Column('is_enabled', sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),

sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('organization_id', 'project_id', 'type', 'stage', name='uq_validator_identity')
)

op.create_index("idx_validator_organization", "validator_config", ["organization_id"])
op.create_index("idx_validator_project", "validator_config", ["project_id"])
op.create_index("idx_validator_type", "validator_config", ["type"])
op.create_index("idx_validator_stage", "validator_config", ["stage"])


def downgrade() -> None:
op.drop_table('validator_config')
3 changes: 2 additions & 1 deletion backend/app/api/main.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from fastapi import APIRouter

from app.api.routes import utils, guardrails
from app.api.routes import utils, guardrails, validator_configs

api_router = APIRouter()
api_router.include_router(utils.router)
api_router.include_router(guardrails.router)
api_router.include_router(validator_configs.router)

# if settings.ENVIRONMENT == "local":
# api_router.include_router(private.router)
20 changes: 13 additions & 7 deletions backend/app/api/routes/guardrails.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import uuid
from uuid import UUID
import uuid

from fastapi import APIRouter
from guardrails.guard import Guard
from guardrails.validators import FailResult
from guardrails.validators import FailResult, PassResult

from app.api.deps import AuthDep, SessionDep
from app.core.constants import REPHRASE_ON_FAIL_PREFIX
from app.core.guardrail_controller import build_guard, get_validator_config_models
from app.crud.request_log import RequestLogCrud
from app.crud.validator_log import ValidatorLogCrud
from app.models.guardrail_config import GuardrailRequest, GuardrailResponse
from app.models.logging.request import RequestLogUpdate, RequestStatus
from app.models.logging.validator import ValidatorLog, ValidatorOutcome
from app.models.logging.request_log import RequestLogUpdate, RequestStatus
from app.models.logging.validator_log import ValidatorLog, ValidatorOutcome
from app.schemas.guardrail_config import GuardrailRequest, GuardrailResponse
from app.utils import APIResponse

router = APIRouter(prefix="/guardrails", tags=["guardrails"])
Expand All @@ -25,6 +25,7 @@ async def run_guardrails(
payload: GuardrailRequest,
session: SessionDep,
_: AuthDep,
include_all_validator_logs: bool = False,
):
request_log_crud = RequestLogCrud(session=session)
validator_log_crud = ValidatorLogCrud(session=session)
Expand All @@ -41,6 +42,7 @@ async def run_guardrails(
request_log_crud,
request_log.id,
validator_log_crud,
include_all_validator_logs
)

@router.get("/")
Expand Down Expand Up @@ -73,6 +75,7 @@ async def _validate_with_guard(
request_log_crud: RequestLogCrud,
request_log_id: UUID,
validator_log_crud: ValidatorLogCrud,
include_all_validator_logs: bool = False,
) -> APIResponse:
"""
Runs Guardrails validation on input/output data, persists request & validator logs,
Expand Down Expand Up @@ -112,7 +115,7 @@ def _finalize(
)

if guard is not None:
add_validator_logs(guard, request_log_id, validator_log_crud)
add_validator_logs(guard, request_log_id, validator_log_crud, include_all_validator_logs)

rephrase_needed = (
validated_output is not None
Expand Down Expand Up @@ -157,7 +160,7 @@ def _finalize(
error_message=str(exc),
)

def add_validator_logs(guard: Guard, request_log_id: UUID, validator_log_crud: ValidatorLogCrud):
def add_validator_logs(guard: Guard, request_log_id: UUID, validator_log_crud: ValidatorLogCrud, include_all_validator_logs: bool = False):
history = getattr(guard, "history", None)
if not history:
return
Expand All @@ -174,6 +177,9 @@ def add_validator_logs(guard: Guard, request_log_id: UUID, validator_log_crud: V
for log in iteration.outputs.validator_logs:
result = log.validation_result

if not include_all_validator_logs and isinstance(result, PassResult):
continue

error_message = None
if isinstance(result, FailResult):
error_message = result.error_message
Expand Down
94 changes: 94 additions & 0 deletions backend/app/api/routes/validator_configs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from typing import Optional
from uuid import UUID

from fastapi import APIRouter

from app.api.deps import AuthDep, SessionDep
from app.core.enum import Stage, ValidatorType
from app.schemas.validator_config import ValidatorCreate, ValidatorResponse, ValidatorUpdate
from app.crud.validator_config import validator_config_crud
from app.utils import APIResponse


router = APIRouter(
prefix="/guardrails/validators/configs",
tags=["validator configs"],
)


@router.post(
"/",
response_model=APIResponse[ValidatorResponse]
)
def create_validator(
payload: ValidatorCreate,
session: SessionDep,
organization_id: int,
project_id: int,
_: AuthDep,
Comment on lines +23 to +28
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's understand the function context and parameter types
cat -n backend/app/api/routes/validator_configs.py | head -50

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 1753


🏁 Script executed:

# Search for how organization_id and project_id are handled in the codebase
rg -n "organization_id|project_id" --type=py -C3 backend/app/api/ | head -200

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 4736


🏁 Script executed:

# Check the auth validation patterns - look for how AuthDep validates tenant access
rg -n "AuthDep|SessionDep" --type=py -C2 backend/app/api/routes/ | head -150

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 4112


🏁 Script executed:

# Check if there's middleware or dependency validation for these parameters
fd -e py backend/app | xargs rg -l "organization_id.*validate|verify.*organization" --type=py

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 429


🏁 Script executed:

# Look for the ValidatorCreate schema to understand parameter definitions
rg -n "class ValidatorCreate|ValidatorCreate" --type=py -C5 backend/

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 3845


🏁 Script executed:

# Find and examine the AuthDep implementation
rg -n "AuthDep" backend/app/api/deps.py -A 10

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 136


🏁 Script executed:

# Check the full deps.py file to understand dependency injection
cat -n backend/app/api/deps.py

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 1438


🏁 Script executed:

# Examine CRUD methods for validation
cat -n backend/app/crud/validator_config.py | head -80

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 2872


🏁 Script executed:

# Check if session is tenant-aware
rg -n "Session|session" backend/app/crud/validator_config.py -B 2 -A 5 | head -100

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 3018


organization_id and project_id are unauthenticated query parameters with no tenant validation.

These tenant-scoping parameters are passed as plain query parameters with zero validation against the authenticated user's identity. The AuthDep dependency only validates a static bearer token—it does not extract or enforce tenant affiliation. Any authenticated caller can pass arbitrary organization_id and project_id values to create, read, update, or delete validators in any tenant. The CRUD layer accepts these parameters without access control checks, enabling cross-tenant data access.

🤖 Prompt for AI Agents
In `@backend/app/api/routes/validator_configs.py` around lines 23 - 28, The
create_validator endpoint accepts organization_id and project_id as
unauthenticated query params—fix by enforcing tenant validation: extract the
authenticated tenant identifiers from the AuthDep (e.g., user or token fields on
the AuthDep dependency used by create_validator), compare them to the incoming
organization_id and project_id, and reject the request (raise an appropriate
HTTP 403/Forbidden) if they do not match; alternatively, remove those query
params and derive organization_id/project_id from the AuthDep principal and pass
only validated IDs into the CRUD layer so ValidatorCreate handling and any calls
that use session, create_validator, and the downstream validator CRUD functions
cannot operate across tenants.

):
response_model = validator_config_crud.create(session, organization_id, project_id, payload)
return APIResponse.success_response(data=response_model)

@router.get(
"/",
response_model=APIResponse[list[ValidatorResponse]]
)
def list_validators(
organization_id: int,
project_id: int,
session: SessionDep,
_: AuthDep,
stage: Optional[Stage] = None,
type: Optional[ValidatorType] = None,
):
response_model = validator_config_crud.list(session, organization_id, project_id, stage, type)
return APIResponse.success_response(data=response_model)


@router.get(
"/{id}",
response_model=APIResponse[ValidatorResponse]
)
def get_validator(
id: UUID,
organization_id: int,
project_id: int,
session: SessionDep,
_: AuthDep,
):
obj = validator_config_crud.get(session, id, organization_id, project_id)
return APIResponse.success_response(data=validator_config_crud.flatten(obj))


@router.patch(
"/{id}",
response_model=APIResponse[ValidatorResponse]
)
def update_validator(
id: UUID,
organization_id: int,
project_id: int,
payload: ValidatorUpdate,
session: SessionDep,
_: AuthDep,
):
obj = validator_config_crud.get(session, id, organization_id, project_id)
response_model = validator_config_crud.update(session, obj, payload.model_dump(exclude_unset=True))
return APIResponse.success_response(data=response_model)


@router.delete(
"/{id}",
response_model=APIResponse[dict]
)
def delete_validator(
id: UUID,
organization_id: int,
project_id: int,
session: SessionDep,
_: AuthDep,
):
obj = validator_config_crud.get(session, id, organization_id, project_id)
validator_config_crud.delete(session, obj)
return APIResponse.success_response(data={"message": "Validator deleted successfully"})
9 changes: 9 additions & 0 deletions backend/app/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,12 @@
SCORE = "score"

REPHRASE_ON_FAIL_PREFIX = "Please rephrase the query without unsafe content."

VALIDATOR_CONFIG_SYSTEM_FIELDS = {
"organization_id",
"project_id",
"type",
"stage",
"on_fail_action",
"is_enabled",
}
12 changes: 11 additions & 1 deletion backend/app/core/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,14 @@ class BiasCategories(Enum):
class GuardrailOnFail(Enum):
Exception = "exception"
Fix = "fix"
Rephrase = "rephrase"
Rephrase = "rephrase"

class Stage(Enum):
Input = "input"
Output = "output"

class ValidatorType(Enum):
LexicalSlur = "uli_slur_match"
PIIRemover = "pii_remover"
GenderAssumptionBias = "gender_assumption_bias"
BanList = "ban_list"
2 changes: 1 addition & 1 deletion backend/app/core/guardrail_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from guardrails import Guard

from app.models.guardrail_config import ValidatorConfigItem
from app.schemas.guardrail_config import ValidatorConfigItem

def build_guard(validator_items):
validators = [v_item.build() for v_item in validator_items]
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from guardrails.hub import BanList

from app.models.base_validator_config import BaseValidatorConfig
from app.core.validators.config.base_validator_config import BaseValidatorConfig

class BanListSafetyValidatorConfig(BaseValidatorConfig):
type: Literal["ban_list"]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from typing import List, Literal, Optional

from app.models.base_validator_config import BaseValidatorConfig
from app.core.enum import BiasCategories
from app.core.validators.gender_assumption_bias import GenderAssumptionBias
from app.core.validators.config.base_validator_config import BaseValidatorConfig

class GenderAssumptionBiasSafetyValidatorConfig(BaseValidatorConfig):
type: Literal["gender_assumption_bias"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from app.core.enum import SlurSeverity
from app.core.validators.lexical_slur import LexicalSlur
from app.models.base_validator_config import BaseValidatorConfig
from app.core.validators.config.base_validator_config import BaseValidatorConfig

class LexicalSlurSafetyValidatorConfig(BaseValidatorConfig):
type: Literal["uli_slur_match"]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from __future__ import annotations
from typing import List, Literal, Optional

from app.models.base_validator_config import BaseValidatorConfig
from app.core.validators.pii_remover import PIIRemover
from app.core.validators.config.base_validator_config import BaseValidatorConfig

class PIIRemoverSafetyValidatorConfig(BaseValidatorConfig):
type: Literal["pii_remover"]
Expand Down
2 changes: 1 addition & 1 deletion backend/app/crud/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from app.crud.request_log import RequestLogCrud
from app.crud.request_log import RequestLogCrud
2 changes: 1 addition & 1 deletion backend/app/crud/request_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from sqlmodel import Session

from app.models.logging.request import RequestLog, RequestLogUpdate, RequestStatus
from app.models.logging.request_log import RequestLog, RequestLogUpdate, RequestStatus
from app.utils import now

class RequestLogCrud:
Expand Down
Loading