Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ecd6f39
Added banlist management code
rkritika1508 Feb 10, 2026
9e364f7
Added tests
rkritika1508 Feb 10, 2026
bb7ccd8
resolved comments
rkritika1508 Feb 10, 2026
dd15382
resolved comment
rkritika1508 Feb 10, 2026
c081981
Added seed database and updated tests
rkritika1508 Feb 10, 2026
7e7523e
resolved comment
rkritika1508 Feb 10, 2026
0df63bd
resolved comments
rkritika1508 Feb 10, 2026
3013e48
resolved comment
rkritika1508 Feb 10, 2026
6052c70
apply formatting
rkritika1508 Feb 11, 2026
e509b04
fixed tests
rkritika1508 Feb 11, 2026
2f81a43
Fixed critical issues - auth, docker setup, db indexing, error handli…
rkritika1508 Feb 12, 2026
0dd25cc
Added banlist management code
rkritika1508 Feb 10, 2026
6c0613c
fixed tests
rkritika1508 Feb 11, 2026
85b9a94
Merge branch 'main' into feat/banlist-management
rkritika1508 Feb 12, 2026
bca7127
resolved comments
rkritika1508 Feb 12, 2026
51135f9
resolved comments
rkritika1508 Feb 16, 2026
8a4e096
resolved comments
rkritika1508 Feb 16, 2026
162fada
Merge branch 'main' into feat/banlist-management
rkritika1508 Feb 16, 2026
42fa6b2
resolved comments
rkritika1508 Feb 16, 2026
9a1509d
resolved comments
rkritika1508 Feb 16, 2026
2dd842f
added ban list preprocessing
rkritika1508 Feb 16, 2026
ba75e02
precommit fix
rkritika1508 Feb 16, 2026
49313c9
precommit
rkritika1508 Feb 16, 2026
2de9dd3
revert
rkritika1508 Feb 16, 2026
4c6cb29
resolved comments
rkritika1508 Feb 16, 2026
a1b9c70
resolved comment
rkritika1508 Feb 16, 2026
278bfba
Merge branch 'main' into feat/banlist-management
rkritika1508 Feb 17, 2026
ecf8bd1
updated banlist api
rkritika1508 Feb 17, 2026
6b87071
resolved comment
rkritika1508 Feb 17, 2026
93f14b8
updated verify api and tests
rkritika1508 Feb 17, 2026
8d12092
resolved comments
rkritika1508 Feb 17, 2026
b3c1acc
resolved comments
rkritika1508 Feb 17, 2026
edf3fc9
fix formatting
AkhileshNegi Feb 17, 2026
9a946dd
update version
AkhileshNegi Feb 17, 2026
0612a8a
formatting files
AkhileshNegi Feb 17, 2026
99a315a
fixed test
rkritika1508 Feb 17, 2026
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
6 changes: 2 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,10 @@ SENTRY_DSN=

DOCKER_IMAGE_BACKEND=kaapi-guardrails-backend

# Callback Timeouts (in seconds)
CALLBACK_CONNECT_TIMEOUT=3
CALLBACK_READ_TIMEOUT=10

# require as a env if you want to use doc transformation
OPENAI_API_KEY="<ADD-KEY>"
GUARDRAILS_HUB_API_KEY="<ADD-KEY>"
# SHA-256 hex digest of your bearer token (64 lowercase hex chars)
AUTH_TOKEN="<ADD-HASH-TOKEN>"
KAAPI_AUTH_URL="<ADD-KAAPI-AUTH-URL>"
KAAPI_AUTH_TIMEOUT=5
6 changes: 2 additions & 4 deletions .env.test.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,10 @@ SENTRY_DSN=

DOCKER_IMAGE_BACKEND=kaapi-guardrails-backend

# Callback Timeouts (in seconds)
CALLBACK_CONNECT_TIMEOUT=3
CALLBACK_READ_TIMEOUT=10

