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
121 changes: 121 additions & 0 deletions .cursor/rules/authorization.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
---
description: Rules for authorization logic and access control patterns
globs: ["main.py"]
alwaysApply: true
---

# Authorization Guidelines

Authorization logic evaluates whether users have access to specific resources based on identity, roles, and configured policies.

## Authorization Pattern

**Basic structure:**
```python
@app.post("/authorize")
def authorize(body: dict, response: Response):
if body["resource"] == "kratos:admin":
return resolve_kratos_admin(body, response)

response.status_code = status.HTTP_403_FORBIDDEN
return {"status": "not authorized"}
```

## Resource-Based Routing

Route authorization decisions by resource type:

```python
def authorize(body: dict, response: Response):
resource = body.get("resource")

if resource == "kratos:admin":
return resolve_kratos_admin(body, response)
elif resource == "other:resource":
return resolve_other_resource(body, response)

# Default: deny access
response.status_code = status.HTTP_403_FORBIDDEN
return {"status": "not authorized"}
```

## Authorization Decision Functions

**Structure:**
```python
def resolve_kratos_admin(body: dict, response: Response) -> dict:
subject = body["subject"]["identity"]

# Check authorization conditions
if is_authorized(subject):
response.status_code = status.HTTP_200_OK
return {"status": "authorized"}

response.status_code = status.HTTP_403_FORBIDDEN
return {"status": "not authorized"}
```

## Authorization Checks

**Role-based check:**
```python
# Handle None metadata_public safely
metadata = subject.get("metadata_public") or {}
if metadata.get("role") == "ADMIN":
# Authorized
```

**Verification check:**
```python
if subject["verifiable_addresses"][0]["verified"]:
# Email verified
```

## Safe Data Access

**Handle optional fields:**
```python
# Use .get() with defaults for optional fields
metadata = subject.get("metadata_public") or {}

# Use .get() with default for nested access
role = (subject.get("metadata_public") or {}).get("role")
```

**Validate required fields exist:**
```python
if "subject" not in body or "identity" not in body["subject"]:
response.status_code = status.HTTP_400_BAD_REQUEST
return {"status": "invalid request"}
```

## Response Patterns

**Authorized:**
```python
response.status_code = status.HTTP_200_OK
return {"status": "authorized"}
```

**Forbidden:**
```python
response.status_code = status.HTTP_403_FORBIDDEN
return {"status": "not authorized"}
```

**Invalid Request:**
```python
response.status_code = status.HTTP_400_BAD_REQUEST
return {"status": "invalid request", "error": "missing required field"}
```

## Best Practices

1. Always check all authorization conditions before granting access
2. Use safe access patterns (`.get()` with defaults) for optional fields
3. Validate required fields exist before accessing nested data
4. Return consistent response format: `{"status": "authorized"}` or `{"status": "not authorized"}`
5. Set appropriate HTTP status codes (200, 403, 400)
6. Keep authorization logic focused and testable
7. Document authorization policies clearly in code comments
8. Handle edge cases (None values, missing fields, empty arrays)
122 changes: 122 additions & 0 deletions .cursor/rules/exceptions.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
---
description: Rules for exception handling and error responses
globs: ["main.py"]
alwaysApply: true
---

# Exceptions Guidelines

Handle errors gracefully with appropriate HTTP status codes and clear error messages.

## HTTP Status Codes

**Standard status codes:**
- `200 OK`: Authorization granted
- `400 Bad Request`: Invalid request format or missing required fields
- `403 Forbidden`: Authorization denied
- `500 Internal Server Error`: Unexpected server error

## Error Response Format

**Consistent error structure:**
```python
# Success
{"status": "authorized"}

# Denied
{"status": "not authorized"}

# Error
{"status": "error", "message": "description of error"}
```

## Request Validation

**Check required fields:**
```python
@app.post("/authorize")
def authorize(body: dict, response: Response):
if "resource" not in body:
response.status_code = status.HTTP_400_BAD_REQUEST
return {"status": "error", "message": "resource is required"}

if "subject" not in body:
response.status_code = status.HTTP_400_BAD_REQUEST
return {"status": "error", "message": "subject is required"}
```

**Validate nested structure:**
```python
subject = body.get("subject", {})
if "identity" not in subject:
response.status_code = status.HTTP_400_BAD_REQUEST
return {"status": "error", "message": "subject.identity is required"}
```

## Safe Data Access

**Handle missing or None values:**
```python
# Use .get() with defaults
metadata = subject.get("metadata_public") or {}

# Check array existence before indexing
if not subject.get("verifiable_addresses"):
response.status_code = status.HTTP_400_BAD_REQUEST
return {"status": "error", "message": "verifiable_addresses is required"}

# Safe array access
addresses = subject.get("verifiable_addresses", [])
if addresses and addresses[0].get("verified"):
# Process
```

## Exception Handling

