Rifqi Hafizuddin commited on
Commit
0e07955
·
1 Parent(s): bb79f64

[NOTICKET][DB] update credential & databaseclient. update settings

Browse files
src/config/settings.py CHANGED
@@ -61,6 +61,11 @@ class Settings(BaseSettings):
61
  # Bcrypt salt (for users - existing)
62
  emarcal_bcrypt_salt: str = Field(alias="emarcal__bcrypt__salt", default="")
63
 
 
 
 
 
 
64
 
65
  # Singleton instance
66
  settings = Settings()
 
61
  # Bcrypt salt (for users - existing)
62
  emarcal_bcrypt_salt: str = Field(alias="emarcal__bcrypt__salt", default="")
63
 
64
+ # DB credential encryption (Fernet key for user-registered database creds)
65
+ dataeyond_db_credential_key: str = Field(
66
+ alias="dataeyond__db__credential__key", default=""
67
+ )
68
+
69
 
70
  # Singleton instance
71
  settings = Settings()
src/db/postgres/init_db.py CHANGED
@@ -2,7 +2,14 @@
2
 
3
  from sqlalchemy import text
4
  from src.db.postgres.connection import engine, Base
5
- from src.db.postgres.models import Document, Room, ChatMessage, User, MessageSource
 
 
 
 
 
 
 
6
 
7
 
8
  async def init_db():
 
2
 
3
  from sqlalchemy import text
4
  from src.db.postgres.connection import engine, Base
5
+ from src.db.postgres.models import (
6
+ ChatMessage,
7
+ DatabaseClient,
8
+ Document,
9
+ MessageSource,
10
+ Room,
11
+ User,
12
+ )
13
 
14
 
15
  async def init_db():
src/db/postgres/models.py CHANGED
@@ -4,6 +4,7 @@ from uuid import uuid4
4
  from sqlalchemy import Column, String, DateTime, Text, Integer, ForeignKey
5
  from sqlalchemy.orm import relationship
6
  from sqlalchemy.sql import func
 
7
  from src.db.postgres.connection import Base
8
 
9
 
@@ -81,3 +82,18 @@ class MessageSource(Base):
81
  created_at = Column(DateTime(timezone=True), server_default=func.now())
82
 
83
  message = relationship("ChatMessage", back_populates="sources")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  from sqlalchemy import Column, String, DateTime, Text, Integer, ForeignKey
5
  from sqlalchemy.orm import relationship
6
  from sqlalchemy.sql import func
7
+ from sqlalchemy.dialects.postgresql import JSONB
8
  from src.db.postgres.connection import Base
9
 
10
 
 
82
  created_at = Column(DateTime(timezone=True), server_default=func.now())
83
 
84
  message = relationship("ChatMessage", back_populates="sources")
85
+
86
+
87
+ class DatabaseClient(Base):
88
+ """User-registered external database connections."""
89
+ __tablename__ = "databases"
90
+
91
+ id = Column(String, primary_key=True, default=lambda: str(uuid4()))
92
+ user_id = Column(String, nullable=False, index=True)
93
+ name = Column(String, nullable=False) # display name, e.g. "Prod DB"
94
+ db_type = Column(String, nullable=False) # postgres|mysql|sqlserver|supabase|bigquery|snowflake
95
+ credentials = Column(JSONB, nullable=False) # per-type JSON; sensitive fields Fernet-encrypted
96
+ status = Column(String, nullable=False, default="active") # active | inactive
97
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
98
+ updated_at = Column(DateTime(timezone=True), onupdate=func.now())
99
+
src/utils/db_credential_encryption.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fernet encryption utilities for user-registered database credentials.
2
+
3
+ Encryption key is sourced from `dataeyond__db__credential__key` env variable,
4
+ intentionally separate from the user-auth bcrypt salt (`emarcal__bcrypt__salt`).
5
+
6
+ Usage:
7
+ from src.utils.db_credential_encryption import encrypt_credentials_dict, decrypt_credentials_dict
8
+
9
+ # Before INSERT:
10
+ safe_creds = encrypt_credentials_dict(raw_credentials)
11
+
12
+ # After SELECT:
13
+ plain_creds = decrypt_credentials_dict(row.credentials)
14
+ """
15
+
16
+ from cryptography.fernet import Fernet
17
+ from src.config.settings import settings
18
+
19
+ # Sensitive credential field names that must be encrypted at rest.
20
+ # Covers all supported DB types:
21
+ # - password : postgres, mysql, sqlserver, supabase, snowflake
22
+ # - service_account_json : bigquery
23
+ SENSITIVE_FIELDS: frozenset[str] = frozenset({"password", "service_account_json"})
24
+
25
+
26
+ def _get_cipher() -> Fernet:
27
+ key = settings.dataeyond_db_credential_key
28
+ if not key:
29
+ raise ValueError(
30
+ "dataeyond__db__credential__key is not set. "
31
+ "Generate one with: Fernet.generate_key().decode()"
32
+ )
33
+ return Fernet(key.encode())
34
+
35
+
36
+ def encrypt_credential(value: str) -> str:
37
+ """Encrypt a single credential string value."""
38
+ return _get_cipher().encrypt(value.encode()).decode()
39
+
40
+
41
+ def decrypt_credential(value: str) -> str:
42
+ """Decrypt a single Fernet-encrypted credential string."""
43
+ return _get_cipher().decrypt(value.encode()).decode()
44
+
45
+
46
+ def encrypt_credentials_dict(creds: dict) -> dict:
47
+ """Return a copy of the credentials dict with sensitive fields encrypted.
48
+
49
+ Call this before inserting a new DatabaseClient record.
50
+ """
51
+ cipher = _get_cipher()
52
+ result = dict(creds)
53
+ for field in SENSITIVE_FIELDS:
54
+ if result.get(field):
55
+ result[field] = cipher.encrypt(result[field].encode()).decode()
56
+ return result
57
+
58
+
59
+ def decrypt_credentials_dict(creds: dict) -> dict:
60
+ """Return a copy of the credentials dict with sensitive fields decrypted.
61
+
62
+ Call this after fetching a DatabaseClient record from DB.
63
+ """
64
+ cipher = _get_cipher()
65
+ result = dict(creds)
66
+ for field in SENSITIVE_FIELDS:
67
+ if result.get(field):
68
+ result[field] = cipher.decrypt(result[field].encode()).decode()
69
+ return result
70
+