2016-08-09 09:27:37 +00:00
|
|
|
# ~*~ coding: utf-8 ~*~
|
|
|
|
#
|
2016-12-19 17:19:50 +00:00
|
|
|
import base64
|
2016-08-30 17:00:20 +00:00
|
|
|
import logging
|
2023-06-21 09:42:25 +00:00
|
|
|
import os
|
|
|
|
import re
|
2020-06-22 09:04:07 +00:00
|
|
|
import time
|
2016-08-30 17:00:20 +00:00
|
|
|
|
2023-06-21 09:42:25 +00:00
|
|
|
import pyotp
|
2016-09-13 13:45:10 +00:00
|
|
|
from django.conf import settings
|
2016-12-19 17:19:50 +00:00
|
|
|
from django.core.cache import cache
|
2016-08-28 15:58:22 +00:00
|
|
|
|
2016-08-31 17:12:02 +00:00
|
|
|
from common.tasks import send_mail_async
|
2023-11-16 03:26:26 +00:00
|
|
|
from common.utils import reverse, get_object_or_none, ip, safe_next_url
|
2020-03-12 08:24:38 +00:00
|
|
|
from .models import User
|
2016-08-30 17:00:20 +00:00
|
|
|
|
2023-06-21 09:42:25 +00:00
|
|
|
logger = logging.getLogger('jumpserver.users')
|
2016-08-30 17:00:20 +00:00
|
|
|
|
2016-08-28 15:58:22 +00:00
|
|
|
|
2019-05-24 10:12:58 +00:00
|
|
|
def send_user_created_mail(user):
|
2021-10-22 12:06:16 +00:00
|
|
|
from .notifications import UserCreatedMsg
|
2019-05-24 10:12:58 +00:00
|
|
|
|
2021-10-22 12:06:16 +00:00
|
|
|
recipient_list = [user.email]
|
2021-10-26 02:52:23 +00:00
|
|
|
msg = UserCreatedMsg(user).html_msg
|
2021-10-22 12:06:16 +00:00
|
|
|
subject = msg['subject']
|
|
|
|
message = msg['message']
|
2019-05-24 10:12:58 +00:00
|
|
|
|
2017-12-12 04:19:45 +00:00
|
|
|
if settings.DEBUG:
|
2018-01-26 08:06:23 +00:00
|
|
|
try:
|
|
|
|
print(message)
|
|
|
|
except OSError:
|
|
|
|
pass
|
2016-09-01 15:09:58 +00:00
|
|
|
|
|
|
|
send_mail_async.delay(subject, message, recipient_list, html_message=message)
|
|
|
|
|
|
|
|
|
2020-03-12 08:24:38 +00:00
|
|
|
def get_user_or_pre_auth_user(request):
|
2018-04-19 09:20:53 +00:00
|
|
|
user = request.user
|
|
|
|
if user.is_authenticated:
|
|
|
|
return user
|
2020-03-12 08:24:38 +00:00
|
|
|
pre_auth_user_id = request.session.get('user_id')
|
|
|
|
user = None
|
|
|
|
if pre_auth_user_id:
|
|
|
|
user = get_object_or_none(User, pk=pre_auth_user_id)
|
2018-04-18 04:48:07 +00:00
|
|
|
return user
|
|
|
|
|
|
|
|
|
|
|
|
def redirect_user_first_login_or_index(request, redirect_field_name):
|
2022-05-17 10:50:16 +00:00
|
|
|
url = request.POST.get(redirect_field_name)
|
|
|
|
if not url:
|
|
|
|
url = request.GET.get(redirect_field_name)
|
2023-11-16 03:26:26 +00:00
|
|
|
url = safe_next_url(url, request=request)
|
2022-05-17 10:50:16 +00:00
|
|
|
# 防止 next 地址为 None
|
|
|
|
if not url or url.lower() in ['none']:
|
|
|
|
url = reverse('index')
|
|
|
|
return url
|
2018-04-18 04:48:07 +00:00
|
|
|
|
|
|
|
|
2020-03-12 08:24:38 +00:00
|
|
|
def generate_otp_uri(username, otp_secret_key=None, issuer="JumpServer"):
|
|
|
|
if otp_secret_key is None:
|
2018-04-19 03:13:11 +00:00
|
|
|
otp_secret_key = base64.b32encode(os.urandom(10)).decode('utf-8')
|
2018-04-18 04:48:07 +00:00
|
|
|
totp = pyotp.TOTP(otp_secret_key)
|
2018-12-10 04:03:42 +00:00
|
|
|
otp_issuer_name = settings.OTP_ISSUER_NAME or issuer
|
2020-03-12 08:24:38 +00:00
|
|
|
uri = totp.provisioning_uri(name=username, issuer_name=otp_issuer_name)
|
|
|
|
return uri, otp_secret_key
|
2018-04-18 04:48:07 +00:00
|
|
|
|
|
|
|
|
|
|
|
def check_otp_code(otp_secret_key, otp_code):
|
2018-09-03 03:24:25 +00:00
|
|
|
if not otp_secret_key or not otp_code:
|
|
|
|
return False
|
2018-04-18 04:48:07 +00:00
|
|
|
totp = pyotp.TOTP(otp_secret_key)
|
2018-12-17 06:26:00 +00:00
|
|
|
otp_valid_window = settings.OTP_VALID_WINDOW or 0
|
|
|
|
return totp.verify(otp=otp_code, valid_window=otp_valid_window)
|
2018-06-05 09:26:31 +00:00
|
|
|
|
|
|
|
|
2021-07-30 07:19:00 +00:00
|
|
|
def get_password_check_rules(user):
|
2018-06-05 09:26:31 +00:00
|
|
|
check_rules = []
|
2018-11-22 04:27:27 +00:00
|
|
|
for rule in settings.SECURITY_PASSWORD_RULES:
|
|
|
|
key = "id_{}".format(rule.lower())
|
2021-07-30 07:19:00 +00:00
|
|
|
if user.is_org_admin and rule == 'SECURITY_PASSWORD_MIN_LENGTH':
|
|
|
|
rule = 'SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH'
|
2018-11-22 04:27:27 +00:00
|
|
|
value = getattr(settings, rule)
|
|
|
|
if not value:
|
|
|
|
continue
|
|
|
|
check_rules.append({'key': key, 'value': int(value)})
|
|
|
|
return check_rules
|
2018-06-05 09:26:31 +00:00
|
|
|
|
|
|
|
|
2021-08-17 08:01:27 +00:00
|
|
|
def check_password_rules(password, is_org_admin=False):
|
2018-11-22 04:27:27 +00:00
|
|
|
pattern = r"^"
|
|
|
|
if settings.SECURITY_PASSWORD_UPPER_CASE:
|
|
|
|
pattern += '(?=.*[A-Z])'
|
|
|
|
if settings.SECURITY_PASSWORD_LOWER_CASE:
|
|
|
|
pattern += '(?=.*[a-z])'
|
|
|
|
if settings.SECURITY_PASSWORD_NUMBER:
|
|
|
|
pattern += '(?=.*\d)'
|
|
|
|
if settings.SECURITY_PASSWORD_SPECIAL_CHAR:
|
2023-12-15 09:14:34 +00:00
|
|
|
pattern += '(?=.*[`~!@#$%^&*()\-=_+\[\]{}|;:\'",.<>/?])'
|
2018-11-22 04:27:27 +00:00
|
|
|
pattern += '[a-zA-Z\d`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'\",\.<>\/\?]'
|
2021-08-17 08:01:27 +00:00
|
|
|
if is_org_admin:
|
2021-07-30 07:19:00 +00:00
|
|
|
min_length = settings.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH
|
|
|
|
else:
|
2021-08-05 02:37:29 +00:00
|
|
|
min_length = settings.SECURITY_PASSWORD_MIN_LENGTH
|
2021-11-11 11:03:01 +00:00
|
|
|
pattern += '.{' + str(min_length - 1) + ',}$'
|
2018-06-05 09:26:31 +00:00
|
|
|
match_obj = re.match(pattern, password)
|
|
|
|
return bool(match_obj)
|
2018-07-05 08:23:33 +00:00
|
|
|
|
|
|
|
|
2021-04-08 04:47:49 +00:00
|
|
|
class BlockUtil:
|
|
|
|
BLOCK_KEY_TMPL: str
|
2018-09-03 03:24:25 +00:00
|
|
|
|
2021-04-08 04:47:49 +00:00
|
|
|
def __init__(self, username):
|
|
|
|
self.block_key = self.BLOCK_KEY_TMPL.format(username)
|
|
|
|
self.key_ttl = int(settings.SECURITY_LOGIN_LIMIT_TIME) * 60
|
2018-09-03 03:24:25 +00:00
|
|
|
|
2021-04-08 04:47:49 +00:00
|
|
|
def block(self):
|
|
|
|
cache.set(self.block_key, True, self.key_ttl)
|
2018-07-05 08:23:33 +00:00
|
|
|
|
2021-04-08 04:47:49 +00:00
|
|
|
def is_block(self):
|
|
|
|
return bool(cache.get(self.block_key))
|
2018-07-16 04:13:13 +00:00
|
|
|
|
|
|
|
|
2021-04-08 04:47:49 +00:00
|
|
|
class BlockUtilBase:
|
|
|
|
LIMIT_KEY_TMPL: str
|
|
|
|
BLOCK_KEY_TMPL: str
|
2019-07-30 11:10:06 +00:00
|
|
|
|
2021-04-08 04:47:49 +00:00
|
|
|
def __init__(self, username, ip):
|
|
|
|
self.username = username
|
|
|
|
self.ip = ip
|
|
|
|
self.limit_key = self.LIMIT_KEY_TMPL.format(username, ip)
|
|
|
|
self.block_key = self.BLOCK_KEY_TMPL.format(username)
|
|
|
|
self.key_ttl = int(settings.SECURITY_LOGIN_LIMIT_TIME) * 60
|
2019-07-30 11:10:06 +00:00
|
|
|
|
2021-04-08 04:47:49 +00:00
|
|
|
def get_remainder_times(self):
|
|
|
|
times_up = settings.SECURITY_LOGIN_LIMIT_COUNT
|
|
|
|
times_failed = self.get_failed_count()
|
|
|
|
times_remainder = int(times_up) - int(times_failed)
|
|
|
|
return times_remainder
|
2018-07-05 08:23:33 +00:00
|
|
|
|
2021-11-10 03:30:48 +00:00
|
|
|
def incr_failed_count(self) -> int:
|
2021-04-08 04:47:49 +00:00
|
|
|
limit_key = self.limit_key
|
|
|
|
count = cache.get(limit_key, 0)
|
|
|
|
count += 1
|
|
|
|
cache.set(limit_key, count, self.key_ttl)
|
2018-07-05 08:23:33 +00:00
|
|
|
|
2021-04-08 04:47:49 +00:00
|
|
|
limit_count = settings.SECURITY_LOGIN_LIMIT_COUNT
|
|
|
|
if count >= limit_count:
|
|
|
|
cache.set(self.block_key, True, self.key_ttl)
|
2021-11-10 03:30:48 +00:00
|
|
|
return limit_count - count
|
2018-07-05 08:23:33 +00:00
|
|
|
|
2021-04-08 04:47:49 +00:00
|
|
|
def get_failed_count(self):
|
|
|
|
count = cache.get(self.limit_key, 0)
|
|
|
|
return count
|
2018-07-05 08:23:33 +00:00
|
|
|
|
2021-04-08 04:47:49 +00:00
|
|
|
def clean_failed_count(self):
|
|
|
|
cache.delete(self.limit_key)
|
|
|
|
cache.delete(self.block_key)
|
2018-07-16 04:13:13 +00:00
|
|
|
|
2021-04-08 04:47:49 +00:00
|
|
|
@classmethod
|
|
|
|
def unblock_user(cls, username):
|
|
|
|
key_limit = cls.LIMIT_KEY_TMPL.format(username, '*')
|
|
|
|
key_block = cls.BLOCK_KEY_TMPL.format(username)
|
|
|
|
# Redis 尽量不要用通配
|
|
|
|
cache.delete_pattern(key_limit)
|
|
|
|
cache.delete(key_block)
|
2018-07-16 04:13:13 +00:00
|
|
|
|
2021-04-08 04:47:49 +00:00
|
|
|
@classmethod
|
|
|
|
def is_user_block(cls, username):
|
|
|
|
block_key = cls.BLOCK_KEY_TMPL.format(username)
|
|
|
|
return bool(cache.get(block_key))
|
|
|
|
|
|
|
|
def is_block(self):
|
|
|
|
return bool(cache.get(self.block_key))
|
|
|
|
|
|
|
|
|
2021-11-11 11:03:01 +00:00
|
|
|
class BlockGlobalIpUtilBase:
|
|
|
|
LIMIT_KEY_TMPL: str
|
|
|
|
BLOCK_KEY_TMPL: str
|
|
|
|
|
|
|
|
def __init__(self, ip):
|
|
|
|
self.ip = ip
|
|
|
|
self.limit_key = self.LIMIT_KEY_TMPL.format(ip)
|
|
|
|
self.block_key = self.BLOCK_KEY_TMPL.format(ip)
|
2021-11-18 14:20:35 +00:00
|
|
|
self.key_ttl = int(settings.SECURITY_LOGIN_IP_LIMIT_TIME) * 60
|
2021-11-11 11:03:01 +00:00
|
|
|
|
2021-11-18 08:55:23 +00:00
|
|
|
@property
|
|
|
|
def ip_in_black_list(self):
|
2021-11-18 14:20:35 +00:00
|
|
|
return ip.contains_ip(self.ip, settings.SECURITY_LOGIN_IP_BLACK_LIST)
|
2021-11-18 08:55:23 +00:00
|
|
|
|
2021-11-18 14:20:35 +00:00
|
|
|
@property
|
|
|
|
def ip_in_white_list(self):
|
2021-11-18 14:20:35 +00:00
|
|
|
return ip.contains_ip(self.ip, settings.SECURITY_LOGIN_IP_WHITE_LIST)
|
2021-11-11 11:03:01 +00:00
|
|
|
|
2021-11-18 14:20:35 +00:00
|
|
|
def set_block_if_need(self):
|
2021-11-18 14:20:35 +00:00
|
|
|
if self.ip_in_white_list or self.ip_in_black_list:
|
|
|
|
return
|
|
|
|
count = cache.get(self.limit_key, 0)
|
|
|
|
count += 1
|
|
|
|
cache.set(self.limit_key, count, self.key_ttl)
|
|
|
|
|
|
|
|
limit_count = settings.SECURITY_LOGIN_IP_LIMIT_COUNT
|
|
|
|
if count < limit_count:
|
|
|
|
return
|
|
|
|
cache.set(self.block_key, True, self.key_ttl)
|
2021-11-18 08:55:23 +00:00
|
|
|
|
|
|
|
def clean_block_if_need(self):
|
|
|
|
cache.delete(self.limit_key)
|
|
|
|
cache.delete(self.block_key)
|
2021-11-11 11:03:01 +00:00
|
|
|
|
|
|
|
def is_block(self):
|
2021-11-18 14:20:35 +00:00
|
|
|
if self.ip_in_white_list:
|
2021-11-11 11:03:01 +00:00
|
|
|
return False
|
2021-11-18 14:20:35 +00:00
|
|
|
if self.ip_in_black_list:
|
|
|
|
return True
|
2021-11-18 14:20:35 +00:00
|
|
|
return bool(cache.get(self.block_key))
|
2021-11-11 11:03:01 +00:00
|
|
|
|
|
|
|
|
2021-04-08 04:47:49 +00:00
|
|
|
class LoginBlockUtil(BlockUtilBase):
|
|
|
|
LIMIT_KEY_TMPL = "_LOGIN_LIMIT_{}_{}"
|
|
|
|
BLOCK_KEY_TMPL = "_LOGIN_BLOCK_{}"
|
|
|
|
|
|
|
|
|
|
|
|
class MFABlockUtils(BlockUtilBase):
|
|
|
|
LIMIT_KEY_TMPL = "_MFA_LIMIT_{}_{}"
|
|
|
|
BLOCK_KEY_TMPL = "_MFA_BLOCK_{}"
|
2019-05-24 03:11:50 +00:00
|
|
|
|
|
|
|
|
2021-11-11 11:03:01 +00:00
|
|
|
class LoginIpBlockUtil(BlockGlobalIpUtilBase):
|
|
|
|
LIMIT_KEY_TMPL = "_LOGIN_LIMIT_{}"
|
|
|
|
BLOCK_KEY_TMPL = "_LOGIN_BLOCK_{}"
|
|
|
|
|
|
|
|
|
2024-03-11 10:40:57 +00:00
|
|
|
def validate_emails(emails):
|
2024-03-07 08:40:16 +00:00
|
|
|
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
2024-03-11 10:40:57 +00:00
|
|
|
for e in emails:
|
|
|
|
e = e or ''
|
|
|
|
if re.match(pattern, e):
|
|
|
|
return e
|
2024-03-07 08:40:16 +00:00
|
|
|
|
|
|
|
|
2022-03-30 11:07:49 +00:00
|
|
|
def construct_user_email(username, email, email_suffix=''):
|
2024-03-11 10:40:57 +00:00
|
|
|
default = f'{username}@{email_suffix or settings.EMAIL_SUFFIX}'
|
|
|
|
emails = [email, username]
|
|
|
|
email = validate_emails(emails)
|
|
|
|
return email or default
|
2019-05-24 03:11:50 +00:00
|
|
|
|
2019-10-18 07:05:45 +00:00
|
|
|
|
2022-02-17 12:13:31 +00:00
|
|
|
def get_current_org_members():
|
2019-10-18 07:05:45 +00:00
|
|
|
from orgs.utils import current_org
|
2022-02-17 12:13:31 +00:00
|
|
|
return current_org.get_members()
|
2019-12-16 08:53:29 +00:00
|
|
|
|
|
|
|
|
2020-06-22 09:04:07 +00:00
|
|
|
def is_auth_time_valid(session, key):
|
|
|
|
return True if session.get(key, 0) > time.time() else False
|
|
|
|
|
|
|
|
|
|
|
|
def is_auth_password_time_valid(session):
|
|
|
|
return is_auth_time_valid(session, 'auth_password_expired_at')
|
|
|
|
|
|
|
|
|
|
|
|
def is_auth_otp_time_valid(session):
|
2021-11-10 03:30:48 +00:00
|
|
|
return is_auth_time_valid(session, 'auth_otp_expired_at')
|
2022-06-07 11:26:07 +00:00
|
|
|
|
|
|
|
|
|
|
|
def is_confirm_time_valid(session, key):
|
|
|
|
if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
|
|
|
return True
|
|
|
|
mfa_verify_time = session.get(key, 0)
|
|
|
|
if time.time() - mfa_verify_time < settings.SECURITY_MFA_VERIFY_TTL:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def is_auth_confirm_time_valid(session):
|
|
|
|
return is_confirm_time_valid(session, 'MFA_VERIFY_TIME')
|