Skip to content

Commit a59e0b8

Browse files
authored
Merge pull request #18 from linto-ai/feat/security-level-integer
refactor: change security_level from text to integer (0-2)
2 parents db42421 + ee43487 commit a59e0b8

33 files changed

+323
-116
lines changed

app/api/v1/providers.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ async def create_provider(
4141
- **provider_type**: Type of provider (openai, anthropic, cohere, custom)
4242
- **api_base_url**: Base URL for the provider API
4343
- **api_key**: API key (will be encrypted)
44-
- **security_level**: Security level (secure, sensitive, insecure)
44+
- **security_level**: Security level (0=Insecure, 1=Medium, 2=Secure)
4545
- **metadata**: Optional additional configuration
4646
"""
4747
try:
@@ -70,7 +70,7 @@ async def create_provider(
7070
}
7171
)
7272
async def list_providers(
73-
security_level: Optional[str] = Query(None, description="Filter by security level"),
73+
security_level: Optional[int] = Query(None, ge=0, le=2, description="Filter by security level (0, 1, or 2)"),
7474
provider_type: Optional[str] = Query(None, description="Filter by provider type"),
7575
page: int = Query(1, ge=1, description="Page number"),
7676
page_size: int = Query(20, ge=1, le=100, description="Items per page"),
@@ -80,7 +80,7 @@ async def list_providers(
8080
"""
8181
List providers with optional filtering and pagination.
8282
83-
- **security_level**: Filter by security level
83+
- **security_level**: Filter by security level (0=Insecure, 1=Medium, 2=Secure)
8484
- **provider_type**: Filter by provider type
8585
- **page**: Page number (default: 1)
8686
- **page_size**: Items per page (default: 20, max: 100)

app/database/models.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import uuid
22
from datetime import datetime
3-
from sqlalchemy import Column, String, DateTime, Text, CheckConstraint, UniqueConstraint, JSON
3+
from sqlalchemy import Column, String, DateTime, Text, CheckConstraint, UniqueConstraint, JSON, Integer
44
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
55
from sqlalchemy.types import TypeDecorator, CHAR
66
from .connection import Base
@@ -55,7 +55,7 @@ class Provider(Base):
5555
provider_type = Column(String(50), nullable=False)
5656
api_base_url = Column(String(500), nullable=False)
5757
api_key_encrypted = Column(Text, nullable=False)
58-
security_level = Column(String(20), nullable=False, default="sensitive")
58+
security_level = Column(Integer, nullable=False, default=1)
5959
provider_metadata = Column("metadata", JSON, nullable=False, default=dict)
6060
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
6161
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
@@ -64,7 +64,7 @@ class Provider(Base):
6464
__table_args__ = (
6565
UniqueConstraint('name', name='uq_provider_name'),
6666
CheckConstraint(
67-
"security_level IN ('secure', 'sensitive', 'insecure')",
67+
"security_level IN (0, 1, 2)",
6868
name='ck_security_level'
6969
),
7070
CheckConstraint(
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
"""security_level_integer - Convert security_level from text to integer
2+
3+
Revision ID: 003
4+
Revises: 002
5+
Create Date: 2025-01-21 00:00:00.000000
6+
7+
This migration converts security_level from text-based values to integers:
8+
- 'insecure' -> 0 (Lowest security)
9+
- 'sensitive' -> 1 (Medium security)
10+
- 'secure' -> 2 (Highest security)
11+
12+
Affected tables:
13+
- providers: security_level NOT NULL with default 1
14+
- models: security_level NULLABLE
15+
"""
16+
from typing import Sequence, Union
17+
18+
from alembic import op
19+
import sqlalchemy as sa
20+
21+
# revision identifiers, used by Alembic.
22+
revision: str = '003'
23+
down_revision: Union[str, None] = '002'
24+
branch_labels: Union[str, Sequence[str], None] = None
25+
depends_on: Union[str, Sequence[str], None] = None
26+
27+
28+
def upgrade() -> None:
29+
# ==========================================================================
30+
# 1. Providers table: Convert security_level from VARCHAR to INTEGER
31+
# ==========================================================================
32+
33+
# Drop existing check constraint
34+
op.execute("ALTER TABLE providers DROP CONSTRAINT IF EXISTS check_security_level;")
35+
36+
# Add temporary column for integer values
37+
op.add_column('providers', sa.Column('security_level_new', sa.Integer(), nullable=True))
38+
39+
# Migrate data: 'insecure' -> 0, 'sensitive' -> 1, 'secure' -> 2
40+
op.execute("""
41+
UPDATE providers SET security_level_new = CASE
42+
WHEN security_level = 'insecure' THEN 0
43+
WHEN security_level = 'sensitive' THEN 1
44+
WHEN security_level = 'secure' THEN 2
45+
ELSE 1
46+
END;
47+
""")
48+
49+
# Drop old column
50+
op.drop_column('providers', 'security_level')
51+
52+
# Rename new column to security_level
53+
op.execute("ALTER TABLE providers RENAME COLUMN security_level_new TO security_level;")
54+
55+
# Set NOT NULL and default
56+
op.execute("ALTER TABLE providers ALTER COLUMN security_level SET NOT NULL;")
57+
op.execute("ALTER TABLE providers ALTER COLUMN security_level SET DEFAULT 1;")
58+
59+
# Add check constraint for integer values
60+
op.execute("""
61+
ALTER TABLE providers ADD CONSTRAINT check_security_level
62+
CHECK (security_level IN (0, 1, 2));
63+
""")
64+
65+
# Recreate index for security_level
66+
op.execute("DROP INDEX IF EXISTS idx_providers_security;")
67+
op.execute("CREATE INDEX idx_providers_security ON providers (security_level);")
68+
69+
# ==========================================================================
70+
# 2. Models table: Convert security_level from VARCHAR to INTEGER (nullable)
71+
# ==========================================================================
72+
73+
# Add temporary column for integer values
74+
op.add_column('models', sa.Column('security_level_new', sa.Integer(), nullable=True))
75+
76+
# Migrate data: 'insecure' -> 0, 'sensitive' -> 1, 'secure' -> 2, NULL stays NULL
77+
op.execute("""
78+
UPDATE models SET security_level_new = CASE
79+
WHEN security_level = 'insecure' THEN 0
80+
WHEN security_level = 'sensitive' THEN 1
81+
WHEN security_level = 'secure' THEN 2
82+
ELSE NULL
83+
END;
84+
""")
85+
86+
# Drop old column
87+
op.drop_column('models', 'security_level')
88+
89+
# Rename new column to security_level
90+
op.execute("ALTER TABLE models RENAME COLUMN security_level_new TO security_level;")
91+
92+
# Add check constraint for integer values (allowing NULL)
93+
op.execute("""
94+
ALTER TABLE models ADD CONSTRAINT check_model_security_level
95+
CHECK (security_level IS NULL OR security_level IN (0, 1, 2));
96+
""")
97+
98+
99+
def downgrade() -> None:
100+
# ==========================================================================
101+
# 1. Models table: Revert to VARCHAR
102+
# ==========================================================================
103+
104+
# Drop check constraint
105+
op.execute("ALTER TABLE models DROP CONSTRAINT IF EXISTS check_model_security_level;")
106+
107+
# Add temporary column for text values
108+
op.add_column('models', sa.Column('security_level_old', sa.String(50), nullable=True))
109+
110+
# Migrate data back: 0 -> 'insecure', 1 -> 'sensitive', 2 -> 'secure'
111+
op.execute("""
112+
UPDATE models SET security_level_old = CASE
113+
WHEN security_level = 0 THEN 'insecure'
114+
WHEN security_level = 1 THEN 'sensitive'
115+
WHEN security_level = 2 THEN 'secure'
116+
ELSE NULL
117+
END;
118+
""")
119+
120+
# Drop integer column
121+
op.drop_column('models', 'security_level')
122+
123+
# Rename old column back
124+
op.execute("ALTER TABLE models RENAME COLUMN security_level_old TO security_level;")
125+
126+
# ==========================================================================
127+
# 2. Providers table: Revert to VARCHAR
128+
# ==========================================================================
129+
130+
# Drop check constraint and index
131+
op.execute("ALTER TABLE providers DROP CONSTRAINT IF EXISTS check_security_level;")
132+
op.execute("DROP INDEX IF EXISTS idx_providers_security;")
133+
134+
# Add temporary column for text values
135+
op.add_column('providers', sa.Column('security_level_old', sa.String(20), nullable=True))
136+
137+
# Migrate data back: 0 -> 'insecure', 1 -> 'sensitive', 2 -> 'secure'
138+
op.execute("""
139+
UPDATE providers SET security_level_old = CASE
140+
WHEN security_level = 0 THEN 'insecure'
141+
WHEN security_level = 1 THEN 'sensitive'
142+
WHEN security_level = 2 THEN 'secure'
143+
ELSE 'sensitive'
144+
END;
145+
""")
146+
147+
# Drop integer column
148+
op.drop_column('providers', 'security_level')
149+
150+
# Rename old column back
151+
op.execute("ALTER TABLE providers RENAME COLUMN security_level_old TO security_level;")
152+
153+
# Set NOT NULL
154+
op.execute("ALTER TABLE providers ALTER COLUMN security_level SET NOT NULL;")
155+
156+
# Recreate original check constraint
157+
op.execute("""
158+
ALTER TABLE providers ADD CONSTRAINT check_security_level
159+
CHECK (security_level IN ('secure', 'sensitive', 'insecure'));
160+
""")
161+
162+
# Recreate index
163+
op.execute("CREATE INDEX idx_providers_security ON providers (security_level);")

app/models/model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class Model(Base):
4444

4545
# Extended fields
4646
huggingface_repo = Column(String(500), nullable=True)
47-
security_level = Column(String(50), nullable=True)
47+
security_level = Column(Integer, nullable=True)
4848
deployment_name = Column(String(200), nullable=True)
4949
description = Column(Text, nullable=True)
5050
best_use = Column(Text, nullable=True)

app/models/provider.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env python3
22
import uuid
3-
from sqlalchemy import Column, String, DateTime, Text, CheckConstraint, Index, UniqueConstraint
3+
from sqlalchemy import Column, String, DateTime, Text, CheckConstraint, Index, UniqueConstraint, Integer
44
from sqlalchemy.dialects.postgresql import UUID, JSONB
55
from sqlalchemy.orm import relationship
66
from sqlalchemy.sql import func
@@ -17,7 +17,7 @@ class Provider(Base):
1717
provider_type = Column(String(50), nullable=False, index=True)
1818
api_base_url = Column(Text, nullable=False)
1919
api_key_encrypted = Column(Text, nullable=False)
20-
security_level = Column(String(20), nullable=False, index=True)
20+
security_level = Column(Integer, nullable=False, default=1, index=True)
2121
provider_metadata = Column("metadata", JSONB, default={}, nullable=False, server_default='{}')
2222
created_at = Column(
2323
DateTime(timezone=True),
@@ -42,7 +42,7 @@ class Provider(Base):
4242
__table_args__ = (
4343
UniqueConstraint("name", name="uq_provider_name"),
4444
CheckConstraint(
45-
"security_level IN ('secure', 'sensitive', 'insecure')",
45+
"security_level IN (0, 1, 2)",
4646
name="check_security_level"
4747
),
4848
Index("idx_providers_security", "security_level"),

app/schemas/model.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class ModelBase(BaseModel):
1717
metadata: Dict[str, Any] = Field(default_factory=dict)
1818
# Extended fields
1919
huggingface_repo: Optional[str] = Field(None, max_length=500)
20-
security_level: Optional[str] = Field(None, max_length=50)
20+
security_level: Optional[int] = Field(None, ge=0, le=2)
2121
deployment_name: Optional[str] = Field(None, max_length=200)
2222
description: Optional[str] = None
2323
best_use: Optional[str] = None
@@ -42,7 +42,7 @@ class ModelUpdate(BaseModel):
4242
metadata: Optional[Dict[str, Any]] = None
4343
# Extended fields
4444
huggingface_repo: Optional[str] = Field(None, max_length=500)
45-
security_level: Optional[str] = Field(None, max_length=50)
45+
security_level: Optional[int] = Field(None, ge=0, le=2)
4646
deployment_name: Optional[str] = Field(None, max_length=200)
4747
description: Optional[str] = None
4848
best_use: Optional[str] = None

app/schemas/provider.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,34 @@
11
from datetime import datetime
22
from typing import Optional, Dict, Any
33
from uuid import UUID
4-
from pydantic import BaseModel, Field, validator
4+
from pydantic import BaseModel, Field, field_validator
5+
56

67
class CreateProviderRequest(BaseModel):
78
name: str = Field(..., min_length=1, max_length=100)
89
provider_type: str = Field(..., pattern="^(openai|anthropic|cohere|openrouter|custom)$")
910
api_base_url: str = Field(..., min_length=1, max_length=500)
1011
api_key: str = Field(..., min_length=1, max_length=2000)
11-
security_level: str = Field(default="sensitive", pattern="^(secure|sensitive|insecure)$")
12+
security_level: int = Field(default=1, ge=0, le=2)
1213
metadata: Dict[str, Any] = Field(default_factory=dict)
1314

14-
@validator('api_base_url')
15+
@field_validator('api_base_url')
16+
@classmethod
1517
def validate_url(cls, v):
1618
if not (v.startswith('http://') or v.startswith('https://')):
1719
raise ValueError('API base URL must start with http:// or https://')
1820
return v
1921

22+
2023
class UpdateProviderRequest(BaseModel):
2124
name: Optional[str] = Field(None, min_length=1, max_length=100)
2225
api_base_url: Optional[str] = Field(None, min_length=1, max_length=500)
2326
api_key: Optional[str] = Field(None, min_length=1, max_length=2000)
24-
security_level: Optional[str] = Field(None, pattern="^(secure|sensitive|insecure)$")
27+
security_level: Optional[int] = Field(None, ge=0, le=2)
2528
metadata: Optional[Dict[str, Any]] = None
2629

27-
@validator('api_base_url')
30+
@field_validator('api_base_url')
31+
@classmethod
2832
def validate_url(cls, v):
2933
if v is not None and not (v.startswith('http://') or v.startswith('https://')):
3034
raise ValueError('API base URL must start with http:// or https://')
@@ -37,7 +41,7 @@ class ProviderResponse(BaseModel):
3741
provider_type: str
3842
api_base_url: str
3943
api_key_exists: bool
40-
security_level: str
44+
security_level: int
4145
created_at: datetime
4246
updated_at: datetime
4347
metadata: Dict[str, Any]

app/schemas/service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,8 @@ class ModelInfo(BaseModel):
208208
# Token limits - essential for processing decisions
209209
context_length: Optional[int] = None
210210
max_generation_length: Optional[int] = None
211-
# Security classification for the model
212-
security_level: Optional[str] = None
211+
# Security classification for the model (0=Insecure, 1=Medium, 2=Secure)
212+
security_level: Optional[int] = None
213213

214214
class Config:
215215
from_attributes = True

app/seeds/base_seed.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ async def get_or_create_provider(
7676
provider_type=provider_type,
7777
api_base_url=base_url,
7878
api_key_encrypted=encrypted_key,
79-
security_level="sensitive",
79+
security_level=1, # 1 = Medium security
8080
)
8181

8282
db.add(provider)

app/services/provider_service.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ async def get_provider(
114114
async def list_providers(
115115
self,
116116
db: AsyncSession,
117-
security_level: Optional[str] = None,
117+
security_level: Optional[int] = None,
118118
provider_type: Optional[str] = None,
119119
page: int = 1,
120120
limit: int = 20
@@ -124,7 +124,7 @@ async def list_providers(
124124
125125
Args:
126126
db: Database session
127-
security_level: Optional security level filter
127+
security_level: Optional security level filter (0, 1, or 2)
128128
provider_type: Optional provider type filter
129129
page: Page number (1-indexed)
130130
limit: Items per page
@@ -135,7 +135,7 @@ async def list_providers(
135135
query = select(Provider)
136136

137137
# Apply filters
138-
if security_level:
138+
if security_level is not None:
139139
query = query.where(Provider.security_level == security_level)
140140
if provider_type:
141141
query = query.where(Provider.provider_type == provider_type)

0 commit comments

Comments
 (0)