mirror of https://github.com/jumpserver/jumpserver
[Update] Stash
parent
1a247d60e7
commit
21714cc411
|
@ -2,3 +2,5 @@ from django.dispatch import Signal
|
|||
|
||||
|
||||
post_create_openid_user = Signal(providing_args=('user',))
|
||||
post_auth_success = Signal(providing_args=('user', 'request'))
|
||||
post_auth_failed = Signal(providing_args=('username', 'request', 'reason'))
|
||||
|
|
|
@ -2,9 +2,15 @@ from django.http.request import QueryDict
|
|||
from django.conf import settings
|
||||
from django.dispatch import receiver
|
||||
from django.contrib.auth.signals import user_logged_out
|
||||
from django.utils import timezone
|
||||
from django_auth_ldap.backend import populate_user
|
||||
|
||||
from common.utils import get_request_ip
|
||||
from .openid import client
|
||||
from .signals import post_create_openid_user
|
||||
from .tasks import write_login_log_async
|
||||
from .signals import (
|
||||
post_create_openid_user, post_auth_success, post_auth_failed
|
||||
)
|
||||
|
||||
|
||||
@receiver(user_logged_out)
|
||||
|
@ -38,3 +44,36 @@ def on_ldap_create_user(sender, user, ldap_user, **kwargs):
|
|||
if user and user.name != 'admin':
|
||||
user.source = user.SOURCE_LDAP
|
||||
user.save()
|
||||
|
||||
|
||||
def generate_data(username, request):
|
||||
if not request.user.is_anonymous and request.user.is_app:
|
||||
login_ip = request.data.get('remote_addr', None)
|
||||
login_type = request.data.get('login_type', '')
|
||||
user_agent = request.data.get('HTTP_USER_AGENT', '')
|
||||
else:
|
||||
login_ip = get_request_ip(request)
|
||||
user_agent = request.META.get('HTTP_USER_AGENT', '')
|
||||
login_type = 'W'
|
||||
data = {
|
||||
'username': username,
|
||||
'ip': login_ip,
|
||||
'type': login_type,
|
||||
'user_agent': user_agent,
|
||||
'datetime': timezone.now()
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
@receiver(post_auth_success)
|
||||
def on_user_auth_success(sender, user, request, **kwargs):
|
||||
data = generate_data(user.username, request)
|
||||
data.update({'mfa': int(user.otp_enabled), 'status': True})
|
||||
write_login_log_async.delay(**data)
|
||||
|
||||
|
||||
@receiver(post_auth_failed)
|
||||
def on_user_auth_failed(sender, username, request, reason, **kwargs):
|
||||
data = generate_data(username, request)
|
||||
data.update({'reason': reason, 'status': False})
|
||||
write_login_log_async.delay(**data)
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from celery import shared_task
|
||||
|
||||
from .utils import write_login_log
|
||||
|
||||
|
||||
@shared_task
|
||||
def write_login_log_async(*args, **kwargs):
|
||||
write_login_log(*args, **kwargs)
|
|
@ -2,15 +2,13 @@
|
|||
#
|
||||
|
||||
from django.urls import path
|
||||
from authentication.openid import views
|
||||
from .. import views
|
||||
|
||||
app_name = 'authentication'
|
||||
|
||||
urlpatterns = [
|
||||
# openid
|
||||
path('openid/login/', views.LoginView.as_view(), name='openid-login'),
|
||||
path('openid/login/complete/', views.LoginCompleteView.as_view(),
|
||||
path('openid/login/', views.OpenIDLoginView.as_view(), name='openid-login'),
|
||||
path('openid/login/complete/', views.OpenIDLoginCompleteView.as_view(),
|
||||
name='openid-login-complete'),
|
||||
|
||||
# other
|
||||
]
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.utils.translation import ugettext as _
|
||||
from common.utils import get_ip_city, validate_ip
|
||||
|
||||
|
||||
def write_login_log(*args, **kwargs):
|
||||
from users.models import LoginLog
|
||||
default_city = _("Unknown")
|
||||
ip = kwargs.get('ip', '')
|
||||
if not (ip and validate_ip(ip)):
|
||||
ip = ip[:15]
|
||||
city = default_city
|
||||
else:
|
||||
city = get_ip_city(ip) or default_city
|
||||
kwargs.update({'ip': ip, 'city': city})
|
||||
LoginLog.objects.create(**kwargs)
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .openid import *
|
|
@ -0,0 +1,212 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import os
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import render
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth import login as auth_login, logout as auth_logout
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.views.generic import ListView
|
||||
from django.core.files.storage import default_storage
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
from django.shortcuts import reverse, redirect
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.decorators.csrf import csrf_protect
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
from django.views.generic.base import TemplateView
|
||||
from django.views.generic.edit import FormView
|
||||
from django.conf import settings
|
||||
from formtools.wizard.views import SessionWizardView
|
||||
|
||||
from common.utils import get_object_or_none, get_request_ip
|
||||
from authentication.signals import post_auth_success, post_auth_failed
|
||||
from users.models import User, LoginLog
|
||||
from users.utils import send_reset_password_mail, check_otp_code, \
|
||||
redirect_user_first_login_or_index, get_user_or_tmp_user, \
|
||||
set_tmp_user_to_cache, get_password_check_rules, check_password_rules, \
|
||||
is_block_login, increase_login_failed_count, clean_failed_count
|
||||
from users import forms
|
||||
|
||||
|
||||
__all__ = [
|
||||
'UserLoginView', 'UserLoginOtpView', 'UserLogoutView',
|
||||
'UserForgotPasswordView', 'UserForgotPasswordSendmailSuccessView',
|
||||
'UserResetPasswordView', 'UserResetPasswordSuccessView',
|
||||
'UserFirstLoginView', 'LoginLogListView'
|
||||
]
|
||||
|
||||
|
||||
@method_decorator(sensitive_post_parameters(), name='dispatch')
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class UserLoginView(FormView):
|
||||
form_class = forms.UserLoginForm
|
||||
form_class_captcha = forms.UserLoginCaptchaForm
|
||||
redirect_field_name = 'next'
|
||||
key_prefix_captcha = "_LOGIN_INVALID_{}"
|
||||
|
||||
def get_template_names(self):
|
||||
template_name = 'users/login.html'
|
||||
if not settings.XPACK_ENABLED:
|
||||
return template_name
|
||||
|
||||
from xpack.plugins.license.models import License
|
||||
if not License.has_valid_license():
|
||||
return template_name
|
||||
|
||||
template_name = 'users/new_login.html'
|
||||
return template_name
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if request.user.is_staff:
|
||||
return redirect(redirect_user_first_login_or_index(
|
||||
request, self.redirect_field_name)
|
||||
)
|
||||
request.session.set_test_cookie()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
# limit login authentication
|
||||
ip = get_request_ip(request)
|
||||
username = self.request.POST.get('username')
|
||||
if is_block_login(username, ip):
|
||||
return self.render_to_response(self.get_context_data(block_login=True))
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
if not self.request.session.test_cookie_worked():
|
||||
return HttpResponse(_("Please enable cookies and try again."))
|
||||
user = form.get_user()
|
||||
# user password expired
|
||||
if user.password_has_expired:
|
||||
reason = LoginLog.REASON_PASSWORD_EXPIRED
|
||||
self.send_auth_signal(success=False, username=user.username, reason=reason)
|
||||
return self.render_to_response(self.get_context_data(password_expired=True))
|
||||
|
||||
set_tmp_user_to_cache(self.request, user)
|
||||
username = form.cleaned_data.get('username')
|
||||
ip = get_request_ip(self.request)
|
||||
# 登陆成功,清除缓存计数
|
||||
clean_failed_count(username, ip)
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def form_invalid(self, form):
|
||||
# write login failed log
|
||||
username = form.cleaned_data.get('username')
|
||||
exist = User.objects.filter(username=username).first()
|
||||
reason = LoginLog.REASON_PASSWORD if exist else LoginLog.REASON_NOT_EXIST
|
||||
# limit user login failed count
|
||||
ip = get_request_ip(self.request)
|
||||
increase_login_failed_count(username, ip)
|
||||
# show captcha
|
||||
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
|
||||
self.send_auth_signal(success=False, username=username, reason=reason)
|
||||
|
||||
old_form = form
|
||||
form = self.form_class_captcha(data=form.data)
|
||||
form._errors = old_form.errors
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_form_class(self):
|
||||
ip = get_request_ip(self.request)
|
||||
if cache.get(self.key_prefix_captcha.format(ip)):
|
||||
return self.form_class_captcha
|
||||
else:
|
||||
return self.form_class
|
||||
|
||||
def get_success_url(self):
|
||||
user = get_user_or_tmp_user(self.request)
|
||||
|
||||
if user.otp_enabled and user.otp_secret_key:
|
||||
# 1,2,mfa_setting & T
|
||||
return reverse('users:login-otp')
|
||||
elif user.otp_enabled and not user.otp_secret_key:
|
||||
# 1,2,mfa_setting & F
|
||||
return reverse('users:user-otp-enable-authentication')
|
||||
elif not user.otp_enabled:
|
||||
# 0 & T,F
|
||||
auth_login(self.request, user)
|
||||
self.send_auth_signal(success=True, user=user)
|
||||
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'demo_mode': os.environ.get("DEMO_MODE"),
|
||||
'AUTH_OPENID': settings.AUTH_OPENID,
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def send_auth_signal(self, success=True, user=None, username='', reason=''):
|
||||
if success:
|
||||
post_auth_success.send(sender=self.__class__, user=user, request=self.request)
|
||||
else:
|
||||
post_auth_failed.send(
|
||||
sender=self.__class__, username=username,
|
||||
request=self.request, reason=reason
|
||||
)
|
||||
|
||||
|
||||
class UserLoginOtpView(FormView):
|
||||
template_name = 'users/login_otp.html'
|
||||
form_class = forms.UserCheckOtpCodeForm
|
||||
redirect_field_name = 'next'
|
||||
|
||||
def form_valid(self, form):
|
||||
user = get_user_or_tmp_user(self.request)
|
||||
otp_code = form.cleaned_data.get('otp_code')
|
||||
otp_secret_key = user.otp_secret_key
|
||||
|
||||
if check_otp_code(otp_secret_key, otp_code):
|
||||
auth_login(self.request, user)
|
||||
self.send_auth_signal(success=True, user=user)
|
||||
return redirect(self.get_success_url())
|
||||
else:
|
||||
self.send_auth_signal(
|
||||
success=False, username=user.username,
|
||||
reason=LoginLog.REASON_MFA
|
||||
)
|
||||
form.add_error('otp_code', _('MFA code invalid, or ntp sync server time'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
|
||||
|
||||
def send_auth_signal(self, success=True, user=None, username='', reason=''):
|
||||
if success:
|
||||
post_auth_success.send(sender=self.__class__, user=user, request=self.request)
|
||||
else:
|
||||
post_auth_failed.send(
|
||||
sender=self.__class__, username=username,
|
||||
request=self.request, reason=reason
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class UserLogoutView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
auth_logout(request)
|
||||
next_uri = request.COOKIES.get("next")
|
||||
if next_uri:
|
||||
return redirect(next_uri)
|
||||
response = super().get(request, *args, **kwargs)
|
||||
return response
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'title': _('Logout success'),
|
||||
'messages': _('Logout success, return login page'),
|
||||
'interval': 1,
|
||||
'redirect_url': reverse('users:login'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
|
|
@ -14,43 +14,36 @@ from django.http.response import (
|
|||
HttpResponseRedirect
|
||||
)
|
||||
|
||||
from . import client
|
||||
from .models import Nonce
|
||||
from users.models import LoginLog
|
||||
from users.tasks import write_login_log_async
|
||||
from common.utils import get_request_ip
|
||||
from ..openid import client
|
||||
from ..openid.models import Nonce
|
||||
from ..signals import post_auth_success
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_base_site_url():
|
||||
return settings.BASE_SITE_URL
|
||||
__all__ = ['OpenIDLoginView', 'OpenIDLoginCompleteView']
|
||||
|
||||
|
||||
class LoginView(RedirectView):
|
||||
class OpenIDLoginView(RedirectView):
|
||||
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
redirect_uri = settings.BASE_SITE_URL + \
|
||||
reverse("authentication:openid-login-complete")
|
||||
nonce = Nonce(
|
||||
redirect_uri=get_base_site_url() + reverse(
|
||||
"authentication:openid-login-complete"),
|
||||
|
||||
redirect_uri=redirect_uri,
|
||||
next_path=self.request.GET.get('next')
|
||||
)
|
||||
|
||||
cache.set(str(nonce.state), nonce, 24*3600)
|
||||
|
||||
self.request.session['openid_state'] = str(nonce.state)
|
||||
|
||||
authorization_url = client.openid_connect_client.\
|
||||
authorization_url(
|
||||
redirect_uri=nonce.redirect_uri, scope='code',
|
||||
state=str(nonce.state)
|
||||
)
|
||||
|
||||
return authorization_url
|
||||
|
||||
|
||||
class LoginCompleteView(RedirectView):
|
||||
class OpenIDLoginCompleteView(RedirectView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'error' in request.GET:
|
||||
|
@ -79,24 +72,6 @@ class LoginCompleteView(RedirectView):
|
|||
return HttpResponseBadRequest()
|
||||
|
||||
login(self.request, user)
|
||||
|
||||
data = {
|
||||
'username': user.username,
|
||||
'mfa': int(user.otp_enabled),
|
||||
'reason': LoginLog.REASON_NOTHING,
|
||||
'status': True
|
||||
}
|
||||
self.write_login_log(data)
|
||||
|
||||
post_auth_success.send(sender=self.__class__, user=user, request=self.request)
|
||||
return HttpResponseRedirect(nonce.next_path or '/')
|
||||
|
||||
def write_login_log(self, data):
|
||||
login_ip = get_request_ip(self.request)
|
||||
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
|
||||
tmp_data = {
|
||||
'ip': login_ip,
|
||||
'type': 'W',
|
||||
'user_agent': user_agent
|
||||
}
|
||||
data.update(tmp_data)
|
||||
write_login_log_async.delay(**data)
|
|
@ -0,0 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .common import *
|
||||
from .django import *
|
||||
from .encode import *
|
||||
from .http import *
|
||||
from .ipip import *
|
|
@ -1,104 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import re
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from six import string_types
|
||||
import base64
|
||||
import os
|
||||
from itertools import chain
|
||||
import logging
|
||||
import datetime
|
||||
import time
|
||||
import hashlib
|
||||
from email.utils import formatdate
|
||||
import calendar
|
||||
import threading
|
||||
from io import StringIO
|
||||
import uuid
|
||||
from functools import wraps
|
||||
import copy
|
||||
|
||||
import paramiko
|
||||
import sshpubkeys
|
||||
from itsdangerous import TimedJSONWebSignatureSerializer, JSONWebSignatureSerializer, \
|
||||
BadSignature, SignatureExpired
|
||||
from django.shortcuts import reverse as dj_reverse
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
import ipaddress
|
||||
|
||||
|
||||
UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}')
|
||||
|
||||
|
||||
def reverse(view_name, urlconf=None, args=None, kwargs=None,
|
||||
current_app=None, external=False):
|
||||
url = dj_reverse(view_name, urlconf=urlconf, args=args,
|
||||
kwargs=kwargs, current_app=current_app)
|
||||
|
||||
if external:
|
||||
site_url = settings.SITE_URL
|
||||
url = site_url.strip('/') + url
|
||||
return url
|
||||
|
||||
|
||||
def get_object_or_none(model, **kwargs):
|
||||
try:
|
||||
obj = model.objects.get(**kwargs)
|
||||
except model.DoesNotExist:
|
||||
return None
|
||||
return obj
|
||||
|
||||
|
||||
class Singleton(type):
|
||||
def __init__(cls, *args, **kwargs):
|
||||
cls.__instance = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls.__instance is None:
|
||||
cls.__instance = super().__call__(*args, **kwargs)
|
||||
return cls.__instance
|
||||
else:
|
||||
return cls.__instance
|
||||
|
||||
|
||||
class Signer(metaclass=Singleton):
|
||||
"""用来加密,解密,和基于时间戳的方式验证token"""
|
||||
def __init__(self, secret_key=None):
|
||||
self.secret_key = secret_key
|
||||
|
||||
def sign(self, value):
|
||||
s = JSONWebSignatureSerializer(self.secret_key, algorithm_name='HS256')
|
||||
return s.dumps(value).decode()
|
||||
|
||||
def unsign(self, value):
|
||||
if value is None:
|
||||
return value
|
||||
s = JSONWebSignatureSerializer(self.secret_key, algorithm_name='HS256')
|
||||
try:
|
||||
return s.loads(value)
|
||||
except BadSignature:
|
||||
return {}
|
||||
|
||||
def sign_t(self, value, expires_in=3600):
|
||||
s = TimedJSONWebSignatureSerializer(self.secret_key, expires_in=expires_in)
|
||||
return str(s.dumps(value), encoding="utf8")
|
||||
|
||||
def unsign_t(self, value):
|
||||
s = TimedJSONWebSignatureSerializer(self.secret_key)
|
||||
try:
|
||||
return s.loads(value)
|
||||
except (BadSignature, SignatureExpired):
|
||||
return {}
|
||||
|
||||
|
||||
def date_expired_default():
|
||||
try:
|
||||
years = int(settings.DEFAULT_EXPIRED_YEARS)
|
||||
except TypeError:
|
||||
years = 70
|
||||
return timezone.now() + timezone.timedelta(days=365*years)
|
||||
ipip_db = None
|
||||
|
||||
|
||||
def combine_seq(s1, s2, callback=None):
|
||||
|
@ -146,88 +60,6 @@ def timesince(dt, since='', default="just now"):
|
|||
return default
|
||||
|
||||
|
||||
def ssh_key_string_to_obj(text, password=None):
|
||||
key = None
|
||||
try:
|
||||
key = paramiko.RSAKey.from_private_key(StringIO(text), password=password)
|
||||
except paramiko.SSHException:
|
||||
pass
|
||||
|
||||
try:
|
||||
key = paramiko.DSSKey.from_private_key(StringIO(text), password=password)
|
||||
except paramiko.SSHException:
|
||||
pass
|
||||
return key
|
||||
|
||||
|
||||
def ssh_pubkey_gen(private_key=None, username='jumpserver', hostname='localhost', password=None):
|
||||
if isinstance(private_key, bytes):
|
||||
private_key = private_key.decode("utf-8")
|
||||
if isinstance(private_key, string_types):
|
||||
private_key = ssh_key_string_to_obj(private_key, password=password)
|
||||
if not isinstance(private_key, (paramiko.RSAKey, paramiko.DSSKey)):
|
||||
raise IOError('Invalid private key')
|
||||
|
||||
public_key = "%(key_type)s %(key_content)s %(username)s@%(hostname)s" % {
|
||||
'key_type': private_key.get_name(),
|
||||
'key_content': private_key.get_base64(),
|
||||
'username': username,
|
||||
'hostname': hostname,
|
||||
}
|
||||
return public_key
|
||||
|
||||
|
||||
def ssh_key_gen(length=2048, type='rsa', password=None, username='jumpserver', hostname=None):
|
||||
"""Generate user ssh private and public key
|
||||
|
||||
Use paramiko RSAKey generate it.
|
||||
:return private key str and public key str
|
||||
"""
|
||||
|
||||
if hostname is None:
|
||||
hostname = os.uname()[1]
|
||||
|
||||
f = StringIO()
|
||||
try:
|
||||
if type == 'rsa':
|
||||
private_key_obj = paramiko.RSAKey.generate(length)
|
||||
elif type == 'dsa':
|
||||
private_key_obj = paramiko.DSSKey.generate(length)
|
||||
else:
|
||||
raise IOError('SSH private key must be `rsa` or `dsa`')
|
||||
private_key_obj.write_private_key(f, password=password)
|
||||
private_key = f.getvalue()
|
||||
public_key = ssh_pubkey_gen(private_key_obj, username=username, hostname=hostname)
|
||||
return private_key, public_key
|
||||
except IOError:
|
||||
raise IOError('These is error when generate ssh key.')
|
||||
|
||||
|
||||
def validate_ssh_private_key(text, password=None):
|
||||
if isinstance(text, bytes):
|
||||
try:
|
||||
text = text.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return False
|
||||
|
||||
key = ssh_key_string_to_obj(text, password=password)
|
||||
if key is None:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def validate_ssh_public_key(text):
|
||||
ssh = sshpubkeys.SSHKey(text)
|
||||
try:
|
||||
ssh.parse()
|
||||
except (sshpubkeys.InvalidKeyException, UnicodeDecodeError):
|
||||
return False
|
||||
except NotImplementedError as e:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def setattr_bulk(seq, key, value):
|
||||
def set_attr(obj):
|
||||
setattr(obj, key, value)
|
||||
|
@ -243,70 +75,6 @@ def set_or_append_attr_bulk(seq, key, value):
|
|||
setattr(obj, key, value)
|
||||
|
||||
|
||||
def content_md5(data):
|
||||
"""计算data的MD5值,经过Base64编码并返回str类型。
|
||||
|
||||
返回值可以直接作为HTTP Content-Type头部的值
|
||||
"""
|
||||
if isinstance(data, str):
|
||||
data = hashlib.md5(data.encode('utf-8'))
|
||||
value = base64.b64encode(data.hexdigest().encode('utf-8'))
|
||||
return value.decode('utf-8')
|
||||
|
||||
|
||||
_STRPTIME_LOCK = threading.Lock()
|
||||
|
||||
_GMT_FORMAT = "%a, %d %b %Y %H:%M:%S GMT"
|
||||
_ISO8601_FORMAT = "%Y-%m-%dT%H:%M:%S.000Z"
|
||||
|
||||
|
||||
def to_unixtime(time_string, format_string):
|
||||
time_string = time_string.decode("ascii")
|
||||
with _STRPTIME_LOCK:
|
||||
return int(calendar.timegm(time.strptime(time_string, format_string)))
|
||||
|
||||
|
||||
def http_date(timeval=None):
|
||||
"""返回符合HTTP标准的GMT时间字符串,用strftime的格式表示就是"%a, %d %b %Y %H:%M:%S GMT"。
|
||||
但不能使用strftime,因为strftime的结果是和locale相关的。
|
||||
"""
|
||||
return formatdate(timeval, usegmt=True)
|
||||
|
||||
|
||||
def http_to_unixtime(time_string):
|
||||
"""把HTTP Date格式的字符串转换为UNIX时间(自1970年1月1日UTC零点的秒数)。
|
||||
|
||||
HTTP Date形如 `Sat, 05 Dec 2015 11:10:29 GMT` 。
|
||||
"""
|
||||
return to_unixtime(time_string, _GMT_FORMAT)
|
||||
|
||||
|
||||
def iso8601_to_unixtime(time_string):
|
||||
"""把ISO8601时间字符串(形如,2012-02-24T06:07:48.000Z)转换为UNIX时间,精确到秒。"""
|
||||
return to_unixtime(time_string, _ISO8601_FORMAT)
|
||||
|
||||
|
||||
def make_signature(access_key_secret, date=None):
|
||||
if isinstance(date, bytes):
|
||||
date = bytes.decode(date)
|
||||
if isinstance(date, int):
|
||||
date_gmt = http_date(date)
|
||||
elif date is None:
|
||||
date_gmt = http_date(int(time.time()))
|
||||
else:
|
||||
date_gmt = date
|
||||
|
||||
data = str(access_key_secret) + "\n" + date_gmt
|
||||
return content_md5(data)
|
||||
|
||||
|
||||
def encrypt_password(password, salt=None):
|
||||
from passlib.hash import sha512_crypt
|
||||
if password:
|
||||
return sha512_crypt.using(rounds=5000).hash(password, salt=salt)
|
||||
return None
|
||||
|
||||
|
||||
def capacity_convert(size, expect='auto', rate=1000):
|
||||
"""
|
||||
:param size: '100MB', '1G'
|
||||
|
@ -374,11 +142,6 @@ def is_uuid(seq):
|
|||
return True
|
||||
|
||||
|
||||
def get_signer():
|
||||
signer = Signer(settings.SECRET_KEY)
|
||||
return signer
|
||||
|
||||
|
||||
def get_request_ip(request):
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')
|
||||
if x_forwarded_for and x_forwarded_for[0]:
|
||||
|
@ -388,22 +151,13 @@ def get_request_ip(request):
|
|||
return login_ip
|
||||
|
||||
|
||||
def get_command_storage_setting():
|
||||
default = settings.DEFAULT_TERMINAL_COMMAND_STORAGE
|
||||
value = settings.TERMINAL_COMMAND_STORAGE
|
||||
if not value:
|
||||
return default
|
||||
value.update(default)
|
||||
return value
|
||||
|
||||
|
||||
def get_replay_storage_setting():
|
||||
default = settings.DEFAULT_TERMINAL_REPLAY_STORAGE
|
||||
value = settings.TERMINAL_REPLAY_STORAGE
|
||||
if not value:
|
||||
return default
|
||||
value.update(default)
|
||||
return value
|
||||
def validate_ip(ip):
|
||||
try:
|
||||
ipaddress.ip_address(ip)
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def with_cache(func):
|
|
@ -0,0 +1,54 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import re
|
||||
from django.shortcuts import reverse as dj_reverse
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}')
|
||||
|
||||
|
||||
def reverse(view_name, urlconf=None, args=None, kwargs=None,
|
||||
current_app=None, external=False):
|
||||
url = dj_reverse(view_name, urlconf=urlconf, args=args,
|
||||
kwargs=kwargs, current_app=current_app)
|
||||
|
||||
if external:
|
||||
site_url = settings.SITE_URL
|
||||
url = site_url.strip('/') + url
|
||||
return url
|
||||
|
||||
|
||||
def get_object_or_none(model, **kwargs):
|
||||
try:
|
||||
obj = model.objects.get(**kwargs)
|
||||
except model.DoesNotExist:
|
||||
return None
|
||||
return obj
|
||||
|
||||
|
||||
def date_expired_default():
|
||||
try:
|
||||
years = int(settings.DEFAULT_EXPIRED_YEARS)
|
||||
except TypeError:
|
||||
years = 70
|
||||
return timezone.now() + timezone.timedelta(days=365*years)
|
||||
|
||||
|
||||
def get_command_storage_setting():
|
||||
default = settings.DEFAULT_TERMINAL_COMMAND_STORAGE
|
||||
value = settings.TERMINAL_COMMAND_STORAGE
|
||||
if not value:
|
||||
return default
|
||||
value.update(default)
|
||||
return value
|
||||
|
||||
|
||||
def get_replay_storage_setting():
|
||||
default = settings.DEFAULT_TERMINAL_REPLAY_STORAGE
|
||||
value = settings.TERMINAL_REPLAY_STORAGE
|
||||
if not value:
|
||||
return default
|
||||
value.update(default)
|
||||
return value
|
|
@ -0,0 +1,184 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import re
|
||||
from six import string_types
|
||||
import base64
|
||||
import os
|
||||
import time
|
||||
import hashlib
|
||||
from io import StringIO
|
||||
|
||||
import paramiko
|
||||
import sshpubkeys
|
||||
from itsdangerous import (
|
||||
TimedJSONWebSignatureSerializer, JSONWebSignatureSerializer,
|
||||
BadSignature, SignatureExpired
|
||||
)
|
||||
from django.conf import settings
|
||||
|
||||
from .http import http_date
|
||||
|
||||
|
||||
UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}')
|
||||
|
||||
|
||||
class Singleton(type):
|
||||
def __init__(cls, *args, **kwargs):
|
||||
cls.__instance = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls.__instance is None:
|
||||
cls.__instance = super().__call__(*args, **kwargs)
|
||||
return cls.__instance
|
||||
else:
|
||||
return cls.__instance
|
||||
|
||||
|
||||
class Signer(metaclass=Singleton):
|
||||
"""用来加密,解密,和基于时间戳的方式验证token"""
|
||||
def __init__(self, secret_key=None):
|
||||
self.secret_key = secret_key
|
||||
|
||||
def sign(self, value):
|
||||
s = JSONWebSignatureSerializer(self.secret_key, algorithm_name='HS256')
|
||||
return s.dumps(value).decode()
|
||||
|
||||
def unsign(self, value):
|
||||
if value is None:
|
||||
return value
|
||||
s = JSONWebSignatureSerializer(self.secret_key, algorithm_name='HS256')
|
||||
try:
|
||||
return s.loads(value)
|
||||
except BadSignature:
|
||||
return {}
|
||||
|
||||
def sign_t(self, value, expires_in=3600):
|
||||
s = TimedJSONWebSignatureSerializer(self.secret_key, expires_in=expires_in)
|
||||
return str(s.dumps(value), encoding="utf8")
|
||||
|
||||
def unsign_t(self, value):
|
||||
s = TimedJSONWebSignatureSerializer(self.secret_key)
|
||||
try:
|
||||
return s.loads(value)
|
||||
except (BadSignature, SignatureExpired):
|
||||
return {}
|
||||
|
||||
|
||||
def ssh_key_string_to_obj(text, password=None):
|
||||
key = None
|
||||
try:
|
||||
key = paramiko.RSAKey.from_private_key(StringIO(text), password=password)
|
||||
except paramiko.SSHException:
|
||||
pass
|
||||
|
||||
try:
|
||||
key = paramiko.DSSKey.from_private_key(StringIO(text), password=password)
|
||||
except paramiko.SSHException:
|
||||
pass
|
||||
return key
|
||||
|
||||
|
||||
def ssh_pubkey_gen(private_key=None, username='jumpserver', hostname='localhost', password=None):
|
||||
if isinstance(private_key, bytes):
|
||||
private_key = private_key.decode("utf-8")
|
||||
if isinstance(private_key, string_types):
|
||||
private_key = ssh_key_string_to_obj(private_key, password=password)
|
||||
if not isinstance(private_key, (paramiko.RSAKey, paramiko.DSSKey)):
|
||||
raise IOError('Invalid private key')
|
||||
|
||||
public_key = "%(key_type)s %(key_content)s %(username)s@%(hostname)s" % {
|
||||
'key_type': private_key.get_name(),
|
||||
'key_content': private_key.get_base64(),
|
||||
'username': username,
|
||||
'hostname': hostname,
|
||||
}
|
||||
return public_key
|
||||
|
||||
|
||||
def ssh_key_gen(length=2048, type='rsa', password=None, username='jumpserver', hostname=None):
|
||||
"""Generate user ssh private and public key
|
||||
|
||||
Use paramiko RSAKey generate it.
|
||||
:return private key str and public key str
|
||||
"""
|
||||
|
||||
if hostname is None:
|
||||
hostname = os.uname()[1]
|
||||
|
||||
f = StringIO()
|
||||
try:
|
||||
if type == 'rsa':
|
||||
private_key_obj = paramiko.RSAKey.generate(length)
|
||||
elif type == 'dsa':
|
||||
private_key_obj = paramiko.DSSKey.generate(length)
|
||||
else:
|
||||
raise IOError('SSH private key must be `rsa` or `dsa`')
|
||||
private_key_obj.write_private_key(f, password=password)
|
||||
private_key = f.getvalue()
|
||||
public_key = ssh_pubkey_gen(private_key_obj, username=username, hostname=hostname)
|
||||
return private_key, public_key
|
||||
except IOError:
|
||||
raise IOError('These is error when generate ssh key.')
|
||||
|
||||
|
||||
def validate_ssh_private_key(text, password=None):
|
||||
if isinstance(text, bytes):
|
||||
try:
|
||||
text = text.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return False
|
||||
|
||||
key = ssh_key_string_to_obj(text, password=password)
|
||||
if key is None:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def validate_ssh_public_key(text):
|
||||
ssh = sshpubkeys.SSHKey(text)
|
||||
try:
|
||||
ssh.parse()
|
||||
except (sshpubkeys.InvalidKeyException, UnicodeDecodeError):
|
||||
return False
|
||||
except NotImplementedError as e:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def content_md5(data):
|
||||
"""计算data的MD5值,经过Base64编码并返回str类型。
|
||||
|
||||
返回值可以直接作为HTTP Content-Type头部的值
|
||||
"""
|
||||
if isinstance(data, str):
|
||||
data = hashlib.md5(data.encode('utf-8'))
|
||||
value = base64.b64encode(data.hexdigest().encode('utf-8'))
|
||||
return value.decode('utf-8')
|
||||
|
||||
|
||||
def make_signature(access_key_secret, date=None):
|
||||
if isinstance(date, bytes):
|
||||
date = bytes.decode(date)
|
||||
if isinstance(date, int):
|
||||
date_gmt = http_date(date)
|
||||
elif date is None:
|
||||
date_gmt = http_date(int(time.time()))
|
||||
else:
|
||||
date_gmt = date
|
||||
|
||||
data = str(access_key_secret) + "\n" + date_gmt
|
||||
return content_md5(data)
|
||||
|
||||
|
||||
def encrypt_password(password, salt=None):
|
||||
from passlib.hash import sha512_crypt
|
||||
if password:
|
||||
return sha512_crypt.using(rounds=5000).hash(password, salt=salt)
|
||||
return None
|
||||
|
||||
|
||||
def get_signer():
|
||||
signer = Signer(settings.SECRET_KEY)
|
||||
return signer
|
|
@ -0,0 +1,37 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import time
|
||||
from email.utils import formatdate
|
||||
import calendar
|
||||
import threading
|
||||
|
||||
_STRPTIME_LOCK = threading.Lock()
|
||||
|
||||
_GMT_FORMAT = "%a, %d %b %Y %H:%M:%S GMT"
|
||||
_ISO8601_FORMAT = "%Y-%m-%dT%H:%M:%S.000Z"
|
||||
|
||||
|
||||
def to_unixtime(time_string, format_string):
|
||||
time_string = time_string.decode("ascii")
|
||||
with _STRPTIME_LOCK:
|
||||
return int(calendar.timegm(time.strptime(time_string, format_string)))
|
||||
|
||||
|
||||
def http_date(timeval=None):
|
||||
"""返回符合HTTP标准的GMT时间字符串,用strftime的格式表示就是"%a, %d %b %Y %H:%M:%S GMT"。
|
||||
但不能使用strftime,因为strftime的结果是和locale相关的。
|
||||
"""
|
||||
return formatdate(timeval, usegmt=True)
|
||||
|
||||
|
||||
def http_to_unixtime(time_string):
|
||||
"""把HTTP Date格式的字符串转换为UNIX时间(自1970年1月1日UTC零点的秒数)。
|
||||
|
||||
HTTP Date形如 `Sat, 05 Dec 2015 11:10:29 GMT` 。
|
||||
"""
|
||||
return to_unixtime(time_string, _GMT_FORMAT)
|
||||
|
||||
|
||||
def iso8601_to_unixtime(time_string):
|
||||
"""把ISO8601时间字符串(形如,2012-02-24T06:07:48.000Z)转换为UNIX时间,精确到秒。"""
|
||||
return to_unixtime(time_string, _ISO8601_FORMAT)
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .ipdb import *
|
|
@ -0,0 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import os
|
||||
|
||||
import ipdb
|
||||
|
||||
ipip_db = None
|
||||
|
||||
|
||||
def get_ip_city(ip):
|
||||
global ipip_db
|
||||
if ipip_db is None:
|
||||
ipip_db_path = os.path.join(os.path.dirname(__file__), 'ipipfree.ipdb')
|
||||
ipip_db = ipdb.City(ipip_db_path)
|
||||
info = list(set(ipip_db.find(ip, 'CN')))
|
||||
if '' in info:
|
||||
info.remove('')
|
||||
return ' '.join(info)
|
Binary file not shown.
|
@ -14,8 +14,8 @@ from rest_framework.views import APIView
|
|||
from common.utils import get_logger, get_request_ip
|
||||
from common.permissions import IsOrgAdminOrAppUser
|
||||
from orgs.mixins import RootOrgViewMixin
|
||||
from authentication.signals import post_auth_success, post_auth_failed
|
||||
from ..serializers import UserSerializer
|
||||
from ..tasks import write_login_log_async
|
||||
from ..models import User, LoginLog
|
||||
from ..utils import check_user_valid, check_otp_code, \
|
||||
increase_login_failed_count, is_block_login, \
|
||||
|
@ -46,37 +46,22 @@ class UserAuthApi(RootOrgViewMixin, APIView):
|
|||
username = request.data.get('username', '')
|
||||
exist = User.objects.filter(username=username).first()
|
||||
reason = LoginLog.REASON_PASSWORD if exist else LoginLog.REASON_NOT_EXIST
|
||||
data = {
|
||||
'username': username,
|
||||
'mfa': LoginLog.MFA_UNKNOWN,
|
||||
'reason': reason,
|
||||
'status': False
|
||||
}
|
||||
self.write_login_log(request, data)
|
||||
self.send_auth_signal(success=False, username=username, reason=reason)
|
||||
increase_login_failed_count(username, ip)
|
||||
return Response({'msg': msg}, status=401)
|
||||
|
||||
if user.password_has_expired:
|
||||
data = {
|
||||
'username': user.username,
|
||||
'mfa': int(user.otp_enabled),
|
||||
'reason': LoginLog.REASON_PASSWORD_EXPIRED,
|
||||
'status': False
|
||||
}
|
||||
self.write_login_log(request, data)
|
||||
self.send_auth_signal(
|
||||
success=False, username=username,
|
||||
reason=LoginLog.REASON_PASSWORD_EXPIRED
|
||||
)
|
||||
msg = _("The user {} password has expired, please update.".format(
|
||||
user.username))
|
||||
logger.info(msg)
|
||||
return Response({'msg': msg}, status=401)
|
||||
|
||||
if not user.otp_enabled:
|
||||
data = {
|
||||
'username': user.username,
|
||||
'mfa': int(user.otp_enabled),
|
||||
'reason': LoginLog.REASON_NOTHING,
|
||||
'status': True
|
||||
}
|
||||
self.write_login_log(request, data)
|
||||
self.send_auth_signal(success=True, user=user)
|
||||
# 登陆成功,清除原来的缓存计数
|
||||
clean_failed_count(username, ip)
|
||||
token = user.create_bearer_token(request)
|
||||
|
@ -108,22 +93,14 @@ class UserAuthApi(RootOrgViewMixin, APIView):
|
|||
)
|
||||
return user, msg
|
||||
|
||||
@staticmethod
|
||||
def write_login_log(request, data):
|
||||
login_ip = request.data.get('remote_addr', None)
|
||||
login_type = request.data.get('login_type', '')
|
||||
user_agent = request.data.get('HTTP_USER_AGENT', '')
|
||||
|
||||
if not login_ip:
|
||||
login_ip = get_request_ip(request)
|
||||
|
||||
tmp_data = {
|
||||
'ip': login_ip,
|
||||
'type': login_type,
|
||||
'user_agent': user_agent,
|
||||
}
|
||||
data.update(tmp_data)
|
||||
write_login_log_async.delay(**data)
|
||||
def send_auth_signal(self, success=True, user=None, username='', reason=''):
|
||||
if success:
|
||||
post_auth_success.send(sender=self.__class__, user=user, request=self.request)
|
||||
else:
|
||||
post_auth_failed.send(
|
||||
sender=self.__class__, username=username,
|
||||
request=self.request, reason=reason
|
||||
)
|
||||
|
||||
|
||||
class UserConnectionTokenApi(RootOrgViewMixin, APIView):
|
||||
|
@ -197,52 +174,25 @@ class UserOtpAuthApi(RootOrgViewMixin, APIView):
|
|||
def post(self, request):
|
||||
otp_code = request.data.get('otp_code', '')
|
||||
seed = request.data.get('seed', '')
|
||||
|
||||
user = cache.get(seed, None)
|
||||
if not user:
|
||||
return Response(
|
||||
{'msg': _('Please verify the user name and password first')},
|
||||
status=401
|
||||
)
|
||||
|
||||
if not check_otp_code(user.otp_secret_key, otp_code):
|
||||
data = {
|
||||
'username': user.username,
|
||||
'mfa': int(user.otp_enabled),
|
||||
'reason': LoginLog.REASON_MFA,
|
||||
'status': False
|
||||
}
|
||||
self.write_login_log(request, data)
|
||||
self.send_auth_signal(success=False, username=user.username, reason=LoginLog.REASON_MFA)
|
||||
return Response({'msg': _('MFA certification failed')}, status=401)
|
||||
|
||||
data = {
|
||||
'username': user.username,
|
||||
'mfa': int(user.otp_enabled),
|
||||
'reason': LoginLog.REASON_NOTHING,
|
||||
'status': True
|
||||
}
|
||||
self.write_login_log(request, data)
|
||||
self.send_auth_signal(success=True, user=user)
|
||||
token = user.create_bearer_token(request)
|
||||
return Response(
|
||||
{
|
||||
'token': token,
|
||||
'user': self.serializer_class(user).data
|
||||
}
|
||||
)
|
||||
data = {'token': token, 'user': self.serializer_class(user).data}
|
||||
return Response(data)
|
||||
|
||||
@staticmethod
|
||||
def write_login_log(request, data):
|
||||
login_ip = request.data.get('remote_addr', None)
|
||||
login_type = request.data.get('login_type', '')
|
||||
user_agent = request.data.get('HTTP_USER_AGENT', '')
|
||||
|
||||
if not login_ip:
|
||||
login_ip = get_request_ip(request)
|
||||
|
||||
tmp_data = {
|
||||
'ip': login_ip,
|
||||
'type': login_type,
|
||||
'user_agent': user_agent
|
||||
}
|
||||
data.update(tmp_data)
|
||||
write_login_log_async.delay(**data)
|
||||
def send_auth_signal(self, success=True, user=None, username='', reason=''):
|
||||
if success:
|
||||
post_auth_success.send(sender=self.__class__, user=user, request=self.request)
|
||||
else:
|
||||
post_auth_failed.send(
|
||||
sender=self.__class__, username=username,
|
||||
request=self.request, reason=reason
|
||||
)
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import uuid
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.authtoken.models import Token
|
||||
from .user import User
|
||||
|
@ -82,7 +83,7 @@ class LoginLog(models.Model):
|
|||
mfa = models.SmallIntegerField(default=MFA_UNKNOWN, choices=MFA_CHOICE, verbose_name=_('MFA'))
|
||||
reason = models.SmallIntegerField(default=REASON_NOTHING, choices=REASON_CHOICE, verbose_name=_('Reason'))
|
||||
status = models.BooleanField(max_length=2, default=True, choices=STATUS_CHOICE, verbose_name=_('Status'))
|
||||
datetime = models.DateTimeField(auto_now_add=True, verbose_name=_('Date login'))
|
||||
datetime = models.DateTimeField(default=timezone.now, verbose_name=_('Date login'))
|
||||
|
||||
class Meta:
|
||||
ordering = ['-datetime', 'username']
|
||||
|
|
|
@ -2,4 +2,3 @@ from django.dispatch import Signal
|
|||
|
||||
|
||||
post_user_create = Signal(providing_args=('user',))
|
||||
|
||||
|
|
|
@ -7,17 +7,12 @@ from ops.celery.utils import create_or_update_celery_periodic_tasks
|
|||
from ops.celery.decorator import after_app_ready_start
|
||||
from .models import User
|
||||
from common.utils import get_logger
|
||||
from .utils import write_login_log, send_password_expiration_reminder_mail
|
||||
from .utils import send_password_expiration_reminder_mail
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
@shared_task
|
||||
def write_login_log_async(*args, **kwargs):
|
||||
write_login_log(*args, **kwargs)
|
||||
|
||||
|
||||
@shared_task
|
||||
def check_password_expired():
|
||||
users = User.objects.exclude(role=User.ROLE_APP)
|
||||
|
|
|
@ -7,7 +7,6 @@ import pyotp
|
|||
import base64
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import ipaddress
|
||||
from django.http import Http404
|
||||
from django.conf import settings
|
||||
|
@ -18,7 +17,7 @@ from django.core.cache import cache
|
|||
from datetime import datetime
|
||||
|
||||
from common.tasks import send_mail_async
|
||||
from common.utils import reverse, get_object_or_none
|
||||
from common.utils import reverse, get_object_or_none, get_ip_city
|
||||
from .models import User, LoginLog
|
||||
|
||||
|
||||
|
@ -199,51 +198,6 @@ def check_user_valid(**kwargs):
|
|||
return None, _('Password or SSH public key invalid')
|
||||
|
||||
|
||||
def validate_ip(ip):
|
||||
try:
|
||||
ipaddress.ip_address(ip)
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def write_login_log(*args, **kwargs):
|
||||
ip = kwargs.get('ip', '')
|
||||
if not (ip and validate_ip(ip)):
|
||||
ip = ip[:15]
|
||||
city = "Unknown"
|
||||
else:
|
||||
city = get_ip_city(ip)
|
||||
kwargs.update({'ip': ip, 'city': city})
|
||||
LoginLog.objects.create(**kwargs)
|
||||
|
||||
|
||||
def get_ip_city(ip, timeout=10):
|
||||
# Taobao ip api: http://ip.taobao.com/service/getIpInfo.php?ip=8.8.8.8
|
||||
# Sina ip api: http://int.dpool.sina.com.cn/iplookup/iplookup.php?ip=8.8.8.8&format=json
|
||||
|
||||
url = 'http://ip.taobao.com/service/getIpInfo.php?ip=%s' % ip
|
||||
try:
|
||||
r = requests.get(url, timeout=timeout)
|
||||
except:
|
||||
r = None
|
||||
city = 'Unknown'
|
||||
if r and r.status_code == 200:
|
||||
try:
|
||||
data = r.json()
|
||||
if not isinstance(data, int) and data['code'] == 0:
|
||||
country = data['data']['country']
|
||||
_city = data['data']['city']
|
||||
if country == 'XX':
|
||||
city = _city
|
||||
else:
|
||||
city = ' '.join([country, _city])
|
||||
except ValueError:
|
||||
pass
|
||||
return city
|
||||
|
||||
|
||||
def get_user_or_tmp_user(request):
|
||||
user = request.user
|
||||
tmp_user = get_tmp_user_from_cache(request)
|
||||
|
|
|
@ -1,244 +1,35 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import os
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import render
|
||||
from django.contrib.auth import login as auth_login, logout as auth_logout
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.views.generic import ListView
|
||||
from django.views.generic import RedirectView
|
||||
from django.core.files.storage import default_storage
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import reverse, redirect
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.decorators.csrf import csrf_protect
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
from django.views.generic.base import TemplateView
|
||||
from django.views.generic.edit import FormView
|
||||
from django.conf import settings
|
||||
from django.urls import reverse_lazy
|
||||
from formtools.wizard.views import SessionWizardView
|
||||
|
||||
from common.utils import get_object_or_none, get_request_ip
|
||||
from ..models import User, LoginLog
|
||||
from ..utils import send_reset_password_mail, check_otp_code, \
|
||||
redirect_user_first_login_or_index, get_user_or_tmp_user, \
|
||||
set_tmp_user_to_cache, get_password_check_rules, check_password_rules, \
|
||||
is_block_login, increase_login_failed_count, clean_failed_count
|
||||
from ..tasks import write_login_log_async
|
||||
from common.utils import get_object_or_none
|
||||
from ..models import User
|
||||
from ..utils import (
|
||||
send_reset_password_mail, get_password_check_rules, check_password_rules
|
||||
)
|
||||
from .. import forms
|
||||
|
||||
|
||||
__all__ = [
|
||||
'UserLoginView', 'UserLoginOtpView', 'UserLogoutView',
|
||||
'UserForgotPasswordView', 'UserForgotPasswordSendmailSuccessView',
|
||||
'UserResetPasswordView', 'UserResetPasswordSuccessView',
|
||||
'UserFirstLoginView', 'LoginLogListView'
|
||||
'UserLoginView', 'UserForgotPasswordSendmailSuccessView',
|
||||
'UserResetPasswordSuccessView', 'UserResetPasswordSuccessView',
|
||||
'UserResetPasswordView', 'UserForgotPasswordView',
|
||||
]
|
||||
|
||||
|
||||
@method_decorator(sensitive_post_parameters(), name='dispatch')
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class UserLoginView(FormView):
|
||||
form_class = forms.UserLoginForm
|
||||
form_class_captcha = forms.UserLoginCaptchaForm
|
||||
redirect_field_name = 'next'
|
||||
key_prefix_captcha = "_LOGIN_INVALID_{}"
|
||||
|
||||
def get_template_names(self):
|
||||
template_name = 'users/login.html'
|
||||
if not settings.XPACK_ENABLED:
|
||||
return template_name
|
||||
|
||||
from xpack.plugins.license.models import License
|
||||
if not License.has_valid_license():
|
||||
return template_name
|
||||
|
||||
template_name = 'users/new_login.html'
|
||||
return template_name
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if request.user.is_staff:
|
||||
return redirect(redirect_user_first_login_or_index(
|
||||
request, self.redirect_field_name)
|
||||
)
|
||||
request.session.set_test_cookie()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
# limit login authentication
|
||||
ip = get_request_ip(request)
|
||||
username = self.request.POST.get('username')
|
||||
if is_block_login(username, ip):
|
||||
return self.render_to_response(self.get_context_data(block_login=True))
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
if not self.request.session.test_cookie_worked():
|
||||
return HttpResponse(_("Please enable cookies and try again."))
|
||||
|
||||
user = form.get_user()
|
||||
|
||||
# user password expired
|
||||
if user.password_has_expired:
|
||||
data = {
|
||||
'username': user.username,
|
||||
'mfa': int(user.otp_enabled),
|
||||
'reason': LoginLog.REASON_PASSWORD_EXPIRED,
|
||||
'status': False
|
||||
}
|
||||
self.write_login_log(data)
|
||||
return self.render_to_response(self.get_context_data(password_expired=True))
|
||||
|
||||
set_tmp_user_to_cache(self.request, user)
|
||||
username = form.cleaned_data.get('username')
|
||||
ip = get_request_ip(self.request)
|
||||
# 登陆成功,清除缓存计数
|
||||
clean_failed_count(username, ip)
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def form_invalid(self, form):
|
||||
# write login failed log
|
||||
username = form.cleaned_data.get('username')
|
||||
exist = User.objects.filter(username=username).first()
|
||||
reason = LoginLog.REASON_PASSWORD if exist else LoginLog.REASON_NOT_EXIST
|
||||
data = {
|
||||
'username': username,
|
||||
'mfa': LoginLog.MFA_UNKNOWN,
|
||||
'reason': reason,
|
||||
'status': False
|
||||
}
|
||||
self.write_login_log(data)
|
||||
|
||||
# limit user login failed count
|
||||
ip = get_request_ip(self.request)
|
||||
increase_login_failed_count(username, ip)
|
||||
|
||||
# show captcha
|
||||
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
|
||||
old_form = form
|
||||
form = self.form_class_captcha(data=form.data)
|
||||
form._errors = old_form.errors
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_form_class(self):
|
||||
ip = get_request_ip(self.request)
|
||||
if cache.get(self.key_prefix_captcha.format(ip)):
|
||||
return self.form_class_captcha
|
||||
else:
|
||||
return self.form_class
|
||||
|
||||
def get_success_url(self):
|
||||
user = get_user_or_tmp_user(self.request)
|
||||
|
||||
if user.otp_enabled and user.otp_secret_key:
|
||||
# 1,2,mfa_setting & T
|
||||
return reverse('users:login-otp')
|
||||
elif user.otp_enabled and not user.otp_secret_key:
|
||||
# 1,2,mfa_setting & F
|
||||
return reverse('users:user-otp-enable-authentication')
|
||||
elif not user.otp_enabled:
|
||||
# 0 & T,F
|
||||
auth_login(self.request, user)
|
||||
data = {
|
||||
'username': self.request.user.username,
|
||||
'mfa': int(self.request.user.otp_enabled),
|
||||
'reason': LoginLog.REASON_NOTHING,
|
||||
'status': True
|
||||
}
|
||||
self.write_login_log(data)
|
||||
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'demo_mode': os.environ.get("DEMO_MODE"),
|
||||
'AUTH_OPENID': settings.AUTH_OPENID,
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def write_login_log(self, data):
|
||||
login_ip = get_request_ip(self.request)
|
||||
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
|
||||
tmp_data = {
|
||||
'ip': login_ip,
|
||||
'type': 'W',
|
||||
'user_agent': user_agent
|
||||
}
|
||||
data.update(tmp_data)
|
||||
write_login_log_async.delay(**data)
|
||||
|
||||
|
||||
class UserLoginOtpView(FormView):
|
||||
template_name = 'users/login_otp.html'
|
||||
form_class = forms.UserCheckOtpCodeForm
|
||||
redirect_field_name = 'next'
|
||||
|
||||
def form_valid(self, form):
|
||||
user = get_user_or_tmp_user(self.request)
|
||||
otp_code = form.cleaned_data.get('otp_code')
|
||||
otp_secret_key = user.otp_secret_key
|
||||
|
||||
if check_otp_code(otp_secret_key, otp_code):
|
||||
auth_login(self.request, user)
|
||||
data = {
|
||||
'username': self.request.user.username,
|
||||
'mfa': int(self.request.user.otp_enabled),
|
||||
'reason': LoginLog.REASON_NOTHING,
|
||||
'status': True
|
||||
}
|
||||
self.write_login_log(data)
|
||||
return redirect(self.get_success_url())
|
||||
else:
|
||||
data = {
|
||||
'username': user.username,
|
||||
'mfa': int(user.otp_enabled),
|
||||
'reason': LoginLog.REASON_MFA,
|
||||
'status': False
|
||||
}
|
||||
self.write_login_log(data)
|
||||
form.add_error('otp_code', _('MFA code invalid, or ntp sync server time'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
|
||||
|
||||
def write_login_log(self, data):
|
||||
login_ip = get_request_ip(self.request)
|
||||
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
|
||||
tmp_data = {
|
||||
'ip': login_ip,
|
||||
'type': 'W',
|
||||
'user_agent': user_agent
|
||||
}
|
||||
data.update(tmp_data)
|
||||
write_login_log_async.delay(**data)
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class UserLogoutView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
auth_logout(request)
|
||||
next_uri = request.COOKIES.get("next")
|
||||
if next_uri:
|
||||
return redirect(next_uri)
|
||||
response = super().get(request, *args, **kwargs)
|
||||
return response
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'title': _('Logout success'),
|
||||
'messages': _('Logout success, return login page'),
|
||||
'interval': 1,
|
||||
'redirect_url': reverse('users:login'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
class UserLoginView(RedirectView):
|
||||
urls = reverse_lazy('authentication:login')
|
||||
|
||||
|
||||
class UserForgotPasswordView(TemplateView):
|
||||
|
@ -386,8 +177,3 @@ class UserFirstLoginView(LoginRequiredMixin, SessionWizardView):
|
|||
form.fields["otp_level"].initial = self.request.user.otp_level
|
||||
|
||||
return form
|
||||
|
||||
|
||||
class LoginLogListView(ListView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
return redirect(reverse('audits:login-log-list'))
|
||||
|
|
|
@ -17,7 +17,7 @@ decorator==4.1.2
|
|||
Django==2.1.7
|
||||
django-auth-ldap==1.7.0
|
||||
django-bootstrap3==9.1.0
|
||||
django-celery-beat==1.1.1
|
||||
django-celery-beat==1.4.0
|
||||
django-filter==2.0.0
|
||||
django-formtools==2.1
|
||||
django-ranged-response==0.2.0
|
||||
|
@ -79,3 +79,4 @@ rest_condition==1.0.3
|
|||
python-ldap==3.1.0
|
||||
tencentcloud-sdk-python==3.0.40
|
||||
django-radius==1.3.3
|
||||
ipip-ipdb==1.2.1
|
||||
|
|
Loading…
Reference in New Issue