**Catch and handle exceptions:**
```python
@app.post("/authorize")
def authorize(body: dict, response: Response):
try:
# Authorization logic
if body["resource"] == "kratos:admin":
return resolve_kratos_admin(body, response)
except KeyError as e:
response.status_code = status.HTTP_400_BAD_REQUEST
return {"status": "error", "message": f"Missing required field: {e}"}
except (IndexError, AttributeError) as e:
response.status_code = status.HTTP_400_BAD_REQUEST
return {"status": "error", "message": f"Invalid data structure: {e}"}
except Exception as e:
# Log the full exception for debugging
import logging
logging.error(f"Unexpected error: {e}", exc_info=True)
response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
return {"status": "error", "message": "Internal server error"}
```

## Authorization Denial

**Return 403 for authorization failures:**
```python
def resolve_kratos_admin(body: dict, response: Response):
subject = body["subject"]["identity"]

if not is_authorized(subject):
response.status_code = status.HTTP_403_FORBIDDEN
return {"status": "not authorized"}

response.status_code = status.HTTP_200_OK
return {"status": "authorized"}
```

## Best Practices

1. Always validate request structure before processing
2. Use appropriate HTTP status codes (400 for bad requests, 403 for denied, 500 for errors)
3. Return consistent error response format
4. Use safe access patterns (`.get()` with defaults) for optional fields
5. Log unexpected exceptions with full traceback for debugging
6. Don't expose internal error details in production responses
7. Handle edge cases (None values, empty arrays, missing keys)
8. Provide clear error messages for debugging
147 changes: 147 additions & 0 deletions .cursor/rules/fastapi-routes.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
---
description: Rules for FastAPI route definitions and HTTP handling
globs: ["main.py"]
alwaysApply: true
---

# FastAPI Routes Guidelines

Routes handle HTTP request/response logic. Keep routes simple and focused on authorization decisions.

## Route Structure

**Basic route:**
```python
from fastapi import FastAPI, Response, status

app = FastAPI(title="refinery-authorizer")

@app.post("/authorize")
def authorize(body: dict, response: Response):
# Authorization logic
pass
```

## Health Check Routes

**JSON health endpoint:**
```python
@app.get("/health")
async def root():
return {"alive": "true"}
```

**Plain text healthcheck (for load balancers):**
```python
from fastapi import responses

@app.get("/healthcheck")
def healthcheck() -> responses.PlainTextResponse:
return responses.PlainTextResponse("OK")
```

## Request Body Handling

**Accept dict for flexible authorization requests:**
```python
@app.post("/authorize")
def authorize(body: dict, response: Response):
resource = body.get("resource")
subject = body.get("subject", {})
# Process authorization
```

**For type safety, consider Pydantic models:**
```python
from pydantic import BaseModel
from typing import Optional, Dict, Any

class AuthorizeRequest(BaseModel):
resource: str
subject: Dict[str, Any]

@app.post("/authorize")
def authorize(body: AuthorizeRequest, response: Response):
if body.resource == "kratos:admin":
return resolve_kratos_admin(body.dict(), response)
```

## Response Handling

**Set status codes explicitly:**
```python
from fastapi import Response, status

response.status_code = status.HTTP_200_OK
return {"status": "authorized"}

response.status_code = status.HTTP_403_FORBIDDEN
return {"status": "not authorized"}
```

**Use FastAPI status constants:**
```python
status.HTTP_200_OK # Success
status.HTTP_400_BAD_REQUEST # Invalid request
status.HTTP_403_FORBIDDEN # Forbidden
status.HTTP_500_INTERNAL_SERVER_ERROR # Server error
```

## Error Handling

**Handle missing fields:**
```python
@app.post("/authorize")
def authorize(body: dict, response: Response):
if "resource" not in body:
response.status_code = status.HTTP_400_BAD_REQUEST
return {"status": "error", "message": "resource is required"}

# Continue processing
```

**Handle exceptions:**
```python
from fastapi import HTTPException

@app.post("/authorize")
def authorize(body: dict, response: Response):
try:
# Authorization logic
pass
except KeyError as e:
response.status_code = status.HTTP_400_BAD_REQUEST
return {"status": "error", "message": f"Missing field: {e}"}
except Exception as e:
response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
return {"status": "error", "message": "Internal server error"}
```

## Async vs Sync

**Use async for I/O operations:**
```python
@app.get("/health")
async def root():
# If you need async I/O
return {"alive": "true"}
```

**Use sync for simple logic:**
```python
@app.post("/authorize")
def authorize(body: dict, response: Response):
# Simple authorization logic doesn't need async
pass
```

## Best Practices

1. Keep routes thin - delegate complex logic to separate functions
2. Always set appropriate HTTP status codes
3. Return consistent response formats
4. Validate request data before processing
5. Handle errors gracefully with appropriate status codes
6. Use type hints for all parameters
7. Document route purpose with clear function names
8. Use async only when needed (I/O operations)
Loading