# require as a env if you want to use doc transformation
OPENAI_API_KEY="<ADD-KEY>"
GUARDRAILS_HUB_API_KEY="<ADD-KEY>"
# SHA-256 hex digest of your bearer token (64 lowercase hex chars)
AUTH_TOKEN="<ADD-HASH-TOKEN>"
KAAPI_AUTH_URL="<ADD-KAAPI-AUTH-URL>"
KAAPI_AUTH_TIMEOUT=5
2 changes: 1 addition & 1 deletion .github/workflows/continuous_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "0.4.15"
version: "0.7.2"
enable-cache: true

- name: Install dependencies
Expand Down
6 changes: 4 additions & 2 deletions backend/app/alembic/versions/001_added_request_log.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
"""Added request log

Revision ID: 001
Revises:
Revises:
Create Date: 2026-01-07 09:42:54.128852

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
import sqlmodel


# revision identifiers, used by Alembic.
revision: str = "001"
down_revision: str | None = None
Expand All @@ -23,6 +23,8 @@ def upgrade() -> None:
op.create_table(
"request_log",
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("request_id", sa.Uuid(), nullable=False),
sa.Column("response_id", sa.Uuid(), nullable=True),
sa.Column(
Expand Down
4 changes: 3 additions & 1 deletion backend/app/alembic/versions/002_added_validator_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
Create Date: 2026-01-07 09:43:48.002351

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
import sqlmodel


# revision identifiers, used by Alembic.
revision: str = "002"
down_revision: str = "001"
Expand All @@ -23,6 +23,8 @@ def upgrade() -> None:
op.create_table(
"validator_log",
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("request_id", sa.Uuid(), nullable=False),
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("input", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
Expand Down
1 change: 1 addition & 0 deletions backend/app/alembic/versions/003_added_validator_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Create Date: 2026-02-05 09:42:54.128852

"""

from typing import Sequence, Union

from alembic import op
Expand Down
14 changes: 13 additions & 1 deletion backend/app/alembic/versions/004_added_log_indexes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
Create Date: 2026-02-11 10:45:00.000000

