Add in proper passord check lib, show results in form
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Enhanced SME Server Utilities Module - Python 3.6.8 Compatible
|
||||
Corrected SME Server Utilities Module - Python 3.6.8 Compatible
|
||||
|
||||
This module provides utilities for interfacing with SME Server's
|
||||
configuration database and system commands with enhanced password
|
||||
strength validation.
|
||||
configuration database using the correct passwordstrength structure:
|
||||
passwordstrength=configuration
|
||||
Admin=strong
|
||||
Ibays=strong
|
||||
Users=strong
|
||||
|
||||
Compatible with Python 3.6.8
|
||||
"""
|
||||
@@ -13,7 +16,6 @@ import subprocess
|
||||
import os
|
||||
import logging
|
||||
import re
|
||||
import hashlib
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
@@ -81,9 +83,18 @@ class SMEConfigDB:
|
||||
account_type = self.get_account_type(username)
|
||||
return account_type == 'user'
|
||||
|
||||
def get_password_strength_setting(self):
|
||||
"""Get the password strength setting from configuration database"""
|
||||
success, output = self._run_db_command(['configuration', 'getprop', 'passwordstrength', 'Passwordstrength'])
|
||||
def is_admin_account(self, username):
|
||||
"""Check if the account is an admin account"""
|
||||
account_type = self.get_account_type(username)
|
||||
return account_type == 'admin'
|
||||
|
||||
def get_password_strength_setting(self, account_type='Users'):
|
||||
"""Get the password strength setting for specific account type
|
||||
|
||||
Args:
|
||||
account_type: 'Users', 'Admin', or 'Ibays'
|
||||
"""
|
||||
success, output = self._run_db_command(['configuration', 'getprop', 'passwordstrength', account_type])
|
||||
|
||||
if success and output:
|
||||
strength = output.strip().lower()
|
||||
@@ -93,54 +104,67 @@ class SMEConfigDB:
|
||||
# Default to 'normal' if not set or invalid
|
||||
return 'normal'
|
||||
|
||||
def set_password_strength_setting(self, strength):
|
||||
"""Set the password strength setting in configuration database"""
|
||||
def set_password_strength_setting(self, strength, account_type='Users'):
|
||||
"""Set the password strength setting for specific account type
|
||||
|
||||
Args:
|
||||
strength: 'none', 'normal', or 'strong'
|
||||
account_type: 'Users', 'Admin', or 'Ibays'
|
||||
"""
|
||||
if strength.lower() not in ['none', 'normal', 'strong']:
|
||||
return False, "Invalid strength level. Must be 'none', 'normal', or 'strong'"
|
||||
|
||||
success, output = self._run_db_command(['configuration', 'setprop', 'passwordstrength', 'Passwordstrength', strength.lower()])
|
||||
success, output = self._run_db_command(['configuration', 'setprop', 'passwordstrength', account_type, strength.lower()])
|
||||
return success, output
|
||||
|
||||
def get_all_password_strength_settings(self):
|
||||
"""Get all password strength settings"""
|
||||
success, output = self._run_db_command(['configuration', 'show', 'passwordstrength'])
|
||||
|
||||
settings = {
|
||||
'Users': 'normal',
|
||||
'Admin': 'normal',
|
||||
'Ibays': 'normal'
|
||||
}
|
||||
|
||||
if success and output:
|
||||
for line in output.split('\n'):
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
key = key.strip()
|
||||
value = value.strip().lower()
|
||||
if key in ['Users', 'Admin', 'Ibays'] and value in ['none', 'normal', 'strong']:
|
||||
settings[key] = value
|
||||
|
||||
return settings
|
||||
|
||||
class PasswordStrengthValidator:
|
||||
"""Enhanced password strength validation with configurable levels"""
|
||||
class BasicPasswordValidator:
|
||||
"""Basic password validation for fallback when external library not available"""
|
||||
|
||||
def __init__(self):
|
||||
# Common weak passwords and patterns for strong validation
|
||||
# Common weak passwords for basic validation
|
||||
self.common_passwords = {
|
||||
'password', 'password123', '123456', '123456789', 'qwerty', 'abc123',
|
||||
'password1', 'admin', 'administrator', 'root', 'user', 'guest',
|
||||
'welcome', 'login', 'pass', 'secret', 'default', 'changeme',
|
||||
'letmein', 'monkey', 'dragon', 'master', 'shadow', 'superman',
|
||||
'michael', 'jennifer', 'jordan', 'michelle', 'daniel', 'andrew'
|
||||
'letmein', 'monkey', 'dragon', 'master', 'shadow', 'superman'
|
||||
}
|
||||
|
||||
self.common_patterns = [
|
||||
r'^(.)\1+$', # All same character
|
||||
r'^\d+$', # All numbers
|
||||
r'^[a-z]+$', # All lowercase
|
||||
r'^[A-Z]+$', # All uppercase
|
||||
r'^(abc|123|qwe|asd|zxc)', # Common sequences
|
||||
r'(password|admin|user|guest|login)', # Common words
|
||||
]
|
||||
|
||||
def validate_password_strength(self, password, strength_level='normal'):
|
||||
"""Validate password based on configured strength level"""
|
||||
"""Basic password validation"""
|
||||
errors = []
|
||||
|
||||
if strength_level == 'none':
|
||||
# No validation - only basic length check
|
||||
if len(password) < 1:
|
||||
errors.append("Password cannot be empty")
|
||||
return errors
|
||||
|
||||
elif strength_level == 'normal':
|
||||
# Normal validation: 12+ chars, complexity requirements
|
||||
errors.extend(self._validate_normal_strength(password))
|
||||
|
||||
elif strength_level == 'strong':
|
||||
# Strong validation: Normal + crypto testing
|
||||
errors.extend(self._validate_normal_strength(password))
|
||||
errors.extend(self._validate_strong_crypto(password))
|
||||
errors.extend(self._validate_strong_basic(password))
|
||||
|
||||
return errors
|
||||
|
||||
@@ -148,19 +172,16 @@ class PasswordStrengthValidator:
|
||||
"""Validate normal strength requirements"""
|
||||
errors = []
|
||||
|
||||
# Minimum 12 characters
|
||||
if len(password) < 12:
|
||||
errors.append("Password must be at least 12 characters long")
|
||||
|
||||
# Maximum length check
|
||||
if len(password) > 127:
|
||||
errors.append("Password must be no more than 127 characters long")
|
||||
|
||||
# Character type requirements
|
||||
has_upper = bool(re.search(r'[A-Z]', password))
|
||||
has_lower = bool(re.search(r'[a-z]', password))
|
||||
has_numeric = bool(re.search(r'\d', password))
|
||||
has_non_alpha = bool(re.search(r'[^a-zA-Z0-9]', password))
|
||||
has_upper = any(c.isupper() for c in password)
|
||||
has_lower = any(c.islower() for c in password)
|
||||
has_numeric = any(c.isdigit() for c in password)
|
||||
has_non_alpha = any(not c.isalnum() for c in password)
|
||||
|
||||
missing_types = []
|
||||
if not has_upper:
|
||||
@@ -177,71 +198,38 @@ class PasswordStrengthValidator:
|
||||
|
||||
return errors
|
||||
|
||||
def _validate_strong_crypto(self, password):
|
||||
"""Validate strong crypto requirements"""
|
||||
def _validate_strong_basic(self, password):
|
||||
"""Basic strong validation"""
|
||||
errors = []
|
||||
|
||||
# Check against common passwords
|
||||
if password.lower() in self.common_passwords:
|
||||
errors.append("Password is too common and easily guessable")
|
||||
|
||||
# Check for common patterns
|
||||
for pattern in self.common_patterns:
|
||||
if re.search(pattern, password.lower()):
|
||||
errors.append("Password contains common patterns that are easily guessable")
|
||||
break
|
||||
|
||||
# Check for keyboard patterns
|
||||
keyboard_patterns = [
|
||||
'qwertyuiop', 'asdfghjkl', 'zxcvbnm',
|
||||
'1234567890', '0987654321',
|
||||
'qwerty', 'asdfgh', 'zxcvbn'
|
||||
]
|
||||
|
||||
# Basic keyboard pattern check
|
||||
keyboard_patterns = ['qwerty', 'asdfgh', 'zxcvbn', '123456', '654321']
|
||||
password_lower = password.lower()
|
||||
for pattern in keyboard_patterns:
|
||||
if pattern in password_lower or pattern[::-1] in password_lower:
|
||||
if pattern in password_lower:
|
||||
errors.append("Password contains keyboard patterns that are easily guessable")
|
||||
break
|
||||
|
||||
# Check for repeated sequences
|
||||
if self._has_repeated_sequences(password):
|
||||
errors.append("Password contains repeated sequences that reduce security")
|
||||
|
||||
# Check for dictionary words (basic check)
|
||||
if self._contains_dictionary_words(password):
|
||||
errors.append("Password contains common dictionary words")
|
||||
|
||||
return errors
|
||||
|
||||
def _has_repeated_sequences(self, password):
|
||||
"""Check for repeated character sequences"""
|
||||
for i in range(len(password) - 2):
|
||||
sequence = password[i:i+3]
|
||||
if password.count(sequence) > 1:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _contains_dictionary_words(self, password):
|
||||
"""Basic check for common dictionary words"""
|
||||
common_words = [
|
||||
'password', 'admin', 'user', 'login', 'welcome', 'secret',
|
||||
'computer', 'internet', 'security', 'system', 'network',
|
||||
'server', 'database', 'application', 'software', 'hardware'
|
||||
]
|
||||
|
||||
password_lower = password.lower()
|
||||
for word in common_words:
|
||||
if len(word) >= 4 and word in password_lower:
|
||||
return True
|
||||
return False
|
||||
|
||||
class SMEPasswordManager:
|
||||
"""Handle password operations for SME Server with enhanced validation"""
|
||||
"""Handle password operations for SME Server with corrected DB structure"""
|
||||
|
||||
def __init__(self):
|
||||
self.config_db = SMEConfigDB()
|
||||
self.strength_validator = PasswordStrengthValidator()
|
||||
self.basic_validator = BasicPasswordValidator()
|
||||
|
||||
# Try to import external password validation library
|
||||
self.external_validator = None
|
||||
try:
|
||||
import zxcvbn
|
||||
self.external_validator = zxcvbn
|
||||
logger.info("Using zxcvbn library for password validation")
|
||||
except ImportError:
|
||||
logger.info("zxcvbn library not available, using basic validation")
|
||||
|
||||
def validate_username(self, username):
|
||||
"""Validate username format and existence"""
|
||||
@@ -262,41 +250,95 @@ class SMEPasswordManager:
|
||||
|
||||
return True, "Username is valid"
|
||||
|
||||
def validate_password_strength(self, password):
|
||||
"""Validate password meets configured SME Server requirements"""
|
||||
# Get current password strength setting
|
||||
strength_level = self.config_db.get_password_strength_setting()
|
||||
def validate_password_strength(self, password, username=None):
|
||||
"""Validate password using external library or fallback to basic validation"""
|
||||
# Get appropriate password strength setting
|
||||
strength_level = self.config_db.get_password_strength_setting('Users')
|
||||
|
||||
# Use the enhanced validator
|
||||
errors = self.strength_validator.validate_password_strength(password, strength_level)
|
||||
if self.external_validator:
|
||||
return self._validate_with_zxcvbn(password, strength_level, username)
|
||||
else:
|
||||
return self.basic_validator.validate_password_strength(password, strength_level)
|
||||
|
||||
def _validate_with_zxcvbn(self, password, strength_level, username=None):
|
||||
"""Validate password using zxcvbn library"""
|
||||
errors = []
|
||||
|
||||
if strength_level == 'none':
|
||||
if len(password) < 1:
|
||||
errors.append("Password cannot be empty")
|
||||
return errors
|
||||
|
||||
# Basic length and complexity checks first
|
||||
if len(password) < 12:
|
||||
errors.append("Password must be at least 12 characters long")
|
||||
|
||||
if len(password) > 127:
|
||||
errors.append("Password must be no more than 127 characters long")
|
||||
|
||||
# Character type requirements for normal and strong
|
||||
if strength_level in ['normal', 'strong']:
|
||||
has_upper = any(c.isupper() for c in password)
|
||||
has_lower = any(c.islower() for c in password)
|
||||
has_numeric = any(c.isdigit() for c in password)
|
||||
has_non_alpha = any(not c.isalnum() for c in password)
|
||||
|
||||
missing_types = []
|
||||
if not has_upper:
|
||||
missing_types.append("uppercase letter")
|
||||
if not has_lower:
|
||||
missing_types.append("lowercase letter")
|
||||
if not has_numeric:
|
||||
missing_types.append("number")
|
||||
if not has_non_alpha:
|
||||
missing_types.append("special character")
|
||||
|
||||
if missing_types:
|
||||
errors.append("Password must contain at least one: {}".format(", ".join(missing_types)))
|
||||
|
||||
# Use zxcvbn for advanced validation on strong passwords
|
||||
if strength_level == 'strong' and not errors:
|
||||
user_inputs = [username] if username else []
|
||||
result = self.external_validator.zxcvbn(password, user_inputs)
|
||||
|
||||
# zxcvbn scores: 0-4 (0 = very weak, 4 = very strong)
|
||||
if result["score"] < 3: # Require score of 3 or higher for strong
|
||||
feedback = result.get('feedback', {})
|
||||
warning = feedback.get('warning', '')
|
||||
suggestions = feedback.get('suggestions', [])
|
||||
|
||||
if warning:
|
||||
errors.append("Password weakness: {}".format(warning))
|
||||
|
||||
for suggestion in suggestions:
|
||||
errors.append("Suggestion: {}".format(suggestion))
|
||||
|
||||
if not warning and not suggestions:
|
||||
errors.append("Password is not strong enough (zxcvbn score: {}/4)".format(result['score']))
|
||||
|
||||
return errors
|
||||
|
||||
def get_password_strength_info(self):
|
||||
def get_password_strength_info(self, account_type='Users'):
|
||||
"""Get current password strength setting and requirements"""
|
||||
strength_level = self.config_db.get_password_strength_setting()
|
||||
strength_level = self.config_db.get_password_strength_setting(account_type)
|
||||
|
||||
requirements = {
|
||||
'level': strength_level,
|
||||
'description': self._get_strength_description(strength_level)
|
||||
}
|
||||
|
||||
return requirements
|
||||
|
||||
def _get_strength_description(self, strength_level):
|
||||
"""Get human-readable description of strength requirements"""
|
||||
descriptions = {
|
||||
'none': 'No specific password requirements',
|
||||
'normal': 'Minimum 12 characters with uppercase, lowercase, number, and special character',
|
||||
'strong': 'Normal requirements plus protection against common passwords and patterns'
|
||||
'strong': 'Normal requirements plus advanced security validation using zxcvbn library' if self.external_validator else 'Normal requirements plus basic pattern protection'
|
||||
}
|
||||
|
||||
return {
|
||||
'level': strength_level,
|
||||
'description': descriptions.get(strength_level, 'Unknown strength level'),
|
||||
'account_type': account_type,
|
||||
'using_zxcvbn': self.external_validator is not None
|
||||
}
|
||||
return descriptions.get(strength_level, 'Unknown strength level')
|
||||
|
||||
def verify_current_password(self, username, password):
|
||||
"""Verify the current password for a user using system authentication"""
|
||||
try:
|
||||
# Use the 'su' command to verify password
|
||||
# This is safer than directly accessing shadow files
|
||||
process = subprocess.Popen(
|
||||
['su', username, '-c', 'true'],
|
||||
stdin=subprocess.PIPE,
|
||||
@@ -367,7 +409,6 @@ class SMESystemInfo:
|
||||
def get_version():
|
||||
"""Get SME Server version"""
|
||||
try:
|
||||
# Try to read version from release file
|
||||
version_files = [
|
||||
'/etc/e-smith-release',
|
||||
'/etc/sme-release',
|
||||
|
Reference in New Issue
Block a user