Files
Bjorn/db_utils/credentials.py

125 lines
4.9 KiB
Python

# db_utils/credentials.py
# Credential storage and management operations
import json
import sqlite3
from typing import Any, Dict, List, Optional
import logging
from logger import Logger
logger = Logger(name="db_utils.credentials", level=logging.DEBUG)
class CredentialOps:
"""Credential storage and retrieval operations"""
def __init__(self, base):
self.base = base
def create_tables(self):
"""Create credentials table"""
self.base.execute("""
CREATE TABLE IF NOT EXISTS creds (
id INTEGER PRIMARY KEY AUTOINCREMENT,
service TEXT NOT NULL,
mac_address TEXT,
ip TEXT,
hostname TEXT,
"user" TEXT,
"password" TEXT,
port INTEGER,
"database" TEXT,
extra TEXT,
first_seen TEXT DEFAULT CURRENT_TIMESTAMP,
last_seen TEXT DEFAULT CURRENT_TIMESTAMP
);
""")
# Indexes to support real UPSERT and dedup
try:
self.base.execute("""
CREATE UNIQUE INDEX IF NOT EXISTS uq_creds_identity
ON creds(service, mac_address, ip, "user", "database", port);
""")
except Exception:
pass
# Optional NULL-safe dedup guard for future rows
try:
self.base.execute("""
CREATE UNIQUE INDEX IF NOT EXISTS uq_creds_identity_norm
ON creds(
service,
COALESCE(mac_address,''),
COALESCE(ip,''),
COALESCE("user",''),
COALESCE("database",''),
COALESCE(port,0)
);
""")
except Exception:
pass
logger.debug("Credentials table created/verified")
# =========================================================================
# CREDENTIAL OPERATIONS
# =========================================================================
def insert_cred(self, service: str, mac: Optional[str] = None, ip: Optional[str] = None,
hostname: Optional[str] = None, user: Optional[str] = None,
password: Optional[str] = None, port: Optional[int] = None,
database: Optional[str] = None, extra: Optional[Dict[str, Any]] = None):
"""Insert or update a credential identity; last_seen is touched on update"""
self.base.invalidate_stats_cache()
# NULL-safe normalization to keep a single identity form
mac_n = mac or ""
ip_n = ip or ""
user_n = user or ""
db_n = database or ""
port_n = int(port or 0)
js = json.dumps(extra, ensure_ascii=False) if extra else None
try:
self.base.execute("""
INSERT INTO creds(service,mac_address,ip,hostname,"user","password",port,"database",extra)
VALUES(?,?,?,?,?,?,?,?,?)
ON CONFLICT(service, mac_address, ip, "user", "database", port) DO UPDATE SET
"password"=excluded."password",
hostname=COALESCE(excluded.hostname, creds.hostname),
last_seen=CURRENT_TIMESTAMP,
extra=COALESCE(excluded.extra, creds.extra);
""", (service, mac_n, ip_n, hostname, user_n, password, port_n, db_n, js))
except sqlite3.OperationalError:
# Fallback if unique index not available: manual upsert
row = self.base.query_one("""
SELECT id FROM creds
WHERE service=? AND COALESCE(mac_address,'')=? AND COALESCE(ip,'')=?
AND COALESCE("user",'')=? AND COALESCE("database",'')=? AND COALESCE(port,0)=?
LIMIT 1
""", (service, mac_n, ip_n, user_n, db_n, port_n))
if row:
self.base.execute("""
UPDATE creds
SET "password"=?,
hostname=COALESCE(?, hostname),
last_seen=CURRENT_TIMESTAMP,
extra=COALESCE(?, extra)
WHERE id=?
""", (password, hostname, js, row["id"]))
else:
self.base.execute("""
INSERT INTO creds(service,mac_address,ip,hostname,"user","password",port,"database",extra)
VALUES(?,?,?,?,?,?,?,?,?)
""", (service, mac_n, ip_n, hostname, user_n, password, port_n, db_n, js))
def list_creds_grouped(self) -> List[Dict[str, Any]]:
"""List all credential rows grouped/sorted by service/ip/user/port for UI"""
return self.base.query("""
SELECT service, mac_address, ip, hostname, "user", "password", port, "database", last_seen
FROM creds
ORDER BY service, ip, "user", port
""")