188 lines
5.6 KiB
Python
188 lines
5.6 KiB
Python
"""
|
|
Authentication & Authorization Service
|
|
|
|
Provides password hashing, JWT token management, and permission checking.
|
|
"""
|
|
|
|
from datetime import datetime, timedelta
|
|
from typing import Optional
|
|
import hashlib
|
|
import hmac
|
|
import os
|
|
|
|
# Try to import passlib, fallback to simple hashing
|
|
try:
|
|
from passlib.context import CryptContext
|
|
from jose import JWTError, jwt
|
|
HAS_PASSLIB = True
|
|
except ImportError:
|
|
HAS_PASSLIB = False
|
|
# Fallback JWT implementation
|
|
try:
|
|
import jwt as jwt_lib
|
|
jwt = jwt_lib
|
|
JWTError = Exception
|
|
except ImportError:
|
|
jwt = None
|
|
JWTError = Exception
|
|
|
|
# JWT Configuration
|
|
SECRET_KEY = os.getenv("SECRET_KEY", "neural-sync-secret-key-change-in-production")
|
|
ALGORITHM = "HS256"
|
|
ACCESS_TOKEN_EXPIRE_HOURS = 24
|
|
|
|
# Password hashing context (if passlib available)
|
|
if HAS_PASSLIB:
|
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
|
|
|
|
class AuthService:
|
|
"""Service for authentication operations"""
|
|
|
|
@staticmethod
|
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|
"""Verify a plain password against a hashed password"""
|
|
if HAS_PASSLIB:
|
|
return pwd_context.verify(plain_password, hashed_password)
|
|
else:
|
|
# Simple SHA256 fallback for development
|
|
return hmac.compare_digest(
|
|
AuthService._simple_hash(plain_password),
|
|
hashed_password
|
|
)
|
|
|
|
@staticmethod
|
|
def hash_password(password: str) -> str:
|
|
"""Hash a password for storage"""
|
|
if HAS_PASSLIB:
|
|
return pwd_context.hash(password)
|
|
else:
|
|
# Simple SHA256 fallback for development
|
|
return AuthService._simple_hash(password)
|
|
|
|
@staticmethod
|
|
def _simple_hash(password: str) -> str:
|
|
"""Simple SHA256 hash with salt for development use"""
|
|
salt = SECRET_KEY[:16].encode() if SECRET_KEY else b'neural-sync-salt'
|
|
return hashlib.sha256(password.encode() + salt).hexdigest()
|
|
|
|
@staticmethod
|
|
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
|
"""Create a JWT access token"""
|
|
if jwt is None:
|
|
# Fallback: return a simple token (not recommended for production)
|
|
import base64
|
|
import json
|
|
token_data = {
|
|
**data,
|
|
"exp": (datetime.utcnow() + (expires_delta or timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS))).timestamp()
|
|
}
|
|
return base64.b64encode(json.dumps(token_data).encode()).decode()
|
|
|
|
to_encode = data.copy()
|
|
if expires_delta:
|
|
expire = datetime.utcnow() + expires_delta
|
|
else:
|
|
expire = datetime.utcnow() + timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS)
|
|
to_encode.update({"exp": expire})
|
|
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
|
|
|
@staticmethod
|
|
def verify_token(token: str) -> Optional[str]:
|
|
"""
|
|
Verify a JWT token and return the username
|
|
|
|
Returns None if token is invalid or expired
|
|
"""
|
|
if jwt is None:
|
|
# Fallback: decode simple token
|
|
import base64
|
|
import json
|
|
try:
|
|
token_data = json.loads(base64.b64decode(token).decode())
|
|
if token_data.get("exp", 0) > datetime.utcnow().timestamp():
|
|
return token_data.get("sub")
|
|
return None
|
|
except:
|
|
return None
|
|
|
|
try:
|
|
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
|
username: str = payload.get("sub")
|
|
return username
|
|
except JWTError:
|
|
return None
|
|
|
|
|
|
# Permission matrix for role-based access control
|
|
PERMISSION_MATRIX = {
|
|
"BOSS": [
|
|
"view_all_signals",
|
|
"assign_tasks",
|
|
"manage_users",
|
|
"delete_users",
|
|
"manage_domains",
|
|
"view_projects",
|
|
"view_context",
|
|
"view_settings",
|
|
"manage_staff",
|
|
],
|
|
"ADMIN": [
|
|
"view_all_signals",
|
|
"assign_tasks",
|
|
"manage_domains",
|
|
"view_projects",
|
|
"view_context",
|
|
"view_settings",
|
|
"manage_staff",
|
|
],
|
|
"AI": [
|
|
"view_assigned_signals",
|
|
"create_experiments",
|
|
"view_context",
|
|
],
|
|
"FE": [
|
|
"view_assigned_signals",
|
|
"view_assigned_projects",
|
|
],
|
|
"BE": [
|
|
"view_assigned_signals",
|
|
"view_assigned_projects",
|
|
],
|
|
}
|
|
|
|
|
|
def has_permission(user_role: str, permission: str) -> bool:
|
|
"""Check if a role has a specific permission"""
|
|
roles = parse_role_values(user_role)
|
|
return any(permission in PERMISSION_MATRIX.get(role, []) for role in roles)
|
|
|
|
|
|
def has_any_role(user_role: str, allowed_roles: list) -> bool:
|
|
"""Check if user_role is in the list of allowed roles"""
|
|
allowed = {str(role).upper() for role in allowed_roles}
|
|
return any(role in allowed for role in parse_role_values(user_role))
|
|
|
|
|
|
def parse_role_values(user_role: str | None) -> list[str]:
|
|
"""Parse single/multi role string into uppercase role tokens."""
|
|
raw = str(user_role or "")
|
|
if not raw:
|
|
return []
|
|
values: list[str] = []
|
|
for token in raw.split(","):
|
|
role = token.strip().upper()
|
|
if role and role not in values:
|
|
values.append(role)
|
|
return values
|
|
|
|
|
|
def can_manage_users(user_role: str) -> bool:
|
|
"""Check if user can manage other users (BOSS only)"""
|
|
return has_any_role(user_role, ["BOSS"])
|
|
|
|
|
|
def can_assign_tasks(user_role: str) -> bool:
|
|
"""Check if user can assign tasks to others"""
|
|
return has_any_role(user_role, ["BOSS", "ADMIN"])
|