Skip to content

Commit 8794d17

Browse files
feat: GAP-43 - Implement Pydantic input validation for all VF endpoints
- Create validation models for ResourceSpec, Agent, Commitment, Match, Exchange - Update all POST/PATCH endpoints to use Pydantic models - Add field constraints: types, lengths, ranges, enums - Prevent empty strings, validate URLs, enforce numeric ranges - Remove raw dict parameters from all endpoints Security: Prevents injection attacks, data corruption, and application crashes Files: valueflows_node/app/models/requests/vf_objects.py and 5 endpoint files
1 parent d521ef6 commit 8794d17

6 files changed

Lines changed: 315 additions & 57 deletions

File tree

valueflows_node/app/api/vf/agents.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import uuid
1313

1414
from ...models.vf.agent import Agent, AgentType
15+
from ...models.requests.vf_objects import AgentCreate
1516
from ...database import get_database
1617
from ...repositories.vf.agent_repo import AgentRepository
1718
from ...services.signing_service import SigningService
@@ -20,32 +21,33 @@
2021

2122

2223
@router.post("", response_model=dict)
23-
async def create_agent(agent_data: dict):
24+
async def create_agent(agent_data: AgentCreate):
2425
"""
2526
Create a new agent.
2627
27-
Request body should contain:
28-
- name: Agent name
29-
- type: Agent type ("person", "group", or "place")
30-
- description (optional): Description of agent
31-
- image_url (optional): URL to image
32-
- primary_location_id (optional): Primary location ID
33-
- contact_info (optional): Contact information
28+
GAP-43: Now uses Pydantic validation model.
29+
30+
Validates:
31+
- Required fields present (name)
32+
- Field types correct
33+
- String lengths reasonable
34+
- URLs have valid format
3435
"""
3536
try:
36-
# Generate ID if not provided
37-
if "id" not in agent_data:
38-
agent_data["id"] = f"agent:{uuid.uuid4()}"
37+
# Convert validated Pydantic model to dict
38+
data = agent_data.model_dump()
39+
40+
# Generate ID
41+
data["id"] = f"agent:{uuid.uuid4()}"
3942

4043
# Set timestamps
41-
agent_data["created_at"] = datetime.now().isoformat()
44+
data["created_at"] = datetime.now().isoformat()
4245

43-
# Handle field name mapping: "type" in request -> "agent_type" in model
44-
if "type" in agent_data:
45-
agent_data["agent_type"] = agent_data.pop("type")
46+
# Map fields: "note" -> note is already correct in Pydantic model
47+
# No mapping needed for agent create
4648

4749
# Create Agent object
48-
agent = Agent.from_dict(agent_data)
50+
agent = Agent.from_dict(data)
4951

5052
# Sign the agent
5153
# Use the node's signing service

valueflows_node/app/api/vf/commitments.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import uuid
77

88
from ...models.vf.commitment import Commitment, CommitmentStatus
9+
from ...models.requests.vf_objects import CommitmentCreate, CommitmentUpdate
910
from ...database import get_database
1011
from ...repositories.vf.commitment_repo import CommitmentRepository
1112
from ...services.vf_bundle_publisher import VFBundlePublisher
@@ -64,14 +65,26 @@ async def get_commitment(commitment_id: str):
6465

6566

6667
@router.post("/", response_model=dict)
67-
async def create_commitment(commitment_data: dict):
68-
"""Create a new commitment"""
68+
async def create_commitment(commitment_data: CommitmentCreate):
69+
"""
70+
Create a new commitment.
71+
72+
GAP-43: Now uses Pydantic validation model.
73+
74+
Validates:
75+
- Required fields present
76+
- Field types correct
77+
- Numeric ranges valid
78+
- String lengths reasonable
79+
"""
6980
try:
70-
if "id" not in commitment_data:
71-
commitment_data["id"] = f"commitment:{uuid.uuid4()}"
72-
commitment_data["created_at"] = datetime.now().isoformat()
81+
# Convert validated Pydantic model to dict
82+
data = commitment_data.model_dump()
7383

74-
commitment = Commitment.from_dict(commitment_data)
84+
data["id"] = f"commitment:{uuid.uuid4()}"
85+
data["created_at"] = datetime.now().isoformat()
86+
87+
commitment = Commitment.from_dict(data)
7588

7689
db = get_database()
7790
db.connect()
@@ -93,8 +106,12 @@ async def create_commitment(commitment_data: dict):
93106

94107

95108
@router.patch("/{commitment_id}", response_model=dict)
96-
async def update_commitment(commitment_id: str, updates: dict):
97-
"""Update a commitment's status"""
109+
async def update_commitment(commitment_id: str, updates: CommitmentUpdate):
110+
"""
111+
Update a commitment's status.
112+
113+
GAP-43: Now uses Pydantic validation model.
114+
"""
98115
try:
99116
db = get_database()
100117
db.connect()
@@ -104,13 +121,13 @@ async def update_commitment(commitment_id: str, updates: dict):
104121
if not commitment:
105122
raise HTTPException(status_code=404, detail="Commitment not found")
106123

107-
# Update status if provided
108-
if "status" in updates:
109-
commitment.status = CommitmentStatus(updates["status"])
124+
# Convert validated Pydantic model to dict
125+
update_dict = updates.model_dump(exclude_unset=True)
110126

111-
# Update fulfilled_by_event_id if provided
112-
if "fulfilled_by_event_id" in updates:
113-
commitment.fulfilled_by_event_id = updates["fulfilled_by_event_id"]
127+
# Update fields from validated model
128+
for key, value in update_dict.items():
129+
if hasattr(commitment, key):
130+
setattr(commitment, key, value)
114131

