实现数据库的连接串加密
This commit is contained in:
@@ -12,6 +12,7 @@ SECRET_KEY=your-secret-key-here-change-in-production-use-openssl-rand-hex-32
|
|||||||
# 数据加密密钥 - 用于敏感数据加密
|
# 数据加密密钥 - 用于敏感数据加密
|
||||||
# 生成方式: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
# 生成方式: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
||||||
ENCRYPTION_KEY=
|
ENCRYPTION_KEY=
|
||||||
|
DATABASE_ENCRYPTION_KEY="rJC2VqLg4KrlSq+DGJcYm869q4v5KB2dFAeuQTe0I50="
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# 数据库配置 (PostgreSQL)
|
# 数据库配置 (PostgreSQL)
|
||||||
|
|||||||
@@ -20,3 +20,4 @@ METADATA_DB_HOST="192.168.1.114"
|
|||||||
METADATA_DB_PORT="5432"
|
METADATA_DB_PORT="5432"
|
||||||
METADATA_DB_USER="tjwater"
|
METADATA_DB_USER="tjwater"
|
||||||
METADATA_DB_PASSWORD="Tjwater@123456"
|
METADATA_DB_PASSWORD="Tjwater@123456"
|
||||||
|
DATABASE_ENCRYPTION_KEY="rJC2VqLg4KrlSq+DGJcYm869q4v5KB2dFAeuQTe0I50="
|
||||||
|
|||||||
@@ -68,9 +68,7 @@ async def get_project_context(
|
|||||||
status_code=status.HTTP_403_FORBIDDEN, detail="Inactive user"
|
status_code=status.HTTP_403_FORBIDDEN, detail="Inactive user"
|
||||||
)
|
)
|
||||||
|
|
||||||
membership_role = await metadata_repo.get_membership_role(
|
membership_role = await metadata_repo.get_membership_role(project_uuid, user.id)
|
||||||
project_uuid, user.id
|
|
||||||
)
|
|
||||||
if not membership_role:
|
if not membership_role:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail="No access to project"
|
status_code=status.HTTP_403_FORBIDDEN, detail="No access to project"
|
||||||
@@ -102,12 +100,12 @@ async def get_project_pg_session(
|
|||||||
)
|
)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
logger.error(
|
logger.error(
|
||||||
"Missing ENCRYPTION_KEY while resolving project PostgreSQL routing",
|
"Invalid project PostgreSQL routing DSN configuration",
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
detail="ENCRYPTION_KEY is not configured for project PostgreSQL access",
|
detail=f"Project PostgreSQL routing DSN is invalid: {exc}",
|
||||||
) from exc
|
) from exc
|
||||||
if not routing:
|
if not routing:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -143,12 +141,12 @@ async def get_project_pg_connection(
|
|||||||
)
|
)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
logger.error(
|
logger.error(
|
||||||
"Missing ENCRYPTION_KEY while resolving project PostgreSQL routing",
|
"Invalid project PostgreSQL routing DSN configuration",
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
detail="ENCRYPTION_KEY is not configured for project PostgreSQL access",
|
detail=f"Project PostgreSQL routing DSN is invalid: {exc}",
|
||||||
) from exc
|
) from exc
|
||||||
if not routing:
|
if not routing:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -184,12 +182,12 @@ async def get_project_timescale_connection(
|
|||||||
)
|
)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
logger.error(
|
logger.error(
|
||||||
"Missing ENCRYPTION_KEY while resolving project TimescaleDB routing",
|
"Invalid project TimescaleDB routing DSN configuration",
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
detail="ENCRYPTION_KEY is not configured for project TimescaleDB access",
|
detail=f"Project TimescaleDB routing DSN is invalid: {exc}",
|
||||||
) from exc
|
) from exc
|
||||||
if not routing:
|
if not routing:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
# 数据加密密钥 (使用 Fernet)
|
# 数据加密密钥 (使用 Fernet)
|
||||||
ENCRYPTION_KEY: str = "" # 必须从环境变量设置
|
ENCRYPTION_KEY: str = "" # 必须从环境变量设置
|
||||||
|
DATABASE_ENCRYPTION_KEY: str = "" # project_databases.dsn_encrypted 专用
|
||||||
|
|
||||||
# Database Config (PostgreSQL)
|
# Database Config (PostgreSQL)
|
||||||
DB_NAME: str = "tjwater"
|
DB_NAME: str = "tjwater"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import os
|
|||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
|
|
||||||
|
|
||||||
class Encryptor:
|
class Encryptor:
|
||||||
"""
|
"""
|
||||||
使用 Fernet (对称加密) 实现数据加密/解密
|
使用 Fernet (对称加密) 实现数据加密/解密
|
||||||
@@ -72,12 +73,25 @@ class Encryptor:
|
|||||||
key = Fernet.generate_key()
|
key = Fernet.generate_key()
|
||||||
return key.decode()
|
return key.decode()
|
||||||
|
|
||||||
|
|
||||||
# 全局加密器实例(懒加载)
|
# 全局加密器实例(懒加载)
|
||||||
_encryptor: Optional[Encryptor] = None
|
_encryptor: Optional[Encryptor] = None
|
||||||
|
_database_encryptor: Optional[Encryptor] = None
|
||||||
|
|
||||||
|
|
||||||
def is_encryption_configured() -> bool:
|
def is_encryption_configured() -> bool:
|
||||||
return bool(os.getenv("ENCRYPTION_KEY") or settings.ENCRYPTION_KEY)
|
return bool(os.getenv("ENCRYPTION_KEY") or settings.ENCRYPTION_KEY)
|
||||||
|
|
||||||
|
|
||||||
|
def is_database_encryption_configured() -> bool:
|
||||||
|
return bool(
|
||||||
|
os.getenv("DATABASE_ENCRYPTION_KEY")
|
||||||
|
or settings.DATABASE_ENCRYPTION_KEY
|
||||||
|
or os.getenv("ENCRYPTION_KEY")
|
||||||
|
or settings.ENCRYPTION_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_encryptor() -> Encryptor:
|
def get_encryptor() -> Encryptor:
|
||||||
"""获取全局加密器实例"""
|
"""获取全局加密器实例"""
|
||||||
global _encryptor
|
global _encryptor
|
||||||
@@ -85,6 +99,26 @@ def get_encryptor() -> Encryptor:
|
|||||||
_encryptor = Encryptor()
|
_encryptor = Encryptor()
|
||||||
return _encryptor
|
return _encryptor
|
||||||
|
|
||||||
|
|
||||||
|
def get_database_encryptor() -> Encryptor:
|
||||||
|
"""获取 project DB DSN 专用加密器实例"""
|
||||||
|
global _database_encryptor
|
||||||
|
if _database_encryptor is None:
|
||||||
|
key_str = (
|
||||||
|
os.getenv("DATABASE_ENCRYPTION_KEY")
|
||||||
|
or settings.DATABASE_ENCRYPTION_KEY
|
||||||
|
or os.getenv("ENCRYPTION_KEY")
|
||||||
|
or settings.ENCRYPTION_KEY
|
||||||
|
)
|
||||||
|
if not key_str:
|
||||||
|
raise ValueError(
|
||||||
|
"DATABASE_ENCRYPTION_KEY not found in environment variables or .env. "
|
||||||
|
"Generate one using: Encryptor.generate_key()"
|
||||||
|
)
|
||||||
|
_database_encryptor = Encryptor(key=key_str.encode())
|
||||||
|
return _database_encryptor
|
||||||
|
|
||||||
|
|
||||||
# 向后兼容(延迟加载)
|
# 向后兼容(延迟加载)
|
||||||
def __getattr__(name):
|
def __getattr__(name):
|
||||||
if name == "encryptor":
|
if name == "encryptor":
|
||||||
|
|||||||
@@ -2,10 +2,16 @@ from dataclasses import dataclass
|
|||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
from cryptography.fernet import InvalidToken
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.encryption import get_encryptor, is_encryption_configured
|
from app.core.encryption import (
|
||||||
|
get_database_encryptor,
|
||||||
|
get_encryptor,
|
||||||
|
is_database_encryption_configured,
|
||||||
|
is_encryption_configured,
|
||||||
|
)
|
||||||
from app.infra.db.metadata import models
|
from app.infra.db.metadata import models
|
||||||
|
|
||||||
|
|
||||||
@@ -65,9 +71,7 @@ class MetadataRepository:
|
|||||||
def __init__(self, session: AsyncSession):
|
def __init__(self, session: AsyncSession):
|
||||||
self.session = session
|
self.session = session
|
||||||
|
|
||||||
async def get_user_by_keycloak_id(
|
async def get_user_by_keycloak_id(self, keycloak_id: UUID) -> Optional[models.User]:
|
||||||
self, keycloak_id: UUID
|
|
||||||
) -> Optional[models.User]:
|
|
||||||
result = await self.session.execute(
|
result = await self.session.execute(
|
||||||
select(models.User).where(models.User.keycloak_id == keycloak_id)
|
select(models.User).where(models.User.keycloak_id == keycloak_id)
|
||||||
)
|
)
|
||||||
@@ -102,11 +106,16 @@ class MetadataRepository:
|
|||||||
record = result.scalar_one_or_none()
|
record = result.scalar_one_or_none()
|
||||||
if not record:
|
if not record:
|
||||||
return None
|
return None
|
||||||
if is_encryption_configured():
|
if not is_database_encryption_configured():
|
||||||
encryptor = get_encryptor()
|
raise ValueError("DATABASE_ENCRYPTION_KEY is not configured")
|
||||||
|
encryptor = get_database_encryptor()
|
||||||
|
try:
|
||||||
dsn = encryptor.decrypt(record.dsn_encrypted)
|
dsn = encryptor.decrypt(record.dsn_encrypted)
|
||||||
else:
|
except InvalidToken:
|
||||||
dsn = record.dsn_encrypted
|
raise ValueError(
|
||||||
|
"Failed to decrypt project DB DSN: DATABASE_ENCRYPTION_KEY mismatch "
|
||||||
|
"or invalid dsn_encrypted value"
|
||||||
|
)
|
||||||
dsn = _normalize_postgres_dsn(dsn)
|
dsn = _normalize_postgres_dsn(dsn)
|
||||||
return ProjectDbRouting(
|
return ProjectDbRouting(
|
||||||
project_id=record.project_id,
|
project_id=record.project_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user