MorphGuard / utils /auth.py
juanquy's picture
fix: bootstrap SQLite database and table structure if users.db does not exist
7da54df
Raw
History Blame Contribute Delete
11.9 kB
#!/usr/bin/env python3
"""
auth.py
Authentication utilities for MorphGuard using SQLite database.
"""
import os
import json
import time
from typing import Dict, Any, List, Optional, Tuple
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
# Try to import psycopg2
try:
import psycopg2
from psycopg2.extras import RealDictCursor
HAS_POSTGRES = True
except ImportError:
HAS_POSTGRES = False
RealDictCursor = None
import sqlite3
IS_HF_SPACE = os.environ.get('HF_SPACE', '0') == '1'
USE_SQLITE = IS_HF_SPACE or not HAS_POSTGRES
# Database configuration
DB_HOST = os.environ.get('MORPHGUARD_DB_HOST', 'localhost')
DB_PORT = int(os.environ.get('MORPHGUARD_DB_PORT', 5432))
DB_NAME = os.environ.get('MORPHGUARD_DB_NAME', 'morphguard')
DB_USER = os.environ.get('MORPHGUARD_DB_USER', 'morphguard')
DB_PASS = os.environ.get('MORPHGUARD_DB_PASS', 'morphguard')
def _is_sqlite(conn):
return isinstance(conn, sqlite3.Connection)
def _get_conn():
if USE_SQLITE:
try:
conn = sqlite3.connect('users.db')
conn.row_factory = sqlite3.Row
return conn
except Exception as e:
print(f"SQLite connection failed: {e}")
return None
else:
try:
conn = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
dbname=DB_NAME,
user=DB_USER,
password=DB_PASS
)
return conn
except Exception as e:
print(f"Database connection failed: {e}")
# Try falling back to SQLite if Postgres connection fails
try:
conn = sqlite3.connect('users.db')
conn.row_factory = sqlite3.Row
return conn
except Exception:
return None
def _execute(conn, query: str, params: tuple = (), fetch_all: bool = False, fetch_one: bool = False, commit: bool = False):
is_sqlite = _is_sqlite(conn)
if is_sqlite:
query = query.replace('%s', '?')
new_params = []
for p in params:
if isinstance(p, datetime):
new_params.append(p.timestamp())
elif isinstance(p, bool):
new_params.append(1 if p else 0)
else:
new_params.append(p)
params = tuple(new_params)
cur = conn.cursor()
else:
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute(query, params)
result = None
if fetch_all:
rows = cur.fetchall()
result = [dict(r) for r in rows]
elif fetch_one:
row = cur.fetchone()
result = dict(row) if row else None
if commit:
conn.commit()
return cur, result
def init_db():
conn = _get_conn()
if conn:
db_type = "SQLite" if _is_sqlite(conn) else "PostgreSQL"
if _is_sqlite(conn):
try:
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL,
email TEXT,
created_at REAL,
last_login REAL,
active INTEGER NOT NULL DEFAULT 1
);
""")
conn.commit()
cur.execute("SELECT id FROM users WHERE username = ?;", ("admin",))
if not cur.fetchone():
pwd_hash = generate_password_hash("morphguard_admin", method='scrypt')
cur.execute(
"INSERT INTO users (username, password_hash, role, created_at) VALUES (?, ?, ?, ?);",
("admin", pwd_hash, "admin", datetime.now().timestamp())
)
conn.commit()
print("Default admin user created in SQLite database.")
except Exception as e:
print(f"Error initializing SQLite database: {e}")
conn.close()
print(f"Auth module connected to {db_type} successfully")
else:
print("WARNING: Auth module failed to connect to database")
# Initialize check
init_db()
def get_users() -> List[Dict[str, Any]]:
"""Fetch all users"""
conn = _get_conn()
if not conn: return []
try:
cur, rows = _execute(conn, 'SELECT id, username, role, email, last_login, active FROM users;', fetch_all=True)
if _is_sqlite(conn) and rows:
for row in rows:
if row.get('last_login'):
try:
row['last_login'] = datetime.fromtimestamp(row['last_login'])
except Exception:
pass
if 'active' in row:
row['active'] = bool(row['active'])
return rows
finally:
conn.close()
def get_user_by_username(username: str) -> Optional[Dict[str, Any]]:
"""Get a user by username"""
conn = _get_conn()
if not conn: return None
try:
cur, row = _execute(conn, 'SELECT * FROM users WHERE username = %s;', (username,), fetch_one=True)
if row and _is_sqlite(conn):
row = dict(row)
if row.get('created_at'):
try:
row['created_at'] = datetime.fromtimestamp(row['created_at'])
except Exception:
pass
if row.get('last_login'):
try:
row['last_login'] = datetime.fromtimestamp(row['last_login'])
except Exception:
pass
if 'active' in row:
row['active'] = bool(row['active'])
return row
finally:
conn.close()
def authenticate_user(username: str, password: str) -> Tuple[bool, Any]:
"""Authenticate user credentials"""
user = get_user_by_username(username)
if not user or not check_password_hash(user['password_hash'], password):
return False, {'error': 'Invalid credentials.'}
# Update last login
conn = _get_conn()
if conn:
try:
now = datetime.now()
_execute(conn, 'UPDATE users SET last_login = %s WHERE id = %s;', (now, user['id']), commit=True)
except Exception as e:
print(f"Warning: Failed to update last_login: {e}")
finally:
conn.close()
return True, user
def create_user(username: str, password: str, email: Optional[str] = None, role: str = 'user') -> Tuple[bool, Any]:
"""Create a new user"""
if get_user_by_username(username):
return False, {'error': 'Username already exists.'}
pwd_hash = generate_password_hash(password, method='scrypt')
conn = _get_conn()
if not conn: return False, {'error': 'Database connection failed'}
now = datetime.now()
try:
if _is_sqlite(conn):
cur, _ = _execute(
conn,
'INSERT INTO users (username, password_hash, role, email, created_at) VALUES (%s, %s, %s, %s, %s);',
(username, pwd_hash, role, email, now),
commit=True
)
user_id = cur.lastrowid
else:
cur, row = _execute(
conn,
'INSERT INTO users (username, password_hash, role, email, created_at) VALUES (%s, %s, %s, %s, %s) RETURNING id;',
(username, pwd_hash, role, email, now),
fetch_one=True,
commit=True
)
user_id = row['id'] if isinstance(row, dict) else row[0]
return True, {'id': user_id, 'username': username, 'role': role}
except Exception as e:
conn.rollback()
print(f"Error creating user: {e}")
return False, {'error': str(e)}
finally:
conn.close()
def update_user(user_id: int, data: Dict[str, Any]) -> Tuple[bool, Any]:
"""Update user's password and/or role"""
fields = []
params = []
if data.get('password'):
pwd_hash = generate_password_hash(data['password'], method='scrypt')
fields.append('password_hash = %s')
params.append(pwd_hash)
if data.get('role'):
fields.append('role = %s')
params.append(data['role'])
if not fields:
return False, {'error': 'No fields to update.'}
params.append(user_id)
conn = _get_conn()
if not conn: return False, {'error': 'Database connection failed'}
try:
if _is_sqlite(conn):
cur, row = _execute(conn, 'SELECT username FROM users WHERE id = %s;', (user_id,), fetch_one=True)
if not row:
return False, {'error': 'User not found.'}
username = row['username']
sql_query = f"UPDATE users SET {', '.join(fields)} WHERE id = %s;"
_execute(conn, sql_query, params, commit=True)
return True, {'username': username}
else:
sql_query = f"UPDATE users SET {', '.join(fields)} WHERE id = %s RETURNING username;"
cur, row = _execute(conn, sql_query, params, fetch_one=True, commit=True)
if row:
return True, {'username': row['username']}
return False, {'error': 'User not found.'}
except Exception as e:
conn.rollback()
return False, {'error': str(e)}
finally:
conn.close()
def delete_user(user_id: int) -> bool:
"""Delete a user, preventing deletion of last admin"""
conn = _get_conn()
if not conn: return False
try:
cur, row = _execute(conn, 'SELECT role FROM users WHERE id = %s;', (user_id,), fetch_one=True)
if not row:
return False
if row['role'] == 'admin':
_, count_row = _execute(conn, "SELECT COUNT(*) AS cnt FROM users WHERE role = 'admin';", fetch_one=True)
if count_row and count_row['cnt'] <= 1:
return False
_execute(conn, 'DELETE FROM users WHERE id = %s;', (user_id,), commit=True)
return True
finally:
conn.close()
def get_all_users() -> List[Dict[str, Any]]:
"""Alias for get_users to match expected API usage"""
return get_users()
def update_user_details(user_id: int, role: str = None) -> bool:
"""Helper for simple user updates"""
data = {}
if role:
data['role'] = role
success, _ = update_user(user_id, data)
return success
def get_user_by_id(user_id: int) -> Optional[Dict[str, Any]]:
"""Fetch user by ID"""
conn = _get_conn()
if not conn: return None
try:
cur, row = _execute(conn, 'SELECT * FROM users WHERE id = %s;', (user_id,), fetch_one=True)
if row and _is_sqlite(conn):
row = dict(row)
if row.get('created_at'):
try:
row['created_at'] = datetime.fromtimestamp(row['created_at'])
except Exception:
pass
if row.get('last_login'):
try:
row['last_login'] = datetime.fromtimestamp(row['last_login'])
except Exception:
pass
if 'active' in row:
row['active'] = bool(row['active'])
return row
finally:
conn.close()
def update_user_status(user_id: int, active: bool) -> bool:
"""Toggle user active status"""
conn = _get_conn()
if not conn: return False
try:
_execute(conn, 'UPDATE users SET active = %s WHERE id = %s;', (active, user_id), commit=True)
return True
except Exception as e:
conn.rollback()
return False
finally:
conn.close()