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',
|
'common.apps.CommonConfig',
|
||||||
'terminal.apps.TerminalConfig',
|
'terminal.apps.TerminalConfig',
|
||||||
'audits.apps.AuditsConfig',
|
'audits.apps.AuditsConfig',
|
||||||
|
'authentication.apps.AuthenticationConfig', # authentication
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'rest_framework_swagger',
|
'rest_framework_swagger',
|
||||||
'drf_yasg',
|
'drf_yasg',
|
||||||
|
@ -94,6 +95,7 @@ MIDDLEWARE = [
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
'authentication.openid.middleware.OpenIDAuthenticationMiddleware', # openid
|
||||||
'jumpserver.middleware.TimezoneMiddleware',
|
'jumpserver.middleware.TimezoneMiddleware',
|
||||||
'jumpserver.middleware.DemoMiddleware',
|
'jumpserver.middleware.DemoMiddleware',
|
||||||
'jumpserver.middleware.RequestMiddleware',
|
'jumpserver.middleware.RequestMiddleware',
|
||||||
|
@ -389,6 +391,24 @@ AUTH_LDAP_BACKEND = 'django_auth_ldap.backend.LDAPBackend'
|
||||||
if AUTH_LDAP:
|
if AUTH_LDAP:
|
||||||
AUTHENTICATION_BACKENDS.insert(0, AUTH_LDAP_BACKEND)
|
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 using redis as broker
|
||||||
CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % {
|
CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % {
|
||||||
'password': CONFIG.REDIS_PASSWORD if CONFIG.REDIS_PASSWORD else '',
|
'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('ops/', include('ops.urls.view_urls', namespace='ops')),
|
||||||
path('audits/', include('audits.urls.view_urls', namespace='audits')),
|
path('audits/', include('audits.urls.view_urls', namespace='audits')),
|
||||||
path('orgs/', include('orgs.urls.views_urls', namespace='orgs')),
|
path('orgs/', include('orgs.urls.views_urls', namespace='orgs')),
|
||||||
|
path('auth/', include('authentication.urls.view_urls'), name='auth'),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.XPACK_ENABLED:
|
if settings.XPACK_ENABLED:
|
||||||
|
|
Binary file not shown.
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Jumpserver 0.3.3\n"
|
"Project-Id-Version: Jumpserver 0.3.3\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: ibuler <ibuler@qq.com>\n"
|
"Last-Translator: ibuler <ibuler@qq.com>\n"
|
||||||
"Language-Team: Jumpserver team<ibuler@qq.com>\n"
|
"Language-Team: Jumpserver team<ibuler@qq.com>\n"
|
||||||
|
@ -115,7 +115,7 @@ msgid "Port"
|
||||||
msgstr "端口"
|
msgstr "端口"
|
||||||
|
|
||||||
#: assets/forms/domain.py:15 assets/forms/label.py:13
|
#: 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_detail.html:60
|
||||||
#: assets/templates/assets/domain_list.html:26
|
#: assets/templates/assets/domain_list.html:26
|
||||||
#: assets/templates/assets/label_list.html:16
|
#: 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
|
#: 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: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: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
|
#: users/views/user.py:354 users/views/user.py:404 users/views/user.py:439
|
||||||
msgid "Users"
|
msgid "Users"
|
||||||
|
@ -2438,7 +2438,7 @@ msgstr "任务列表"
|
||||||
msgid "Task run history"
|
msgid "Task run history"
|
||||||
msgstr "执行历史"
|
msgstr "执行历史"
|
||||||
|
|
||||||
#: orgs/mixins.py:78 orgs/models.py:24
|
#: orgs/mixins.py:77 orgs/models.py:24
|
||||||
msgid "Organization"
|
msgid "Organization"
|
||||||
msgstr "组织管理"
|
msgstr "组织管理"
|
||||||
|
|
||||||
|
@ -3092,11 +3092,11 @@ msgstr "登录频繁, 稍后重试"
|
||||||
msgid "Please carry seed value and conduct MFA secondary certification"
|
msgid "Please carry seed value and conduct MFA secondary certification"
|
||||||
msgstr "请携带seed值, 进行MFA二次认证"
|
msgstr "请携带seed值, 进行MFA二次认证"
|
||||||
|
|
||||||
#: users/api/auth.py:195
|
#: users/api/auth.py:196
|
||||||
msgid "Please verify the user name and password first"
|
msgid "Please verify the user name and password first"
|
||||||
msgstr "请先进行用户名和密码验证"
|
msgstr "请先进行用户名和密码验证"
|
||||||
|
|
||||||
#: users/api/auth.py:207
|
#: users/api/auth.py:208
|
||||||
msgid "MFA certification failed"
|
msgid "MFA certification failed"
|
||||||
msgstr "MFA认证失败"
|
msgstr "MFA认证失败"
|
||||||
|
|
||||||
|
@ -3448,7 +3448,7 @@ msgstr "获取更多信息"
|
||||||
|
|
||||||
#: users/templates/users/forgot_password.html:11
|
#: users/templates/users/forgot_password.html:11
|
||||||
#: users/templates/users/forgot_password.html:27
|
#: users/templates/users/forgot_password.html:27
|
||||||
#: users/templates/users/login.html:79
|
#: users/templates/users/login.html:81
|
||||||
msgid "Forgot password"
|
msgid "Forgot password"
|
||||||
msgstr "忘记密码"
|
msgstr "忘记密码"
|
||||||
|
|
||||||
|
@ -3497,6 +3497,14 @@ msgstr "改变世界,从一点点开始。"
|
||||||
msgid "Captcha invalid"
|
msgid "Captcha invalid"
|
||||||
msgstr "验证码错误"
|
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/login_otp.html:46
|
||||||
#: users/templates/users/user_detail.html:91
|
#: users/templates/users/user_detail.html:91
|
||||||
#: users/templates/users/user_profile.html:85
|
#: users/templates/users/user_profile.html:85
|
||||||
|
@ -3995,19 +4003,19 @@ msgstr ""
|
||||||
" </br>\n"
|
" </br>\n"
|
||||||
" "
|
" "
|
||||||
|
|
||||||
#: users/utils.py:148
|
#: users/utils.py:162
|
||||||
msgid "User not exist"
|
msgid "User not exist"
|
||||||
msgstr "用户不存在"
|
msgstr "用户不存在"
|
||||||
|
|
||||||
#: users/utils.py:150
|
#: users/utils.py:164
|
||||||
msgid "Disabled or expired"
|
msgid "Disabled or expired"
|
||||||
msgstr "禁用或失效"
|
msgstr "禁用或失效"
|
||||||
|
|
||||||
#: users/utils.py:163
|
#: users/utils.py:177
|
||||||
msgid "Password or SSH public key invalid"
|
msgid "Password or SSH public key invalid"
|
||||||
msgstr "密码或密钥不合法"
|
msgstr "密码或密钥不合法"
|
||||||
|
|
||||||
#: users/utils.py:286 users/utils.py:296
|
#: users/utils.py:300 users/utils.py:310
|
||||||
msgid "Bit"
|
msgid "Bit"
|
||||||
msgstr " 位"
|
msgstr " 位"
|
||||||
|
|
||||||
|
@ -4027,52 +4035,52 @@ msgstr "用户组授权资产"
|
||||||
msgid "Please enable cookies and try again."
|
msgid "Please enable cookies and try again."
|
||||||
msgstr "设置你的浏览器支持cookie"
|
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"
|
msgid "MFA code invalid, or ntp sync server time"
|
||||||
msgstr "MFA验证码不正确,或者服务器端时间不对"
|
msgstr "MFA验证码不正确,或者服务器端时间不对"
|
||||||
|
|
||||||
#: users/views/login.py:208
|
#: users/views/login.py:211
|
||||||
msgid "Logout success"
|
msgid "Logout success"
|
||||||
msgstr "退出登录成功"
|
msgstr "退出登录成功"
|
||||||
|
|
||||||
#: users/views/login.py:209
|
#: users/views/login.py:212
|
||||||
msgid "Logout success, return login page"
|
msgid "Logout success, return login page"
|
||||||
msgstr "退出登录成功,返回到登录页面"
|
msgstr "退出登录成功,返回到登录页面"
|
||||||
|
|
||||||
#: users/views/login.py:225
|
#: users/views/login.py:228
|
||||||
msgid "Email address invalid, please input again"
|
msgid "Email address invalid, please input again"
|
||||||
msgstr "邮箱地址错误,重新输入"
|
msgstr "邮箱地址错误,重新输入"
|
||||||
|
|
||||||
#: users/views/login.py:238
|
#: users/views/login.py:241
|
||||||
msgid "Send reset password message"
|
msgid "Send reset password message"
|
||||||
msgstr "发送重置密码邮件"
|
msgstr "发送重置密码邮件"
|
||||||
|
|
||||||
#: users/views/login.py:239
|
#: users/views/login.py:242
|
||||||
msgid "Send reset password mail success, login your mail box and follow it "
|
msgid "Send reset password mail success, login your mail box and follow it "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)"
|
"发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)"
|
||||||
|
|
||||||
#: users/views/login.py:252
|
#: users/views/login.py:255
|
||||||
msgid "Reset password success"
|
msgid "Reset password success"
|
||||||
msgstr "重置密码成功"
|
msgstr "重置密码成功"
|
||||||
|
|
||||||
#: users/views/login.py:253
|
#: users/views/login.py:256
|
||||||
msgid "Reset password success, return to login page"
|
msgid "Reset password success, return to login page"
|
||||||
msgstr "重置密码成功,返回到登录页面"
|
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"
|
msgid "Token invalid or expired"
|
||||||
msgstr "Token错误或失效"
|
msgstr "Token错误或失效"
|
||||||
|
|
||||||
#: users/views/login.py:283
|
#: users/views/login.py:286
|
||||||
msgid "Password not same"
|
msgid "Password not same"
|
||||||
msgstr "密码不一致"
|
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"
|
msgid "* Your password does not meet the requirements"
|
||||||
msgstr "* 您的密码不符合要求"
|
msgstr "* 您的密码不符合要求"
|
||||||
|
|
||||||
#: users/views/login.py:331
|
#: users/views/login.py:334
|
||||||
msgid "First login"
|
msgid "First login"
|
||||||
msgstr "首次登陆"
|
msgstr "首次登陆"
|
||||||
|
|
||||||
|
|
|
@ -40,9 +40,11 @@ class User(AbstractUser):
|
||||||
)
|
)
|
||||||
SOURCE_LOCAL = 'local'
|
SOURCE_LOCAL = 'local'
|
||||||
SOURCE_LDAP = 'ldap'
|
SOURCE_LDAP = 'ldap'
|
||||||
|
SOURCE_OPENID = 'openid'
|
||||||
SOURCE_CHOICES = (
|
SOURCE_CHOICES = (
|
||||||
(SOURCE_LOCAL, 'Local'),
|
(SOURCE_LOCAL, 'Local'),
|
||||||
(SOURCE_LDAP, 'LDAP/AD'),
|
(SOURCE_LDAP, 'LDAP/AD'),
|
||||||
|
(SOURCE_OPENID, 'OpenID'),
|
||||||
)
|
)
|
||||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||||
username = models.CharField(
|
username = models.CharField(
|
||||||
|
|
|
@ -75,9 +75,23 @@
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="text-muted text-center">
|
||||||
|
<div>
|
||||||
<a href="{% url 'users:forgot-password' %}">
|
<a href="{% url 'users:forgot-password' %}">
|
||||||
<small>{% trans 'Forgot password' %}?</small>
|
<small>{% trans 'Forgot password' %}?</small>
|
||||||
</a>
|
</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>
|
</form>
|
||||||
<p class="m-t">
|
<p class="m-t">
|
||||||
|
|
|
@ -132,6 +132,7 @@ class UserLoginView(FormView):
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = {
|
context = {
|
||||||
'demo_mode': os.environ.get("DEMO_MODE"),
|
'demo_mode': os.environ.get("DEMO_MODE"),
|
||||||
|
'AUTH_OPENID': settings.AUTH_OPENID,
|
||||||
}
|
}
|
||||||
kwargs.update(context)
|
kwargs.update(context)
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
@ -200,6 +201,9 @@ class UserLogoutView(TemplateView):
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
auth_logout(request)
|
auth_logout(request)
|
||||||
|
next_uri = request.COOKIES.get("next")
|
||||||
|
if next_uri:
|
||||||
|
return redirect(next_uri)
|
||||||
response = super().get(request, *args, **kwargs)
|
response = super().get(request, *args, **kwargs)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,14 @@ class Config:
|
||||||
REDIS_DB_CELERY = os.environ.get('REDIS_DB') or 3
|
REDIS_DB_CELERY = os.environ.get('REDIS_DB') or 3
|
||||||
REDIS_DB_CACHE = os.environ.get('REDIS_DB') or 4
|
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):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue