Files
tfm_ainventory/backend/routers/users.py

270 lines
10 KiB
Python

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List
from passlib.context import CryptContext
import ldap3
import json
import os
from .. import models, schemas, database
router = APIRouter(prefix="/users", tags=["users"])
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
def get_ldap_config():
config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "ldap_config.json")
if os.path.exists(config_path):
with open(config_path, "r") as f:
return json.load(f)
return {"ldap_enabled": False}
def authenticate_ldap(username, password):
config = get_ldap_config()
if not config.get("ldap_enabled"):
return None
try:
server = ldap3.Server(config["server_uri"], use_ssl=config.get("use_tls", False), get_info=ldap3.ALL)
user_dn = config["user_template"].format(username=username)
print(f"DEBUG LDAP: Attempting bind for DN: {user_dn}")
conn = ldap3.Connection(server, user=user_dn, password=password, auto_bind=True)
print(f"DEBUG LDAP: Bind successful for {user_dn}")
# Search for the user to get their CANONICAL DN
base_dn = config.get("base_dn", "dc=example,dc=org")
search_filter = f"(|(cn={username})(uid={username}))"
conn.search(base_dn, search_filter, attributes=['cn', 'uid'])
if not conn.entries:
print(f"DEBUG LDAP: User {username} not found in search after bind.")
return None
real_user_dn = conn.entries[0].entry_dn
print(f"DEBUG LDAP: Canonical DN found: {real_user_dn}")
# Check roles based on group membership
assigned_role = None
# New multi-group mapping support
role_mappings = config.get("role_mappings", [])
if not role_mappings and config.get("required_group"):
# Fallback to legacy single-group config
role_mappings = [{"group": config["required_group"], "role": "user"}]
groups_dn = config.get("groups_dn", "ou=groups")
# Iterate through mappings to find the highest role
# Priority: admin > user
potential_roles = []
for mapping in role_mappings:
group_name = mapping["group"]
target_role = mapping["role"]
# Construct group DN if it's just a common name, else use as is
if "=" not in group_name:
full_group_dn = f"cn={group_name},{groups_dn},{base_dn}"
else:
full_group_dn = group_name
print(f"DEBUG LDAP: Checking membership in group: {full_group_dn}")
conn.search(full_group_dn, '(objectClass=*)', attributes=['member'])
if conn.entries:
members = conn.entries[0].member.values
if real_user_dn in members or user_dn in members or \
any(m.lower().replace(" ", "") == real_user_dn.lower().replace(" ", "") for m in members):
print(f"DEBUG LDAP: User is in group {group_name}, assigning role: {target_role}")
potential_roles.append(target_role)
if "admin" in potential_roles:
assigned_role = "admin"
elif "user" in potential_roles:
assigned_role = "user"
elif potential_roles:
assigned_role = potential_roles[0]
return assigned_role
except Exception as e:
print(f"DEBUG LDAP: Auth Error: {str(e)}")
return None
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
def get_db():
db = database.SessionLocal()
try:
yield db
finally:
db.close()
def get_password_hash(password):
return pwd_context.hash(password)
def verify_password(plain_password, hashed_password):
if not hashed_password: return False
return pwd_context.verify(plain_password, hashed_password)
@router.get("/", response_model=List[schemas.User])
def get_users(db: Session = Depends(get_db)):
users = db.query(models.User).all()
# Auto-seed if empty
if not users:
new_user = models.User(
username="Admin",
role="admin",
origin="local",
hashed_password=get_password_hash("admin") # Default password
)
db.add(new_user)
db.commit()
db.refresh(new_user)
return [new_user]
return users
@router.post("/", response_model=schemas.User)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
existing = db.query(models.User).filter(models.User.username == user.username).first()
if existing:
raise HTTPException(status_code=400, detail="Username already exists")
hashed = get_password_hash(user.password) if user.password else None
new_user = models.User(username=user.username, role=user.role, origin="local", hashed_password=hashed)
db.add(new_user)
db.commit()
db.refresh(new_user)
return new_user
@router.post("/login")
def login(form_data: schemas.UserLogin, db: Session = Depends(get_db)):
user = db.query(models.User).filter(models.User.username == form_data.username).first()
# Try local authentication
authenticated = False
if user and user.hashed_password:
if verify_password(form_data.password, user.hashed_password):
authenticated = True
elif user and not user.hashed_password:
# Legacy user without password - allow skip for now or force set
authenticated = True
# If local failed, try LDAP
if not authenticated:
ldap_role = authenticate_ldap(form_data.username, form_data.password)
if ldap_role:
authenticated = True
# If user doesn't exist locally, create a stub for role management
if not user:
user = models.User(username=form_data.username, role=ldap_role, origin="ldap")
db.add(user)
db.commit()
db.refresh(user)
else:
# Update role if it changed in LDAP
if user.role != ldap_role:
user.role = ldap_role
db.commit()
db.refresh(user)
else:
raise HTTPException(status_code=400, detail="Invalid username or password, or insufficient permissions")
return user
@router.put("/{user_id}", response_model=schemas.User)
def update_user(user_id: int, user_update: schemas.UserUpdate, db: Session = Depends(get_db)):
db_user = db.query(models.User).filter(models.User.id == user_id).first()
if not db_user:
raise HTTPException(status_code=404, detail="User not found")
if user_update.username and db_user.username == "Admin" and user_update.username != "Admin":
raise HTTPException(status_code=400, detail="Cannot change Admin username")
if user_update.username:
# Check if username already taken by another user
existing = db.query(models.User).filter(models.User.username == user_update.username, models.User.id != user_id).first()
if existing:
raise HTTPException(status_code=400, detail="Username already exists")
db_user.username = user_update.username
if user_update.password:
db_user.hashed_password = get_password_hash(user_update.password)
if user_update.role:
db_user.role = user_update.role
db.commit()
db.refresh(db_user)
return db_user
@router.get("/ldap-config")
def get_ldap_settings():
return get_ldap_config()
@router.post("/ldap-config")
def update_ldap_settings(config: dict):
config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "ldap_config.json")
with open(config_path, "w") as f:
json.dump(config, f)
return {"message": "Config saved"}
@router.post("/test-ldap")
def test_ldap_connection(config: dict):
import socket
try:
# Extract host and port
uri = config["server_uri"]
host = uri.replace("ldap://", "").replace("ldaps://", "")
port = 389
if ":" in host:
host, port_str = host.split(":")
port = int(port_str)
elif "ldaps://" in uri:
port = 636
elif uri.endswith(":3890"): # Special case for LLDAP
port = 3890
# Try raw socket first
print(f"DEBUG: Probing raw socket {host}:{port}")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
result = s.connect_ex((host, port))
s.close()
if result == 0:
# Socket is open! Now try LDAP library
try:
server = ldap3.Server(config["server_uri"], connect_timeout=5)
conn = ldap3.Connection(server, auto_bind=False)
if conn.open():
return {"status": "success", "message": "Connection Successful"}
return {"status": "success", "message": "Server reachable (Socket open, but LDAP probe failed)"}
except:
return {"status": "success", "message": "Server reachable (Socket open)"}
else:
# Socket failed, let's try calling system 'ldapsearch' as a last resort diagnostic
import subprocess
try:
# We just try to reach the server with a 2s timeout
cmd = ["ldapsearch", "-h", host, "-p", str(port), "-x", "-s", "base", "-b", "", "namingContexts"]
proc = subprocess.run(cmd, capture_output=True, timeout=2)
if proc.returncode == 0 or b"namingContexts" in proc.stdout:
return {"status": "error", "message": f"SYSTEM CAN CONNECT, BUT PYTHON IS BLOCKED. Check Mac Firewall settings for Python."}
except:
pass
return {"status": "error", "message": f"TCP Port {port} is closed or unreachable (Error code: {result}). Check firewall on {host}."}
except Exception as e:
return {"status": "error", "message": f"Network Error: {str(e)}"}
@router.delete("/{user_id}")
def delete_user(user_id: int, db: Session = Depends(get_db)):
user = db.query(models.User).filter(models.User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
if user.username == "Admin":
raise HTTPException(status_code=400, detail="Cannot delete default Admin")
db.delete(user)
db.commit()
return {"message": "User deleted"}