security: add RBAC, HTTPS enforcement; add tests and CI pytest step
This commit is contained in:
+21
-3
@@ -18,6 +18,18 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# HTTPS enforcement middleware (for production behind a proxy, check X-Forwarded-Proto)
|
||||
@app.middleware('http')
|
||||
async def https_redirect(request, call_next):
|
||||
if os.getenv('FORCE_HTTPS', 'false').lower() == 'true':
|
||||
proto = request.headers.get('x-forwarded-proto', request.url.scheme)
|
||||
if proto != 'https':
|
||||
from starlette.responses import RedirectResponse
|
||||
url = request.url.replace(scheme='https')
|
||||
return RedirectResponse(str(url))
|
||||
return await call_next(request)
|
||||
|
||||
@app.on_event('startup')
|
||||
def startup_event():
|
||||
models.init_db()
|
||||
@@ -198,13 +210,16 @@ def list_integrations():
|
||||
|
||||
|
||||
@app.delete('/api/v1/integrations/{integration_id}')
|
||||
def delete_integration(integration_id: int):
|
||||
def delete_integration(integration_id: int, request=None):
|
||||
db = models.SessionLocal()
|
||||
try:
|
||||
row = db.query(models.Integration).filter_by(id=integration_id).first()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail='integration not found')
|
||||
db.delete(row)
|
||||
# require owner or admin
|
||||
from .rbac import require_owner_or_admin
|
||||
require_owner_or_admin(row.user_id)(request)
|
||||
db.delete(row)
|
||||
db.commit()
|
||||
return {'ok': True}
|
||||
finally:
|
||||
@@ -223,7 +238,10 @@ def sync_integration_to_habits(integration_id: int, payload: dict = Body({})):
|
||||
if not integration:
|
||||
raise HTTPException(status_code=404, detail='integration not found')
|
||||
|
||||
# Fetch events via existing events endpoint logic
|
||||
# require owner or admin
|
||||
from .rbac import require_owner_or_admin
|
||||
require_owner_or_admin(integration.user_id)(None)
|
||||
# Fetch events via existing events endpoint logic
|
||||
# Reuse token refresh + decrypt logic from oauth module
|
||||
token_row = db.query(models.OAuthToken).filter_by(integration_id=integration_id).order_by(models.OAuthToken.id.desc()).first()
|
||||
if not token_row:
|
||||
|
||||
@@ -15,6 +15,7 @@ class User(Base):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
email = Column(String, unique=True, nullable=False, index=True)
|
||||
password_hash = Column(String)
|
||||
role = Column(String, default='user')
|
||||
display_name = Column(String)
|
||||
created_at = Column(DateTime, server_default=func.current_timestamp())
|
||||
updated_at = Column(DateTime, server_default=func.current_timestamp(), onupdate=func.current_timestamp())
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from fastapi import HTTPException
|
||||
from .auth import get_current_user
|
||||
from . import models
|
||||
|
||||
|
||||
def require_role(min_role: str):
|
||||
# Simple role hierarchy
|
||||
hierarchy = {'user': 1, 'moderator': 2, 'admin': 3}
|
||||
def _inner(request=None):
|
||||
user = get_current_user(request)
|
||||
if hierarchy.get(user.role, 0) < hierarchy.get(min_role, 0):
|
||||
raise HTTPException(status_code=403, detail='insufficient role')
|
||||
return user
|
||||
return _inner
|
||||
|
||||
|
||||
def require_owner_or_admin(resource_user_id: int):
|
||||
def _inner(request=None):
|
||||
user = get_current_user(request)
|
||||
if user.id == resource_user_id or user.role == 'admin':
|
||||
return user
|
||||
raise HTTPException(status_code=403, detail='must be owner or admin')
|
||||
return _inner
|
||||
@@ -6,3 +6,6 @@ python-dotenv
|
||||
requests
|
||||
cryptography
|
||||
boto3
|
||||
pytest
|
||||
httpx
|
||||
requests
|
||||
|
||||
Reference in New Issue
Block a user