157 lines
4.7 KiB
Python
157 lines
4.7 KiB
Python
#!/usr/bin/python3
|
|||
|
|
# -*- coding: utf-8 -*-
|
||
|
|
"""
|
||
|
|
Flask-Login authentication provider
|
||
|
|
"""
|
||
|
|
|
||
|
|
import logging
|
||
|
|
from functools import wraps
|
||
|
|
|
||
|
|
from flask import redirect, request, session, url_for
|
||
|
|
|
||
|
|
from .base import AuthProvider
|
||
|
|
|
||
|
|
|
||
|
|
class FlaskLoginProvider(AuthProvider):
|
||
|
|
"""Flask-Login based authentication provider.
|
||
|
|
|
||
|
|
Configuration:
|
||
|
|
users: Dict of {username: password_hash} or path to user file
|
||
|
|
secret_key: Flask secret key for sessions
|
||
|
|
login_view: Route name for login page (default: 'login')
|
||
|
|
use_bcrypt: If True, passwords are bcrypt hashes (default: True)
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, config):
|
||
|
|
super().__init__(config)
|
||
|
|
self.logger = logging.getLogger("tisbackup.auth")
|
||
|
|
self.login_manager = None
|
||
|
|
self.users = config.get("users", {})
|
||
|
|
self.login_view = config.get("login_view", "login")
|
||
|
|
self.use_bcrypt = config.get("use_bcrypt", True)
|
||
|
|
|
||
|
|
if self.use_bcrypt:
|
||
|
|
try:
|
||
|
|
import bcrypt
|
||
|
|
self.bcrypt = bcrypt
|
||
|
|
except ImportError:
|
||
|
|
raise ImportError("bcrypt library required. Install with: pip install bcrypt")
|
||
|
|
|
||
|
|
# Load users from file if path provided
|
||
|
|
users_file = config.get("users_file")
|
||
|
|
if users_file:
|
||
|
|
self._load_users_from_file(users_file)
|
||
|
|
|
||
|
|
def _load_users_from_file(self, filepath):
|
||
|
|
"""Load users from file (username:password_hash per line)."""
|
||
|
|
try:
|
||
|
|
with open(filepath) as f:
|
||
|
|
for line in f:
|
||
|
|
line = line.strip()
|
||
|
|
if line and not line.startswith('#'):
|
||
|
|
username, password_hash = line.split(':', 1)
|
||
|
|
self.users[username.strip()] = password_hash.strip()
|
||
|
|
except Exception as e:
|
||
|
|
self.logger.error(f"Failed to load users from {filepath}: {e}")
|
||
|
|
raise
|
||
|
|
|
||
|
|
def init_app(self, app):
|
||
|
|
"""Initialize Flask-Login with the app."""
|
||
|
|
try:
|
||
|
|
from flask_login import LoginManager, UserMixin
|
||
|
|
except ImportError:
|
||
|
|
raise ImportError("flask-login library required. Install with: pip install flask-login")
|
||
|
|
|
||
|
|
self.login_manager = LoginManager()
|
||
|
|
self.login_manager.init_app(app)
|
||
|
|
self.login_manager.login_view = self.login_view
|
||
|
|
|
||
|
|
# Simple User class
|
||
|
|
class User(UserMixin):
|
||
|
|
def __init__(self, username):
|
||
|
|
self.id = username
|
||
|
|
self.username = username
|
||
|
|
|
||
|
|
self.User = User
|
||
|
|
|
||
|
|
@self.login_manager.user_loader
|
||
|
|
def load_user(user_id):
|
||
|
|
if user_id in self.users:
|
||
|
|
return User(user_id)
|
||
|
|
return None
|
||
|
|
|
||
|
|
def verify_password(self, username, password):
|
||
|
|
"""Verify username and password.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
username: Username to check
|
||
|
|
password: Password to verify
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
bool: True if valid, False otherwise
|
||
|
|
"""
|
||
|
|
if username not in self.users:
|
||
|
|
return False
|
||
|
|
|
||
|
|
stored_hash = self.users[username]
|
||
|
|
|
||
|
|
if self.use_bcrypt:
|
||
|
|
password_bytes = password.encode('utf-8')
|
||
|
|
hash_bytes = stored_hash.encode('utf-8') if isinstance(stored_hash, str) else stored_hash
|
||
|
|
return self.bcrypt.checkpw(password_bytes, hash_bytes)
|
||
|
|
else:
|
||
|
|
return password == stored_hash
|
||
|
|
|
||
|
|
def login_user(self, username):
|
||
|
|
"""Login a user by username.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
username: Username to login
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
bool: True if successful
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
from flask_login import login_user
|
||
|
|
except ImportError:
|
||
|
|
return False
|
||
|
|
|
||
|
|
if username in self.users:
|
||
|
|
user = self.User(username)
|
||
|
|
login_user(user)
|
||
|
|
return True
|
||
|
|
return False
|
||
|
|
|
||
|
|
def is_authenticated(self):
|
||
|
|
"""Check if current user is authenticated."""
|
||
|
|
try:
|
||
|
|
from flask_login import current_user
|
||
|
|
return current_user.is_authenticated
|
||
|
|
except ImportError:
|
||
|
|
return False
|
||
|
|
|
||
|
|
def handle_unauthorized(self):
|
||
|
|
"""Redirect to login page."""
|
||
|
|
return redirect(url_for(self.login_view))
|
||
|
|
|
||
|
|
def get_current_user(self):
|
||
|
|
"""Get current authenticated user."""
|
||
|
|
try:
|
||
|
|
from flask_login import current_user
|
||
|
|
if current_user.is_authenticated:
|
||
|
|
return {
|
||
|
|
"username": current_user.username,
|
||
|
|
"auth_type": "flask-login"
|
||
|
|
}
|
||
|
|
except ImportError:
|
||
|
|
pass
|
||
|
|
return None
|
||
|
|
|
||
|
|
def logout(self):
|
||
|
|
"""Logout current user."""
|
||
|
|
try:
|
||
|
|
from flask_login import logout_user
|
||
|
|
logout_user()
|
||
|
|
except ImportError:
|
||
|
|
pass
|