115132
updated_commitment = commitment_repo.update(commitment)
116133

valueflows_node/app/api/vf/exchanges.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import uuid
66

77
from ...models.vf.exchange import Exchange
8+
from ...models.requests.vf_objects import ExchangeCreate
89
from ...database import get_database
910
from ...repositories.vf.exchange_repo import ExchangeRepository
1011
from ...services.vf_bundle_publisher import VFBundlePublisher
@@ -39,14 +40,25 @@ async def get_exchanges(status: str = None, agent_id: str = None):
3940

4041

4142
@router.post("/", response_model=dict)
42-
async def create_exchange(exchange_data: dict):
43-
"""Create a new exchange (from accepted match)"""
43+
async def create_exchange(exchange_data: ExchangeCreate):
44+
"""
45+
Create a new exchange (from accepted match).
46+
47+
GAP-43: Now uses Pydantic validation model.
48+
49+
Validates:
50+
- Required fields present (name)
51+
- Field types correct
52+
- String lengths reasonable
53+
"""
4454
try:
45-
if "id" not in exchange_data:
46-
exchange_data["id"] = f"exchange:{uuid.uuid4()}"
47-
exchange_data["created_at"] = datetime.now().isoformat()
55+
# Convert validated Pydantic model to dict
56+
data = exchange_data.model_dump()
57+
58+
data["id"] = f"exchange:{uuid.uuid4()}"
59+
data["created_at"] = datetime.now().isoformat()
4860

49-
exchange = Exchange.from_dict(exchange_data)
61+
exchange = Exchange.from_dict(data)
5062

5163
db = get_database()
5264
db.connect()

valueflows_node/app/api/vf/matches.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import sqlite3
77

88
from ...models.vf.match import Match
9+
from ...models.requests.vf_objects import MatchCreate
910
from ...database import get_database
1011
from ...repositories.vf.match_repo import MatchRepository
1112
from ...services.vf_bundle_publisher import VFBundlePublisher
@@ -48,14 +49,25 @@ async def get_matches(status: str = None, agent_id: str = None):
4849

4950

5051
@router.post("/", response_model=dict)
51-
async def create_match(match_data: dict):
52-
"""Create a new match (offer + need pairing)"""
52+
async def create_match(match_data: MatchCreate):
53+
"""
54+
Create a new match (offer + need pairing).
55+
56+
GAP-43: Now uses Pydantic validation model.
57+
58+
Validates:
59+
- Required fields present
60+
- Field types correct
61+
- Numeric ranges valid
62+
"""
5363
try:
54-
if "id" not in match_data:
55-
match_data["id"] = f"match:{uuid.uuid4()}"
56-
match_data["created_at"] = datetime.now().isoformat()
64+
# Convert validated Pydantic model to dict
65+
data = match_data.model_dump()
66+
67+
data["id"] = f"match:{uuid.uuid4()}"
68+
data["created_at"] = datetime.now().isoformat()
5769

58-
match = Match.from_dict(match_data)
70+
match = Match.from_dict(data)
5971

6072
# GAP-107: Check if either user has blocked the other
6173
block_repo = get_block_repo()

valueflows_node/app/api/vf/resource_specs.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import uuid
1313

1414
from ...models.vf.resource_spec import ResourceSpec, ResourceCategory
15+
from ...models.requests.vf_objects import ResourceSpecCreate
1516
from ...database import get_database
1617
from ...repositories.vf.resource_spec_repo import ResourceSpecRepository
1718
from ...services.signing_service import SigningService
@@ -20,32 +21,35 @@
2021

2122

2223
@router.post("", response_model=dict)
23-
async def create_resource_spec(spec_data: dict):
24+
async def create_resource_spec(spec_data: ResourceSpecCreate):
2425
"""
2526
Create a new resource specification.
2627
27-
Request body should contain:
28-
- name: Resource spec name (e.g., "Tomatoes")
29-
- category: Resource category (e.g., "food", "tools", "skills")
30-
- unit (optional): Default unit of measure (e.g., "lbs", "hours", "items")
31-
- description (optional): Description of resource spec
32-
- subcategory (optional): Subcategory (e.g., "Vegetables")
33-
- image_url (optional): URL to image
28+
GAP-43: Now uses Pydantic validation model.
29+
30+
Validates:
31+
- Required fields present (name, category)
32+
- Field types correct
33+
- String lengths reasonable
34+
- Category is valid enum value
35+
- URLs have valid format
3436
"""
3537
try:
36-
# Generate ID if not provided
37-
if "id" not in spec_data:
38-
spec_data["id"] = f"resource_spec:{uuid.uuid4()}"
38+
# Convert validated Pydantic model to dict
39+
data = spec_data.model_dump()
40+
41+
# Generate ID
42+
data["id"] = f"resource_spec:{uuid.uuid4()}"
3943

4044
# Set timestamps
41-
spec_data["created_at"] = datetime.now().isoformat()
45+
data["created_at"] = datetime.now().isoformat()
4246

4347
# Handle field name mapping: "unit" in request -> "default_unit" in model
44-
if "unit" in spec_data:
45-
spec_data["default_unit"] = spec_data.pop("unit")
48+
if "unit" in data:
49+
data["default_unit"] = data.pop("unit")
4650

4751
# Create ResourceSpec object
48-
spec = ResourceSpec.from_dict(spec_data)
52+
spec = ResourceSpec.from_dict(data)
4953

5054
# Sign the resource spec
5155
# Use the node's signing service

0 commit comments

Comments
 (0)