mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-09-24 04:50:30 +00:00
merge v3
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import abc
|
||||
import os
|
||||
import json
|
||||
import base64
|
||||
@@ -16,12 +17,11 @@ from orgs.mixins.api import RootOrgViewMixin
|
||||
from perms.models import Action
|
||||
from terminal.models import EndpointRule
|
||||
from ..serializers import (
|
||||
ConnectionTokenSerializer, ConnectionTokenSecretSerializer, SuperConnectionTokenSerializer,
|
||||
ConnectionTokenDisplaySerializer,
|
||||
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
|
||||
SuperConnectionTokenSerializer, ConnectionTokenDisplaySerializer,
|
||||
)
|
||||
from ..models import ConnectionToken
|
||||
|
||||
|
||||
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
|
||||
|
||||
|
||||
@@ -34,9 +34,12 @@ class ConnectionTokenMixin:
|
||||
if not is_valid:
|
||||
raise PermissionDenied(error)
|
||||
|
||||
@staticmethod
|
||||
def get_request_resources(serializer):
|
||||
user = serializer.validated_data.get('user')
|
||||
@abc.abstractmethod
|
||||
def get_request_resource_user(self, serializer):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_request_resources(self, serializer):
|
||||
user = self.get_request_resource_user(serializer)
|
||||
asset = serializer.validated_data.get('asset')
|
||||
application = serializer.validated_data.get('application')
|
||||
system_user = serializer.validated_data.get('system_user')
|
||||
@@ -164,9 +167,8 @@ class ConnectionTokenMixin:
|
||||
rdp_options['remoteapplicationname:s'] = name
|
||||
else:
|
||||
name = '*'
|
||||
|
||||
filename = "{}-{}-jumpserver".format(token.user.username, name)
|
||||
filename = urllib.parse.quote(filename)
|
||||
prefix_name = f'{token.user.username}-{name}'
|
||||
filename = self.get_connect_filename(prefix_name)
|
||||
|
||||
content = ''
|
||||
for k, v in rdp_options.items():
|
||||
@@ -174,6 +176,15 @@ class ConnectionTokenMixin:
|
||||
|
||||
return filename, content
|
||||
|
||||
@staticmethod
|
||||
def get_connect_filename(prefix_name):
|
||||
prefix_name = prefix_name.replace('/', '_')
|
||||
prefix_name = prefix_name.replace('\\', '_')
|
||||
prefix_name = prefix_name.replace('.', '_')
|
||||
filename = f'{prefix_name}-jumpserver'
|
||||
filename = urllib.parse.quote(filename)
|
||||
return filename
|
||||
|
||||
def get_ssh_token(self, token: ConnectionToken):
|
||||
if token.asset:
|
||||
name = token.asset.name
|
||||
@@ -181,7 +192,8 @@ class ConnectionTokenMixin:
|
||||
name = token.application.name
|
||||
else:
|
||||
name = '*'
|
||||
filename = f'{token.user.username}-{name}-jumpserver'
|
||||
prefix_name = f'{token.user.username}-{name}'
|
||||
filename = self.get_connect_filename(prefix_name)
|
||||
|
||||
endpoint = self.get_smart_endpoint(
|
||||
protocol='ssh', asset=token.asset, application=token.application
|
||||
@@ -198,7 +210,12 @@ class ConnectionTokenMixin:
|
||||
|
||||
class ConnectionTokenViewSet(ConnectionTokenMixin, RootOrgViewMixin, JMSModelViewSet):
|
||||
filterset_fields = (
|
||||
<<<<<<< HEAD
|
||||
'type', 'user_display', 'asset_display'
|
||||
=======
|
||||
'type', 'user_display', 'system_user_display',
|
||||
'application_display', 'asset_display'
|
||||
>>>>>>> origin
|
||||
)
|
||||
search_fields = filterset_fields
|
||||
serializer_classes = {
|
||||
@@ -215,7 +232,20 @@ class ConnectionTokenViewSet(ConnectionTokenMixin, RootOrgViewMixin, JMSModelVie
|
||||
'get_rdp_file': 'authentication.add_connectiontoken',
|
||||
'get_client_protocol_url': 'authentication.add_connectiontoken',
|
||||
}
|
||||
queryset = ConnectionToken.objects.all()
|
||||
|
||||
def get_queryset(self):
|
||||
return ConnectionToken.objects.filter(user=self.request.user)
|
||||
|
||||
def get_request_resource_user(self, serializer):
|
||||
return self.request.user
|
||||
|
||||
def get_object(self):
|
||||
if self.request.user.is_service_account:
|
||||
# TODO: 组件获取 token 详情,将来放在 Super-connection-token API 中
|
||||
obj = get_object_or_404(ConnectionToken, pk=self.kwargs.get('pk'))
|
||||
else:
|
||||
obj = super(ConnectionTokenViewSet, self).get_object()
|
||||
return obj
|
||||
|
||||
def create_connection_token(self):
|
||||
data = self.request.query_params if self.request.method == 'GET' else self.request.data
|
||||
@@ -284,6 +314,9 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
|
||||
'renewal': 'authentication.add_superconnectiontoken'
|
||||
}
|
||||
|
||||
def get_request_resource_user(self, serializer):
|
||||
return serializer.validated_data.get('user')
|
||||
|
||||
@action(methods=['PATCH'], detail=False)
|
||||
def renewal(self, request, *args, **kwargs):
|
||||
from common.utils.timezone import as_current_tz
|
||||
@@ -299,4 +332,3 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
|
||||
'msg': f'Token is renewed, date expired: {date_expired}'
|
||||
}
|
||||
return Response(data=data, status=status.HTTP_200_OK)
|
||||
|
||||
|
@@ -6,6 +6,8 @@ from rest_framework.permissions import AllowAny
|
||||
|
||||
from common.utils import get_logger
|
||||
from .. import errors, mixins
|
||||
from django.contrib.auth import logout as auth_logout
|
||||
|
||||
|
||||
__all__ = ['TicketStatusApi']
|
||||
logger = get_logger(__name__)
|
||||
@@ -17,7 +19,15 @@ class TicketStatusApi(mixins.AuthMixin, APIView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
try:
|
||||
self.check_user_login_confirm()
|
||||
self.request.session['auth_third_party_done'] = 1
|
||||
return Response({"msg": "ok"})
|
||||
except errors.LoginConfirmOtherError as e:
|
||||
reason = e.msg
|
||||
username = e.username
|
||||
self.send_auth_signal(success=False, username=username, reason=reason)
|
||||
# 若为三方登录,此时应退出登录
|
||||
auth_logout(request)
|
||||
return Response(e.as_data(), status=200)
|
||||
except errors.NeedMoreInfoError as e:
|
||||
return Response(e.as_data(), status=200)
|
||||
|
||||
|
@@ -49,7 +49,7 @@ class JMSBaseAuthBackend:
|
||||
if not allow:
|
||||
info = 'User {} skip authentication backend {}, because it not in {}'
|
||||
info = info.format(username, backend_name, ','.join(allowed_backend_names))
|
||||
logger.debug(info)
|
||||
logger.info(info)
|
||||
return allow
|
||||
|
||||
|
||||
|
@@ -3,9 +3,10 @@
|
||||
from django.urls import path
|
||||
import django_cas_ng.views
|
||||
|
||||
from .views import CASLoginView
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', django_cas_ng.views.LoginView.as_view(), name='cas-login'),
|
||||
path('login/', CASLoginView.as_view(), name='cas-login'),
|
||||
path('logout/', django_cas_ng.views.LogoutView.as_view(), name='cas-logout'),
|
||||
path('callback/', django_cas_ng.views.CallbackView.as_view(), name='cas-proxy-callback'),
|
||||
]
|
||||
|
15
apps/authentication/backends/cas/views.py
Normal file
15
apps/authentication/backends/cas/views.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from django_cas_ng.views import LoginView
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpResponseRedirect
|
||||
|
||||
__all__ = ['LoginView']
|
||||
|
||||
|
||||
class CASLoginView(LoginView):
|
||||
def get(self, request):
|
||||
try:
|
||||
return super().get(request)
|
||||
except PermissionDenied:
|
||||
return HttpResponseRedirect('/')
|
||||
|
||||
|
4
apps/authentication/backends/oauth2/__init__.py
Normal file
4
apps/authentication/backends/oauth2/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .backends import *
|
157
apps/authentication/backends/oauth2/backends.py
Normal file
157
apps/authentication/backends/oauth2/backends.py
Normal file
@@ -0,0 +1,157 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import requests
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.http import urlencode
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
|
||||
from common.utils import get_logger
|
||||
from users.utils import construct_user_email
|
||||
from authentication.utils import build_absolute_uri
|
||||
from common.exceptions import JMSException
|
||||
|
||||
from .signals import (
|
||||
oauth2_create_or_update_user, oauth2_user_login_failed,
|
||||
oauth2_user_login_success
|
||||
)
|
||||
from ..base import JMSModelBackend
|
||||
|
||||
|
||||
__all__ = ['OAuth2Backend']
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class OAuth2Backend(JMSModelBackend):
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return settings.AUTH_OAUTH2
|
||||
|
||||
def get_or_create_user_from_userinfo(self, request, userinfo):
|
||||
log_prompt = "Get or Create user [OAuth2Backend]: {}"
|
||||
logger.debug(log_prompt.format('start'))
|
||||
|
||||
# Construct user attrs value
|
||||
user_attrs = {}
|
||||
for field, attr in settings.AUTH_OAUTH2_USER_ATTR_MAP.items():
|
||||
user_attrs[field] = userinfo.get(attr, '')
|
||||
|
||||
username = user_attrs.get('username')
|
||||
if not username:
|
||||
error_msg = 'username is missing'
|
||||
logger.error(log_prompt.format(error_msg))
|
||||
raise JMSException(error_msg)
|
||||
|
||||
email = user_attrs.get('email', '')
|
||||
email = construct_user_email(user_attrs.get('username'), email)
|
||||
user_attrs.update({'email': email})
|
||||
|
||||
logger.debug(log_prompt.format(user_attrs))
|
||||
user, created = get_user_model().objects.get_or_create(
|
||||
username=username, defaults=user_attrs
|
||||
)
|
||||
logger.debug(log_prompt.format("user: {}|created: {}".format(user, created)))
|
||||
logger.debug(log_prompt.format("Send signal => oauth2 create or update user"))
|
||||
oauth2_create_or_update_user.send(
|
||||
sender=self.__class__, request=request, user=user, created=created,
|
||||
attrs=user_attrs
|
||||
)
|
||||
return user, created
|
||||
|
||||
@staticmethod
|
||||
def get_response_data(response_data):
|
||||
if response_data.get('data') is not None:
|
||||
response_data = response_data['data']
|
||||
return response_data
|
||||
|
||||
@staticmethod
|
||||
def get_query_dict(response_data, query_dict):
|
||||
query_dict.update({
|
||||
'uid': response_data.get('uid', ''),
|
||||
'access_token': response_data.get('access_token', '')
|
||||
})
|
||||
return query_dict
|
||||
|
||||
def authenticate(self, request, code=None, **kwargs):
|
||||
log_prompt = "Process authenticate [OAuth2Backend]: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
if code is None:
|
||||
logger.error(log_prompt.format('code is missing'))
|
||||
return None
|
||||
|
||||
query_dict = {
|
||||
'client_id': settings.AUTH_OAUTH2_CLIENT_ID,
|
||||
'client_secret': settings.AUTH_OAUTH2_CLIENT_SECRET,
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': build_absolute_uri(
|
||||
request, path=reverse(settings.AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME)
|
||||
)
|
||||
}
|
||||
access_token_url = '{url}?{query}'.format(
|
||||
url=settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT, query=urlencode(query_dict)
|
||||
)
|
||||
token_method = settings.AUTH_OAUTH2_ACCESS_TOKEN_METHOD.lower()
|
||||
requests_func = getattr(requests, token_method, requests.get)
|
||||
logger.debug(log_prompt.format('Call the access token endpoint[method: %s]' % token_method))
|
||||
headers = {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
access_token_response = requests_func(access_token_url, headers=headers)
|
||||
try:
|
||||
access_token_response.raise_for_status()
|
||||
access_token_response_data = access_token_response.json()
|
||||
response_data = self.get_response_data(access_token_response_data)
|
||||
except Exception as e:
|
||||
error = "Json access token response error, access token response " \
|
||||
"content is: {}, error is: {}".format(access_token_response.content, str(e))
|
||||
logger.error(log_prompt.format(error))
|
||||
return None
|
||||
|
||||
query_dict = self.get_query_dict(response_data, query_dict)
|
||||
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'token {}'.format(response_data.get('access_token', ''))
|
||||
}
|
||||
|
||||
logger.debug(log_prompt.format('Get userinfo endpoint'))
|
||||
userinfo_url = '{url}?{query}'.format(
|
||||
url=settings.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT,
|
||||
query=urlencode(query_dict)
|
||||
)
|
||||
userinfo_response = requests.get(userinfo_url, headers=headers)
|
||||
try:
|
||||
userinfo_response.raise_for_status()
|
||||
userinfo_response_data = userinfo_response.json()
|
||||
if 'data' in userinfo_response_data:
|
||||
userinfo = userinfo_response_data['data']
|
||||
else:
|
||||
userinfo = userinfo_response_data
|
||||
except Exception as e:
|
||||
error = "Json userinfo response error, userinfo response " \
|
||||
"content is: {}, error is: {}".format(userinfo_response.content, str(e))
|
||||
logger.error(log_prompt.format(error))
|
||||
return None
|
||||
|
||||
try:
|
||||
logger.debug(log_prompt.format('Update or create oauth2 user'))
|
||||
user, created = self.get_or_create_user_from_userinfo(request, userinfo)
|
||||
except JMSException:
|
||||
return None
|
||||
|
||||
if self.user_can_authenticate(user):
|
||||
logger.debug(log_prompt.format('OAuth2 user login success'))
|
||||
logger.debug(log_prompt.format('Send signal => oauth2 user login success'))
|
||||
oauth2_user_login_success.send(sender=self.__class__, request=request, user=user)
|
||||
return user
|
||||
else:
|
||||
logger.debug(log_prompt.format('OAuth2 user login failed'))
|
||||
logger.debug(log_prompt.format('Send signal => oauth2 user login failed'))
|
||||
oauth2_user_login_failed.send(
|
||||
sender=self.__class__, request=request, username=user.username,
|
||||
reason=_('User invalid, disabled or expired')
|
||||
)
|
||||
return None
|
9
apps/authentication/backends/oauth2/signals.py
Normal file
9
apps/authentication/backends/oauth2/signals.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.dispatch import Signal
|
||||
|
||||
|
||||
oauth2_create_or_update_user = Signal(
|
||||
providing_args=['request', 'user', 'created', 'name', 'username', 'email']
|
||||
)
|
||||
oauth2_user_login_success = Signal(providing_args=['request', 'user'])
|
||||
oauth2_user_login_failed = Signal(providing_args=['request', 'username', 'reason'])
|
||||
|
11
apps/authentication/backends/oauth2/urls.py
Normal file
11
apps/authentication/backends/oauth2/urls.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', views.OAuth2AuthRequestView.as_view(), name='login'),
|
||||
path('callback/', views.OAuth2AuthCallbackView.as_view(), name='login-callback')
|
||||
]
|
58
apps/authentication/backends/oauth2/views.py
Normal file
58
apps/authentication/backends/oauth2/views.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from django.views import View
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import login
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
|
||||
from authentication.utils import build_absolute_uri
|
||||
from common.utils import get_logger
|
||||
from authentication.mixins import authenticate
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class OAuth2AuthRequestView(View):
|
||||
|
||||
def get(self, request):
|
||||
log_prompt = "Process OAuth2 GET requests: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
|
||||
query_dict = {
|
||||
'client_id': settings.AUTH_OAUTH2_CLIENT_ID, 'response_type': 'code',
|
||||
'scope': settings.AUTH_OAUTH2_SCOPE,
|
||||
'redirect_uri': build_absolute_uri(
|
||||
request, path=reverse(settings.AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME)
|
||||
)
|
||||
}
|
||||
|
||||
redirect_url = '{url}?{query}'.format(
|
||||
url=settings.AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT,
|
||||
query=urlencode(query_dict)
|
||||
)
|
||||
logger.debug(log_prompt.format('Redirect login url'))
|
||||
return HttpResponseRedirect(redirect_url)
|
||||
|
||||
|
||||
class OAuth2AuthCallbackView(View):
|
||||
http_method_names = ['get', ]
|
||||
|
||||
def get(self, request):
|
||||
""" Processes GET requests. """
|
||||
log_prompt = "Process GET requests [OAuth2AuthCallbackView]: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
callback_params = request.GET
|
||||
|
||||
if 'code' in callback_params:
|
||||
logger.debug(log_prompt.format('Process authenticate'))
|
||||
user = authenticate(code=callback_params['code'], request=request)
|
||||
if user and user.is_valid:
|
||||
logger.debug(log_prompt.format('Login: {}'.format(user)))
|
||||
login(self.request, user)
|
||||
logger.debug(log_prompt.format('Redirect'))
|
||||
return HttpResponseRedirect(
|
||||
settings.AUTH_OAUTH2_AUTHENTICATION_REDIRECT_URI
|
||||
)
|
||||
|
||||
logger.debug(log_prompt.format('Redirect'))
|
||||
return HttpResponseRedirect(settings.AUTH_OAUTH2_AUTHENTICATION_FAILURE_REDIRECT_URI)
|
@@ -9,6 +9,7 @@
|
||||
|
||||
import base64
|
||||
import requests
|
||||
|
||||
from rest_framework.exceptions import ParseError
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
@@ -18,10 +19,11 @@ from django.urls import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import get_logger
|
||||
from authentication.utils import build_absolute_uri_for_oidc
|
||||
from users.utils import construct_user_email
|
||||
|
||||
from ..base import JMSBaseAuthBackend
|
||||
from .utils import validate_and_return_id_token, build_absolute_uri
|
||||
from .utils import validate_and_return_id_token
|
||||
from .decorator import ssl_verification
|
||||
from .signals import (
|
||||
openid_create_or_update_user, openid_user_login_failed, openid_user_login_success
|
||||
@@ -127,7 +129,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
|
||||
token_payload = {
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': build_absolute_uri(
|
||||
'redirect_uri': build_absolute_uri_for_oidc(
|
||||
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
|
||||
)
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@
|
||||
|
||||
import datetime as dt
|
||||
from calendar import timegm
|
||||
from urllib.parse import urlparse, urljoin
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.utils.encoding import force_bytes, smart_bytes
|
||||
@@ -110,17 +110,3 @@ def _validate_claims(id_token, nonce=None, validate_nonce=True):
|
||||
raise SuspiciousOperation('Incorrect id_token: nonce')
|
||||
|
||||
logger.debug(log_prompt.format('End'))
|
||||
|
||||
|
||||
def build_absolute_uri(request, path=None):
|
||||
"""
|
||||
Build absolute redirect uri
|
||||
"""
|
||||
if path is None:
|
||||
path = '/'
|
||||
|
||||
if settings.BASE_SITE_URL:
|
||||
redirect_uri = urljoin(settings.BASE_SITE_URL, path)
|
||||
else:
|
||||
redirect_uri = request.build_absolute_uri(path)
|
||||
return redirect_uri
|
||||
|
@@ -20,7 +20,8 @@ from django.utils.crypto import get_random_string
|
||||
from django.utils.http import is_safe_url, urlencode
|
||||
from django.views.generic import View
|
||||
|
||||
from .utils import get_logger, build_absolute_uri
|
||||
from authentication.utils import build_absolute_uri_for_oidc
|
||||
from .utils import get_logger
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@@ -50,7 +51,7 @@ class OIDCAuthRequestView(View):
|
||||
'scope': settings.AUTH_OPENID_SCOPES,
|
||||
'response_type': 'code',
|
||||
'client_id': settings.AUTH_OPENID_CLIENT_ID,
|
||||
'redirect_uri': build_absolute_uri(
|
||||
'redirect_uri': build_absolute_uri_for_oidc(
|
||||
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
|
||||
)
|
||||
})
|
||||
@@ -216,7 +217,7 @@ class OIDCEndSessionView(View):
|
||||
""" Returns the end-session URL. """
|
||||
q = QueryDict(mutable=True)
|
||||
q[settings.AUTH_OPENID_PROVIDER_END_SESSION_REDIRECT_URI_PARAMETER] = \
|
||||
build_absolute_uri(self.request, path=settings.LOGOUT_REDIRECT_URL or '/')
|
||||
build_absolute_uri_for_oidc(self.request, path=settings.LOGOUT_REDIRECT_URL or '/')
|
||||
q[settings.AUTH_OPENID_PROVIDER_END_SESSION_ID_TOKEN_PARAMETER] = \
|
||||
self.request.session['oidc_auth_id_token']
|
||||
return '{}?{}'.format(settings.AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT, q.urlencode())
|
||||
|
@@ -39,7 +39,7 @@ class SAML2Backend(JMSModelBackend):
|
||||
return user, created
|
||||
|
||||
def authenticate(self, request, saml_user_data=None, **kwargs):
|
||||
log_prompt = "Process authenticate [SAML2AuthCodeBackend]: {}"
|
||||
log_prompt = "Process authenticate [SAML2Backend]: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
if saml_user_data is None:
|
||||
logger.error(log_prompt.format('saml_user_data is missing'))
|
||||
@@ -48,7 +48,7 @@ class SAML2Backend(JMSModelBackend):
|
||||
logger.debug(log_prompt.format('saml data, {}'.format(saml_user_data)))
|
||||
username = saml_user_data.get('username')
|
||||
if not username:
|
||||
logger.debug(log_prompt.format('username is missing'))
|
||||
logger.warning(log_prompt.format('username is missing'))
|
||||
return None
|
||||
|
||||
user, created = self.get_or_create_from_saml_data(request, **saml_user_data)
|
||||
|
@@ -12,12 +12,13 @@ class AuthFailedNeedLogMixin:
|
||||
username = ''
|
||||
request = None
|
||||
error = ''
|
||||
msg = ''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
post_auth_failed.send(
|
||||
sender=self.__class__, username=self.username,
|
||||
request=self.request, reason=self.error
|
||||
request=self.request, reason=self.msg
|
||||
)
|
||||
|
||||
|
||||
@@ -55,7 +56,8 @@ class BlockGlobalIpLoginError(AuthFailedError):
|
||||
error = 'block_global_ip_login'
|
||||
|
||||
def __init__(self, username, ip, **kwargs):
|
||||
self.msg = const.block_ip_login_msg.format(settings.SECURITY_LOGIN_IP_LIMIT_TIME)
|
||||
if not self.msg:
|
||||
self.msg = const.block_ip_login_msg.format(settings.SECURITY_LOGIN_IP_LIMIT_TIME)
|
||||
LoginIpBlockUtil(ip).set_block_if_need()
|
||||
super().__init__(username=username, ip=ip, **kwargs)
|
||||
|
||||
@@ -65,22 +67,21 @@ class CredentialError(
|
||||
BlockGlobalIpLoginError, AuthFailedError
|
||||
):
|
||||
def __init__(self, error, username, ip, request):
|
||||
super().__init__(error=error, username=username, ip=ip, request=request)
|
||||
util = LoginBlockUtil(username, ip)
|
||||
times_remainder = util.get_remainder_times()
|
||||
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
|
||||
|
||||
if times_remainder < 1:
|
||||
self.msg = const.block_user_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
|
||||
return
|
||||
|
||||
default_msg = const.invalid_login_msg.format(
|
||||
times_try=times_remainder, block_time=block_time
|
||||
)
|
||||
if error == const.reason_password_failed:
|
||||
self.msg = default_msg
|
||||
else:
|
||||
self.msg = const.reason_choices.get(error, default_msg)
|
||||
default_msg = const.invalid_login_msg.format(
|
||||
times_try=times_remainder, block_time=block_time
|
||||
)
|
||||
if error == const.reason_password_failed:
|
||||
self.msg = default_msg
|
||||
else:
|
||||
self.msg = const.reason_choices.get(error, default_msg)
|
||||
# 先处理 msg 在 super,记录日志时原因才准确
|
||||
super().__init__(error=error, username=username, ip=ip, request=request)
|
||||
|
||||
|
||||
class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError):
|
||||
@@ -138,18 +139,11 @@ class ACLError(AuthFailedNeedLogMixin, AuthFailedError):
|
||||
}
|
||||
|
||||
|
||||
class LoginIPNotAllowed(ACLError):
|
||||
class LoginACLIPAndTimePeriodNotAllowed(ACLError):
|
||||
def __init__(self, username, request, **kwargs):
|
||||
self.username = username
|
||||
self.request = request
|
||||
super().__init__(_("IP is not allowed"), **kwargs)
|
||||
|
||||
|
||||
class TimePeriodNotAllowed(ACLError):
|
||||
def __init__(self, username, request, **kwargs):
|
||||
self.username = username
|
||||
self.request = request
|
||||
super().__init__(_("Time Period is not allowed"), **kwargs)
|
||||
super().__init__(_("Current IP and Time period is not allowed"), **kwargs)
|
||||
|
||||
|
||||
class MFACodeRequiredError(AuthFailedError):
|
||||
|
@@ -14,23 +14,23 @@ class WeComCodeInvalid(JMSException):
|
||||
|
||||
|
||||
class WeComBindAlready(JMSException):
|
||||
default_code = 'wecom_bind_already'
|
||||
default_detail = 'WeCom already binded'
|
||||
default_code = 'wecom_not_bound'
|
||||
default_detail = _('WeCom is already bound')
|
||||
|
||||
|
||||
class WeComNotBound(JMSException):
|
||||
default_code = 'wecom_not_bound'
|
||||
default_detail = 'WeCom is not bound'
|
||||
default_detail = _('WeCom is not bound')
|
||||
|
||||
|
||||
class DingTalkNotBound(JMSException):
|
||||
default_code = 'dingtalk_not_bound'
|
||||
default_detail = 'DingTalk is not bound'
|
||||
default_detail = _('DingTalk is not bound')
|
||||
|
||||
|
||||
class FeiShuNotBound(JMSException):
|
||||
default_code = 'feishu_not_bound'
|
||||
default_detail = 'FeiShu is not bound'
|
||||
default_detail = _('FeiShu is not bound')
|
||||
|
||||
|
||||
class PasswordInvalid(JMSException):
|
||||
|
@@ -69,10 +69,16 @@ class LoginConfirmWaitError(LoginConfirmBaseError):
|
||||
class LoginConfirmOtherError(LoginConfirmBaseError):
|
||||
error = 'login_confirm_error'
|
||||
|
||||
def __init__(self, ticket_id, status):
|
||||
def __init__(self, ticket_id, status, username):
|
||||
self.username = username
|
||||
msg = const.login_confirm_error_msg.format(status)
|
||||
super().__init__(ticket_id=ticket_id, msg=msg)
|
||||
|
||||
def as_data(self):
|
||||
ret = super().as_data()
|
||||
ret['data']['username'] = self.username
|
||||
return ret
|
||||
|
||||
|
||||
class PasswordTooSimple(NeedRedirectError):
|
||||
default_code = 'passwd_too_simple'
|
||||
|
@@ -1,11 +1,16 @@
|
||||
import base64
|
||||
|
||||
from django.shortcuts import redirect, reverse
|
||||
from django.shortcuts import redirect, reverse, render
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.http import HttpResponse
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.contrib.auth import logout as auth_logout
|
||||
|
||||
from apps.authentication import mixins
|
||||
from common.utils import gen_key_pair
|
||||
from common.utils import get_request_ip
|
||||
from .signals import post_auth_failed
|
||||
|
||||
|
||||
class MFAMiddleware:
|
||||
@@ -13,6 +18,7 @@ class MFAMiddleware:
|
||||
这个 中间件 是用来全局拦截开启了 MFA 却没有认证的,如 OIDC, CAS,使用第三方库做的登录,直接 login 了,
|
||||
所以只能在 Middleware 中控制
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
@@ -42,6 +48,50 @@ class MFAMiddleware:
|
||||
return redirect(url)
|
||||
|
||||
|
||||
class ThirdPartyLoginMiddleware(mixins.AuthMixin):
|
||||
"""OpenID、CAS、SAML2登录规则设置验证"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
response = self.get_response(request)
|
||||
# 没有认证过,证明不是从 第三方 来的
|
||||
if request.user.is_anonymous:
|
||||
return response
|
||||
if not request.session.get('auth_third_party_required'):
|
||||
return response
|
||||
ip = get_request_ip(request)
|
||||
try:
|
||||
self.request = request
|
||||
self._check_login_acl(request.user, ip)
|
||||
except Exception as e:
|
||||
post_auth_failed.send(
|
||||
sender=self.__class__, username=request.user.username,
|
||||
request=self.request, reason=e.msg
|
||||
)
|
||||
auth_logout(request)
|
||||
context = {
|
||||
'title': _('Authentication failed'),
|
||||
'message': _('Authentication failed (before login check failed): {}').format(e),
|
||||
'interval': 10,
|
||||
'redirect_url': reverse('authentication:login'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
response = render(request, 'authentication/auth_fail_flash_message_standalone.html', context)
|
||||
else:
|
||||
if not self.request.session['auth_confirm_required']:
|
||||
return response
|
||||
guard_url = reverse('authentication:login-guard')
|
||||
args = request.META.get('QUERY_STRING', '')
|
||||
if args:
|
||||
guard_url = "%s?%s" % (guard_url, args)
|
||||
response = redirect(guard_url)
|
||||
finally:
|
||||
request.session.pop('auth_third_party_required', '')
|
||||
return response
|
||||
|
||||
|
||||
class SessionCookieMiddleware(MiddlewareMixin):
|
||||
|
||||
@staticmethod
|
||||
|
@@ -328,13 +328,59 @@ class AuthACLMixin:
|
||||
|
||||
def _check_login_acl(self, user, ip):
|
||||
# ACL 限制用户登录
|
||||
is_allowed, limit_type = LoginACL.allow_user_to_login(user, ip)
|
||||
if is_allowed:
|
||||
acl = LoginACL.match(user, ip)
|
||||
if not acl:
|
||||
return
|
||||
if limit_type == 'ip':
|
||||
raise errors.LoginIPNotAllowed(username=user.username, request=self.request)
|
||||
elif limit_type == 'time':
|
||||
raise errors.TimePeriodNotAllowed(username=user.username, request=self.request)
|
||||
|
||||
acl: LoginACL
|
||||
if acl.is_action(acl.ActionChoices.allow):
|
||||
return
|
||||
|
||||
if acl.is_action(acl.ActionChoices.reject):
|
||||
raise errors.LoginACLIPAndTimePeriodNotAllowed(user.username, request=self.request)
|
||||
|
||||
if acl.is_action(acl.ActionChoices.confirm):
|
||||
self.request.session['auth_confirm_required'] = '1'
|
||||
self.request.session['auth_acl_id'] = str(acl.id)
|
||||
return
|
||||
|
||||
def check_user_login_confirm_if_need(self, user):
|
||||
if not self.request.session.get("auth_confirm_required"):
|
||||
return
|
||||
acl_id = self.request.session.get('auth_acl_id')
|
||||
logger.debug('Login confirm acl id: {}'.format(acl_id))
|
||||
if not acl_id:
|
||||
return
|
||||
acl = LoginACL.filter_acl(user).filter(id=acl_id).first()
|
||||
if not acl:
|
||||
return
|
||||
if not acl.is_action(acl.ActionChoices.confirm):
|
||||
return
|
||||
self.get_ticket_or_create(acl)
|
||||
self.check_user_login_confirm()
|
||||
|
||||
def get_ticket_or_create(self, acl):
|
||||
ticket = self.get_ticket()
|
||||
if not ticket or ticket.is_state(ticket.State.closed):
|
||||
ticket = acl.create_confirm_ticket(self.request)
|
||||
self.request.session['auth_ticket_id'] = str(ticket.id)
|
||||
return ticket
|
||||
|
||||
def check_user_login_confirm(self):
|
||||
ticket = self.get_ticket()
|
||||
if not ticket:
|
||||
raise errors.LoginConfirmOtherError('', "Not found")
|
||||
elif ticket.is_state(ticket.State.approved):
|
||||
self.request.session["auth_confirm_required"] = ''
|
||||
return
|
||||
elif ticket.is_status(ticket.Status.open):
|
||||
raise errors.LoginConfirmWaitError(ticket.id)
|
||||
else:
|
||||
# rejected, closed
|
||||
ticket_id = ticket.id
|
||||
status = ticket.get_state_display()
|
||||
username = ticket.applicant.username
|
||||
raise errors.LoginConfirmOtherError(ticket_id, status, username)
|
||||
|
||||
def get_ticket(self):
|
||||
from tickets.models import ApplyLoginTicket
|
||||
@@ -346,44 +392,6 @@ class AuthACLMixin:
|
||||
ticket = ApplyLoginTicket.all().filter(id=ticket_id).first()
|
||||
return ticket
|
||||
|
||||
def get_ticket_or_create(self, confirm_setting):
|
||||
ticket = self.get_ticket()
|
||||
if not ticket or ticket.is_status(ticket.Status.closed):
|
||||
ticket = confirm_setting.create_confirm_ticket(self.request)
|
||||
self.request.session['auth_ticket_id'] = str(ticket.id)
|
||||
return ticket
|
||||
|
||||
def check_user_login_confirm(self):
|
||||
ticket = self.get_ticket()
|
||||
if not ticket:
|
||||
raise errors.LoginConfirmOtherError('', "Not found")
|
||||
|
||||
if ticket.is_status(ticket.Status.open):
|
||||
raise errors.LoginConfirmWaitError(ticket.id)
|
||||
elif ticket.is_state(ticket.State.approved):
|
||||
self.request.session["auth_confirm"] = "1"
|
||||
return
|
||||
elif ticket.is_state(ticket.State.rejected):
|
||||
raise errors.LoginConfirmOtherError(
|
||||
ticket.id, ticket.get_state_display()
|
||||
)
|
||||
elif ticket.is_state(ticket.State.closed):
|
||||
raise errors.LoginConfirmOtherError(
|
||||
ticket.id, ticket.get_state_display()
|
||||
)
|
||||
else:
|
||||
raise errors.LoginConfirmOtherError(
|
||||
ticket.id, ticket.get_status_display()
|
||||
)
|
||||
|
||||
def check_user_login_confirm_if_need(self, user):
|
||||
ip = self.get_request_ip()
|
||||
is_allowed, confirm_setting = LoginACL.allow_user_confirm_if_need(user, ip)
|
||||
if self.request.session.get('auth_confirm') or not is_allowed:
|
||||
return
|
||||
self.get_ticket_or_create(confirm_setting)
|
||||
self.check_user_login_confirm()
|
||||
|
||||
|
||||
class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPostCheckMixin):
|
||||
request = None
|
||||
@@ -482,7 +490,9 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
|
||||
return self.check_user_auth(valid_data)
|
||||
|
||||
def clear_auth_mark(self):
|
||||
keys = ['auth_password', 'user_id', 'auth_confirm', 'auth_ticket_id']
|
||||
keys = [
|
||||
'auth_password', 'user_id', 'auth_confirm_required', 'auth_ticket_id', 'auth_acl_id'
|
||||
]
|
||||
for k in keys:
|
||||
self.request.session.pop(k, '')
|
||||
|
||||
|
@@ -216,6 +216,13 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
|
||||
return {}
|
||||
return self.application.get_rdp_remote_app_setting()
|
||||
|
||||
@lazyproperty
|
||||
def asset_or_remote_app_asset(self):
|
||||
if self.asset:
|
||||
return self.asset
|
||||
if self.application and self.application.category_remote_app:
|
||||
return self.application.get_remote_app_asset()
|
||||
|
||||
@lazyproperty
|
||||
def cmd_filter_rules(self):
|
||||
from assets.models import CommandFilterRule
|
||||
|
@@ -25,9 +25,8 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
|
||||
model = ConnectionToken
|
||||
fields_mini = ['id', 'type']
|
||||
fields_small = fields_mini + [
|
||||
'secret', 'date_expired',
|
||||
'date_created', 'date_updated', 'created_by', 'updated_by',
|
||||
'org_id', 'org_name',
|
||||
'secret', 'date_expired', 'date_created', 'date_updated',
|
||||
'created_by', 'updated_by', 'org_id', 'org_name',
|
||||
]
|
||||
fields_fk = [
|
||||
'user', 'system_user', 'asset', 'application',
|
||||
@@ -35,8 +34,8 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
|
||||
read_only_fields = [
|
||||
# 普通 Token 不支持指定 user
|
||||
'user', 'is_valid', 'expire_time',
|
||||
'type_display', 'user_display', 'system_user_display', 'asset_display',
|
||||
'application_display',
|
||||
'type_display', 'user_display', 'system_user_display',
|
||||
'asset_display', 'application_display',
|
||||
]
|
||||
fields = fields_small + fields_fk + read_only_fields
|
||||
|
||||
@@ -59,7 +58,7 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
|
||||
system_user = attrs.get('system_user') or ''
|
||||
asset = attrs.get('asset') or ''
|
||||
application = attrs.get('application') or ''
|
||||
secret = attrs.get('secret') or random_string(64)
|
||||
secret = attrs.get('secret') or random_string(16)
|
||||
date_expired = attrs.get('date_expired') or ConnectionToken.get_default_date_expired()
|
||||
|
||||
if isinstance(asset, Asset):
|
||||
@@ -97,8 +96,8 @@ class SuperConnectionTokenSerializer(ConnectionTokenSerializer):
|
||||
|
||||
class Meta(ConnectionTokenSerializer.Meta):
|
||||
read_only_fields = [
|
||||
'validity',
|
||||
'user_display', 'system_user_display', 'asset_display', 'application_display',
|
||||
'validity', 'user_display', 'system_user_display',
|
||||
'asset_display', 'application_display',
|
||||
]
|
||||
|
||||
def get_user(self, attrs):
|
||||
@@ -154,7 +153,12 @@ class ConnectionTokenCmdFilterRuleSerializer(serializers.ModelSerializer):
|
||||
|
||||
class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
|
||||
user = ConnectionTokenUserSerializer(read_only=True)
|
||||
<<<<<<< HEAD
|
||||
asset = ConnectionTokenAssetSerializer(read_only=True)
|
||||
=======
|
||||
asset = ConnectionTokenAssetSerializer(read_only=True, source='asset_or_remote_app_asset')
|
||||
application = ConnectionTokenApplicationSerializer(read_only=True)
|
||||
>>>>>>> origin
|
||||
remote_app = ConnectionTokenRemoteAppSerializer(read_only=True)
|
||||
account = serializers.CharField(read_only=True)
|
||||
gateway = ConnectionTokenGatewaySerializer(read_only=True)
|
||||
|
@@ -6,12 +6,16 @@ from django.core.cache import cache
|
||||
from django.dispatch import receiver
|
||||
from django_cas_ng.signals import cas_user_authenticated
|
||||
|
||||
from apps.jumpserver.settings.auth import AUTHENTICATION_BACKENDS_THIRD_PARTY
|
||||
from authentication.backends.oidc.signals import (
|
||||
openid_user_login_failed, openid_user_login_success
|
||||
)
|
||||
from authentication.backends.saml2.signals import (
|
||||
saml2_user_authenticated, saml2_user_authentication_failed
|
||||
)
|
||||
from authentication.backends.oauth2.signals import (
|
||||
oauth2_user_login_failed, oauth2_user_login_success
|
||||
)
|
||||
from .signals import post_auth_success, post_auth_failed
|
||||
|
||||
|
||||
@@ -25,7 +29,8 @@ def on_user_auth_login_success(sender, user, request, **kwargs):
|
||||
and user.mfa_enabled \
|
||||
and not request.session.get('auth_mfa'):
|
||||
request.session['auth_mfa_required'] = 1
|
||||
|
||||
if not request.session.get("auth_third_party_done") and request.session.get('auth_backend') in AUTHENTICATION_BACKENDS_THIRD_PARTY:
|
||||
request.session['auth_third_party_required'] = 1
|
||||
# 单点登录,超过了自动退出
|
||||
if settings.USER_LOGIN_SINGLE_MACHINE_ENABLED:
|
||||
lock_key = 'single_machine_login_' + str(user.id)
|
||||
@@ -67,3 +72,15 @@ def on_saml2_user_login_success(sender, request, user, **kwargs):
|
||||
def on_saml2_user_login_failed(sender, request, username, reason, **kwargs):
|
||||
request.session['auth_backend'] = settings.AUTH_BACKEND_SAML2
|
||||
post_auth_failed.send(sender, username=username, request=request, reason=reason)
|
||||
|
||||
|
||||
@receiver(oauth2_user_login_success)
|
||||
def on_oauth2_user_login_success(sender, request, user, **kwargs):
|
||||
request.session['auth_backend'] = settings.AUTH_BACKEND_OAUTH2
|
||||
post_auth_success.send(sender, user=user, request=request)
|
||||
|
||||
|
||||
@receiver(oauth2_user_login_failed)
|
||||
def on_oauth2_user_login_failed(sender, username, request, reason, **kwargs):
|
||||
request.session['auth_backend'] = settings.AUTH_BACKEND_OAUTH2
|
||||
post_auth_failed.send(sender, username=username, request=request, reason=reason)
|
||||
|
@@ -0,0 +1,70 @@
|
||||
{% extends '_base_only_content.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% block html_title %} {{ title }} {% endblock %}
|
||||
{% block title %} {{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.alert.alert-msg {
|
||||
background: #F5F5F7;
|
||||
}
|
||||
</style>
|
||||
<div>
|
||||
<p>
|
||||
<div class="alert alert-msg" id="messages">
|
||||
{% if error %}
|
||||
{{ error }}
|
||||
{% else %}
|
||||
{{ message|safe }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
{% if has_cancel %}
|
||||
<div class="col-sm-3">
|
||||
<a href="{{ cancel_url }}" class="btn btn-default block full-width m-b">
|
||||
{% trans 'Cancel' %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-sm-3">
|
||||
<a href="{{ redirect_url }}" class="btn btn-primary block full-width m-b">
|
||||
{% if confirm_button %}
|
||||
{{ confirm_button }}
|
||||
{% else %}
|
||||
{% trans 'Confirm' %}
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
var message = ''
|
||||
var time = '{{ interval }}'
|
||||
{% if error %}
|
||||
message = '{{ error }}'
|
||||
{% else %}
|
||||
message = '{{ message|safe }}'
|
||||
{% endif %}
|
||||
|
||||
function redirect_page() {
|
||||
if (time >= 0) {
|
||||
var msg = message + ', <b>' + time + '</b> ...';
|
||||
$('#messages').html(msg);
|
||||
time--;
|
||||
setTimeout(redirect_page, 1000);
|
||||
} else {
|
||||
window.location.href = "{{ redirect_url }}";
|
||||
}
|
||||
}
|
||||
{% if auto_redirect %}
|
||||
window.onload = redirect_page;
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@@ -79,6 +79,9 @@ function doRequestAuth() {
|
||||
requestApi({
|
||||
url: url,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-JMS-LOGIN-TYPE": "W"
|
||||
},
|
||||
success: function (data) {
|
||||
if (!data.error && data.msg === 'ok') {
|
||||
window.onbeforeunload = function(){};
|
||||
@@ -98,7 +101,7 @@ function doRequestAuth() {
|
||||
},
|
||||
error: function (text, data) {
|
||||
},
|
||||
flash_message: false
|
||||
flash_message: false, // 是否显示flash消息
|
||||
})
|
||||
}
|
||||
function initClipboard() {
|
||||
|
@@ -56,9 +56,11 @@ urlpatterns = [
|
||||
path('profile/otp/disable/', users_view.UserOtpDisableView.as_view(),
|
||||
name='user-otp-disable'),
|
||||
|
||||
# openid
|
||||
# other authentication protocol
|
||||
path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')),
|
||||
path('openid/', include(('authentication.backends.oidc.urls', 'authentication'), namespace='openid')),
|
||||
path('saml2/', include(('authentication.backends.saml2.urls', 'authentication'), namespace='saml2')),
|
||||
path('oauth2/', include(('authentication.backends.oauth2.urls', 'authentication'), namespace='oauth2')),
|
||||
|
||||
path('captcha/', include('captcha.urls')),
|
||||
]
|
||||
|
@@ -1,7 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import ipaddress
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import validate_ip, get_ip_city, get_request_ip
|
||||
from common.utils import get_logger
|
||||
@@ -22,10 +25,34 @@ def check_different_city_login_if_need(user, request):
|
||||
else:
|
||||
city = get_ip_city(ip) or DEFAULT_CITY
|
||||
|
||||
city_white = ['LAN', ]
|
||||
if city not in city_white:
|
||||
city_white = [_('LAN'), 'LAN']
|
||||
is_private = ipaddress.ip_address(ip).is_private
|
||||
if not is_private:
|
||||
last_user_login = UserLoginLog.objects.exclude(city__in=city_white) \
|
||||
.filter(username=user.username, status=True).first()
|
||||
|
||||
if last_user_login and last_user_login.city != city:
|
||||
DifferentCityLoginMessage(user, ip, city).publish_async()
|
||||
|
||||
|
||||
def build_absolute_uri(request, path=None):
|
||||
""" Build absolute redirect """
|
||||
if path is None:
|
||||
path = '/'
|
||||
site_url = urlparse(settings.SITE_URL)
|
||||
scheme = site_url.scheme or request.scheme
|
||||
host = request.get_host()
|
||||
url = f'{scheme}://{host}'
|
||||
redirect_uri = urljoin(url, path)
|
||||
return redirect_uri
|
||||
|
||||
|
||||
def build_absolute_uri_for_oidc(request, path=None):
|
||||
""" Build absolute redirect uri for OIDC """
|
||||
if path is None:
|
||||
path = '/'
|
||||
if settings.BASE_SITE_URL:
|
||||
# OIDC 专用配置项
|
||||
redirect_uri = urljoin(settings.BASE_SITE_URL, path)
|
||||
return redirect_uri
|
||||
return build_absolute_uri(request, path=path)
|
||||
|
@@ -21,7 +21,7 @@ from django.conf import settings
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib.auth import BACKEND_SESSION_KEY
|
||||
|
||||
from common.utils import FlashMessageUtil
|
||||
from common.utils import FlashMessageUtil, static_or_direct
|
||||
from users.utils import (
|
||||
redirect_user_first_login_or_index
|
||||
)
|
||||
@@ -39,8 +39,7 @@ class UserLoginContextMixin:
|
||||
get_user_mfa_context: Callable
|
||||
request: HttpRequest
|
||||
|
||||
@staticmethod
|
||||
def get_support_auth_methods():
|
||||
def get_support_auth_methods(self):
|
||||
auth_methods = [
|
||||
{
|
||||
'name': 'OpenID',
|
||||
@@ -63,6 +62,13 @@ class UserLoginContextMixin:
|
||||
'logo': static('img/login_saml2_logo.png'),
|
||||
'auto_redirect': True
|
||||
},
|
||||
{
|
||||
'name': settings.AUTH_OAUTH2_PROVIDER,
|
||||
'enabled': settings.AUTH_OAUTH2,
|
||||
'url': reverse('authentication:oauth2:login'),
|
||||
'logo': static_or_direct(settings.AUTH_OAUTH2_LOGO_PATH),
|
||||
'auto_redirect': True
|
||||
},
|
||||
{
|
||||
'name': _('WeCom'),
|
||||
'enabled': settings.AUTH_WECOM,
|
||||
|
Reference in New Issue
Block a user