"""

from typing import Sequence, Union

from alembic import op


# revision identifiers, used by Alembic.
revision: str = "004"
down_revision: str = "003"
Expand All @@ -21,19 +21,31 @@ def upgrade() -> None:
op.create_index("idx_request_log_request_id", "request_log", ["request_id"])
op.create_index("idx_request_log_status", "request_log", ["status"])
op.create_index("idx_request_log_inserted_at", "request_log", ["inserted_at"])
op.create_index(
"idx_request_log_organization_id", "request_log", ["organization_id"]
)
op.create_index("idx_request_log_project_id", "request_log", ["project_id"])

op.create_index("idx_validator_log_request_id", "validator_log", ["request_id"])
op.create_index("idx_validator_log_inserted_at", "validator_log", ["inserted_at"])
op.create_index("idx_validator_log_outcome", "validator_log", ["outcome"])
op.create_index("idx_validator_log_name", "validator_log", ["name"])
op.create_index(
"idx_validator_log_organization_id", "validator_log", ["organization_id"]
)
op.create_index("idx_validator_log_project_id", "validator_log", ["project_id"])


def downgrade() -> None:
op.drop_index("idx_validator_log_inserted_at", table_name="validator_log")
op.drop_index("idx_validator_log_request_id", table_name="validator_log")
op.drop_index("idx_validator_log_outcome", table_name="validator_log")
op.drop_index("idx_validator_log_name", table_name="validator_log")
op.drop_index("idx_validator_log_project_id", table_name="validator_log")
op.drop_index("idx_validator_log_organization_id", table_name="validator_log")

op.drop_index("idx_request_log_inserted_at", table_name="request_log")
op.drop_index("idx_request_log_status", table_name="request_log")
op.drop_index("idx_request_log_request_id", table_name="request_log")
op.drop_index("idx_request_log_organization_id", table_name="request_log")
op.drop_index("idx_request_log_project_id", table_name="request_log")
62 changes: 62 additions & 0 deletions backend/app/alembic/versions/005_added_banlist_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Added ban_list table

Revision ID: 005
Revises: 004
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 = "005"
down_revision = "004"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"ban_list",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("name", sa.String(), nullable=False),
sa.Column("description", sa.String(), nullable=False),
sa.Column("organization_id", sa.Integer(), nullable=False),
sa.Column("project_id", sa.Integer(), nullable=False),
sa.Column("domain", sa.String(), nullable=False),
sa.Column("is_public", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column(
"banned_words",
postgresql.ARRAY(sa.String(length=100)),
nullable=False,
server_default="{}",
),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"name", "organization_id", "project_id", name="uq_ban_list_name_org_project"
),
sa.CheckConstraint(
"coalesce(array_length(banned_words, 1), 0) <= 1000",
name="ck_ban_list_banned_words_max_items",
),
)

op.create_index("idx_ban_list_organization", "ban_list", ["organization_id"])
op.create_index("idx_ban_list_project", "ban_list", ["project_id"])
op.create_index("idx_ban_list_domain", "ban_list", ["domain"])
op.create_index(
"idx_ban_list_is_public_true",
"ban_list",
["is_public"],
postgresql_where=sa.text("is_public = true"),
)


def downgrade() -> None:
op.drop_table("ban_list")
95 changes: 83 additions & 12 deletions backend/app/api/deps.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from collections.abc import Generator
from dataclasses import dataclass
from typing import Annotated

import hashlib
import secrets
import httpx

from fastapi import Depends, HTTPException, status, Security
from fastapi import Depends, Header, HTTPException, Security, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlmodel import Session

Expand All @@ -17,34 +20,102 @@ def get_db() -> Generator[Session, None, None]:


SessionDep = Annotated[Session, Depends(get_db)]


# Static bearer token auth for internal routes.
security = HTTPBearer(auto_error=False)


def _hash_token(token: str) -> str:
return hashlib.sha256(token.encode("utf-8")).hexdigest()


def _unauthorized(detail: str) -> HTTPException:
return HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=detail,
)


def verify_bearer_token(
credentials: Annotated[
HTTPAuthorizationCredentials | None,
Security(security),
]
):
],
) -> bool:
if credentials is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing Authorization header",
)
raise _unauthorized("Missing Authorization header")

if not secrets.compare_digest(
_hash_token(credentials.credentials), settings.AUTH_TOKEN
_hash_token(credentials.credentials),
settings.AUTH_TOKEN,
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authorization token",
)
raise _unauthorized("Invalid authorization token")

return True


AuthDep = Annotated[bool, Depends(verify_bearer_token)]


# Multitenant auth context resolved from X-API-KEY.
@dataclass
class TenantContext:
organization_id: int
project_id: int


def _fetch_tenant_from_backend(token: str) -> TenantContext:
if not settings.KAAPI_AUTH_URL:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="KAAPI_AUTH_URL is not configured",
)

try:
response = httpx.get(
f"{settings.KAAPI_AUTH_URL}/apikeys/verify",
headers={"X-API-KEY": f"ApiKey {token}"},
timeout=settings.KAAPI_AUTH_TIMEOUT,
)
except httpx.RequestError:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Auth service unavailable",
)

if response.status_code != 200:
raise _unauthorized("Invalid API key")

data = response.json()
if not isinstance(data, dict) or data.get("success") is not True:
raise _unauthorized("Invalid API key")

record = data.get("data")
if not isinstance(record, dict):
raise _unauthorized("Invalid API key")

organization_id = record.get("organization_id")
project_id = record.get("project_id")
if not isinstance(organization_id, int) or not isinstance(project_id, int):
raise _unauthorized("Invalid API key")

return TenantContext(
organization_id=organization_id,
project_id=project_id,
)


def validate_multitenant_key(
x_api_key: Annotated[str | None, Header(alias="X-API-KEY")] = None,
) -> TenantContext:
if not x_api_key or not x_api_key.strip():
raise _unauthorized("Missing X-API-KEY header")

return _fetch_tenant_from_backend(x_api_key.strip())


MultitenantAuthDep = Annotated[
TenantContext,
Depends(validate_multitenant_key),
]
5 changes: 3 additions & 2 deletions backend/app/api/main.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from fastapi import APIRouter

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

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

# if settings.ENVIRONMENT == "local":
# api_router.include_router(private.router)
Loading