mirror of https://github.com/jumpserver/jumpserver
[Update] 支持 OpenID 认证 (#2008)
* [Update] core支持openid登录,coco还不支持 * [Update] coco支持openid登录 * [Update] 修改注释 * [Update] 修改 OpenID Auth Code Backend 用户认证失败返回None, 不是Anonymoususer * [Update] 修改OpenID Code用户认证异常捕获 * [Update] 修改OpenID Auth Middleware, check用户是否单点退出的异常捕获 * [Update] 修改config_example Auth OpenID 配置 * [Update] 登录页面添加 更多登录方式 * [Update] 重构OpenID认证架构 * [Update] 修改小细节 * [Update] OpenID用户认证成功后,更新用户来源 * [update] 添加OpenID用户登录成功日志pull/2013/head
parent
1fcb272ddc
commit
e09f3ca4fd
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuthenticationConfig(AppConfig):
|
||||
name = 'authentication'
|
||||
|
||||
def ready(self):
|
||||
from . import signals_handlers
|
||||
super().ready()
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.conf import settings
|
||||
from .models import Client
|
||||
|
||||
|
||||
def new_client():
|
||||
"""
|
||||
:return: authentication.models.Client
|
||||
"""
|
||||
return Client(
|
||||
server_url=settings.AUTH_OPENID_SERVER_URL,
|
||||
realm_name=settings.AUTH_OPENID_REALM_NAME,
|
||||
client_id=settings.AUTH_OPENID_CLIENT_ID,
|
||||
client_secret=settings.AUTH_OPENID_CLIENT_SECRET
|
||||
)
|
||||
|
||||
|
||||
client = new_client()
|
|
@ -0,0 +1,90 @@
|
|||
# coding:utf-8
|
||||
#
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.conf import settings
|
||||
|
||||
from . import client
|
||||
from common.utils import get_logger
|
||||
from authentication.openid.models import OIDT_ACCESS_TOKEN
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
BACKEND_OPENID_AUTH_CODE = \
|
||||
'authentication.openid.backends.OpenIDAuthorizationCodeBackend'
|
||||
|
||||
|
||||
class BaseOpenIDAuthorizationBackend(object):
|
||||
|
||||
@staticmethod
|
||||
def user_can_authenticate(user):
|
||||
"""
|
||||
Reject users with is_active=False. Custom user models that don't have
|
||||
that attribute are allowed.
|
||||
"""
|
||||
is_active = getattr(user, 'is_active', None)
|
||||
return is_active or is_active is None
|
||||
|
||||
def get_user(self, user_id):
|
||||
try:
|
||||
user = UserModel._default_manager.get(pk=user_id)
|
||||
except UserModel.DoesNotExist:
|
||||
return None
|
||||
|
||||
return user if self.user_can_authenticate(user) else None
|
||||
|
||||
|
||||
class OpenIDAuthorizationCodeBackend(BaseOpenIDAuthorizationBackend):
|
||||
|
||||
def authenticate(self, request, **kwargs):
|
||||
logger.info('1.openid code backend')
|
||||
|
||||
code = kwargs.get('code')
|
||||
redirect_uri = kwargs.get('redirect_uri')
|
||||
|
||||
if not code or not redirect_uri:
|
||||
return None
|
||||
|
||||
try:
|
||||
oidt_profile = client.update_or_create_from_code(
|
||||
code=code,
|
||||
redirect_uri=redirect_uri
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
else:
|
||||
# Check openid user single logout or not with access_token
|
||||
request.session[OIDT_ACCESS_TOKEN] = oidt_profile.access_token
|
||||
|
||||
user = oidt_profile.user
|
||||
|
||||
return user if self.user_can_authenticate(user) else None
|
||||
|
||||
|
||||
class OpenIDAuthorizationPasswordBackend(BaseOpenIDAuthorizationBackend):
|
||||
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
logger.info('2.openid password backend')
|
||||
|
||||
if not settings.AUTH_OPENID:
|
||||
return None
|
||||
|
||||
elif not username:
|
||||
return None
|
||||
|
||||
try:
|
||||
oidt_profile = client.update_or_create_from_password(
|
||||
username=username, password=password
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
else:
|
||||
user = oidt_profile.user
|
||||
return user if self.user_can_authenticate(user) else None
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# coding:utf-8
|
||||
#
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import logout
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.contrib.auth import BACKEND_SESSION_KEY
|
||||
|
||||
from . import client
|
||||
from common.utils import get_logger
|
||||
from .backends import BACKEND_OPENID_AUTH_CODE
|
||||
from authentication.openid.models import OIDT_ACCESS_TOKEN
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class OpenIDAuthenticationMiddleware(MiddlewareMixin):
|
||||
"""
|
||||
Check openid user single logout (with access_token)
|
||||
"""
|
||||
|
||||
def process_request(self, request):
|
||||
|
||||
# Don't need openid auth if AUTH_OPENID is False
|
||||
if not settings.AUTH_OPENID:
|
||||
return
|
||||
|
||||
# Don't need check single logout if user not authenticated
|
||||
if not request.user.is_authenticated:
|
||||
return
|
||||
|
||||
elif request.session[BACKEND_SESSION_KEY] != BACKEND_OPENID_AUTH_CODE:
|
||||
return
|
||||
|
||||
# Check openid user single logout or not with access_token
|
||||
try:
|
||||
client.openid_connect_client.userinfo(
|
||||
token=request.session.get(OIDT_ACCESS_TOKEN))
|
||||
|
||||
except Exception as e:
|
||||
logout(request)
|
||||
logger.error(e)
|
|
@ -0,0 +1,159 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.db import transaction
|
||||
from django.contrib.auth import get_user_model
|
||||
from keycloak.realm import KeycloakRealm
|
||||
from keycloak.keycloak_openid import KeycloakOpenID
|
||||
from ..signals import post_create_openid_user
|
||||
|
||||
OIDT_ACCESS_TOKEN = 'oidt_access_token'
|
||||
|
||||
|
||||
class OpenIDTokenProfile(object):
|
||||
|
||||
def __init__(self, user, access_token, refresh_token):
|
||||
"""
|
||||
:param user: User object
|
||||
:param access_token:
|
||||
:param refresh_token:
|
||||
"""
|
||||
self.user = user
|
||||
self.access_token = access_token
|
||||
self.refresh_token = refresh_token
|
||||
|
||||
def __str__(self):
|
||||
return "{}'s OpenID token profile".format(self.user.username)
|
||||
|
||||
|
||||
class Client(object):
|
||||
|
||||
def __init__(self, server_url, realm_name, client_id, client_secret):
|
||||
self.server_url = server_url
|
||||
self.realm_name = realm_name
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
self.realm = self.new_realm()
|
||||
self.openid_client = self.new_openid_client()
|
||||
self.openid_connect_client = self.new_openid_connect_client()
|
||||
|
||||
def new_realm(self):
|
||||
"""
|
||||
:param authentication.openid.models.Realm realm:
|
||||
:return keycloak.realm.Realm:
|
||||
"""
|
||||
return KeycloakRealm(
|
||||
server_url=self.server_url,
|
||||
realm_name=self.realm_name,
|
||||
headers={}
|
||||
)
|
||||
|
||||
def new_openid_connect_client(self):
|
||||
"""
|
||||
:rtype: keycloak.openid_connect.KeycloakOpenidConnect
|
||||
"""
|
||||
openid_connect = self.realm.open_id_connect(
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret
|
||||
)
|
||||
return openid_connect
|
||||
|
||||
def new_openid_client(self):
|
||||
"""
|
||||
:rtype: keycloak.keycloak_openid.KeycloakOpenID
|
||||
"""
|
||||
|
||||
return KeycloakOpenID(
|
||||
server_url='%sauth/' % self.server_url,
|
||||
realm_name=self.realm_name,
|
||||
client_id=self.client_id,
|
||||
client_secret_key=self.client_secret,
|
||||
)
|
||||
|
||||
def update_or_create_from_password(self, username, password):
|
||||
"""
|
||||
Update or create an user based on an authentication username and password.
|
||||
|
||||
:param str username: authentication username
|
||||
:param str password: authentication password
|
||||
:return: authentication.models.OpenIDTokenProfile
|
||||
"""
|
||||
token_response = self.openid_client.token(
|
||||
username=username, password=password
|
||||
)
|
||||
|
||||
return self._update_or_create(token_response=token_response)
|
||||
|
||||
def update_or_create_from_code(self, code, redirect_uri):
|
||||
"""
|
||||
Update or create an user based on an authentication code.
|
||||
Response as specified in:
|
||||
|
||||
https://tools.ietf.org/html/rfc6749#section-4.1.4
|
||||
|
||||
:param str code: authentication code
|
||||
:param str redirect_uri:
|
||||
:rtype: authentication.models.OpenIDTokenProfile
|
||||
"""
|
||||
|
||||
token_response = self.openid_connect_client.authorization_code(
|
||||
code=code, redirect_uri=redirect_uri)
|
||||
|
||||
return self._update_or_create(token_response=token_response)
|
||||
|
||||
def _update_or_create(self, token_response):
|
||||
"""
|
||||
Update or create an user based on a token response.
|
||||
|
||||
`token_response` contains the items returned by the OpenIDConnect Token API
|
||||
end-point:
|
||||
- id_token
|
||||
- access_token
|
||||
- expires_in
|
||||
- refresh_token
|
||||
- refresh_expires_in
|
||||
|
||||
:param dict token_response:
|
||||
:rtype: authentication.openid.models.OpenIDTokenProfile
|
||||
"""
|
||||
|
||||
userinfo = self.openid_connect_client.userinfo(
|
||||
token=token_response['access_token'])
|
||||
|
||||
with transaction.atomic():
|
||||
user, _ = get_user_model().objects.update_or_create(
|
||||
username=userinfo.get('preferred_username', ''),
|
||||
defaults={
|
||||
'email': userinfo.get('email', ''),
|
||||
'first_name': userinfo.get('given_name', ''),
|
||||
'last_name': userinfo.get('family_name', '')
|
||||
}
|
||||
)
|
||||
|
||||
oidt_profile = OpenIDTokenProfile(
|
||||
user=user,
|
||||
access_token=token_response['access_token'],
|
||||
refresh_token=token_response['refresh_token'],
|
||||
)
|
||||
|
||||
if user:
|
||||
post_create_openid_user.send(sender=user.__class__, user=user)
|
||||
|
||||
return oidt_profile
|
||||
|
||||
def __str__(self):
|
||||
return self.client_id
|
||||
|
||||
|
||||
class Nonce(object):
|
||||
"""
|
||||
The openid-login is stored in cache as a temporary object, recording the
|
||||
user's redirect_uri and next_pat
|
||||
"""
|
||||
|
||||
def __init__(self, redirect_uri, next_path):
|
||||
import uuid
|
||||
self.state = uuid.uuid4()
|
||||
self.redirect_uri = redirect_uri
|
||||
self.next_path = next_path
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import logging
|
||||
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.views.generic.base import RedirectView
|
||||
from django.contrib.auth import authenticate, login
|
||||
from django.http.response import (
|
||||
HttpResponseBadRequest,
|
||||
HttpResponseServerError,
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_base_site_url():
|
||||
return settings.BASE_SITE_URL
|
||||
|
||||
|
||||
class LoginView(RedirectView):
|
||||
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
nonce = Nonce(
|
||||
redirect_uri=get_base_site_url() + reverse(
|
||||
"authentication:openid-login-complete"),
|
||||
|
||||
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):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'error' in request.GET:
|
||||
return HttpResponseServerError(self.request.GET['error'])
|
||||
|
||||
if 'code' not in self.request.GET and 'state' not in self.request.GET:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
if self.request.GET['state'] != self.request.session['openid_state']:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
nonce = cache.get(self.request.GET['state'])
|
||||
|
||||
if not nonce:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
user = authenticate(
|
||||
request=self.request,
|
||||
code=self.request.GET['code'],
|
||||
redirect_uri=nonce.redirect_uri
|
||||
)
|
||||
|
||||
cache.delete(str(nonce.state))
|
||||
|
||||
if not user:
|
||||
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)
|
||||
|
||||
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,4 @@
|
|||
from django.dispatch import Signal
|
||||
|
||||
|
||||
post_create_openid_user = Signal(providing_args=('user',))
|
|
@ -0,0 +1,33 @@
|
|||
from django.http.request import QueryDict
|
||||
from django.contrib.auth.signals import user_logged_out
|
||||
from django.dispatch import receiver
|
||||
from django.conf import settings
|
||||
from .openid import client
|
||||
from .signals import post_create_openid_user
|
||||
|
||||
|
||||
@receiver(user_logged_out)
|
||||
def on_user_logged_out(sender, request, user, **kwargs):
|
||||
if not settings.AUTH_OPENID:
|
||||
return
|
||||
|
||||
query = QueryDict('', mutable=True)
|
||||
query.update({
|
||||
'redirect_uri': settings.BASE_SITE_URL
|
||||
})
|
||||
|
||||
openid_logout_url = "%s?%s" % (
|
||||
client.openid_connect_client.get_url(
|
||||
name='end_session_endpoint'),
|
||||
query.urlencode()
|
||||
)
|
||||
|
||||
request.COOKIES['next'] = openid_logout_url
|
||||
|
||||
|
||||
@receiver(post_create_openid_user)
|
||||
def on_post_create_openid_user(sender, user=None, **kwargs):
|
||||
if user and user.username != 'admin':
|
||||
user.source = user.SOURCE_OPENID
|
||||
user.save()
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
# coding:utf-8
|
||||
#
|
||||
|
||||
from django.urls import path
|
||||
from authentication.openid 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(),
|
||||
name='openid-login-complete'),
|
||||
|
||||
# other
|
||||
]
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -64,6 +64,7 @@ INSTALLED_APPS = [
|
|||
'common.apps.CommonConfig',
|
||||
'terminal.apps.TerminalConfig',
|
||||
'audits.apps.AuditsConfig',
|
||||
'authentication.apps.AuthenticationConfig', # authentication
|
||||
'rest_framework',
|
||||
'rest_framework_swagger',
|
||||
'drf_yasg',
|
||||
|
@ -94,6 +95,7 @@ MIDDLEWARE = [
|
|||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'authentication.openid.middleware.OpenIDAuthenticationMiddleware', # openid
|
||||
'jumpserver.middleware.TimezoneMiddleware',
|
||||
'jumpserver.middleware.DemoMiddleware',
|
||||
'jumpserver.middleware.RequestMiddleware',
|
||||
|
@ -389,6 +391,24 @@ AUTH_LDAP_BACKEND = 'django_auth_ldap.backend.LDAPBackend'
|
|||
if AUTH_LDAP:
|
||||
AUTHENTICATION_BACKENDS.insert(0, AUTH_LDAP_BACKEND)
|
||||
|
||||
# openid
|
||||
# Auth OpenID settings
|
||||
BASE_SITE_URL = CONFIG.BASE_SITE_URL
|
||||
AUTH_OPENID = CONFIG.AUTH_OPENID
|
||||
AUTH_OPENID_SERVER_URL = CONFIG.AUTH_OPENID_SERVER_URL
|
||||
AUTH_OPENID_REALM_NAME = CONFIG.AUTH_OPENID_REALM_NAME
|
||||
AUTH_OPENID_CLIENT_ID = CONFIG.AUTH_OPENID_CLIENT_ID
|
||||
AUTH_OPENID_CLIENT_SECRET = CONFIG.AUTH_OPENID_CLIENT_SECRET
|
||||
AUTH_OPENID_BACKENDS = [
|
||||
'authentication.openid.backends.OpenIDAuthorizationPasswordBackend',
|
||||
'authentication.openid.backends.OpenIDAuthorizationCodeBackend',
|
||||
]
|
||||
|
||||
if AUTH_OPENID:
|
||||
LOGIN_URL = reverse_lazy("authentication:openid-login")
|
||||
AUTHENTICATION_BACKENDS.insert(0, AUTH_OPENID_BACKENDS[0])
|
||||
AUTHENTICATION_BACKENDS.insert(0, AUTH_OPENID_BACKENDS[1])
|
||||
|
||||
# Celery using redis as broker
|
||||
CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % {
|
||||
'password': CONFIG.REDIS_PASSWORD if CONFIG.REDIS_PASSWORD else '',
|
||||
|
|
|
@ -75,6 +75,7 @@ app_view_patterns = [
|
|||
path('ops/', include('ops.urls.view_urls', namespace='ops')),
|
||||
path('audits/', include('audits.urls.view_urls', namespace='audits')),
|
||||
path('orgs/', include('orgs.urls.views_urls', namespace='orgs')),
|
||||
path('auth/', include('authentication.urls.view_urls'), name='auth'),
|
||||
]
|
||||
|
||||
if settings.XPACK_ENABLED:
|
||||
|
|
Binary file not shown.
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: Jumpserver 0.3.3\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-11-01 13:58+0800\n"
|
||||
"POT-Creation-Date: 2018-11-08 19:18+0800\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: ibuler <ibuler@qq.com>\n"
|
||||
"Language-Team: Jumpserver team<ibuler@qq.com>\n"
|
||||
|
@ -115,7 +115,7 @@ msgid "Port"
|
|||
msgstr "端口"
|
||||
|
||||
#: assets/forms/domain.py:15 assets/forms/label.py:13
|
||||
#: assets/models/asset.py:243 assets/templates/assets/admin_user_list.html:28
|
||||
#: assets/models/asset.py:253 assets/templates/assets/admin_user_list.html:28
|
||||
#: assets/templates/assets/domain_detail.html:60
|
||||
#: assets/templates/assets/domain_list.html:26
|
||||
#: assets/templates/assets/label_list.html:16
|
||||
|
@ -1758,7 +1758,7 @@ msgstr "改密日志"
|
|||
|
||||
#: audits/views.py:187 templates/_nav.html:10 users/views/group.py:28
|
||||
#: users/views/group.py:44 users/views/group.py:60 users/views/group.py:76
|
||||
#: users/views/group.py:92 users/views/login.py:331 users/views/user.py:68
|
||||
#: users/views/group.py:92 users/views/login.py:334 users/views/user.py:68
|
||||
#: users/views/user.py:83 users/views/user.py:111 users/views/user.py:193
|
||||
#: users/views/user.py:354 users/views/user.py:404 users/views/user.py:439
|
||||
msgid "Users"
|
||||
|
@ -2438,7 +2438,7 @@ msgstr "任务列表"
|
|||
msgid "Task run history"
|
||||
msgstr "执行历史"
|
||||
|
||||
#: orgs/mixins.py:78 orgs/models.py:24
|
||||
#: orgs/mixins.py:77 orgs/models.py:24
|
||||
msgid "Organization"
|
||||
msgstr "组织管理"
|
||||
|
||||
|
@ -3092,11 +3092,11 @@ msgstr "登录频繁, 稍后重试"
|
|||
msgid "Please carry seed value and conduct MFA secondary certification"
|
||||
msgstr "请携带seed值, 进行MFA二次认证"
|
||||
|
||||
#: users/api/auth.py:195
|
||||
#: users/api/auth.py:196
|
||||
msgid "Please verify the user name and password first"
|
||||
msgstr "请先进行用户名和密码验证"
|
||||
|
||||
#: users/api/auth.py:207
|
||||
#: users/api/auth.py:208
|
||||
msgid "MFA certification failed"
|
||||
msgstr "MFA认证失败"
|
||||
|
||||
|
@ -3448,7 +3448,7 @@ msgstr "获取更多信息"
|
|||
|
||||
#: users/templates/users/forgot_password.html:11
|
||||
#: users/templates/users/forgot_password.html:27
|
||||
#: users/templates/users/login.html:79
|
||||
#: users/templates/users/login.html:81
|
||||
msgid "Forgot password"
|
||||
msgstr "忘记密码"
|
||||
|
||||
|
@ -3497,6 +3497,14 @@ msgstr "改变世界,从一点点开始。"
|
|||
msgid "Captcha invalid"
|
||||
msgstr "验证码错误"
|
||||
|
||||
#: users/templates/users/login.html:87
|
||||
msgid "More login options"
|
||||
msgstr "更多登录方式"
|
||||
|
||||
#: users/templates/users/login.html:91
|
||||
msgid "Keycloak"
|
||||
msgstr ""
|
||||
|
||||
#: users/templates/users/login_otp.html:46
|
||||
#: users/templates/users/user_detail.html:91
|
||||
#: users/templates/users/user_profile.html:85
|
||||
|
@ -3995,19 +4003,19 @@ msgstr ""
|
|||
" </br>\n"
|
||||
" "
|
||||
|
||||
#: users/utils.py:148
|
||||
#: users/utils.py:162
|
||||
msgid "User not exist"
|
||||
msgstr "用户不存在"
|
||||
|
||||
#: users/utils.py:150
|
||||
#: users/utils.py:164
|
||||
msgid "Disabled or expired"
|
||||
msgstr "禁用或失效"
|
||||
|
||||
#: users/utils.py:163
|
||||
#: users/utils.py:177
|
||||
msgid "Password or SSH public key invalid"
|
||||
msgstr "密码或密钥不合法"
|
||||
|
||||
#: users/utils.py:286 users/utils.py:296
|
||||
#: users/utils.py:300 users/utils.py:310
|
||||
msgid "Bit"
|
||||
msgstr " 位"
|
||||
|
||||
|
@ -4027,52 +4035,52 @@ msgstr "用户组授权资产"
|
|||
msgid "Please enable cookies and try again."
|
||||
msgstr "设置你的浏览器支持cookie"
|
||||
|
||||
#: users/views/login.py:179 users/views/user.py:526 users/views/user.py:551
|
||||
#: users/views/login.py:180 users/views/user.py:526 users/views/user.py:551
|
||||
msgid "MFA code invalid, or ntp sync server time"
|
||||
msgstr "MFA验证码不正确,或者服务器端时间不对"
|
||||
|
||||
#: users/views/login.py:208
|
||||
#: users/views/login.py:211
|
||||
msgid "Logout success"
|
||||
msgstr "退出登录成功"
|
||||
|
||||
#: users/views/login.py:209
|
||||
#: users/views/login.py:212
|
||||
msgid "Logout success, return login page"
|
||||
msgstr "退出登录成功,返回到登录页面"
|
||||
|
||||
#: users/views/login.py:225
|
||||
#: users/views/login.py:228
|
||||
msgid "Email address invalid, please input again"
|
||||
msgstr "邮箱地址错误,重新输入"
|
||||
|
||||
#: users/views/login.py:238
|
||||
#: users/views/login.py:241
|
||||
msgid "Send reset password message"
|
||||
msgstr "发送重置密码邮件"
|
||||
|
||||
#: users/views/login.py:239
|
||||
#: users/views/login.py:242
|
||||
msgid "Send reset password mail success, login your mail box and follow it "
|
||||
msgstr ""
|
||||
"发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)"
|
||||
|
||||
#: users/views/login.py:252
|
||||
#: users/views/login.py:255
|
||||
msgid "Reset password success"
|
||||
msgstr "重置密码成功"
|
||||
|
||||
#: users/views/login.py:253
|
||||
#: users/views/login.py:256
|
||||
msgid "Reset password success, return to login page"
|
||||
msgstr "重置密码成功,返回到登录页面"
|
||||
|
||||
#: users/views/login.py:274 users/views/login.py:287
|
||||
#: users/views/login.py:277 users/views/login.py:290
|
||||
msgid "Token invalid or expired"
|
||||
msgstr "Token错误或失效"
|
||||
|
||||
#: users/views/login.py:283
|
||||
#: users/views/login.py:286
|
||||
msgid "Password not same"
|
||||
msgstr "密码不一致"
|
||||
|
||||
#: users/views/login.py:293 users/views/user.py:127 users/views/user.py:422
|
||||
#: users/views/login.py:296 users/views/user.py:127 users/views/user.py:422
|
||||
msgid "* Your password does not meet the requirements"
|
||||
msgstr "* 您的密码不符合要求"
|
||||
|
||||
#: users/views/login.py:331
|
||||
#: users/views/login.py:334
|
||||
msgid "First login"
|
||||
msgstr "首次登陆"
|
||||
|
||||
|
|
|
@ -40,9 +40,11 @@ class User(AbstractUser):
|
|||
)
|
||||
SOURCE_LOCAL = 'local'
|
||||
SOURCE_LDAP = 'ldap'
|
||||
SOURCE_OPENID = 'openid'
|
||||
SOURCE_CHOICES = (
|
||||
(SOURCE_LOCAL, 'Local'),
|
||||
(SOURCE_LDAP, 'LDAP/AD'),
|
||||
(SOURCE_OPENID, 'OpenID'),
|
||||
)
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
username = models.CharField(
|
||||
|
|
|
@ -75,9 +75,23 @@
|
|||
</p>
|
||||
{% endif %}
|
||||
|
||||
<a href="{% url 'users:forgot-password' %}">
|
||||
<small>{% trans 'Forgot password' %}?</small>
|
||||
</a>
|
||||
<div class="text-muted text-center">
|
||||
<div>
|
||||
<a href="{% url 'users:forgot-password' %}">
|
||||
<small>{% trans 'Forgot password' %}?</small>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if AUTH_OPENID %}
|
||||
<div class="hr-line-dashed"></div>
|
||||
<p class="text-muted text-center">{% trans "More login options" %}</p>
|
||||
<div>
|
||||
<button type="button" class="btn btn-default btn-sm btn-block" onclick="location.href='{% url 'authentication:openid-login' %}'">
|
||||
<i class="fa fa-openid"></i>
|
||||
{% trans 'Keycloak' %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</form>
|
||||
<p class="m-t">
|
||||
|
|
|
@ -132,6 +132,7 @@ class UserLoginView(FormView):
|
|||
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)
|
||||
|
@ -200,6 +201,9 @@ class UserLogoutView(TemplateView):
|
|||
|
||||
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
|
||||
|
||||
|
|
|
@ -54,6 +54,14 @@ class Config:
|
|||
REDIS_DB_CELERY = os.environ.get('REDIS_DB') or 3
|
||||
REDIS_DB_CACHE = os.environ.get('REDIS_DB') or 4
|
||||
|
||||
# Use OpenID authorization
|
||||
# BASE_SITE_URL = 'http://localhost:8080'
|
||||
# AUTH_OPENID = False # True or False
|
||||
# AUTH_OPENID_SERVER_URL = 'https://openid-auth-server.com/'
|
||||
# AUTH_OPENID_REALM_NAME = 'realm-name'
|
||||
# AUTH_OPENID_CLIENT_ID = 'client-id'
|
||||
# AUTH_OPENID_CLIENT_SECRET = 'client-secret'
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
|
Loading…
Reference in New Issue