From 61407331bcc19ac5310d200dfc69f8690cb9d18e Mon Sep 17 00:00:00 2001 From: ibuler Date: Mon, 20 Apr 2020 10:37:07 +0800 Subject: [PATCH 01/30] =?UTF-8?q?[Bugfix]=20=E4=BF=AE=E5=A4=8Dotp=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E6=97=B6=E5=AF=BC=E8=87=B4=E7=9A=84500?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/views/login.py | 2 +- apps/authentication/views/mfa.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index c51ccbfc6..e2332cae5 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -133,7 +133,7 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView): user = self.check_user_auth_if_need() self.check_user_mfa_if_need(user) self.check_user_login_confirm_if_need(user) - except errors.CredentialError: + except (errors.CredentialError, errors.SessionEmptyError): return self.format_redirect_url(self.login_url) except errors.MFARequiredError: return self.format_redirect_url(self.login_otp_url) diff --git a/apps/authentication/views/mfa.py b/apps/authentication/views/mfa.py index 57d6751da..2e4b93aff 100644 --- a/apps/authentication/views/mfa.py +++ b/apps/authentication/views/mfa.py @@ -22,4 +22,6 @@ class UserLoginOtpView(mixins.AuthMixin, FormView): except errors.MFAFailedError as e: form.add_error('otp_code', e.msg) return super().form_invalid(form) + except: + return redirect_to_guard_view() From 91c994924ffbaed23c4a882f31157cf697243e13 Mon Sep 17 00:00:00 2001 From: ibuler Date: Mon, 20 Apr 2020 10:44:45 +0800 Subject: [PATCH 02/30] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9=E6=8A=A5?= =?UTF-8?q?=E9=94=99=E6=96=87=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/errors.py | 3 +++ apps/authentication/views/mfa.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py index 183e69288..23323b15a 100644 --- a/apps/authentication/errors.py +++ b/apps/authentication/errors.py @@ -93,6 +93,9 @@ class AuthFailedError(Exception): 'msg': self.msg, } + def __str__(self): + return str(self.msg) + class CredentialError(AuthFailedNeedLogMixin, AuthFailedNeedBlockMixin, AuthFailedError): def __init__(self, error, username, ip, request): diff --git a/apps/authentication/views/mfa.py b/apps/authentication/views/mfa.py index 2e4b93aff..bedbf9bcf 100644 --- a/apps/authentication/views/mfa.py +++ b/apps/authentication/views/mfa.py @@ -6,6 +6,9 @@ from django.views.generic.edit import FormView from .. import forms, errors, mixins from .utils import redirect_to_guard_view +from common.utils import get_logger + +logger = get_logger(__name__) __all__ = ['UserLoginOtpView'] @@ -22,6 +25,7 @@ class UserLoginOtpView(mixins.AuthMixin, FormView): except errors.MFAFailedError as e: form.add_error('otp_code', e.msg) return super().form_invalid(form) - except: + except Exception as e: + logger.error(e) return redirect_to_guard_view() From c8c6ba1c194d443cdc7d6f9b1363fcc99a55f314 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 21 Apr 2020 17:37:29 +0800 Subject: [PATCH 03/30] [Update] add terminal model fileds --- apps/terminal/serializers/terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/terminal/serializers/terminal.py b/apps/terminal/serializers/terminal.py index 1df4b40e6..c7a91d009 100644 --- a/apps/terminal/serializers/terminal.py +++ b/apps/terminal/serializers/terminal.py @@ -16,7 +16,7 @@ class TerminalSerializer(serializers.ModelSerializer): fields = [ 'id', 'name', 'remote_addr', 'http_port', 'ssh_port', 'comment', 'is_accepted', "is_active", 'session_online', - 'is_alive' + 'is_alive', 'date_created', 'command_storage', 'replay_storage' ] @staticmethod From 272701a8fdca20443d596fa9bf9b49961d742c37 Mon Sep 17 00:00:00 2001 From: BaiJiangJie <32935519+BaiJiangJie@users.noreply.github.com> Date: Wed, 22 Apr 2020 00:22:24 +0800 Subject: [PATCH 04/30] Dev oidc (#3930) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 添加django-oidc-rp支持 * [Update] 添加django-oidc-rp支持2 * [Update] 调试django-oidc-rp对keycloak的支持 * [Update] 调试django-oidc-rp对keycloak的支持2 * [Update] 修改oidc_rp创建用户/更新用户的功能 * [Update] oidc_rp添加支持password认证 * [Update] 重写oidc_rp end session view * [Update] 优化 oidc_rp view backend url 等引用关系 --- apps/authentication/backends/oidc/__init__.py | 0 apps/authentication/backends/oidc/backends.py | 181 ++++++++++++++++++ apps/authentication/backends/oidc/urls.py | 10 + apps/authentication/backends/oidc/views.py | 77 ++++++++ apps/authentication/signals_handlers.py | 23 +-- apps/authentication/urls/view_urls.py | 1 + apps/authentication/views/login.py | 10 +- apps/jumpserver/conf.py | 14 ++ apps/jumpserver/settings/auth.py | 17 ++ apps/jumpserver/settings/base.py | 3 + 10 files changed, 324 insertions(+), 12 deletions(-) create mode 100644 apps/authentication/backends/oidc/__init__.py create mode 100644 apps/authentication/backends/oidc/backends.py create mode 100644 apps/authentication/backends/oidc/urls.py create mode 100644 apps/authentication/backends/oidc/views.py diff --git a/apps/authentication/backends/oidc/__init__.py b/apps/authentication/backends/oidc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/authentication/backends/oidc/backends.py b/apps/authentication/backends/oidc/backends.py new file mode 100644 index 000000000..2fa4a2555 --- /dev/null +++ b/apps/authentication/backends/oidc/backends.py @@ -0,0 +1,181 @@ +import requests +from django.contrib.auth import get_user_model +from django.contrib.auth.backends import ModelBackend +from django.core.exceptions import SuspiciousOperation +from django.conf import settings +from django.db import transaction +from django.urls import reverse +from django.utils.module_loading import import_string + +from oidc_rp.conf import settings as oidc_rp_settings +from oidc_rp.models import OIDCUser +from oidc_rp.signals import oidc_user_created +from oidc_rp.backends import OIDCAuthBackend +from oidc_rp.utils import validate_and_return_id_token + + +__all__ = ['OIDCAuthCodeBackend', 'OIDCAuthPasswordBackend'] + + +class OIDCAuthCodeBackend(OIDCAuthBackend): + def authenticate(self, request, nonce=None, **kwargs): + """ Authenticates users in case of the OpenID Connect Authorization code flow. """ + # NOTE: the request object is mandatory to perform the authentication using an authorization + # code provided by the OIDC supplier. + if (nonce is None and oidc_rp_settings.USE_NONCE) or request is None: + return + + # Fetches required GET parameters from the HTTP request object. + state = request.GET.get('state') + code = request.GET.get('code') + + # Don't go further if the state value or the authorization code is not present in the GET + # parameters because we won't be able to get a valid token for the user in that case. + if (state is None and oidc_rp_settings.USE_STATE) or code is None: + raise SuspiciousOperation('Authorization code or state value is missing') + + # Prepares the token payload that will be used to request an authentication token to the + # token endpoint of the OIDC provider. + token_payload = { + 'client_id': oidc_rp_settings.CLIENT_ID, + 'client_secret': oidc_rp_settings.CLIENT_SECRET, + 'grant_type': 'authorization_code', + 'code': code, + 'redirect_uri': request.build_absolute_uri( + reverse(settings.OIDC_RP_LOGIN_CALLBACK_URL_NAME) + ), + } + + # Calls the token endpoint. + token_response = requests.post(oidc_rp_settings.PROVIDER_TOKEN_ENDPOINT, data=token_payload) + token_response.raise_for_status() + token_response_data = token_response.json() + + # Validates the token. + raw_id_token = token_response_data.get('id_token') + id_token = validate_and_return_id_token(raw_id_token, nonce) + if id_token is None: + return + + # Retrieves the access token and refresh token. + access_token = token_response_data.get('access_token') + refresh_token = token_response_data.get('refresh_token') + + # Stores the ID token, the related access token and the refresh token in the session. + request.session['oidc_auth_id_token'] = raw_id_token + request.session['oidc_auth_access_token'] = access_token + request.session['oidc_auth_refresh_token'] = refresh_token + + # If the id_token contains userinfo scopes and claims we don't have to hit the userinfo + # endpoint. + if oidc_rp_settings.ID_TOKEN_INCLUDE_USERINFO: + userinfo_data = id_token + else: + # Fetches the user information from the userinfo endpoint provided by the OP. + userinfo_response = requests.get( + oidc_rp_settings.PROVIDER_USERINFO_ENDPOINT, + headers={'Authorization': 'Bearer {0}'.format(access_token)}) + userinfo_response.raise_for_status() + userinfo_data = userinfo_response.json() + + # Tries to retrieve a corresponding user in the local database and creates it if applicable. + try: + oidc_user = OIDCUser.objects.select_related('user').get(sub=userinfo_data.get('sub')) + except OIDCUser.DoesNotExist: + oidc_user = create_oidc_user_from_claims(userinfo_data) + oidc_user_created.send(sender=self.__class__, request=request, oidc_user=oidc_user) + else: + update_oidc_user_from_claims(oidc_user, userinfo_data) + + # Runs a custom user details handler if applicable. Such handler could be responsible for + # creating / updating whatever is necessary to manage the considered user (eg. a profile). + user_details_handler(oidc_user, userinfo_data) + + return oidc_user.user + + +class OIDCAuthPasswordBackend(ModelBackend): + + def authenticate(self, request, username=None, password=None, **kwargs): + + if username is None and password is None: + return + + # Prepares the token payload that will be used to request an authentication token to the + # token endpoint of the OIDC provider. + token_payload = { + 'client_id': oidc_rp_settings.CLIENT_ID, + 'client_secret': oidc_rp_settings.CLIENT_SECRET, + 'grant_type': 'password', + 'username': username, + 'password': password, + } + + token_response = requests.post(oidc_rp_settings.PROVIDER_TOKEN_ENDPOINT, data=token_payload) + token_response.raise_for_status() + token_response_data = token_response.json() + + access_token = token_response_data.get('access_token') + + # Fetches the user information from the userinfo endpoint provided by the OP. + userinfo_response = requests.get( + oidc_rp_settings.PROVIDER_USERINFO_ENDPOINT, + headers={'Authorization': 'Bearer {0}'.format(access_token)}) + userinfo_response.raise_for_status() + userinfo_data = userinfo_response.json() + + # Tries to retrieve a corresponding user in the local database and creates it if applicable. + try: + oidc_user = OIDCUser.objects.select_related('user').get(sub=userinfo_data.get('sub')) + except OIDCUser.DoesNotExist: + oidc_user = create_oidc_user_from_claims(userinfo_data) + oidc_user_created.send(sender=self.__class__, request=request, oidc_user=oidc_user) + else: + update_oidc_user_from_claims(oidc_user, userinfo_data) + + # Runs a custom user details handler if applicable. Such handler could be responsible for + # creating / updating whatever is necessary to manage the considered user (eg. a profile). + user_details_handler(oidc_user, userinfo_data) + + return oidc_user.user + + +def get_or_create_user(username, email): + user, created = get_user_model().objects.get_or_create(username=username) + return user + + +@transaction.atomic +def create_oidc_user_from_claims(claims): + """ + Creates an ``OIDCUser`` instance using the claims extracted + from an id_token. + """ + sub = claims['sub'] + email = claims.get('email') + username = claims.get('preferred_username') + user = get_or_create_user(username, email) + oidc_user = OIDCUser.objects.create(user=user, sub=sub, userinfo=claims) + + return oidc_user + + +@transaction.atomic +def update_oidc_user_from_claims(oidc_user, claims): + """ + Updates an ``OIDCUser`` instance using the claims extracted + from an id_token. + """ + oidc_user.userinfo = claims + oidc_user.save() + + +@transaction.atomic +def user_details_handler(oidc_user, userinfo_data): + name = userinfo_data.get('name') + username = userinfo_data.get('preferred_username') + email = userinfo_data.get('email') + oidc_user.user.name = name or username + oidc_user.user.username = username + oidc_user.user.email = email + oidc_user.user.save() diff --git a/apps/authentication/backends/oidc/urls.py b/apps/authentication/backends/oidc/urls.py new file mode 100644 index 000000000..173269d0d --- /dev/null +++ b/apps/authentication/backends/oidc/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from oidc_rp import views as oidc_rp_views +from .views import OverwriteOIDCAuthRequestView, OverwriteOIDCEndSessionView + + +urlpatterns = [ + path('login/', OverwriteOIDCAuthRequestView.as_view(), name='oidc-login'), + path('callback/', oidc_rp_views.OIDCAuthCallbackView.as_view(), name='oidc-callback'), + path('logout/', OverwriteOIDCEndSessionView.as_view(), name='oidc-logout'), +] diff --git a/apps/authentication/backends/oidc/views.py b/apps/authentication/backends/oidc/views.py new file mode 100644 index 000000000..4db0c4138 --- /dev/null +++ b/apps/authentication/backends/oidc/views.py @@ -0,0 +1,77 @@ +from django.conf import settings +from django.http import HttpResponseRedirect, QueryDict +from django.urls import reverse +from django.utils.crypto import get_random_string +from django.utils.http import is_safe_url, urlencode + +from oidc_rp.conf import settings as oidc_rp_settings +from oidc_rp.views import OIDCEndSessionView, OIDCAuthRequestView + +__all__ = ['OverwriteOIDCAuthRequestView', 'OverwriteOIDCEndSessionView'] + + +class OverwriteOIDCAuthRequestView(OIDCAuthRequestView): + def get(self, request): + """ Processes GET requests. """ + # Defines common parameters used to bootstrap the authentication request. + authentication_request_params = request.GET.dict() + authentication_request_params.update({ + 'scope': oidc_rp_settings.SCOPES, + 'response_type': 'code', + 'client_id': oidc_rp_settings.CLIENT_ID, + 'redirect_uri': request.build_absolute_uri( + reverse(settings.OIDC_RP_LOGIN_CALLBACK_URL_NAME) + ), + }) + + # States should be used! They are recommended in order to maintain state between the + # authentication request and the callback. + if oidc_rp_settings.USE_STATE: + state = get_random_string(oidc_rp_settings.STATE_LENGTH) + authentication_request_params.update({'state': state}) + request.session['oidc_auth_state'] = state + + # Nonces should be used too! In that case the generated nonce is stored both in the + # authentication request parameters and in the user's session. + if oidc_rp_settings.USE_NONCE: + nonce = get_random_string(oidc_rp_settings.NONCE_LENGTH) + authentication_request_params.update({'nonce': nonce, }) + request.session['oidc_auth_nonce'] = nonce + + # Stores the "next" URL in the session if applicable. + next_url = request.GET.get('next') + request.session['oidc_auth_next_url'] = next_url \ + if is_safe_url(url=next_url, allowed_hosts=(request.get_host(), )) else None + + # Redirects the user to authorization endpoint. + query = urlencode(authentication_request_params) + redirect_url = '{url}?{query}'.format( + url=oidc_rp_settings.PROVIDER_AUTHORIZATION_ENDPOINT, query=query) + return HttpResponseRedirect(redirect_url) + + +class OverwriteOIDCEndSessionView(OIDCEndSessionView): + def post(self, request): + """ Processes POST requests. """ + logout_url = settings.LOGOUT_REDIRECT_URL or '/' + + try: + logout_url = self.provider_end_session_url \ + if oidc_rp_settings.PROVIDER_END_SESSION_ENDPOINT else logout_url + except KeyError: # pragma: no cover + logout_url = logout_url + + # Redirects the user to the appropriate URL. + return HttpResponseRedirect(logout_url) + + @property + def provider_end_session_url(self): + """ Returns the end-session URL. """ + q = QueryDict(mutable=True) + q[oidc_rp_settings.PROVIDER_END_SESSION_REDIRECT_URI_PARAMETER] = \ + self.request.build_absolute_uri(settings.LOGOUT_REDIRECT_URL or '/') + if self.request.session.get('oidc_auth_id_token'): + q[oidc_rp_settings.PROVIDER_END_SESSION_ID_TOKEN_PARAMETER] = \ + self.request.session['oidc_auth_id_token'] + return '{}?{}'.format(oidc_rp_settings.PROVIDER_END_SESSION_ENDPOINT, q.urlencode()) + diff --git a/apps/authentication/signals_handlers.py b/apps/authentication/signals_handlers.py index aac64df4c..2d73ae9b7 100644 --- a/apps/authentication/signals_handlers.py +++ b/apps/authentication/signals_handlers.py @@ -4,6 +4,7 @@ from django.dispatch import receiver from django.contrib.auth.signals import user_logged_out from django_auth_ldap.backend import populate_user +from oidc_rp.signals import oidc_user_created from users.models import User from .backends.openid import new_client from .backends.openid.signals import ( @@ -14,20 +15,17 @@ from .signals import post_auth_success @receiver(user_logged_out) def on_user_logged_out(sender, request, user, **kwargs): - if not settings.AUTH_OPENID: - return - if not settings.AUTH_OPENID_SHARE_SESSION: - return query = QueryDict('', mutable=True) query.update({ 'redirect_uri': settings.BASE_SITE_URL }) - client = new_client() - openid_logout_url = "%s?%s" % ( - client.get_url_end_session_endpoint(), - query.urlencode() - ) - request.COOKIES['next'] = openid_logout_url + # openid (keycloak) + if settings.AUTH_OPENID and settings.AUTH_OPENID_SHARE_SESSION: + client = new_client() + end_session_endpoint = client.get_url_end_session_endpoint() + openid_logout_url = "%s?%s" % (end_session_endpoint, query.urlencode()) + request.COOKIES['next'] = openid_logout_url + return @receiver(post_create_or_update_openid_user) @@ -51,4 +49,7 @@ def on_ldap_create_user(sender, user, ldap_user, **kwargs): user.save() - +@receiver(oidc_user_created) +def on_oidc_user_created(sender, request, oidc_user, **kwargs): + oidc_user.user.source = User.SOURCE_OPENID + oidc_user.user.save() diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py index b9f76e731..c33ea453c 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -18,4 +18,5 @@ urlpatterns = [ # openid path('openid/', include(('authentication.backends.openid.urls', 'authentication'), namespace='openid')), path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')), + path('oidc-rp/', include(('authentication.backends.oidc.urls', 'authentication'), namespace='oidc-rp')), ] diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index e2332cae5..4a9225f94 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -61,6 +61,8 @@ class UserLoginView(mixins.AuthMixin, FormView): redirect_url = reverse("authentication:openid:openid-login") elif settings.AUTH_CAS: redirect_url = reverse(settings.CAS_LOGIN_URL_NAME) + elif settings.AUTH_OIDC_RP: + redirect_url = reverse(settings.OIDC_RP_LOGIN_URL_NAME) if redirect_url: query_string = request.GET.urlencode() @@ -187,16 +189,22 @@ class UserLogoutView(TemplateView): def get_backend_logout_url(): # if settings.AUTH_CAS: # return settings.CAS_LOGOUT_URL_NAME - return None + + # oidc rp + if settings.AUTH_OIDC_RP: + return reverse(settings.OIDC_RP_LOGOUT_URL_NAME) def get(self, request, *args, **kwargs): auth_logout(request) + backend_logout_url = self.get_backend_logout_url() if backend_logout_url: return redirect(backend_logout_url) + next_uri = request.COOKIES.get("next") if next_uri: return redirect(next_uri) + response = super().get(request, *args, **kwargs) return response diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 75978ea41..1a6e24278 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -143,6 +143,17 @@ class Config(dict): 'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True, 'AUTH_OPENID_SHARE_SESSION': True, + 'AUTH_OIDC_RP': False, + 'OIDC_RP_CLIENT_ID': 'client-id', + 'OIDC_RP_CLIENT_SECRET': 'client-secret', + 'OIDC_RP_PROVIDER_ENDPOINT': 'provider-endpoint', + 'OIDC_RP_PROVIDER_AUTHORIZATION_ENDPOINT': 'provider-authorization-endpoint', + 'OIDC_RP_PROVIDER_TOKEN_ENDPOINT': 'provider-token-endpoint', + 'OIDC_RP_PROVIDER_JWKS_ENDPOINT': 'provider-jwks-endpoint', + 'OIDC_RP_PROVIDER_USERINFO_ENDPOINT': 'provider-userinfo-endpoint', + 'OIDC_RP_PROVIDER_END_SESSION_ENDPOINT': 'end-session-endpoint', + 'OIDC_RP_ID_TOKEN_MAX_AGE': 60, + 'AUTH_RADIUS': False, 'RADIUS_SERVER': 'localhost', 'RADIUS_PORT': 1812, @@ -298,6 +309,9 @@ class DynamicConfig: if self.static_config.get('AUTH_OPENID'): backends.insert(0, 'authentication.backends.openid.backends.OpenIDAuthorizationPasswordBackend') backends.insert(0, 'authentication.backends.openid.backends.OpenIDAuthorizationCodeBackend') + if self.static_config.get('AUTH_OIDC_RP'): + backends.insert(0, 'authentication.backends.oidc.backends.OIDCAuthCodeBackend') + backends.insert(0, 'authentication.backends.oidc.backends.OIDCAuthPasswordBackend',) if self.static_config.get('AUTH_RADIUS'): backends.insert(0, 'authentication.backends.radius.RadiusBackend') return backends diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index bad698e4c..16fa3e54e 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -56,6 +56,23 @@ AUTH_OPENID_SHARE_SESSION = CONFIG.AUTH_OPENID_SHARE_SESSION AUTH_OPENID_LOGIN_URL = reverse_lazy("authentication:openid:openid-login") AUTH_OPENID_LOGIN_COMPLETE_URL = reverse_lazy("authentication:openid:openid-login-complete") +# oidc rp +# jumpserver +AUTH_OIDC_RP = CONFIG.AUTH_OIDC_RP +OIDC_RP_LOGIN_URL_NAME = "authentication:oidc-rp:oidc-login" +OIDC_RP_LOGIN_CALLBACK_URL_NAME = "authentication:oidc-rp:oidc-callback" +OIDC_RP_LOGOUT_URL_NAME = "authentication:oidc-rp:oidc-logout" +# https://django-oidc-rp.readthedocs.io/en/stable/settings.html#required-settings +OIDC_RP_CLIENT_ID = CONFIG.OIDC_RP_CLIENT_ID +OIDC_RP_CLIENT_SECRET = CONFIG.OIDC_RP_CLIENT_SECRET +OIDC_RP_PROVIDER_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_ENDPOINT +OIDC_RP_PROVIDER_AUTHORIZATION_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_AUTHORIZATION_ENDPOINT +OIDC_RP_PROVIDER_TOKEN_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_TOKEN_ENDPOINT +OIDC_RP_PROVIDER_JWKS_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_JWKS_ENDPOINT +OIDC_RP_PROVIDER_USERINFO_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_USERINFO_ENDPOINT +OIDC_RP_PROVIDER_END_SESSION_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_END_SESSION_ENDPOINT +OIDC_RP_ID_TOKEN_MAX_AGE = CONFIG.OIDC_RP_ID_TOKEN_MAX_AGE + # Radius Auth AUTH_RADIUS = CONFIG.AUTH_RADIUS diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index 4a2f2ca88..c2b558742 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -56,6 +56,7 @@ INSTALLED_APPS = [ 'django_filters', 'bootstrap3', 'captcha', + 'oidc_rp', 'django_celery_beat', 'django.contrib.auth', 'django.contrib.admin', @@ -81,6 +82,7 @@ MIDDLEWARE = [ 'jumpserver.middleware.DemoMiddleware', 'jumpserver.middleware.RequestMiddleware', 'orgs.middleware.OrgMiddleware', + 'oidc_rp.middleware.OIDCRefreshIDTokenMiddleware', ] @@ -103,6 +105,7 @@ TEMPLATES = [ 'django.template.context_processors.media', 'jumpserver.context_processor.jumpserver_processor', 'orgs.context_processor.org_processor', + 'oidc_rp.context_processors.oidc', ], }, }, From 306605915c83e6c3e28583d5ade6b2f96a851e1a Mon Sep 17 00:00:00 2001 From: Bai Date: Wed, 22 Apr 2020 00:31:59 +0800 Subject: [PATCH 05/30] =?UTF-8?q?[Update]=20=E6=B7=BB=E5=8A=A0=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=20django-oidc-rp=3D=3D0.3.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index f459ae2fa..467a04fcb 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -98,3 +98,4 @@ ipython huaweicloud-sdk-python==1.0.21 django-redis==4.11.0 python-redis-lock==3.5.0 +django-oidc-rp==0.3.4 From 586c04cba62be70adb1bbe277fbcbaaec6921290 Mon Sep 17 00:00:00 2001 From: Bai Date: Wed, 22 Apr 2020 11:09:13 +0800 Subject: [PATCH 06/30] =?UTF-8?q?[Update]=20=E6=B7=BB=E5=8A=A0oidc-op?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../templates/authentication/login.html | 9 ++++++++- apps/authentication/views/login.py | 1 + apps/jumpserver/conf.py | 16 +++++++--------- apps/jumpserver/settings/auth.py | 3 +++ apps/users/utils.py | 2 +- config_example.yml | 19 ++++++++++++++++++- 6 files changed, 38 insertions(+), 12 deletions(-) diff --git a/apps/authentication/templates/authentication/login.html b/apps/authentication/templates/authentication/login.html index bff33eb17..21d775633 100644 --- a/apps/authentication/templates/authentication/login.html +++ b/apps/authentication/templates/authentication/login.html @@ -52,14 +52,21 @@ - {% if AUTH_OPENID %} + {% if AUTH_OPENID or AUTH_OIDC_RP %}

{% trans "More login options" %}

+ {% if AUTH_OIDC_RP %} + + {% elif AUTH_OPENID %} + {% endif %}
{% endif %} diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index 4a9225f94..d27c4ccc3 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -113,6 +113,7 @@ class UserLoginView(mixins.AuthMixin, FormView): context = { 'demo_mode': os.environ.get("DEMO_MODE"), 'AUTH_OPENID': settings.AUTH_OPENID, + 'AUTH_OIDC_RP': settings.AUTH_OIDC_RP, } kwargs.update(context) return super().get_context_data(**kwargs) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 1a6e24278..68e5d6e86 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -143,15 +143,16 @@ class Config(dict): 'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True, 'AUTH_OPENID_SHARE_SESSION': True, + 'AUTH_OIDC_RP': False, 'OIDC_RP_CLIENT_ID': 'client-id', 'OIDC_RP_CLIENT_SECRET': 'client-secret', - 'OIDC_RP_PROVIDER_ENDPOINT': 'provider-endpoint', - 'OIDC_RP_PROVIDER_AUTHORIZATION_ENDPOINT': 'provider-authorization-endpoint', - 'OIDC_RP_PROVIDER_TOKEN_ENDPOINT': 'provider-token-endpoint', - 'OIDC_RP_PROVIDER_JWKS_ENDPOINT': 'provider-jwks-endpoint', - 'OIDC_RP_PROVIDER_USERINFO_ENDPOINT': 'provider-userinfo-endpoint', - 'OIDC_RP_PROVIDER_END_SESSION_ENDPOINT': 'end-session-endpoint', + 'OIDC_RP_PROVIDER_ENDPOINT': 'https://op-endpoint.com', + 'OIDC_RP_PROVIDER_AUTHORIZATION_ENDPOINT': 'https://op-endpoint.com/authorize', + 'OIDC_RP_PROVIDER_TOKEN_ENDPOINT': 'https://op-endpoint.com/token', + 'OIDC_RP_PROVIDER_JWKS_ENDPOINT': 'https://op-endpoint.com/jwk', + 'OIDC_RP_PROVIDER_USERINFO_ENDPOINT': 'https://op-endpoint.com/userinfo', + 'OIDC_RP_PROVIDER_END_SESSION_ENDPOINT': 'https://op-endpoint.com/logout', 'OIDC_RP_ID_TOKEN_MAX_AGE': 60, 'AUTH_RADIUS': False, @@ -292,9 +293,6 @@ class DynamicConfig: return lambda: self.get(item) def LOGIN_URL(self): - auth_openid = self.get('AUTH_OPENID') - if auth_openid: - return reverse_lazy("authentication:openid:openid-login") return self.get('LOGIN_URL') def AUTHENTICATION_BACKENDS(self): diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 16fa3e54e..e46362638 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -59,6 +59,9 @@ AUTH_OPENID_LOGIN_COMPLETE_URL = reverse_lazy("authentication:openid:openid-logi # oidc rp # jumpserver AUTH_OIDC_RP = CONFIG.AUTH_OIDC_RP +if AUTH_OIDC_RP: + # 优先使用AUTH_OIDC_RP + AUTH_OPENID = False OIDC_RP_LOGIN_URL_NAME = "authentication:oidc-rp:oidc-login" OIDC_RP_LOGIN_CALLBACK_URL_NAME = "authentication:oidc-rp:oidc-callback" OIDC_RP_LOGOUT_URL_NAME = "authentication:oidc-rp:oidc-logout" diff --git a/apps/users/utils.py b/apps/users/utils.py index 0729115b6..9488b5877 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -326,7 +326,7 @@ def get_source_choices(): ] if settings.AUTH_LDAP: choices.append((User.SOURCE_LDAP, choices_all[User.SOURCE_LDAP])) - if settings.AUTH_OPENID: + if settings.AUTH_OPENID or settings.AUTH_OIDC_RP: choices.append((User.SOURCE_OPENID, choices_all[User.SOURCE_OPENID])) if settings.AUTH_RADIUS: choices.append((User.SOURCE_RADIUS, choices_all[User.SOURCE_RADIUS])) diff --git a/config_example.yml b/config_example.yml index 5699f4ff0..cec3b7eb6 100644 --- a/config_example.yml +++ b/config_example.yml @@ -55,7 +55,11 @@ REDIS_PORT: 6379 # REDIS_DB_CACHE: 4 # Use OpenID authorization -# 使用OpenID 来进行认证设置 +# +# 配置说明: 如果您使用的是Keycloak作为OP,可以使用方式1或方式2; 如果OP不是Keycloak, 请使用方式2 +# +# 方式1: OpenID认证 (基于 oidc 协议的 keycloak 的实现) +# # BASE_SITE_URL: http://localhost:8080 # AUTH_OPENID: false # True or False # AUTH_OPENID_SERVER_URL: https://openid-auth-server.com/ @@ -64,6 +68,19 @@ REDIS_PORT: 6379 # AUTH_OPENID_CLIENT_SECRET: client-secret # AUTH_OPENID_IGNORE_SSL_VERIFICATION: True # AUTH_OPENID_SHARE_SESSION: True +# +# 方式2: OpenID认证 (使用标准 oidc 协议进行认证) +# 配置参数详细信息参考: https://django-oidc-rp.readthedocs.io/en/stable/settings.html +# +# AUTH_OIDC_RP: False +# OIDC_RP_CLIENT_ID: client-id +# OIDC_RP_CLIENT_SECRET: client-secret +# OIDC_RP_PROVIDER_ENDPOINT: https://op-endpoint.com +# OIDC_RP_PROVIDER_AUTHORIZATION_ENDPOINT: https://op-endpoint.com/authorize +# OIDC_RP_PROVIDER_TOKEN_ENDPOINT: https://op-endpoint.com/token +# OIDC_RP_PROVIDER_JWKS_ENDPOINT: https://op-endpoint.com/jwk +# OIDC_RP_PROVIDER_USERINFO_ENDPOINT: https://op-endpoint.com/userinfo +# OIDC_RP_PROVIDER_END_SESSION_ENDPOINT: https://op-endpoint.com/logout # Use Radius authorization # 使用Radius来认证 From fc5ec3f21c2ecb59561c601a8eb82a050f8186f5 Mon Sep 17 00:00:00 2001 From: Bai Date: Fri, 24 Apr 2020 11:29:01 +0800 Subject: [PATCH 07/30] =?UTF-8?q?[Update]=20=E6=9B=B4=E6=96=B0=E4=BB=AA?= =?UTF-8?q?=E8=A1=A8=E7=9B=98=E6=95=B0=E6=8D=AE=E6=98=BE=E7=A4=BA=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/views/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/jumpserver/views/index.py b/apps/jumpserver/views/index.py index 1bcb2a57e..120e7b2d0 100644 --- a/apps/jumpserver/views/index.py +++ b/apps/jumpserver/views/index.py @@ -36,7 +36,7 @@ class MonthLoginMetricMixin: @staticmethod def get_cache_key(date, tp): date_str = date.strftime("%Y%m%d") - key = "SESSION_MONTH_{}_{}".format(tp, date_str) + key = "SESSION_MONTH_{}_{}_{}".format(current_org.id, tp, date_str) return key def __get_data_from_cache(self, date, tp): From 7833ff6671e9382e42d6b250e8eaffd63e1e96ce Mon Sep 17 00:00:00 2001 From: BaiJiangJie <32935519+BaiJiangJie@users.noreply.github.com> Date: Sun, 26 Apr 2020 20:36:17 +0800 Subject: [PATCH 08/30] Dev oidc (#3941) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] oidc_rp获取token添加headers base64编码 * [Update] 移除对oidc_rp的支持 * [Update] 移除对oidc_rp的支持2 * [Update] 修改OpenID配置(添加新配置项,并对旧配置项做兼容) * [Update] 移除所有与Keycloak相关的模块 * [Update] 添加jumpserver-django-oidc-rp的使用 * [Update] 更新登录重定向地址(oidc) * [Update] oidc添加一些配置参数;处理用户登录/创建/更新等信号 * [Update] 修改退出登录逻辑 * [Update] 添加oidc user登录成功的信号机制 * [Update] 修改mfa认证choices内容 (otp => code) * [Update] 添加OpenID backend password 认证失败信号机制;修改引入common包问题 * [Update] 用户Token/Auth API 校验用户时,传入request参数(解决登录成功日志记录的问题) * [Update] 添加依赖jumpserver-django-oidc-rp==0.3.7.1 * [Update] oidc认证模块说明 --- apps/authentication/backends/oidc/__init__.py | 4 + apps/authentication/backends/oidc/backends.py | 181 ----------------- apps/authentication/backends/oidc/urls.py | 10 - apps/authentication/backends/oidc/views.py | 77 -------- .../backends/openid/__init__.py | 7 - .../backends/openid/backends.py | 82 -------- .../backends/openid/decorator.py | 57 ------ .../backends/openid/middleware.py | 41 ---- apps/authentication/backends/openid/models.py | 185 ------------------ .../authentication/backends/openid/signals.py | 5 - apps/authentication/backends/openid/tests.py | 0 apps/authentication/backends/openid/urls.py | 11 -- apps/authentication/backends/openid/utils.py | 19 -- apps/authentication/backends/openid/views.py | 71 ------- apps/authentication/errors.py | 2 +- apps/authentication/mixins.py | 3 +- apps/authentication/signals_handlers.py | 56 +----- .../templates/authentication/login.html | 13 +- apps/authentication/urls/view_urls.py | 3 +- apps/authentication/views/login.py | 19 +- apps/jumpserver/conf.py | 174 +++++++++++++--- apps/jumpserver/settings/auth.py | 57 +++--- apps/jumpserver/settings/base.py | 7 +- apps/jumpserver/utils.py | 1 - apps/users/signals_handler.py | 34 ++++ apps/users/utils.py | 2 +- config_example.yml | 20 +- requirements/requirements.txt | 4 +- 28 files changed, 240 insertions(+), 905 deletions(-) delete mode 100644 apps/authentication/backends/oidc/backends.py delete mode 100644 apps/authentication/backends/oidc/urls.py delete mode 100644 apps/authentication/backends/oidc/views.py delete mode 100644 apps/authentication/backends/openid/__init__.py delete mode 100644 apps/authentication/backends/openid/backends.py delete mode 100644 apps/authentication/backends/openid/decorator.py delete mode 100644 apps/authentication/backends/openid/middleware.py delete mode 100644 apps/authentication/backends/openid/models.py delete mode 100644 apps/authentication/backends/openid/signals.py delete mode 100644 apps/authentication/backends/openid/tests.py delete mode 100644 apps/authentication/backends/openid/urls.py delete mode 100644 apps/authentication/backends/openid/utils.py delete mode 100644 apps/authentication/backends/openid/views.py diff --git a/apps/authentication/backends/oidc/__init__.py b/apps/authentication/backends/oidc/__init__.py index e69de29bb..a82161b8e 100644 --- a/apps/authentication/backends/oidc/__init__.py +++ b/apps/authentication/backends/oidc/__init__.py @@ -0,0 +1,4 @@ +""" +使用下面的工程,进行jumpserver 的 oidc 认证 +https://github.com/BaiJiangJie/jumpserver-django-oidc-rp +""" \ No newline at end of file diff --git a/apps/authentication/backends/oidc/backends.py b/apps/authentication/backends/oidc/backends.py deleted file mode 100644 index 2fa4a2555..000000000 --- a/apps/authentication/backends/oidc/backends.py +++ /dev/null @@ -1,181 +0,0 @@ -import requests -from django.contrib.auth import get_user_model -from django.contrib.auth.backends import ModelBackend -from django.core.exceptions import SuspiciousOperation -from django.conf import settings -from django.db import transaction -from django.urls import reverse -from django.utils.module_loading import import_string - -from oidc_rp.conf import settings as oidc_rp_settings -from oidc_rp.models import OIDCUser -from oidc_rp.signals import oidc_user_created -from oidc_rp.backends import OIDCAuthBackend -from oidc_rp.utils import validate_and_return_id_token - - -__all__ = ['OIDCAuthCodeBackend', 'OIDCAuthPasswordBackend'] - - -class OIDCAuthCodeBackend(OIDCAuthBackend): - def authenticate(self, request, nonce=None, **kwargs): - """ Authenticates users in case of the OpenID Connect Authorization code flow. """ - # NOTE: the request object is mandatory to perform the authentication using an authorization - # code provided by the OIDC supplier. - if (nonce is None and oidc_rp_settings.USE_NONCE) or request is None: - return - - # Fetches required GET parameters from the HTTP request object. - state = request.GET.get('state') - code = request.GET.get('code') - - # Don't go further if the state value or the authorization code is not present in the GET - # parameters because we won't be able to get a valid token for the user in that case. - if (state is None and oidc_rp_settings.USE_STATE) or code is None: - raise SuspiciousOperation('Authorization code or state value is missing') - - # Prepares the token payload that will be used to request an authentication token to the - # token endpoint of the OIDC provider. - token_payload = { - 'client_id': oidc_rp_settings.CLIENT_ID, - 'client_secret': oidc_rp_settings.CLIENT_SECRET, - 'grant_type': 'authorization_code', - 'code': code, - 'redirect_uri': request.build_absolute_uri( - reverse(settings.OIDC_RP_LOGIN_CALLBACK_URL_NAME) - ), - } - - # Calls the token endpoint. - token_response = requests.post(oidc_rp_settings.PROVIDER_TOKEN_ENDPOINT, data=token_payload) - token_response.raise_for_status() - token_response_data = token_response.json() - - # Validates the token. - raw_id_token = token_response_data.get('id_token') - id_token = validate_and_return_id_token(raw_id_token, nonce) - if id_token is None: - return - - # Retrieves the access token and refresh token. - access_token = token_response_data.get('access_token') - refresh_token = token_response_data.get('refresh_token') - - # Stores the ID token, the related access token and the refresh token in the session. - request.session['oidc_auth_id_token'] = raw_id_token - request.session['oidc_auth_access_token'] = access_token - request.session['oidc_auth_refresh_token'] = refresh_token - - # If the id_token contains userinfo scopes and claims we don't have to hit the userinfo - # endpoint. - if oidc_rp_settings.ID_TOKEN_INCLUDE_USERINFO: - userinfo_data = id_token - else: - # Fetches the user information from the userinfo endpoint provided by the OP. - userinfo_response = requests.get( - oidc_rp_settings.PROVIDER_USERINFO_ENDPOINT, - headers={'Authorization': 'Bearer {0}'.format(access_token)}) - userinfo_response.raise_for_status() - userinfo_data = userinfo_response.json() - - # Tries to retrieve a corresponding user in the local database and creates it if applicable. - try: - oidc_user = OIDCUser.objects.select_related('user').get(sub=userinfo_data.get('sub')) - except OIDCUser.DoesNotExist: - oidc_user = create_oidc_user_from_claims(userinfo_data) - oidc_user_created.send(sender=self.__class__, request=request, oidc_user=oidc_user) - else: - update_oidc_user_from_claims(oidc_user, userinfo_data) - - # Runs a custom user details handler if applicable. Such handler could be responsible for - # creating / updating whatever is necessary to manage the considered user (eg. a profile). - user_details_handler(oidc_user, userinfo_data) - - return oidc_user.user - - -class OIDCAuthPasswordBackend(ModelBackend): - - def authenticate(self, request, username=None, password=None, **kwargs): - - if username is None and password is None: - return - - # Prepares the token payload that will be used to request an authentication token to the - # token endpoint of the OIDC provider. - token_payload = { - 'client_id': oidc_rp_settings.CLIENT_ID, - 'client_secret': oidc_rp_settings.CLIENT_SECRET, - 'grant_type': 'password', - 'username': username, - 'password': password, - } - - token_response = requests.post(oidc_rp_settings.PROVIDER_TOKEN_ENDPOINT, data=token_payload) - token_response.raise_for_status() - token_response_data = token_response.json() - - access_token = token_response_data.get('access_token') - - # Fetches the user information from the userinfo endpoint provided by the OP. - userinfo_response = requests.get( - oidc_rp_settings.PROVIDER_USERINFO_ENDPOINT, - headers={'Authorization': 'Bearer {0}'.format(access_token)}) - userinfo_response.raise_for_status() - userinfo_data = userinfo_response.json() - - # Tries to retrieve a corresponding user in the local database and creates it if applicable. - try: - oidc_user = OIDCUser.objects.select_related('user').get(sub=userinfo_data.get('sub')) - except OIDCUser.DoesNotExist: - oidc_user = create_oidc_user_from_claims(userinfo_data) - oidc_user_created.send(sender=self.__class__, request=request, oidc_user=oidc_user) - else: - update_oidc_user_from_claims(oidc_user, userinfo_data) - - # Runs a custom user details handler if applicable. Such handler could be responsible for - # creating / updating whatever is necessary to manage the considered user (eg. a profile). - user_details_handler(oidc_user, userinfo_data) - - return oidc_user.user - - -def get_or_create_user(username, email): - user, created = get_user_model().objects.get_or_create(username=username) - return user - - -@transaction.atomic -def create_oidc_user_from_claims(claims): - """ - Creates an ``OIDCUser`` instance using the claims extracted - from an id_token. - """ - sub = claims['sub'] - email = claims.get('email') - username = claims.get('preferred_username') - user = get_or_create_user(username, email) - oidc_user = OIDCUser.objects.create(user=user, sub=sub, userinfo=claims) - - return oidc_user - - -@transaction.atomic -def update_oidc_user_from_claims(oidc_user, claims): - """ - Updates an ``OIDCUser`` instance using the claims extracted - from an id_token. - """ - oidc_user.userinfo = claims - oidc_user.save() - - -@transaction.atomic -def user_details_handler(oidc_user, userinfo_data): - name = userinfo_data.get('name') - username = userinfo_data.get('preferred_username') - email = userinfo_data.get('email') - oidc_user.user.name = name or username - oidc_user.user.username = username - oidc_user.user.email = email - oidc_user.user.save() diff --git a/apps/authentication/backends/oidc/urls.py b/apps/authentication/backends/oidc/urls.py deleted file mode 100644 index 173269d0d..000000000 --- a/apps/authentication/backends/oidc/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.urls import path -from oidc_rp import views as oidc_rp_views -from .views import OverwriteOIDCAuthRequestView, OverwriteOIDCEndSessionView - - -urlpatterns = [ - path('login/', OverwriteOIDCAuthRequestView.as_view(), name='oidc-login'), - path('callback/', oidc_rp_views.OIDCAuthCallbackView.as_view(), name='oidc-callback'), - path('logout/', OverwriteOIDCEndSessionView.as_view(), name='oidc-logout'), -] diff --git a/apps/authentication/backends/oidc/views.py b/apps/authentication/backends/oidc/views.py deleted file mode 100644 index 4db0c4138..000000000 --- a/apps/authentication/backends/oidc/views.py +++ /dev/null @@ -1,77 +0,0 @@ -from django.conf import settings -from django.http import HttpResponseRedirect, QueryDict -from django.urls import reverse -from django.utils.crypto import get_random_string -from django.utils.http import is_safe_url, urlencode - -from oidc_rp.conf import settings as oidc_rp_settings -from oidc_rp.views import OIDCEndSessionView, OIDCAuthRequestView - -__all__ = ['OverwriteOIDCAuthRequestView', 'OverwriteOIDCEndSessionView'] - - -class OverwriteOIDCAuthRequestView(OIDCAuthRequestView): - def get(self, request): - """ Processes GET requests. """ - # Defines common parameters used to bootstrap the authentication request. - authentication_request_params = request.GET.dict() - authentication_request_params.update({ - 'scope': oidc_rp_settings.SCOPES, - 'response_type': 'code', - 'client_id': oidc_rp_settings.CLIENT_ID, - 'redirect_uri': request.build_absolute_uri( - reverse(settings.OIDC_RP_LOGIN_CALLBACK_URL_NAME) - ), - }) - - # States should be used! They are recommended in order to maintain state between the - # authentication request and the callback. - if oidc_rp_settings.USE_STATE: - state = get_random_string(oidc_rp_settings.STATE_LENGTH) - authentication_request_params.update({'state': state}) - request.session['oidc_auth_state'] = state - - # Nonces should be used too! In that case the generated nonce is stored both in the - # authentication request parameters and in the user's session. - if oidc_rp_settings.USE_NONCE: - nonce = get_random_string(oidc_rp_settings.NONCE_LENGTH) - authentication_request_params.update({'nonce': nonce, }) - request.session['oidc_auth_nonce'] = nonce - - # Stores the "next" URL in the session if applicable. - next_url = request.GET.get('next') - request.session['oidc_auth_next_url'] = next_url \ - if is_safe_url(url=next_url, allowed_hosts=(request.get_host(), )) else None - - # Redirects the user to authorization endpoint. - query = urlencode(authentication_request_params) - redirect_url = '{url}?{query}'.format( - url=oidc_rp_settings.PROVIDER_AUTHORIZATION_ENDPOINT, query=query) - return HttpResponseRedirect(redirect_url) - - -class OverwriteOIDCEndSessionView(OIDCEndSessionView): - def post(self, request): - """ Processes POST requests. """ - logout_url = settings.LOGOUT_REDIRECT_URL or '/' - - try: - logout_url = self.provider_end_session_url \ - if oidc_rp_settings.PROVIDER_END_SESSION_ENDPOINT else logout_url - except KeyError: # pragma: no cover - logout_url = logout_url - - # Redirects the user to the appropriate URL. - return HttpResponseRedirect(logout_url) - - @property - def provider_end_session_url(self): - """ Returns the end-session URL. """ - q = QueryDict(mutable=True) - q[oidc_rp_settings.PROVIDER_END_SESSION_REDIRECT_URI_PARAMETER] = \ - self.request.build_absolute_uri(settings.LOGOUT_REDIRECT_URL or '/') - if self.request.session.get('oidc_auth_id_token'): - q[oidc_rp_settings.PROVIDER_END_SESSION_ID_TOKEN_PARAMETER] = \ - self.request.session['oidc_auth_id_token'] - return '{}?{}'.format(oidc_rp_settings.PROVIDER_END_SESSION_ENDPOINT, q.urlencode()) - diff --git a/apps/authentication/backends/openid/__init__.py b/apps/authentication/backends/openid/__init__.py deleted file mode 100644 index 9ed3bea78..000000000 --- a/apps/authentication/backends/openid/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from .backends import * -from .middleware import * -from .utils import * -from .decorator import * diff --git a/apps/authentication/backends/openid/backends.py b/apps/authentication/backends/openid/backends.py deleted file mode 100644 index 938566e2a..000000000 --- a/apps/authentication/backends/openid/backends.py +++ /dev/null @@ -1,82 +0,0 @@ -# coding:utf-8 -# - -from django.contrib.auth import get_user_model -from django.conf import settings - -from common.utils import get_logger -from .utils import new_client -from .models import OIDT_ACCESS_TOKEN - -UserModel = get_user_model() - -logger = get_logger(__file__) -client = new_client() - - -__all__ = [ - 'OpenIDAuthorizationCodeBackend', 'OpenIDAuthorizationPasswordBackend', -] - - -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_valid = getattr(user, 'is_valid', None) - return is_valid or is_valid 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('Authentication OpenID code backend') - code = kwargs.get('code') - redirect_uri = kwargs.get('redirect_uri') - if not code or not redirect_uri: - logger.info('Authenticate failed: No code or No redirect uri') - return None - try: - oidt_profile = client.update_or_create_from_code( - code=code, redirect_uri=redirect_uri - ) - except Exception as e: - logger.info('Authenticate failed: get oidt_profile: {}'.format(e)) - return None - else: - # Check openid user single logout or not with access_token - request.session[OIDT_ACCESS_TOKEN] = oidt_profile.access_token - user = oidt_profile.user - logger.info('Authenticate success: user -> {}'.format(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('Authentication OpenID password backend') - if not username: - logger.info('Authenticate failed: Not username') - return None - try: - oidt_profile = client.update_or_create_from_password( - username=username, password=password - ) - except Exception as e: - logger.error(e, exc_info=True) - logger.info('Authenticate failed: get oidt_profile: {}'.format(e)) - return None - else: - user = oidt_profile.user - logger.info('Authenticate success: user -> {}'.format(user)) - return user if self.user_can_authenticate(user) else None - diff --git a/apps/authentication/backends/openid/decorator.py b/apps/authentication/backends/openid/decorator.py deleted file mode 100644 index 7286b7a2f..000000000 --- a/apps/authentication/backends/openid/decorator.py +++ /dev/null @@ -1,57 +0,0 @@ -# coding: utf-8 -# - -import warnings -import contextlib - -import requests -from urllib3.exceptions import InsecureRequestWarning -from django.conf import settings - -__all__ = [ - 'ssl_verification', -] - -old_merge_environment_settings = requests.Session.merge_environment_settings - - -@contextlib.contextmanager -def no_ssl_verification(): - """ - https://stackoverflow.com/questions/15445981/ - how-do-i-disable-the-security-certificate-check-in-python-requests - """ - opened_adapters = set() - - def merge_environment_settings(self, url, proxies, stream, verify, cert): - # Verification happens only once per connection so we need to close - # all the opened adapters once we're done. Otherwise, the effects of - # verify=False persist beyond the end of this context manager. - opened_adapters.add(self.get_adapter(url)) - _settings = old_merge_environment_settings( - self, url, proxies, stream, verify, cert - ) - _settings['verify'] = False - return _settings - - requests.Session.merge_environment_settings = merge_environment_settings - try: - with warnings.catch_warnings(): - warnings.simplefilter('ignore', InsecureRequestWarning) - yield - finally: - requests.Session.merge_environment_settings = old_merge_environment_settings - for adapter in opened_adapters: - try: - adapter.close() - except: - pass - - -def ssl_verification(func): - def wrapper(*args, **kwargs): - if not settings.AUTH_OPENID_IGNORE_SSL_VERIFICATION: - return func(*args, **kwargs) - with no_ssl_verification(): - return func(*args, **kwargs) - return wrapper diff --git a/apps/authentication/backends/openid/middleware.py b/apps/authentication/backends/openid/middleware.py deleted file mode 100644 index bacb4858c..000000000 --- a/apps/authentication/backends/openid/middleware.py +++ /dev/null @@ -1,41 +0,0 @@ -# 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 common.utils import get_logger -from .utils import new_client -from .models import OIDT_ACCESS_TOKEN - -BACKEND_OPENID_AUTH_CODE = 'OpenIDAuthorizationCodeBackend' -logger = get_logger(__file__) -__all__ = ['OpenIDAuthenticationMiddleware'] - - -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 openid auth if no shared session enabled - if not settings.AUTH_OPENID_SHARE_SESSION: - return - # Don't need check single logout if user not authenticated - if not request.user.is_authenticated: - return - elif not request.session[BACKEND_SESSION_KEY].endswith( - BACKEND_OPENID_AUTH_CODE): - return - # Check openid user single logout or not with access_token - try: - client = new_client() - client.get_userinfo(token=request.session.get(OIDT_ACCESS_TOKEN)) - except Exception as e: - logout(request) - logger.error(e) diff --git a/apps/authentication/backends/openid/models.py b/apps/authentication/backends/openid/models.py deleted file mode 100644 index a945e8eb3..000000000 --- a/apps/authentication/backends/openid/models.py +++ /dev/null @@ -1,185 +0,0 @@ -# -*- 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 users.utils import construct_user_email - -from .signals import post_create_or_update_openid_user -from .decorator import ssl_verification - -OIDT_ACCESS_TOKEN = 'oidt_access_token' - - -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 - - -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._openid_client = None - self._realm = None - self._openid_connect_client = None - - @property - def realm(self): - if self._realm is None: - self._realm = KeycloakRealm( - server_url=self.server_url, - realm_name=self.realm_name, - headers={} - ) - return self._realm - - @property - def openid_connect_client(self): - """ - :rtype: keycloak.openid_connect.KeycloakOpenidConnect - """ - if self._openid_connect_client is None: - self._openid_connect_client = self.realm.open_id_connect( - client_id=self.client_id, - client_secret=self.client_secret - ) - return self._openid_connect_client - - @property - def openid_client(self): - """ - :rtype: keycloak.keycloak_openid.KeycloakOpenID - """ - if self._openid_client is None: - self._openid_client = KeycloakOpenID( - server_url='%sauth/' % self.server_url, - realm_name=self.realm_name, - client_id=self.client_id, - client_secret_key=self.client_secret, - ) - return self._openid_client - - @ssl_verification - def get_url(self, name): - return self.openid_connect_client.get_url(name=name) - - def get_url_end_session_endpoint(self): - return self.get_url(name='end_session_endpoint') - - @ssl_verification - def get_authorization_url(self, redirect_uri, scope, state): - url = self.openid_connect_client.authorization_url( - redirect_uri=redirect_uri, scope=scope, state=state - ) - return url - - @ssl_verification - def get_userinfo(self, token): - user_info = self.openid_connect_client.userinfo(token=token) - return user_info - - @ssl_verification - def authorization_code(self, code, redirect_uri): - token_response = self.openid_connect_client.authorization_code( - code=code, redirect_uri=redirect_uri - ) - return token_response - - @ssl_verification - def authorization_password(self, username, password): - token_response = self.openid_client.token( - username=username, password=password - ) - return 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: OpenIDTokenProfile - """ - token_response = self.authorization_code(code, redirect_uri) - return self._update_or_create(token_response=token_response) - - 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: OpenIDTokenProfile - """ - token_response = self.authorization_password(username, password) - 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: OpenIDTokenProfile - """ - userinfo = self.get_userinfo(token=token_response['access_token']) - with transaction.atomic(): - name = userinfo.get('name', '') - username = userinfo.get('preferred_username', '') - email = userinfo.get('email', '') - email = construct_user_email(username, email) - - user, created = get_user_model().objects.update_or_create( - username=username, - defaults={ - 'name': name, 'email': 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_or_update_openid_user.send( - sender=user.__class__, user=user, created=created - ) - - return oidt_profile - - def __str__(self): - return self.client_id diff --git a/apps/authentication/backends/openid/signals.py b/apps/authentication/backends/openid/signals.py deleted file mode 100644 index ad81bca4a..000000000 --- a/apps/authentication/backends/openid/signals.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.dispatch import Signal - - -post_create_or_update_openid_user = Signal(providing_args=('user',)) -post_openid_login_success = Signal(providing_args=('user', 'request')) diff --git a/apps/authentication/backends/openid/tests.py b/apps/authentication/backends/openid/tests.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/authentication/backends/openid/urls.py b/apps/authentication/backends/openid/urls.py deleted file mode 100644 index 019529e12..000000000 --- a/apps/authentication/backends/openid/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# -from django.urls import path - -from . import views - -urlpatterns = [ - path('login/', views.OpenIDLoginView.as_view(), name='openid-login'), - path('login/complete/', views.OpenIDLoginCompleteView.as_view(), - name='openid-login-complete'), -] diff --git a/apps/authentication/backends/openid/utils.py b/apps/authentication/backends/openid/utils.py deleted file mode 100644 index 15160d224..000000000 --- a/apps/authentication/backends/openid/utils.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from django.conf import settings -from .models import Client - -__all__ = ['new_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 - ) diff --git a/apps/authentication/backends/openid/views.py b/apps/authentication/backends/openid/views.py deleted file mode 100644 index 89c935452..000000000 --- a/apps/authentication/backends/openid/views.py +++ /dev/null @@ -1,71 +0,0 @@ -# -*- coding: utf-8 -*- -# - -import logging - -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 .utils import new_client -from .models import Nonce -from .signals import post_openid_login_success - -logger = logging.getLogger(__name__) -client = new_client() - -__all__ = ['OpenIDLoginView', 'OpenIDLoginCompleteView'] - - -class OpenIDLoginView(RedirectView): - def get_redirect_url(self, *args, **kwargs): - redirect_uri = settings.BASE_SITE_URL + \ - str(settings.AUTH_OPENID_LOGIN_COMPLETE_URL) - nonce = Nonce( - redirect_uri=redirect_uri, - next_path=self.request.GET.get('next') - ) - cache.set(str(nonce.state), nonce, 24*3600) - - self.request.session['openid_state'] = str(nonce.state) - authorization_url = client.get_authorization_url( - redirect_uri=nonce.redirect_uri, - scope='code', - state=str(nonce.state) - ) - return authorization_url - - -class OpenIDLoginCompleteView(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(content='Code or State is empty') - if self.request.GET['state'] != self.request.session['openid_state']: - return HttpResponseBadRequest(content='State invalid') - nonce = cache.get(self.request.GET['state']) - if not nonce: - return HttpResponseBadRequest(content='State failure') - - 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(content='Authenticate user failed') - - login(self.request, user) - post_openid_login_success.send( - sender=self.__class__, user=user, request=self.request - ) - return HttpResponseRedirect(nonce.next_path or '/') - diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py index 23323b15a..d782a05fc 100644 --- a/apps/authentication/errors.py +++ b/apps/authentication/errors.py @@ -171,7 +171,7 @@ class MFARequiredError(NeedMoreInfoError): 'error': self.error, 'msg': self.msg, 'data': { - 'choices': ['otp'], + 'choices': ['code'], 'url': reverse('api-auth:mfa-challenge') } } diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 1c4fb5aa1..5b3738c98 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -62,8 +62,7 @@ class AuthMixin: password = request.POST.get('password', '') public_key = request.POST.get('public_key', '') user, error = check_user_valid( - username=username, password=password, - public_key=public_key + request=request, username=username, password=password, public_key=public_key ) ip = self.get_request_ip() if not user: diff --git a/apps/authentication/signals_handlers.py b/apps/authentication/signals_handlers.py index 2d73ae9b7..645b202c2 100644 --- a/apps/authentication/signals_handlers.py +++ b/apps/authentication/signals_handlers.py @@ -1,55 +1,15 @@ -from django.http.request import QueryDict -from django.conf import settings from django.dispatch import receiver -from django.contrib.auth.signals import user_logged_out -from django_auth_ldap.backend import populate_user -from oidc_rp.signals import oidc_user_created -from users.models import User -from .backends.openid import new_client -from .backends.openid.signals import ( - post_create_or_update_openid_user, post_openid_login_success -) -from .signals import post_auth_success +from jms_oidc_rp.signals import oidc_user_login_success, oidc_user_login_failed +from .signals import post_auth_success, post_auth_failed -@receiver(user_logged_out) -def on_user_logged_out(sender, request, user, **kwargs): - query = QueryDict('', mutable=True) - query.update({ - 'redirect_uri': settings.BASE_SITE_URL - }) - # openid (keycloak) - if settings.AUTH_OPENID and settings.AUTH_OPENID_SHARE_SESSION: - client = new_client() - end_session_endpoint = client.get_url_end_session_endpoint() - openid_logout_url = "%s?%s" % (end_session_endpoint, query.urlencode()) - request.COOKIES['next'] = openid_logout_url - return +@receiver(oidc_user_login_success) +def on_oidc_user_login_success(sender, request, user, **kwargs): + post_auth_success.send(sender, user=user, request=request) -@receiver(post_create_or_update_openid_user) -def on_post_create_or_update_openid_user(sender, user=None, created=True, **kwargs): - if created and user and user.username != 'admin': - user.source = user.SOURCE_OPENID - user.save() - -@receiver(post_openid_login_success) -def on_openid_login_success(sender, user=None, request=None, **kwargs): - post_auth_success.send(sender=sender, user=user, request=request) - - -@receiver(populate_user) -def on_ldap_create_user(sender, user, ldap_user, **kwargs): - if user and user.username not in ['admin']: - exists = User.objects.filter(username=user.username).exists() - if not exists: - user.source = user.SOURCE_LDAP - user.save() - - -@receiver(oidc_user_created) -def on_oidc_user_created(sender, request, oidc_user, **kwargs): - oidc_user.user.source = User.SOURCE_OPENID - oidc_user.user.save() +@receiver(oidc_user_login_failed) +def on_oidc_user_login_failed(sender, username, request, reason, **kwargs): + post_auth_failed.send(sender, username=username, request=request, reason=reason) diff --git a/apps/authentication/templates/authentication/login.html b/apps/authentication/templates/authentication/login.html index 21d775633..1f842d7a2 100644 --- a/apps/authentication/templates/authentication/login.html +++ b/apps/authentication/templates/authentication/login.html @@ -52,21 +52,14 @@ - {% if AUTH_OPENID or AUTH_OIDC_RP %} + {% if AUTH_OPENID %}

{% trans "More login options" %}

- {% if AUTH_OIDC_RP %} - - {% elif AUTH_OPENID %} - - {% endif %}
{% endif %} diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py index c33ea453c..6c6d110d1 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -16,7 +16,6 @@ urlpatterns = [ path('logout/', views.UserLogoutView.as_view(), name='logout'), # openid - path('openid/', include(('authentication.backends.openid.urls', 'authentication'), namespace='openid')), path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')), - path('oidc-rp/', include(('authentication.backends.oidc.urls', 'authentication'), namespace='oidc-rp')), + path('oidc/', include(('jms_oidc_rp.urls', 'authentication'), namespace='oidc')), ] diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index d27c4ccc3..c67cf2090 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -58,11 +58,9 @@ class UserLoginView(mixins.AuthMixin, FormView): if self.request.GET.get("admin", 0): return None if settings.AUTH_OPENID: - redirect_url = reverse("authentication:openid:openid-login") + redirect_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME) elif settings.AUTH_CAS: redirect_url = reverse(settings.CAS_LOGIN_URL_NAME) - elif settings.AUTH_OIDC_RP: - redirect_url = reverse(settings.OIDC_RP_LOGIN_URL_NAME) if redirect_url: query_string = request.GET.urlencode() @@ -113,7 +111,6 @@ class UserLoginView(mixins.AuthMixin, FormView): context = { 'demo_mode': os.environ.get("DEMO_MODE"), 'AUTH_OPENID': settings.AUTH_OPENID, - 'AUTH_OIDC_RP': settings.AUTH_OIDC_RP, } kwargs.update(context) return super().get_context_data(**kwargs) @@ -188,24 +185,18 @@ class UserLogoutView(TemplateView): @staticmethod def get_backend_logout_url(): + if settings.AUTH_OPENID: + return settings.AUTH_OPENID_AUTH_LOGOUT_URL_NAME # if settings.AUTH_CAS: # return settings.CAS_LOGOUT_URL_NAME - - # oidc rp - if settings.AUTH_OIDC_RP: - return reverse(settings.OIDC_RP_LOGOUT_URL_NAME) + return None def get(self, request, *args, **kwargs): - auth_logout(request) - backend_logout_url = self.get_backend_logout_url() if backend_logout_url: return redirect(backend_logout_url) - next_uri = request.COOKIES.get("next") - if next_uri: - return redirect(next_uri) - + auth_logout(request) response = super().get(request, *args, **kwargs) return response diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 68e5d6e86..2c0b5bf5b 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -8,6 +8,7 @@ 3. 程序需要, 用户需要更改的写到本config中 """ import os +import re import sys import types import errno @@ -15,6 +16,7 @@ import json import yaml from importlib import import_module from django.urls import reverse_lazy +from urllib.parse import urljoin, urlparse BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.dirname(BASE_DIR) @@ -36,6 +38,38 @@ def import_string(dotted_path): ) from err +def is_absolute_uri(uri): + """ 判断一个uri是否是绝对地址 """ + if not isinstance(uri, str): + return False + + result = re.match(r'^http[s]?://.*', uri) + if result is None: + return False + + return True + + +def build_absolute_uri(base, uri): + """ 构建绝对uri地址 """ + if uri is None: + return base + + if isinstance(uri, int): + uri = str(uri) + + if not isinstance(uri, str): + return base + + if is_absolute_uri(uri): + return uri + + parsed_base = urlparse(base) + url = "{}://{}".format(parsed_base.scheme, parsed_base.netloc) + path = '{}/{}/'.format(parsed_base.path.strip('/'), uri.strip('/')) + return urljoin(url, path) + + class DoesNotExist(Exception): pass @@ -134,26 +168,35 @@ class Config(dict): 'AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS': False, 'AUTH_LDAP_OPTIONS_OPT_REFERRALS': -1, + # OpenID 配置参数 + # OpenID 公有配置参数 (version <= 1.5.8 或 version >= 1.5.8) 'AUTH_OPENID': False, + 'AUTH_OPENID_CLIENT_ID': 'client-id', + 'AUTH_OPENID_CLIENT_SECRET': 'client-secret', + 'AUTH_OPENID_SHARE_SESSION': True, + 'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True, + # OpenID 新配置参数 (version >= 1.5.8) + 'AUTH_OPENID_PROVIDER_ENDPOINT': 'https://op-example.com/', + 'AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT': 'https://op-example.com/authorize', + 'AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT': 'https://op-example.com/token', + 'AUTH_OPENID_PROVIDER_JWKS_ENDPOINT': 'https://op-example.com/jwks', + 'AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT': 'https://op-example.com/userinfo', + 'AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT': 'https://op-example.com/logout', + 'AUTH_OPENID_PROVIDER_SIGNATURE_ALG': 'HS256', + 'AUTH_OPENID_PROVIDER_SIGNATURE_KEY': None, + 'AUTH_OPENID_PROVIDER_CLAIMS_NAME': None, + 'AUTH_OPENID_PROVIDER_CLAIMS_USERNAME': None, + 'AUTH_OPENID_PROVIDER_CLAIMS_EMAIL': None, + 'AUTH_OPENID_SCOPES': 'openid profile email', + 'AUTH_OPENID_ID_TOKEN_MAX_AGE': 60, + 'AUTH_OPENID_ID_TOKEN_INCLUDE_USERINFO': True, + 'AUTH_OPENID_USE_STATE': True, + 'AUTH_OPENID_USE_NONCE': True, + 'AUTH_OPENID_ALWAYS_UPDATE_USER_INFORMATION': True, + # OpenID 旧配置参数 (version <= 1.5.8 (discarded)) 'BASE_SITE_URL': 'http://localhost:8080', 'AUTH_OPENID_SERVER_URL': 'http://openid', - 'AUTH_OPENID_REALM_NAME': 'jumpserver', - 'AUTH_OPENID_CLIENT_ID': 'jumpserver', - 'AUTH_OPENID_CLIENT_SECRET': '', - 'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True, - 'AUTH_OPENID_SHARE_SESSION': True, - - - 'AUTH_OIDC_RP': False, - 'OIDC_RP_CLIENT_ID': 'client-id', - 'OIDC_RP_CLIENT_SECRET': 'client-secret', - 'OIDC_RP_PROVIDER_ENDPOINT': 'https://op-endpoint.com', - 'OIDC_RP_PROVIDER_AUTHORIZATION_ENDPOINT': 'https://op-endpoint.com/authorize', - 'OIDC_RP_PROVIDER_TOKEN_ENDPOINT': 'https://op-endpoint.com/token', - 'OIDC_RP_PROVIDER_JWKS_ENDPOINT': 'https://op-endpoint.com/jwk', - 'OIDC_RP_PROVIDER_USERINFO_ENDPOINT': 'https://op-endpoint.com/userinfo', - 'OIDC_RP_PROVIDER_END_SESSION_ENDPOINT': 'https://op-endpoint.com/logout', - 'OIDC_RP_ID_TOKEN_MAX_AGE': 60, + 'AUTH_OPENID_REALM_NAME': None, 'AUTH_RADIUS': False, 'RADIUS_SERVER': 'localhost', @@ -217,6 +260,88 @@ class Config(dict): 'ORG_CHANGE_TO_URL': '' } + def compatible_auth_openid_of_key(self): + """ + 兼容OpenID旧配置 (即 version <= 1.5.8) + 因为旧配置只支持OpenID协议的Keycloak实现, + 所以只需要根据旧配置和Keycloak的Endpoint说明文档, + 构造出新配置中标准OpenID协议中所需的Endpoint即可 + (Keycloak说明文档参考: https://www.keycloak.org/docs/latest/securing_apps/) + """ + if not self.AUTH_OPENID: + return + + realm_name = self.AUTH_OPENID_REALM_NAME + if realm_name is None: + return + + compatible_keycloak_config = [ + ( + 'AUTH_OPENID_PROVIDER_ENDPOINT', + self.AUTH_OPENID_SERVER_URL + ), + ( + 'AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT', + '/realms/{}/protocol/openid-connect/auth'.format(realm_name) + ), + ( + 'AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT', + '/realms/{}/protocol/openid-connect/token'.format(realm_name) + ), + ( + 'AUTH_OPENID_PROVIDER_JWKS_ENDPOINT', + '/realms/{}/protocol/openid-connect/certs'.format(realm_name) + ), + ( + 'AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT', + '/realms/{}/protocol/openid-connect/userinfo'.format(realm_name) + ), + ( + 'AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT', + '/realms/{}/protocol/openid-connect/logout'.format(realm_name) + ) + ] + for key, value in compatible_keycloak_config: + self[key] = value + + def compatible_auth_openid_of_value(self): + """ + 兼容值的绝对路径、相对路径 + (key 为 AUTH_OPENID_PROVIDER_*_ENDPOINT 的配置) + """ + if not self.AUTH_OPENID: + return + + base = self.AUTH_OPENID_PROVIDER_ENDPOINT + config = list(self.items()) + for key, value in config: + result = re.match(r'^AUTH_OPENID_PROVIDER_.*_ENDPOINT$', key) + if result is None: + continue + if value is None: + # None 在 url 中有特殊含义 (比如对于: end_session_endpoint) + continue + value = build_absolute_uri(base, value) + self[key] = value + + def compatible(self): + """ + 对配置做兼容处理 + 1. 对`key`的兼容 (例如:版本升级) + 2. 对`value`做兼容 (例如:True、true、1 => True) + + 处理顺序要保持先对key做处理, 再对value做处理, + 因为处理value的时候,只根据最新版本支持的key进行 + """ + parts = ['key', 'value'] + targets = ['auth_openid'] + for part in parts: + for target in targets: + method_name = 'compatible_{}_of_{}'.format(target, part) + method = getattr(self, method_name, None) + if method is not None: + method() + def convert_type(self, k, v): default_value = self.defaults.get(k) if default_value is None: @@ -305,11 +430,8 @@ class DynamicConfig: if self.static_config.get('AUTH_CAS'): backends.insert(0, 'authentication.backends.cas.CASBackend') if self.static_config.get('AUTH_OPENID'): - backends.insert(0, 'authentication.backends.openid.backends.OpenIDAuthorizationPasswordBackend') - backends.insert(0, 'authentication.backends.openid.backends.OpenIDAuthorizationCodeBackend') - if self.static_config.get('AUTH_OIDC_RP'): - backends.insert(0, 'authentication.backends.oidc.backends.OIDCAuthCodeBackend') - backends.insert(0, 'authentication.backends.oidc.backends.OIDCAuthPasswordBackend',) + backends.insert(0, 'jms_oidc_rp.backends.OIDCAuthCodeBackend') + backends.insert(0, 'jms_oidc_rp.backends.OIDCAuthPasswordBackend') if self.static_config.get('AUTH_RADIUS'): backends.insert(0, 'authentication.backends.radius.RadiusBackend') return backends @@ -490,9 +612,9 @@ class ConfigManager: manager = cls(root_path=root_path) if manager.load_from_object(): - return manager.config + config = manager.config elif manager.load_from_yml(): - return manager.config + config = manager.config else: msg = """ @@ -502,6 +624,10 @@ class ConfigManager: """ raise ImportError(msg) + # 对config进行兼容处理 + config.compatible() + return config + @classmethod def get_dynamic_config(cls, config): return DynamicConfig(config) diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index e46362638..4bb431bd5 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -2,7 +2,6 @@ # import os import ldap -from django.urls import reverse_lazy from ..const import CONFIG, DYNAMIC, PROJECT_DIR @@ -43,39 +42,37 @@ AUTH_LDAP_SYNC_INTERVAL = CONFIG.AUTH_LDAP_SYNC_INTERVAL AUTH_LDAP_SYNC_CRONTAB = CONFIG.AUTH_LDAP_SYNC_CRONTAB AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS = CONFIG.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS -# openid -# Auth OpenID settings -BASE_SITE_URL = CONFIG.BASE_SITE_URL + +# ============================================================================== +# 认证 OpenID 配置参数 +# 参考: https://django-oidc-rp.readthedocs.io/en/stable/settings.html +# ============================================================================== 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_IGNORE_SSL_VERIFICATION = CONFIG.AUTH_OPENID_IGNORE_SSL_VERIFICATION +AUTH_OPENID_PROVIDER_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_ENDPOINT +AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT +AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT +AUTH_OPENID_PROVIDER_JWKS_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_JWKS_ENDPOINT +AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT +AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT +AUTH_OPENID_PROVIDER_SIGNATURE_ALG = CONFIG.AUTH_OPENID_PROVIDER_SIGNATURE_ALG +AUTH_OPENID_PROVIDER_SIGNATURE_KEY = CONFIG.AUTH_OPENID_PROVIDER_SIGNATURE_KEY +AUTH_OPENID_PROVIDER_CLAIMS_NAME = CONFIG.AUTH_OPENID_PROVIDER_CLAIMS_NAME +AUTH_OPENID_PROVIDER_CLAIMS_USERNAME = CONFIG.AUTH_OPENID_PROVIDER_CLAIMS_USERNAME +AUTH_OPENID_PROVIDER_CLAIMS_EMAIL = CONFIG.AUTH_OPENID_PROVIDER_CLAIMS_EMAIL +AUTH_OPENID_SCOPES = CONFIG.AUTH_OPENID_SCOPES +AUTH_OPENID_ID_TOKEN_MAX_AGE = CONFIG.AUTH_OPENID_ID_TOKEN_MAX_AGE +AUTH_OPENID_ID_TOKEN_INCLUDE_USERINFO = CONFIG.AUTH_OPENID_ID_TOKEN_INCLUDE_USERINFO AUTH_OPENID_SHARE_SESSION = CONFIG.AUTH_OPENID_SHARE_SESSION -AUTH_OPENID_LOGIN_URL = reverse_lazy("authentication:openid:openid-login") -AUTH_OPENID_LOGIN_COMPLETE_URL = reverse_lazy("authentication:openid:openid-login-complete") - -# oidc rp -# jumpserver -AUTH_OIDC_RP = CONFIG.AUTH_OIDC_RP -if AUTH_OIDC_RP: - # 优先使用AUTH_OIDC_RP - AUTH_OPENID = False -OIDC_RP_LOGIN_URL_NAME = "authentication:oidc-rp:oidc-login" -OIDC_RP_LOGIN_CALLBACK_URL_NAME = "authentication:oidc-rp:oidc-callback" -OIDC_RP_LOGOUT_URL_NAME = "authentication:oidc-rp:oidc-logout" -# https://django-oidc-rp.readthedocs.io/en/stable/settings.html#required-settings -OIDC_RP_CLIENT_ID = CONFIG.OIDC_RP_CLIENT_ID -OIDC_RP_CLIENT_SECRET = CONFIG.OIDC_RP_CLIENT_SECRET -OIDC_RP_PROVIDER_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_ENDPOINT -OIDC_RP_PROVIDER_AUTHORIZATION_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_AUTHORIZATION_ENDPOINT -OIDC_RP_PROVIDER_TOKEN_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_TOKEN_ENDPOINT -OIDC_RP_PROVIDER_JWKS_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_JWKS_ENDPOINT -OIDC_RP_PROVIDER_USERINFO_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_USERINFO_ENDPOINT -OIDC_RP_PROVIDER_END_SESSION_ENDPOINT = CONFIG.OIDC_RP_PROVIDER_END_SESSION_ENDPOINT -OIDC_RP_ID_TOKEN_MAX_AGE = CONFIG.OIDC_RP_ID_TOKEN_MAX_AGE - +AUTH_OPENID_IGNORE_SSL_VERIFICATION = CONFIG.AUTH_OPENID_IGNORE_SSL_VERIFICATION +AUTH_OPENID_USE_STATE = CONFIG.AUTH_OPENID_USE_STATE +AUTH_OPENID_USE_NONCE = CONFIG.AUTH_OPENID_USE_NONCE +AUTH_OPENID_ALWAYS_UPDATE_USER_INFORMATION = CONFIG.AUTH_OPENID_ALWAYS_UPDATE_USER_INFORMATION +AUTH_OPENID_AUTH_LOGIN_URL_NAME = 'authentication:oidc:login' +AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME = 'authentication:oidc:login-callback' +AUTH_OPENID_AUTH_LOGOUT_URL_NAME = 'authentication:oidc:logout' +# ============================================================================== # Radius Auth AUTH_RADIUS = CONFIG.AUTH_RADIUS diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index c2b558742..d1fbb5a36 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -48,6 +48,7 @@ INSTALLED_APPS = [ 'authentication.apps.AuthenticationConfig', # authentication 'applications.apps.ApplicationsConfig', 'tickets.apps.TicketsConfig', + 'jms_oidc_rp', 'rest_framework', 'rest_framework_swagger', 'drf_yasg', @@ -56,7 +57,6 @@ INSTALLED_APPS = [ 'django_filters', 'bootstrap3', 'captcha', - 'oidc_rp', 'django_celery_beat', 'django.contrib.auth', 'django.contrib.admin', @@ -76,13 +76,12 @@ MIDDLEWARE = [ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'authentication.backends.openid.middleware.OpenIDAuthenticationMiddleware', + 'jms_oidc_rp.middleware.OIDCRefreshIDTokenMiddleware', 'django_cas_ng.middleware.CASMiddleware', 'jumpserver.middleware.TimezoneMiddleware', 'jumpserver.middleware.DemoMiddleware', 'jumpserver.middleware.RequestMiddleware', 'orgs.middleware.OrgMiddleware', - 'oidc_rp.middleware.OIDCRefreshIDTokenMiddleware', ] @@ -105,7 +104,7 @@ TEMPLATES = [ 'django.template.context_processors.media', 'jumpserver.context_processor.jumpserver_processor', 'orgs.context_processor.org_processor', - 'oidc_rp.context_processors.oidc', + 'jms_oidc_rp.context_processors.oidc', ], }, }, diff --git a/apps/jumpserver/utils.py b/apps/jumpserver/utils.py index aa808d2f2..72a836892 100644 --- a/apps/jumpserver/utils.py +++ b/apps/jumpserver/utils.py @@ -18,4 +18,3 @@ def get_current_request(): current_request = LocalProxy(partial(_find, 'current_request')) - diff --git a/apps/users/signals_handler.py b/apps/users/signals_handler.py index aa1458c7d..17e11af84 100644 --- a/apps/users/signals_handler.py +++ b/apps/users/signals_handler.py @@ -3,12 +3,19 @@ from django.dispatch import receiver from django.db.models.signals import m2m_changed +from django_auth_ldap.backend import populate_user +from django.conf import settings from django_cas_ng.signals import cas_user_authenticated +from jms_oidc_rp.signals import oidc_user_created, oidc_user_updated +from jms_oidc_rp.backends import get_userinfo_from_claims + from common.utils import get_logger +from .utils import construct_user_email from .signals import post_user_create from .models import User + logger = get_logger(__file__) @@ -37,3 +44,30 @@ def on_cas_user_authenticated(sender, user, created, **kwargs): if created: user.source = user.SOURCE_CAS user.save() + + +@receiver(populate_user) +def on_ldap_create_user(sender, user, ldap_user, **kwargs): + if user and user.username not in ['admin']: + exists = User.objects.filter(username=user.username).exists() + if not exists: + user.source = user.SOURCE_LDAP + user.save() + + +@receiver(oidc_user_created) +def on_oidc_user_created(sender, request, oidc_user, **kwargs): + oidc_user.user.source = User.SOURCE_OPENID + oidc_user.user.save() + + +@receiver(oidc_user_updated) +def on_oidc_user_updated(sender, request, oidc_user, **kwargs): + if not settings.AUTH_OPENID_ALWAYS_UPDATE_USER_INFORMATION: + return + name, username, email = get_userinfo_from_claims(oidc_user.userinfo) + email = construct_user_email(username, email) + oidc_user.user.name = name + oidc_user.user.username = username + oidc_user.user.email = email + oidc_user.user.save() diff --git a/apps/users/utils.py b/apps/users/utils.py index 9488b5877..0729115b6 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -326,7 +326,7 @@ def get_source_choices(): ] if settings.AUTH_LDAP: choices.append((User.SOURCE_LDAP, choices_all[User.SOURCE_LDAP])) - if settings.AUTH_OPENID or settings.AUTH_OIDC_RP: + if settings.AUTH_OPENID: choices.append((User.SOURCE_OPENID, choices_all[User.SOURCE_OPENID])) if settings.AUTH_RADIUS: choices.append((User.SOURCE_RADIUS, choices_all[User.SOURCE_RADIUS])) diff --git a/config_example.yml b/config_example.yml index cec3b7eb6..a2e7ccf1a 100644 --- a/config_example.yml +++ b/config_example.yml @@ -29,7 +29,6 @@ BOOTSTRAP_TOKEN: # 使用单文件sqlite数据库 # DB_ENGINE: sqlite3 # DB_NAME: - # MySQL or postgres setting like: # 使用Mysql作为数据库 DB_ENGINE: mysql @@ -55,11 +54,7 @@ REDIS_PORT: 6379 # REDIS_DB_CACHE: 4 # Use OpenID authorization -# -# 配置说明: 如果您使用的是Keycloak作为OP,可以使用方式1或方式2; 如果OP不是Keycloak, 请使用方式2 -# -# 方式1: OpenID认证 (基于 oidc 协议的 keycloak 的实现) -# +# 使用OpenID 来进行认证设置 # BASE_SITE_URL: http://localhost:8080 # AUTH_OPENID: false # True or False # AUTH_OPENID_SERVER_URL: https://openid-auth-server.com/ @@ -68,19 +63,6 @@ REDIS_PORT: 6379 # AUTH_OPENID_CLIENT_SECRET: client-secret # AUTH_OPENID_IGNORE_SSL_VERIFICATION: True # AUTH_OPENID_SHARE_SESSION: True -# -# 方式2: OpenID认证 (使用标准 oidc 协议进行认证) -# 配置参数详细信息参考: https://django-oidc-rp.readthedocs.io/en/stable/settings.html -# -# AUTH_OIDC_RP: False -# OIDC_RP_CLIENT_ID: client-id -# OIDC_RP_CLIENT_SECRET: client-secret -# OIDC_RP_PROVIDER_ENDPOINT: https://op-endpoint.com -# OIDC_RP_PROVIDER_AUTHORIZATION_ENDPOINT: https://op-endpoint.com/authorize -# OIDC_RP_PROVIDER_TOKEN_ENDPOINT: https://op-endpoint.com/token -# OIDC_RP_PROVIDER_JWKS_ENDPOINT: https://op-endpoint.com/jwk -# OIDC_RP_PROVIDER_USERINFO_ENDPOINT: https://op-endpoint.com/userinfo -# OIDC_RP_PROVIDER_END_SESSION_ENDPOINT: https://op-endpoint.com/logout # Use Radius authorization # 使用Radius来认证 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 467a04fcb..81019d249 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -74,8 +74,6 @@ Werkzeug==0.15.3 drf-nested-routers==0.91 aliyun-python-sdk-core-v3==2.9.1 aliyun-python-sdk-ecs==4.10.1 -python-keycloak==0.13.3 -python-keycloak-client==0.1.3 rest_condition==1.0.3 python-ldap==3.1.0 tencentcloud-sdk-python==3.0.40 @@ -98,4 +96,4 @@ ipython huaweicloud-sdk-python==1.0.21 django-redis==4.11.0 python-redis-lock==3.5.0 -django-oidc-rp==0.3.4 +jumpserver-django-oidc-rp==0.3.7.1 From 6e18383531267ed0e772f77b89519dd35cf05a8a Mon Sep 17 00:00:00 2001 From: Michael Bai Date: Sun, 26 Apr 2020 22:40:56 +0800 Subject: [PATCH 09/30] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9Dockerfile?= =?UTF-8?q?=20pypi=20=E9=95=9C=E5=83=8F=E6=BA=90(=E9=98=BF=E9=87=8C=3D>?= =?UTF-8?q?=E6=B8=85=E5=8D=8E)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a45e41a45..6569db2a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ RUN yum -y install epel-release && \ echo -e "[mysql]\nname=mysql\nbaseurl=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo RUN cd /tmp/requirements && yum -y install $(cat rpm_requirements.txt) RUN cd /tmp/requirements && pip install --upgrade pip setuptools && pip install wheel && \ - pip install -i https://mirrors.aliyun.com/pypi/simple/ -r requirements.txt || pip install -r requirements.txt + pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt || pip install -r requirements.txt RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config COPY . /opt/jumpserver From a505995f49317472f2e468e030493c81887273ba Mon Sep 17 00:00:00 2001 From: Bai Date: Mon, 27 Apr 2020 11:36:11 +0800 Subject: [PATCH 10/30] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9config=5Fexa?= =?UTF-8?q?mple(openid)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backends/{oidc/__init__.py => openid.py} | 0 config_example.yml | 38 +++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) rename apps/authentication/backends/{oidc/__init__.py => openid.py} (100%) diff --git a/apps/authentication/backends/oidc/__init__.py b/apps/authentication/backends/openid.py similarity index 100% rename from apps/authentication/backends/oidc/__init__.py rename to apps/authentication/backends/openid.py diff --git a/config_example.yml b/config_example.yml index a2e7ccf1a..3f99d609f 100644 --- a/config_example.yml +++ b/config_example.yml @@ -53,16 +53,48 @@ REDIS_PORT: 6379 # REDIS_DB_CELERY: 3 # REDIS_DB_CACHE: 4 -# Use OpenID authorization -# 使用OpenID 来进行认证设置 +# Use OpenID Authorization +# 使用 OpenID 进行认证设置 +# +# 配置方式1: +# 1. 版本 <= 1.5.8 +# 2. OpenID Provider 是 Keycloak +# # BASE_SITE_URL: http://localhost:8080 -# AUTH_OPENID: false # True or False +# 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 +# AUTH_OPENID_SHARE_SESSION: True # AUTH_OPENID_IGNORE_SSL_VERIFICATION: True +# +# 配置方式2: (version >=1.5.8) +# 1. 版本 >= 1.5.8 +# 2. 支持标准 OpenID Connect Provider +# +# AUTH_OPENID: False # True or False +# AUTH_OPENID_CLIENT_ID: client-id +# AUTH_OPENID_CLIENT_SECRET: client-secret # AUTH_OPENID_SHARE_SESSION: True +# AUTH_OPENID_IGNORE_SSL_VERIFICATION: True +# AUTH_OPENID_PROVIDER_ENDPOINT: https://op-example.com/ +# AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT: https://op-example.com/authorize +# AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT: https://op-example.com/token +# AUTH_OPENID_PROVIDER_JWKS_ENDPOINT: https://op-example.com/jwks +# AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT: https://op-example.com/userinfo +# AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT: https://op-example.com/logout +# AUTH_OPENID_PROVIDER_SIGNATURE_ALG: HS256 +# AUTH_OPENID_PROVIDER_SIGNATURE_KEY: None +# AUTH_OPENID_PROVIDER_CLAIMS_NAME: None +# AUTH_OPENID_PROVIDER_CLAIMS_USERNAME: None +# AUTH_OPENID_PROVIDER_CLAIMS_EMAIL: None +# AUTH_OPENID_SCOPES: "openid profile email" +# AUTH_OPENID_ID_TOKEN_MAX_AGE: 60 +# AUTH_OPENID_ID_TOKEN_INCLUDE_USERINFO: True +# AUTH_OPENID_USE_STATE: True +# AUTH_OPENID_USE_NONCE: True +# AUTH_OPENID_ALWAYS_UPDATE_USER_INFORMATION: True # Use Radius authorization # 使用Radius来认证 From 184432a2a66c27363337d6717ca0ace5c7ea8b96 Mon Sep 17 00:00:00 2001 From: Bai Date: Tue, 28 Apr 2020 21:00:22 +0800 Subject: [PATCH 11/30] =?UTF-8?q?[Update]=20=E6=9B=B4=E6=96=B0OpenID?= =?UTF-8?q?=E7=9A=84=E9=85=8D=E7=BD=AE=E9=A1=B9=E4=BB=A5=E5=8F=8A=E5=AF=B9?= =?UTF-8?q?=E5=BA=94=E7=9A=84=E4=BF=A1=E5=8F=B7=E7=9B=91=E5=90=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/audits/signals_handler.py | 4 ++-- apps/authentication/signals_handlers.py | 6 ++--- apps/jumpserver/conf.py | 7 ++---- apps/jumpserver/settings/auth.py | 7 ++---- apps/users/signals_handler.py | 30 ++++++++++--------------- 5 files changed, 21 insertions(+), 33 deletions(-) diff --git a/apps/audits/signals_handler.py b/apps/audits/signals_handler.py index dab56fa5c..b95f6fbdf 100644 --- a/apps/audits/signals_handler.py +++ b/apps/audits/signals_handler.py @@ -136,8 +136,8 @@ def on_user_auth_success(sender, user, request, **kwargs): @receiver(post_auth_failed) -def on_user_auth_failed(sender, username, request, reason, **kwargs): +def on_user_auth_failed(sender, username, request, reason='', **kwargs): logger.debug('User login failed: {}'.format(username)) data = generate_data(username, request) - data.update({'reason': reason, 'status': False}) + data.update({'reason': reason[:128], 'status': False}) write_login_log(**data) diff --git a/apps/authentication/signals_handlers.py b/apps/authentication/signals_handlers.py index 645b202c2..461ddbb99 100644 --- a/apps/authentication/signals_handlers.py +++ b/apps/authentication/signals_handlers.py @@ -1,15 +1,15 @@ from django.dispatch import receiver -from jms_oidc_rp.signals import oidc_user_login_success, oidc_user_login_failed +from jms_oidc_rp.signals import openid_user_login_failed, openid_user_login_success from .signals import post_auth_success, post_auth_failed -@receiver(oidc_user_login_success) +@receiver(openid_user_login_success) def on_oidc_user_login_success(sender, request, user, **kwargs): post_auth_success.send(sender, user=user, request=request) -@receiver(oidc_user_login_failed) +@receiver(openid_user_login_failed) def on_oidc_user_login_failed(sender, username, request, reason, **kwargs): post_auth_failed.send(sender, username=username, request=request, reason=reason) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 2c0b5bf5b..26a34fed2 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -184,15 +184,12 @@ class Config(dict): 'AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT': 'https://op-example.com/logout', 'AUTH_OPENID_PROVIDER_SIGNATURE_ALG': 'HS256', 'AUTH_OPENID_PROVIDER_SIGNATURE_KEY': None, - 'AUTH_OPENID_PROVIDER_CLAIMS_NAME': None, - 'AUTH_OPENID_PROVIDER_CLAIMS_USERNAME': None, - 'AUTH_OPENID_PROVIDER_CLAIMS_EMAIL': None, 'AUTH_OPENID_SCOPES': 'openid profile email', 'AUTH_OPENID_ID_TOKEN_MAX_AGE': 60, - 'AUTH_OPENID_ID_TOKEN_INCLUDE_USERINFO': True, + 'AUTH_OPENID_ID_TOKEN_INCLUDE_CLAIMS': True, 'AUTH_OPENID_USE_STATE': True, 'AUTH_OPENID_USE_NONCE': True, - 'AUTH_OPENID_ALWAYS_UPDATE_USER_INFORMATION': True, + 'AUTH_OPENID_ALWAYS_UPDATE_USER': True, # OpenID 旧配置参数 (version <= 1.5.8 (discarded)) 'BASE_SITE_URL': 'http://localhost:8080', 'AUTH_OPENID_SERVER_URL': 'http://openid', diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 4bb431bd5..d25a4b7bc 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -58,17 +58,14 @@ AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_USERINFO_EN AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT AUTH_OPENID_PROVIDER_SIGNATURE_ALG = CONFIG.AUTH_OPENID_PROVIDER_SIGNATURE_ALG AUTH_OPENID_PROVIDER_SIGNATURE_KEY = CONFIG.AUTH_OPENID_PROVIDER_SIGNATURE_KEY -AUTH_OPENID_PROVIDER_CLAIMS_NAME = CONFIG.AUTH_OPENID_PROVIDER_CLAIMS_NAME -AUTH_OPENID_PROVIDER_CLAIMS_USERNAME = CONFIG.AUTH_OPENID_PROVIDER_CLAIMS_USERNAME -AUTH_OPENID_PROVIDER_CLAIMS_EMAIL = CONFIG.AUTH_OPENID_PROVIDER_CLAIMS_EMAIL AUTH_OPENID_SCOPES = CONFIG.AUTH_OPENID_SCOPES AUTH_OPENID_ID_TOKEN_MAX_AGE = CONFIG.AUTH_OPENID_ID_TOKEN_MAX_AGE -AUTH_OPENID_ID_TOKEN_INCLUDE_USERINFO = CONFIG.AUTH_OPENID_ID_TOKEN_INCLUDE_USERINFO +AUTH_OPENID_ID_TOKEN_INCLUDE_CLAIMS = CONFIG.AUTH_OPENID_ID_TOKEN_INCLUDE_CLAIMS AUTH_OPENID_SHARE_SESSION = CONFIG.AUTH_OPENID_SHARE_SESSION AUTH_OPENID_IGNORE_SSL_VERIFICATION = CONFIG.AUTH_OPENID_IGNORE_SSL_VERIFICATION AUTH_OPENID_USE_STATE = CONFIG.AUTH_OPENID_USE_STATE AUTH_OPENID_USE_NONCE = CONFIG.AUTH_OPENID_USE_NONCE -AUTH_OPENID_ALWAYS_UPDATE_USER_INFORMATION = CONFIG.AUTH_OPENID_ALWAYS_UPDATE_USER_INFORMATION +AUTH_OPENID_ALWAYS_UPDATE_USER = CONFIG.AUTH_OPENID_ALWAYS_UPDATE_USER AUTH_OPENID_AUTH_LOGIN_URL_NAME = 'authentication:oidc:login' AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME = 'authentication:oidc:login-callback' AUTH_OPENID_AUTH_LOGOUT_URL_NAME = 'authentication:oidc:logout' diff --git a/apps/users/signals_handler.py b/apps/users/signals_handler.py index 17e11af84..191249296 100644 --- a/apps/users/signals_handler.py +++ b/apps/users/signals_handler.py @@ -7,11 +7,9 @@ from django_auth_ldap.backend import populate_user from django.conf import settings from django_cas_ng.signals import cas_user_authenticated -from jms_oidc_rp.signals import oidc_user_created, oidc_user_updated -from jms_oidc_rp.backends import get_userinfo_from_claims +from jms_oidc_rp.signals import openid_user_create_or_update from common.utils import get_logger -from .utils import construct_user_email from .signals import post_user_create from .models import User @@ -55,19 +53,15 @@ def on_ldap_create_user(sender, user, ldap_user, **kwargs): user.save() -@receiver(oidc_user_created) -def on_oidc_user_created(sender, request, oidc_user, **kwargs): - oidc_user.user.source = User.SOURCE_OPENID - oidc_user.user.save() - - -@receiver(oidc_user_updated) -def on_oidc_user_updated(sender, request, oidc_user, **kwargs): - if not settings.AUTH_OPENID_ALWAYS_UPDATE_USER_INFORMATION: +@receiver(openid_user_create_or_update) +def on_openid_user_create_or_update(sender, request, user, created, name, username, email): + if created: + user.source = User.SOURCE_OPENID + user.save() return - name, username, email = get_userinfo_from_claims(oidc_user.userinfo) - email = construct_user_email(username, email) - oidc_user.user.name = name - oidc_user.user.username = username - oidc_user.user.email = email - oidc_user.user.save() + + if not created and settings.AUTH_OPENID_ALWAYS_UPDATE_USER: + user.name = name + user.username = username + user.email = email + user.save() From 87242c13a13d87934859838d5c059d787bec7a7a Mon Sep 17 00:00:00 2001 From: Bai Date: Tue, 28 Apr 2020 21:06:13 +0800 Subject: [PATCH 12/30] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9=E4=BF=A1?= =?UTF-8?q?=E5=8F=B7=E7=9B=91=E5=90=ACkwargs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/users/signals_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/users/signals_handler.py b/apps/users/signals_handler.py index 191249296..622f4a9f8 100644 --- a/apps/users/signals_handler.py +++ b/apps/users/signals_handler.py @@ -54,7 +54,7 @@ def on_ldap_create_user(sender, user, ldap_user, **kwargs): @receiver(openid_user_create_or_update) -def on_openid_user_create_or_update(sender, request, user, created, name, username, email): +def on_openid_user_create_or_update(sender, request, user, created, name, username, email, **kwargs): if created: user.source = User.SOURCE_OPENID user.save() From c0089a98f41caf9874a598ba32dfe99c5f615287 Mon Sep 17 00:00:00 2001 From: Bai Date: Tue, 28 Apr 2020 22:29:56 +0800 Subject: [PATCH 13/30] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9openid?= =?UTF-8?q?=E4=BF=A1=E5=8F=B7=E5=90=8D=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/users/signals_handler.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/users/signals_handler.py b/apps/users/signals_handler.py index 622f4a9f8..9d6c96c02 100644 --- a/apps/users/signals_handler.py +++ b/apps/users/signals_handler.py @@ -7,7 +7,7 @@ from django_auth_ldap.backend import populate_user from django.conf import settings from django_cas_ng.signals import cas_user_authenticated -from jms_oidc_rp.signals import openid_user_create_or_update +from jms_oidc_rp.signals import openid_create_or_update_user from common.utils import get_logger from .signals import post_user_create @@ -53,14 +53,12 @@ def on_ldap_create_user(sender, user, ldap_user, **kwargs): user.save() -@receiver(openid_user_create_or_update) -def on_openid_user_create_or_update(sender, request, user, created, name, username, email, **kwargs): +@receiver(openid_create_or_update_user) +def on_openid_create_or_update_user(sender, request, user, created, name, username, email, **kwargs): if created: user.source = User.SOURCE_OPENID user.save() - return - - if not created and settings.AUTH_OPENID_ALWAYS_UPDATE_USER: + elif not created and settings.AUTH_OPENID_ALWAYS_UPDATE_USER: user.name = name user.username = username user.email = email From 9eee79f7d4b13855d687e12511617d317cad7b3a Mon Sep 17 00:00:00 2001 From: Bai Date: Wed, 29 Apr 2020 00:43:54 +0800 Subject: [PATCH 14/30] =?UTF-8?q?[Update]=20=E8=B0=83=E6=95=B4openid=20bac?= =?UTF-8?q?kend=E9=A1=BA=E5=BA=8F=EF=BC=9Bopenid=20=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=88=9B=E5=BB=BA/=E6=9B=B4=E6=96=B0=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/conf.py | 2 +- apps/users/signals_handler.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 26a34fed2..2b68bfdc5 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -427,8 +427,8 @@ class DynamicConfig: if self.static_config.get('AUTH_CAS'): backends.insert(0, 'authentication.backends.cas.CASBackend') if self.static_config.get('AUTH_OPENID'): - backends.insert(0, 'jms_oidc_rp.backends.OIDCAuthCodeBackend') backends.insert(0, 'jms_oidc_rp.backends.OIDCAuthPasswordBackend') + backends.insert(0, 'jms_oidc_rp.backends.OIDCAuthCodeBackend') if self.static_config.get('AUTH_RADIUS'): backends.insert(0, 'authentication.backends.radius.RadiusBackend') return backends diff --git a/apps/users/signals_handler.py b/apps/users/signals_handler.py index 9d6c96c02..37f7c42fc 100644 --- a/apps/users/signals_handler.py +++ b/apps/users/signals_handler.py @@ -56,9 +56,18 @@ def on_ldap_create_user(sender, user, ldap_user, **kwargs): @receiver(openid_create_or_update_user) def on_openid_create_or_update_user(sender, request, user, created, name, username, email, **kwargs): if created: + logger.debug( + "Receive OpenID user created signal: {}, " + "Set user source is: {}".format(user, User.SOURCE_OPENID) + ) user.source = User.SOURCE_OPENID user.save() elif not created and settings.AUTH_OPENID_ALWAYS_UPDATE_USER: + logger.debug( + "Receive OpenID user updated signal: {}, " + "Update user info: {}" + "".format(user, "name: {}|username: {}|email: {}".format(name, username, email)) + ) user.name = name user.username = username user.email = email From 23f9454e5dc343da825f3eab529881f98508bab7 Mon Sep 17 00:00:00 2001 From: Bai Date: Wed, 29 Apr 2020 01:12:00 +0800 Subject: [PATCH 15/30] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9Url=20name?= =?UTF-8?q?=20(oidc=20=3D>=20openi)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/templates/authentication/login.html | 2 +- apps/authentication/urls/view_urls.py | 2 +- apps/jumpserver/settings/auth.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/authentication/templates/authentication/login.html b/apps/authentication/templates/authentication/login.html index 1f842d7a2..9cacdf4ff 100644 --- a/apps/authentication/templates/authentication/login.html +++ b/apps/authentication/templates/authentication/login.html @@ -56,7 +56,7 @@

{% trans "More login options" %}

- diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py index 6c6d110d1..bee5f8517 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -17,5 +17,5 @@ urlpatterns = [ # openid path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')), - path('oidc/', include(('jms_oidc_rp.urls', 'authentication'), namespace='oidc')), + path('openid/', include(('jms_oidc_rp.urls', 'authentication'), namespace='openid')), ] diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index d25a4b7bc..920fdb119 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -66,9 +66,9 @@ AUTH_OPENID_IGNORE_SSL_VERIFICATION = CONFIG.AUTH_OPENID_IGNORE_SSL_VERIFICATION AUTH_OPENID_USE_STATE = CONFIG.AUTH_OPENID_USE_STATE AUTH_OPENID_USE_NONCE = CONFIG.AUTH_OPENID_USE_NONCE AUTH_OPENID_ALWAYS_UPDATE_USER = CONFIG.AUTH_OPENID_ALWAYS_UPDATE_USER -AUTH_OPENID_AUTH_LOGIN_URL_NAME = 'authentication:oidc:login' -AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME = 'authentication:oidc:login-callback' -AUTH_OPENID_AUTH_LOGOUT_URL_NAME = 'authentication:oidc:logout' +AUTH_OPENID_AUTH_LOGIN_URL_NAME = 'authentication:openid:login' +AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME = 'authentication:openid:login-callback' +AUTH_OPENID_AUTH_LOGOUT_URL_NAME = 'authentication:openid:logout' # ============================================================================== # Radius Auth From 6eaba4e2fb610276dff953a91a3391d26b248f00 Mon Sep 17 00:00:00 2001 From: Bai Date: Wed, 29 Apr 2020 14:03:48 +0800 Subject: [PATCH 16/30] =?UTF-8?q?[Update]=20openid=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=88=86=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/settings/auth.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 920fdb119..516671980 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -61,10 +61,11 @@ AUTH_OPENID_PROVIDER_SIGNATURE_KEY = CONFIG.AUTH_OPENID_PROVIDER_SIGNATURE_KEY AUTH_OPENID_SCOPES = CONFIG.AUTH_OPENID_SCOPES AUTH_OPENID_ID_TOKEN_MAX_AGE = CONFIG.AUTH_OPENID_ID_TOKEN_MAX_AGE AUTH_OPENID_ID_TOKEN_INCLUDE_CLAIMS = CONFIG.AUTH_OPENID_ID_TOKEN_INCLUDE_CLAIMS -AUTH_OPENID_SHARE_SESSION = CONFIG.AUTH_OPENID_SHARE_SESSION -AUTH_OPENID_IGNORE_SSL_VERIFICATION = CONFIG.AUTH_OPENID_IGNORE_SSL_VERIFICATION AUTH_OPENID_USE_STATE = CONFIG.AUTH_OPENID_USE_STATE AUTH_OPENID_USE_NONCE = CONFIG.AUTH_OPENID_USE_NONCE + +AUTH_OPENID_SHARE_SESSION = CONFIG.AUTH_OPENID_SHARE_SESSION +AUTH_OPENID_IGNORE_SSL_VERIFICATION = CONFIG.AUTH_OPENID_IGNORE_SSL_VERIFICATION AUTH_OPENID_ALWAYS_UPDATE_USER = CONFIG.AUTH_OPENID_ALWAYS_UPDATE_USER AUTH_OPENID_AUTH_LOGIN_URL_NAME = 'authentication:openid:login' AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME = 'authentication:openid:login-callback' From e4b788a012c5e47614b2eb95ca01b58a023f871d Mon Sep 17 00:00:00 2001 From: Bai Date: Wed, 29 Apr 2020 14:07:54 +0800 Subject: [PATCH 17/30] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E7=89=88=E6=9C=AC=20jumpserver-dajngo-oidc-rp=20(0.3.?= =?UTF-8?q?7.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 81019d249..b34351e65 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -96,4 +96,4 @@ ipython huaweicloud-sdk-python==1.0.21 django-redis==4.11.0 python-redis-lock==3.5.0 -jumpserver-django-oidc-rp==0.3.7.1 +jumpserver-django-oidc-rp==0.3.7.2 From a860bed34f5afd2f7c2436931aafffb0063884b0 Mon Sep 17 00:00:00 2001 From: BaiJiangJie <32935519+BaiJiangJie@users.noreply.github.com> Date: Wed, 29 Apr 2020 16:57:10 +0800 Subject: [PATCH 18/30] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9config=5Fexa?= =?UTF-8?q?mple=E9=85=8D=E7=BD=AE=EF=BC=88openid=EF=BC=89=20(#3951)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 修改config_example配置(openid) * [Update] 修改config_example配置(openid)2 --- config_example.yml | 29 ++++------------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/config_example.yml b/config_example.yml index 3f99d609f..30cfabc3e 100644 --- a/config_example.yml +++ b/config_example.yml @@ -55,29 +55,9 @@ REDIS_PORT: 6379 # Use OpenID Authorization # 使用 OpenID 进行认证设置 -# -# 配置方式1: -# 1. 版本 <= 1.5.8 -# 2. OpenID Provider 是 Keycloak -# -# 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 -# AUTH_OPENID_SHARE_SESSION: True -# AUTH_OPENID_IGNORE_SSL_VERIFICATION: True -# -# 配置方式2: (version >=1.5.8) -# 1. 版本 >= 1.5.8 -# 2. 支持标准 OpenID Connect Provider -# # AUTH_OPENID: False # True or False # AUTH_OPENID_CLIENT_ID: client-id # AUTH_OPENID_CLIENT_SECRET: client-secret -# AUTH_OPENID_SHARE_SESSION: True -# AUTH_OPENID_IGNORE_SSL_VERIFICATION: True # AUTH_OPENID_PROVIDER_ENDPOINT: https://op-example.com/ # AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT: https://op-example.com/authorize # AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT: https://op-example.com/token @@ -86,15 +66,14 @@ REDIS_PORT: 6379 # AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT: https://op-example.com/logout # AUTH_OPENID_PROVIDER_SIGNATURE_ALG: HS256 # AUTH_OPENID_PROVIDER_SIGNATURE_KEY: None -# AUTH_OPENID_PROVIDER_CLAIMS_NAME: None -# AUTH_OPENID_PROVIDER_CLAIMS_USERNAME: None -# AUTH_OPENID_PROVIDER_CLAIMS_EMAIL: None # AUTH_OPENID_SCOPES: "openid profile email" # AUTH_OPENID_ID_TOKEN_MAX_AGE: 60 -# AUTH_OPENID_ID_TOKEN_INCLUDE_USERINFO: True +# AUTH_OPENID_ID_TOKEN_INCLUDE_CLAIMS: True # AUTH_OPENID_USE_STATE: True # AUTH_OPENID_USE_NONCE: True -# AUTH_OPENID_ALWAYS_UPDATE_USER_INFORMATION: True +# AUTH_OPENID_SHARE_SESSION: True +# AUTH_OPENID_IGNORE_SSL_VERIFICATION: True +# AUTH_OPENID_ALWAYS_UPDATE_USER: True # Use Radius authorization # 使用Radius来认证 From aec78dc3c7e6ccba43db7b92a268d5d00bacd3ae Mon Sep 17 00:00:00 2001 From: Bai Date: Wed, 6 May 2020 15:31:08 +0800 Subject: [PATCH 19/30] =?UTF-8?q?[Update]=20=E6=B7=BB=E5=8A=A0=E7=BB=84?= =?UTF-8?q?=E7=BB=87=E8=AF=A6=E6=83=85->=E7=BB=84=E7=BB=87=E7=94=A8?= =?UTF-8?q?=E6=88=B7tab=E9=A1=B5=EF=BC=8C=E8=A7=A3=E5=86=B3=E7=BB=84?= =?UTF-8?q?=E7=BB=87=E8=AF=A6=E6=83=85=E9=A1=B5=E9=9D=A2=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=E8=B6=85=E6=97=B6=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/orgs/api.py | 18 ++++++++++++++++-- apps/orgs/serializers.py | 12 ++++++++++++ apps/orgs/urls/api_urls.py | 3 ++- apps/users/api/mixins.py | 6 +++++- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/apps/orgs/api.py b/apps/orgs/api.py index ea4a48955..63eebb25d 100644 --- a/apps/orgs/api.py +++ b/apps/orgs/api.py @@ -1,14 +1,16 @@ # -*- coding: utf-8 -*- # -from rest_framework import status +from django.shortcuts import get_object_or_404 +from rest_framework import status, generics from rest_framework.views import Response from rest_framework_bulk import BulkModelViewSet from common.permissions import IsSuperUserOrAppUser from .models import Organization from .serializers import OrgSerializer, OrgReadSerializer, \ - OrgMembershipUserSerializer, OrgMembershipAdminSerializer + OrgMembershipUserSerializer, OrgMembershipAdminSerializer, \ + OrgAllUserSerializer from users.models import User, UserGroup from assets.models import Asset, Domain, AdminUser, SystemUser, Label from perms.models import AssetPermission @@ -67,3 +69,15 @@ class OrgMembershipUsersViewSet(OrgMembershipModelViewSetMixin, BulkModelViewSet membership_class = Organization.users.through permission_classes = (IsSuperUserOrAppUser, ) + +class OrgAllUserListApi(generics.ListAPIView): + permission_classes = (IsSuperUserOrAppUser,) + serializer_class = OrgAllUserSerializer + filter_fields = ("username", "name") + search_fields = filter_fields + + def get_queryset(self): + pk = self.kwargs.get("pk") + org = get_object_or_404(Organization, pk=pk) + users = org.get_org_users().only(*self.serializer_class.Meta.only_fields) + return users diff --git a/apps/orgs/serializers.py b/apps/orgs/serializers.py index 281b9ec75..5ff3b5f4d 100644 --- a/apps/orgs/serializers.py +++ b/apps/orgs/serializers.py @@ -80,3 +80,15 @@ class OrgMembershipUserSerializer(OrgMembershipSerializerMixin, ModelSerializer) model = Organization.users.through list_serializer_class = AdaptedBulkListSerializer fields = '__all__' + + +class OrgAllUserSerializer(serializers.Serializer): + user = serializers.UUIDField(read_only=True, source='id') + user_display = serializers.SerializerMethodField() + + class Meta: + only_fields = ['id', 'username', 'name'] + + @staticmethod + def get_user_display(obj): + return str(obj) diff --git a/apps/orgs/urls/api_urls.py b/apps/orgs/urls/api_urls.py index 782269c81..17f8c3c8a 100644 --- a/apps/orgs/urls/api_urls.py +++ b/apps/orgs/urls/api_urls.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # -from django.urls import re_path +from django.urls import re_path, path from rest_framework.routers import DefaultRouter from common import api as capi @@ -24,6 +24,7 @@ old_version_urlpatterns = [ ] urlpatterns = [ + path('/users/all/', api.OrgAllUserListApi.as_view(), name='org-all-users'), ] urlpatterns += router.urls + old_version_urlpatterns diff --git a/apps/users/api/mixins.py b/apps/users/api/mixins.py index 273ab9074..2ff60b615 100644 --- a/apps/users/api/mixins.py +++ b/apps/users/api/mixins.py @@ -1,9 +1,13 @@ # -*- coding: utf-8 -*- # from .. import utils +from users.models import User class UserQuerysetMixin: def get_queryset(self): - queryset = utils.get_current_org_members() + if self.request.query_params.get('all'): + queryset = User.objects.exclude(role=User.ROLE_APP) + else: + queryset = utils.get_current_org_members() return queryset From 8323de1c07a7638c18d5e8e83ed63d263aa1927b Mon Sep 17 00:00:00 2001 From: Bai Date: Wed, 6 May 2020 21:05:23 +0800 Subject: [PATCH 20/30] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E7=89=88=E6=9C=ACjumpserver-django-oidc-rp=3D=3D0.3.7?= =?UTF-8?q?.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index b34351e65..50e4a1862 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -96,4 +96,4 @@ ipython huaweicloud-sdk-python==1.0.21 django-redis==4.11.0 python-redis-lock==3.5.0 -jumpserver-django-oidc-rp==0.3.7.2 +jumpserver-django-oidc-rp==0.3.7.3 From 2d4498578a2846fdb832588b499860a11a5e2560 Mon Sep 17 00:00:00 2001 From: ibuler Date: Fri, 8 May 2020 13:13:38 +0800 Subject: [PATCH 21/30] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E7=BC=93=E5=AD=98=E9=85=8D=E7=BD=AE=E7=9A=84=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 75978ea41..b30160d3a 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -18,6 +18,8 @@ from django.urls import reverse_lazy BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.dirname(BASE_DIR) +XPACK_DIR = os.path.join(BASE_DIR, 'xpack') +HAS_XPACK = os.path.isdir(XPACK_DIR) def import_string(dotted_path): @@ -190,7 +192,7 @@ class Config(dict): 'TASK_LOG_KEEP_DAYS': 10, 'ASSETS_PERM_CACHE_TIME': 3600 * 24, 'SECURITY_MFA_VERIFY_TTL': 3600, - 'ASSETS_PERM_CACHE_ENABLE': False, + 'ASSETS_PERM_CACHE_ENABLE': HAS_XPACK, 'SYSLOG_ADDR': '', # '192.168.0.1:514' 'SYSLOG_FACILITY': 'user', 'SYSLOG_SOCKTYPE': 2, From 1936a6d5eebbf04e0afeca4b8ac451acebeb22fd Mon Sep 17 00:00:00 2001 From: ibuler Date: Fri, 8 May 2020 14:25:28 +0800 Subject: [PATCH 22/30] =?UTF-8?q?[Update]=20=E5=8E=BB=E6=8E=89perms=20cach?= =?UTF-8?q?e=20enable=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/perms/apps.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/apps/perms/apps.py b/apps/perms/apps.py index d6fa5f712..d40373e08 100644 --- a/apps/perms/apps.py +++ b/apps/perms/apps.py @@ -1,14 +1,7 @@ from __future__ import unicode_literals -from django.conf import settings from django.apps import AppConfig class PermsConfig(AppConfig): name = 'perms' - - def ready(self): - from . import signals_handler - if not settings.XPACK_ENABLED: - settings.ASSETS_PERM_CACHE_ENABLE = False - return super().ready() From 181973f23549c06edaca20d7c7ef809f7a192c96 Mon Sep 17 00:00:00 2001 From: Bai Date: Fri, 8 May 2020 15:41:56 +0800 Subject: [PATCH 23/30] =?UTF-8?q?[Update]=20=E4=BC=98=E5=8C=96=E4=BB=AA?= =?UTF-8?q?=E8=A1=A8=E7=9B=98=E9=A1=B5=E9=9D=A2=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/api.py | 295 ++++++++++++++++++++++++++ apps/jumpserver/urls.py | 3 +- apps/jumpserver/views/index.py | 236 +-------------------- apps/templates/index.html | 376 +++++++++++++++++++++++---------- 4 files changed, 557 insertions(+), 353 deletions(-) create mode 100644 apps/jumpserver/api.py diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py new file mode 100644 index 000000000..e272c9ff3 --- /dev/null +++ b/apps/jumpserver/api.py @@ -0,0 +1,295 @@ +from django.core.cache import cache +from django.utils import timezone +from django.utils.timesince import timesince +from django.db.models import Count, Max +from django.http.response import JsonResponse +from rest_framework.views import APIView +from collections import Counter + +from users.models import User +from assets.models import Asset +from terminal.models import Session +from orgs.utils import current_org +from common.permissions import IsOrgAdmin +from common.utils import lazyproperty + +__all__ = ['IndexApi'] + + +class MonthLoginMetricMixin: + + @lazyproperty + def session_month(self): + month_ago = timezone.now() - timezone.timedelta(days=30) + session_month = Session.objects.filter(date_start__gt=month_ago) + return session_month + + @lazyproperty + def session_month_dates(self): + dates = self.session_month.dates('date_start', 'day') + return dates + + def get_month_metrics_date(self): + month_metrics_date = [d.strftime('%m-%d') for d in self.session_month_dates] or ['0'] + return month_metrics_date + + @staticmethod + def get_cache_key(date, tp): + date_str = date.strftime("%Y%m%d") + key = "SESSION_MONTH_{}_{}".format(tp, date_str) + return key + + def __get_data_from_cache(self, date, tp): + if date == timezone.now().date(): + return None + cache_key = self.get_cache_key(date, tp) + count = cache.get(cache_key) + return count + + def __set_data_to_cache(self, date, tp, count): + cache_key = self.get_cache_key(date, tp) + cache.set(cache_key, count, 3600*24*7) + + @staticmethod + def get_date_start_2_end(d): + time_min = timezone.datetime.min.time() + time_max = timezone.datetime.max.time() + tz = timezone.get_current_timezone() + ds = timezone.datetime.combine(d, time_min).replace(tzinfo=tz) + de = timezone.datetime.combine(d, time_max).replace(tzinfo=tz) + return ds, de + + def get_date_login_count(self, date): + tp = "LOGIN" + count = self.__get_data_from_cache(date, tp) + if count is not None: + return count + ds, de = self.get_date_start_2_end(date) + count = Session.objects.filter(date_start__range=(ds, de)).count() + self.__set_data_to_cache(date, tp, count) + return count + + def get_month_metrics_total_count_login(self): + data = [] + for d in self.session_month_dates: + count = self.get_date_login_count(d) + data.append(count) + if len(data) == 0: + data = [0] + return data + + def get_date_user_count(self, date): + tp = "USER" + count = self.__get_data_from_cache(date, tp) + if count is not None: + return count + ds, de = self.get_date_start_2_end(date) + count = len(set(Session.objects.filter(date_start__range=(ds, de)).values_list('user', flat=True))) + self.__set_data_to_cache(date, tp, count) + return count + + def get_month_metrics_total_count_active_users(self): + data = [] + for d in self.session_month_dates: + count = self.get_date_user_count(d) + data.append(count) + return data + + def get_date_asset_count(self, date): + tp = "ASSET" + count = self.__get_data_from_cache(date, tp) + if count is not None: + return count + ds, de = self.get_date_start_2_end(date) + count = len(set(Session.objects.filter(date_start__range=(ds, de)).values_list('asset', flat=True))) + self.__set_data_to_cache(date, tp, count) + return count + + def get_month_metrics_total_count_active_assets(self): + data = [] + for d in self.session_month_dates: + count = self.get_date_asset_count(d) + data.append(count) + return data + + @lazyproperty + def month_total_count_active_users(self): + count = len(set(self.session_month.values_list('user', flat=True))) + return count + + @lazyproperty + def month_total_count_inactive_users(self): + total = current_org.get_org_members().count() + active = self.month_total_count_active_users + count = total - active + if count < 0: + count = 0 + return count + + @lazyproperty + def month_total_count_disabled_users(self): + return current_org.get_org_members().filter(is_active=False).count() + + @lazyproperty + def month_total_count_active_assets(self): + return len(set(self.session_month.values_list('asset', flat=True))) + + @lazyproperty + def month_total_count_inactive_assets(self): + total = Asset.objects.all().count() + active = self.month_total_count_active_assets + count = total - active + if count < 0: + count = 0 + return count + + @lazyproperty + def month_total_count_disabled_assets(self): + return Asset.objects.filter(is_active=False).count() + + +class WeekSessionMetricMixin: + session_week = None + + @lazyproperty + def session_week(self): + week_ago = timezone.now() - timezone.timedelta(weeks=1) + session_week = Session.objects.filter(date_start__gt=week_ago) + return session_week + + def get_week_login_times_top5_users(self): + users = self.session_week.values_list('user', flat=True) + users = [ + {'user': user, 'total': total} + for user, total in Counter(users).most_common(5) + ] + return users + + def get_week_total_count_login_users(self): + return len(set(self.session_week.values_list('user', flat=True))) + + def get_week_total_count_login_times(self): + return self.session_week.count() + + def get_week_login_times_top10_assets(self): + assets = self.session_week.values("asset")\ + .annotate(total=Count("asset"))\ + .annotate(last=Max("date_start")).order_by("-total")[:10] + for asset in assets: + asset['last'] = str(asset['last']) + return list(assets) + + def get_week_login_times_top10_users(self): + users = self.session_week.values("user") \ + .annotate(total=Count("user")) \ + .annotate(last=Max("date_start")).order_by("-total")[:10] + for user in users: + user['last'] = str(user['last']) + return list(users) + + def get_week_login_record_top10_sessions(self): + sessions = self.session_week.order_by('-date_start')[:10] + for session in sessions: + session.avatar_url = User.get_avatar_url("") + sessions = [ + { + 'user': session.user, + 'asset': session.asset, + 'is_finished': session.is_finished, + 'date_start': str(session.date_start), + 'timesince': timesince(session.date_start) + } + for session in sessions + ] + return sessions + + +class TotalCountMixin: + @staticmethod + def get_total_count_users(): + return current_org.get_org_members().count() + + @staticmethod + def get_total_count_assets(): + return Asset.objects.all().count() + + @staticmethod + def get_total_count_online_users(): + count = len(set(Session.objects.filter(is_finished=False).values_list('user', flat=True))) + return count + + @staticmethod + def get_total_count_online_assets(): + return Session.objects.filter(is_finished=False).count() + + +class IndexApi(TotalCountMixin, WeekSessionMetricMixin, MonthLoginMetricMixin, APIView): + permission_classes = (IsOrgAdmin,) + http_method_names = ['get'] + + def get(self, request, *args, **kwargs): + data = {} + + query_params = self.request.query_params + + _all = query_params.get('all') + + if _all or query_params.get('total_count'): + data.update({ + 'total_count_assets': self.get_total_count_assets(), + 'total_count_users': self.get_total_count_users(), + 'total_count_online_users': self.get_total_count_online_users(), + 'total_count_online_assets': self.get_total_count_online_assets(), + }) + + if _all or query_params.get('month_metrics'): + data.update({ + 'month_metrics_date': self.get_month_metrics_date(), + 'month_metrics_total_count_login': self.get_month_metrics_total_count_login(), + 'month_metrics_total_count_active_users': self.get_month_metrics_total_count_active_users(), + 'month_metrics_total_count_active_assets': self.get_month_metrics_total_count_active_assets(), + }) + + if _all or query_params.get('month_total_count_users'): + data.update({ + 'month_total_count_active_users': self.month_total_count_active_users, + 'month_total_count_inactive_users': self.month_total_count_inactive_users, + 'month_total_count_disabled_users': self.month_total_count_disabled_users, + }) + + if _all or query_params.get('month_total_count_assets'): + data.update({ + 'month_total_count_active_assets': self.month_total_count_active_assets, + 'month_total_count_inactive_assets': self.month_total_count_inactive_assets, + 'month_total_count_disabled_assets': self.month_total_count_disabled_assets, + }) + + if _all or query_params.get('week_total_count'): + data.update({ + 'week_total_count_login_users': self.get_week_total_count_login_users(), + 'week_total_count_login_times': self.get_week_total_count_login_times(), + }) + + if _all or query_params.get('week_login_times_top5_users'): + data.update({ + 'week_login_times_top5_users': self.get_week_login_times_top5_users(), + }) + + if _all or query_params.get('week_login_times_top10_assets'): + data.update({ + 'week_login_times_top10_assets': self.get_week_login_times_top10_assets(), + }) + + if _all or query_params.get('week_login_times_top10_users'): + data.update({ + 'week_login_times_top10_users': self.get_week_login_times_top10_users(), + }) + + if _all or query_params.get('week_login_record_top10_sessions'): + data.update({ + 'week_login_record_top10_sessions': self.get_week_login_record_top10_sessions() + }) + + return JsonResponse(data, status=200) + + diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index c4653969b..ade854397 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -7,9 +7,10 @@ from django.conf.urls.static import static from django.conf.urls.i18n import i18n_patterns from django.views.i18n import JavaScriptCatalog -from . import views +from . import views, api api_v1 = [ + path('index/', api.IndexApi.as_view()), path('users/', include('users.urls.api_urls', namespace='api-users')), path('assets/', include('assets.urls.api_urls', namespace='api-assets')), path('perms/', include('perms.urls.api_urls', namespace='api-perms')), diff --git a/apps/jumpserver/views/index.py b/apps/jumpserver/views/index.py index 1bcb2a57e..ffcb10493 100644 --- a/apps/jumpserver/views/index.py +++ b/apps/jumpserver/views/index.py @@ -1,224 +1,12 @@ -from django.core.cache import cache from django.views.generic import TemplateView -from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from django.db.models import Count, Max from django.shortcuts import redirect - -from users.models import User -from assets.models import Asset -from terminal.models import Session -from orgs.utils import current_org from common.permissions import PermissionsMixin, IsValidUser -from common.utils import timeit, lazyproperty __all__ = ['IndexView'] -class MonthLoginMetricMixin: - @lazyproperty - def session_month(self): - month_ago = timezone.now() - timezone.timedelta(days=30) - session_month = Session.objects.filter(date_start__gt=month_ago) - return session_month - - @lazyproperty - def session_month_dates(self): - dates = self.session_month.dates('date_start', 'day') - return dates - - def get_month_day_metrics(self): - month_str = [ - d.strftime('%m-%d') for d in self.session_month_dates - ] or ['0'] - return month_str - - @staticmethod - def get_cache_key(date, tp): - date_str = date.strftime("%Y%m%d") - key = "SESSION_MONTH_{}_{}".format(tp, date_str) - return key - - def __get_data_from_cache(self, date, tp): - if date == timezone.now().date(): - return None - cache_key = self.get_cache_key(date, tp) - count = cache.get(cache_key) - return count - - def __set_data_to_cache(self, date, tp, count): - cache_key = self.get_cache_key(date, tp) - cache.set(cache_key, count, 3600*24*7) - - @lazyproperty - def user_disabled_total(self): - return current_org.get_org_members().filter(is_active=False).count() - - @lazyproperty - def asset_disabled_total(self): - return Asset.objects.filter(is_active=False).count() - - @staticmethod - def get_date_start_2_end(d): - time_min = timezone.datetime.min.time() - time_max = timezone.datetime.max.time() - tz = timezone.get_current_timezone() - ds = timezone.datetime.combine(d, time_min).replace(tzinfo=tz) - de = timezone.datetime.combine(d, time_max).replace(tzinfo=tz) - return ds, de - - def get_date_login_count(self, date): - tp = "LOGIN" - count = self.__get_data_from_cache(date, tp) - if count is not None: - return count - ds, de = self.get_date_start_2_end(date) - count = Session.objects.filter(date_start__range=(ds, de)).count() - self.__set_data_to_cache(date, tp, count) - return count - - def get_month_login_metrics(self): - data = [] - for d in self.session_month_dates: - count = self.get_date_login_count(d) - data.append(count) - if len(data) == 0: - data = [0] - return data - - def get_date_user_count(self, date): - tp = "USER" - count = self.__get_data_from_cache(date, tp) - if count is not None: - return count - ds, de = self.get_date_start_2_end(date) - count = Session.objects.filter(date_start__range=(ds, de))\ - .values('user').distinct().count() - self.__set_data_to_cache(date, tp, count) - return count - - def get_month_active_user_metrics(self): - data = [] - for d in self.session_month_dates: - count = self.get_date_user_count(d) - data.append(count) - return data - - def get_date_asset_count(self, date): - tp = "ASSET" - count = self.__get_data_from_cache(date, tp) - if count is not None: - return count - ds, de = self.get_date_start_2_end(date) - count = Session.objects.filter(date_start__range=(ds, de)) \ - .values('asset').distinct().count() - self.__set_data_to_cache(date, tp, count) - return count - - def get_month_active_asset_metrics(self): - data = [] - for d in self.session_month_dates: - count = self.get_date_asset_count(d) - data.append(count) - return data - - @lazyproperty - def month_active_user_total(self): - count = self.session_month.values('user').distinct().count() - return count - - @lazyproperty - def month_inactive_user_total(self): - total = current_org.get_org_members().count() - active = self.month_active_user_total - count = total - active - if count < 0: - count = 0 - return count - - @lazyproperty - def month_active_asset_total(self): - return self.session_month.values('asset').distinct().count() - - @lazyproperty - def month_inactive_asset_total(self): - total = Asset.objects.all().count() - active = self.month_active_asset_total - count = total - active - if count < 0: - count = 0 - return count - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context.update({ - 'month_str': self.get_month_day_metrics(), - 'month_total_visit_count': self.get_month_login_metrics(), - 'month_user': self.get_month_active_user_metrics(), - 'mouth_asset': self.get_month_active_asset_metrics(), - 'month_user_active': self.month_active_user_total, - 'month_user_inactive': self.month_inactive_user_total, - 'month_user_disabled': self.user_disabled_total, - 'month_asset_active': self.month_active_asset_total, - 'month_asset_inactive': self.month_inactive_asset_total, - 'month_asset_disabled': self.asset_disabled_total, - }) - return context - - -class WeekSessionMetricMixin: - session_week = None - - @lazyproperty - def session_week(self): - week_ago = timezone.now() - timezone.timedelta(weeks=1) - session_week = Session.objects.filter(date_start__gt=week_ago) - return session_week - - def get_top5_user_a_week(self): - users = self.session_week.values('user') \ - .annotate(total=Count('user')) \ - .order_by('-total')[:5] - return users - - def get_week_login_user_count(self): - return self.session_week.values('user').distinct().count() - - def get_week_login_asset_count(self): - return self.session_week.count() - - def get_week_top10_assets(self): - assets = self.session_week.values("asset")\ - .annotate(total=Count("asset"))\ - .annotate(last=Max("date_start")).order_by("-total")[:10] - return assets - - def get_week_top10_users(self): - users = self.session_week.values("user") \ - .annotate(total=Count("user")) \ - .annotate(last=Max("date_start")).order_by("-total")[:10] - return users - - def get_last10_sessions(self): - sessions = self.session_week.order_by('-date_start')[:10] - for session in sessions: - session.avatar_url = User.get_avatar_url("") - return sessions - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context.update({ - 'user_visit_count_weekly': self.get_week_login_user_count(), - 'asset_visit_count_weekly': self.get_week_login_asset_count(), - 'user_visit_count_top_five': self.get_top5_user_a_week(), - 'last_login_ten': self.get_last10_sessions(), - 'week_asset_hot_ten': self.get_week_top10_assets(), - 'week_user_hot_ten': self.get_week_top10_users(), - }) - return context - - -class IndexView(PermissionsMixin, MonthLoginMetricMixin, WeekSessionMetricMixin, TemplateView): +class IndexView(PermissionsMixin, TemplateView): template_name = 'index.html' permission_classes = [IsValidUser] @@ -229,31 +17,9 @@ class IndexView(PermissionsMixin, MonthLoginMetricMixin, WeekSessionMetricMixin, return redirect('assets:user-asset-list') return super(IndexView, self).dispatch(request, *args, **kwargs) - @staticmethod - def get_user_count(): - return current_org.get_org_members().count() - - @staticmethod - def get_asset_count(): - return Asset.objects.all().count() - - @staticmethod - def get_online_user_count(): - count = Session.objects.filter(is_finished=False)\ - .values_list('user', flat=True).distinct().count() - return count - - @staticmethod - def get_online_session_count(): - return Session.objects.filter(is_finished=False).count() - def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ - 'assets_count': self.get_asset_count(), - 'users_count': self.get_user_count(), - 'online_user_count': self.get_online_user_count(), - 'online_asset_count': self.get_online_session_count(), 'app': _("Dashboard"), }) return context diff --git a/apps/templates/index.html b/apps/templates/index.html index b667f473d..36f57137c 100644 --- a/apps/templates/index.html +++ b/apps/templates/index.html @@ -11,7 +11,7 @@
{% trans 'Total users' %}
-

{{ users_count }}

+

All users
@@ -19,12 +19,12 @@
- Hosts -
{% trans 'Total hosts' %}
+ Assets +
{% trans 'Total assets' %}
-

{{ assets_count }}

- All hosts +

+ All assets
@@ -36,7 +36,7 @@
{% trans 'Online users' %}
-

{{ online_user_count }}

+

Online users
@@ -50,7 +50,7 @@
-

{{ online_asset_count }}

+

Online sessions
@@ -58,19 +58,11 @@
- {% trans 'In the past week, a total of ' %}{{ user_visit_count_weekly }}{% trans ' users have logged in ' %}{{ asset_visit_count_weekly }}{% trans ' times asset.' %} -
    - {% for data in user_visit_count_top_five %} -
  • - - {{ data.total }}{% trans ' times/week' %} - - {{ forloop.counter }} {{ data.user }} -
  • - {% endfor %} + {% trans 'In the past week, a total of ' %}{% trans ' users have logged in ' %}{% trans ' times asset.' %} +
-
+

@@ -81,13 +73,13 @@

-
+
{% trans 'User' %}
-
-
{% trans 'Host' %}
+
+
{% trans 'Asset' %}
@@ -120,27 +112,7 @@

{% trans 'Top 10 assets in a week'%}

{% trans 'Login frequency and last login record.' %}
-
- {% if week_asset_hot_ten %} - {% for data in week_asset_hot_ten %} -
-
-
- - {{ data.asset }} -
- {{ data.total }}{% trans ' times' %} -
-
-

{% trans 'The time last logged in' %}

-

{% trans 'At' %} {{ data.last|date:"Y-m-d H:i:s" }}

-
-
-
- {% endfor %} - {% else %} -

{% trans '(No)' %}

- {% endif %} +
@@ -158,27 +130,7 @@

-
- {% if last_login_ten %} - {% for login in last_login_ten %} -
- - image - -
- {% ifequal login.is_finished 0 %} - {{ login.date_start|timesince }} {% trans 'Before' %} - {% else %} - {{ login.date_start|timesince }} {% trans 'Before' %} - {% endifequal %} - {{ login.user }} {% trans 'Login in ' %}{{ login.asset }}
- {{ login.date_start }} -
-
- {% endfor %} - {% else %} -

{% trans '(No)' %}

- {% endif %} +
@@ -206,27 +158,7 @@

{% trans 'Top 10 users in a week' %}

{% trans 'User login frequency and last login record.' %}
-
- {% if week_user_hot_ten %} - {% for data in week_user_hot_ten %} -
-
-
- - {{ data.user }} -
- {{ data.total }}{% trans ' times' %} -
-
-

{% trans 'The time last logged in' %}

-

{% trans 'At' %} {{ data.last|date:"Y-m-d H:i:s" }}

-
-
-
- {% endfor %} - {% else %} -

{% trans '(No)' %}

- {% endif %} +
@@ -238,27 +170,15 @@ {% block custom_foot_js %} {% endblock %} From e48dbabef2678b91ca0c54c53ae640d91ca5687d Mon Sep 17 00:00:00 2001 From: Bai Date: Fri, 8 May 2020 15:50:01 +0800 Subject: [PATCH 24/30] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9=E7=BF=BB?= =?UTF-8?q?=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/locale/zh/LC_MESSAGES/django.mo | Bin 89528 -> 90004 bytes apps/locale/zh/LC_MESSAGES/django.po | 292 +++++++++++++++------------ 2 files changed, 163 insertions(+), 129 deletions(-) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 126defb5619feee38247c0be8c684584080455cb..a466cf2e82b4c775389f7513945b211882ed2112 100644 GIT binary patch delta 26742 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�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 UI#mo<5-F-{u`O}cfJF)Z5B<1(2mk;8 delta 26358 zcmZAA1(;RUyT|c8%rFc&Fbp%)z|b)C&?OCnAPtg=Al)e;9J)INk!~cUy97i^LJ5%& z5Ri~A5xn2uSufA^-u*nw&w5wywe~)9X7qpW^u6)7?Tqic5*9SW<2oGVdFio6cF$`a z?0H|-P^sr7Z{vB5F$GS*j@Tbx<7VvD*7F+1^SqdLp7#s!kPe>LD8TbR?&x`AiPv}X zyunziv*%sHU3h}}uex|%cs$SNwdv-0r>Gdv!}Ahijh>#D3L9cDcEY6C6BFYQjKB$) z3743MF(dIE^Fp!L7oL|0hhq|)WX?5Lq87XbQ{n;C4V^>n^kocZe(!HG2{5#;=Ox2PRLAV7 zfn&|GsDWyr25N!o*Vf{$sGaJATHtVu#ObJUzQsV?j+t>c`l88PBcrVk=;v0J7`4SY zF$~LM2-d}P*c4OY7gjz2HQ-Ftooz>L^*M`Qp~g$agQNa=P)8KkpZ(Vzd_+NVY=Gg| z2IJ!Z)D{mh$72fOS>{UAo$kbzcnq}z`3A6iSQ>TKt*|Rjz{dC%>*B{>vj6qStoqXV z4pmWipyw6CrKkZfqZV`zwS|u{CqBm8suY$Vs8mNi7pq{BCsD-~o?PNmcm0>!+FU2Cb3pM@|RJ(VmaS{)9 z<-RmzvQm%@wbdV@25NwL@pII(F%>nyTvYp&s3SXx>F_G*VS9smHc}4pytJ4LHEt!; zQPna(MdtB&UCHQa9%L0GP!r5Rt$c@h7`0_*P!n82E$lJsj^7}kLtfZWw}YurXCH+c zzbb0yYNBqW9%j+|--3*`YB(0c9as|oLjN5Ma~&(8&afJ4;-;7ZTchr5I2OR^R(}XJ z@lU87IEQNYJL-rZqn-orH5sieaJW005Y*X5Se)LBLEUL_)WEgOhPa5hIcfrLgge6I zsEIOS6z0IJSQ&Lx9nhx%hmr}x*{D04hnipo>WtT+j$$8b2aaPJykhmQFg0<)k*+?o zS=g+C`W$J5I=YS+h(kuQ|C(?F1#*%(+gxm}L7n|p)PnY52|SI#n0S=C(=gNyq(!xl zLA5W0I?76@g@1}#NWW3+zkgy1@=-7w6W|fdkEc*O`T0nJ7icL{B+#Y?(^du9F15ppjJWPzsQ5`m-CfaYF zwfa9SevDdp;3T*85vT>kpmwAbYA0)B4s44$x=EARf1TMH3Zn5x)a!N^li)+tg8s2M z{$#hXFbt(U117_Ks5>u%I=VQETcK{S3u?T+sPRUiZfN3U_Fqry0tz(X3e>~14YhU0 zQ9E%Kbq7~b?OvcJ4xZw^2c*K>#2;XB?0~wF#i(&s;4WN;ny=GTx1e4=%M3xSY$9p_ zvrq%9M%}@B)E(|Xy%mQr8gHY12!%}JM>pocN7w^3UaRSDoKC2PbVnU&U(`Ilkz{m- zb5I?YVO3m*I+~ZL4k>52JI{prj#m&XU`>msU^(KosBs>m?(`MvlhT{%o`qn{MVJm# z>HV)v#{X2J8n#ERv@0gZKBzk%g&J@wYJxfDV$_j*i($A6weX)XJKn>L7&goOJ`jsK zs?RZ{-v7yDH1HDCnSP7f@|~y!971i?IgG%&sCEHgxrHRh6vSyzQ2U+Gf)%EL;n$>CfaH7Nz{Os&Fh$- z_%>?%$gf>}Vbu5~EUx%9`>%=PD2T@TsEK={?syRD%*LU1W(sP6g;u{B^^k4DczD&k zj#}^^sGs*w&4{_&J#h)tYy5?eOhYoWu^K)?ZB@x{+?m%z-Ejldf<8yR@7-|~PR9ON zYMy&`wxDj{XVei~M%}<4s2#dz<&Uh~_lk_R>K$q)^38VxltfKj5p~CPQO`&-)Yf-F zO*F(Di@AuWp`MA|sE6(drp33YaZ)dE^_h|Rd|qBMny7?T#JLKu9%_P5F*mkDZS{22 z1Upf8bQtw89>Xs98|uqt^@Z++YGDTArl_OnZ+_#i_j%qvGTPeXsD)faZP^{vM1P@P zzt^ZU)pu<@#mSIQFfSXbT`7yJpcdZD;?}7415o2n#mu-6{qO%lGCHecs0L?I3%ZVa z79L_c3|#ECJ|ku(jzzU^fc|F#b%fnf;|)aJ$OsI?aTZTPEqDg{-~R<s2w|i>URv){sOA~4UC7r zJ7hHAJ=BVxqdJDIaIaN5RL4B1_rDls!D^^yr88=wV^9OnM!k;nQ2o}So~0eA9XyP| zcpMqe=ba^^4u7CNC|;N;R=RIAc~C3;0ux|=)WAbgcQygFwR2EAwFb5IyHE>0jauLh zRQso>BhbfZ5c7NCWC{_d!IJn9YNFAol}|=>n2RB}9`#UeN449HTEHQzzkmsee@FGd zhl%hds(-xI&cv96`Mq#5`e4a~+M@iZEh>WQSQ^!_D(1)9sP=m#-bW4a4)qKqTjPwuG{jX< zJJZ5!hq{4osD<=H-RRgg?7uRTDbStFM6GxgY74(dbv%KZ@Ej(^Tc~H{8S3>4UF(h@ z8>+lG=D>QG6$hZkU4UBPcFc=s*RuZ_AYh#{5_Oh&&2kt^To3hhk3`+sIMl+XnR8JK zTZ)NsBdXnA485?lgS8YmfmI=Rlo(K@7#xsD;!(9fc3Iped-Q zeJ0k$)tD3Cp*~M?eCM9}s%CvmM)_x`o%eMkqY3+<9=gG(tr~^8g9WHNTY<@O3u=ph zK()J$THqVhfQdJ_aZ;fAr^XZ*ZRN43ohX4U#OGCT8Ltj%>sp`|)Co0kAJoJnQ3K6F z-N8x>!)>S?IEMOQx`uivgEqRejz)dLmPFlPW7Ltiz)-#aeaL9Qk*EP@pcb;i$~U35 z^n26*_pu2+wfdTyT>s{nlJZWdhifouN7kZlXcLCx9xRAwF`3@~_?z8B5srC?bEAH! zG_vwOs4bg-I*OU7omql#IfV$H+ zm<@xrx;O@P$AwTwRT1-JUDN`HnqyE4nv80{2z7%SQ1AWss2jPAI_kSy*?%>7OM%WV z*)}&pX4FaxqVBY;#UEOIBUHb(sAr`wYT)4*gHuoo-Gv(W80v_wVl8}&>i3avyIV

Jn*RKI;ALK*a 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 QcHWx1W9yR|0gJ-^2Xy1_Q~&?~ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 01a3bc023..2a2de30c3 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-04-17 20:49+0800\n" +"POT-Creation-Date: 2020-05-08 15:42+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -42,7 +42,7 @@ msgstr "自定义" #: perms/templates/perms/asset_permission_asset.html:53 #: perms/templates/perms/asset_permission_create_update.html:57 #: perms/templates/perms/asset_permission_list.html:35 -#: perms/templates/perms/asset_permission_list.html:87 +#: perms/templates/perms/asset_permission_list.html:87 templates/index.html:82 #: terminal/backends/command/models.py:19 terminal/models.py:187 #: terminal/templates/terminal/command_list.html:31 #: terminal/templates/terminal/command_list.html:106 @@ -58,7 +58,8 @@ msgstr "自定义" #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:54 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:13 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:14 -#: xpack/plugins/cloud/models.py:266 +#: xpack/plugins/cloud/models.py:269 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:37 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:47 #: xpack/plugins/orgs/templates/orgs/org_list.html:17 #: xpack/plugins/vault/forms.py:13 xpack/plugins/vault/forms.py:15 @@ -169,8 +170,9 @@ msgstr "运行参数" #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:53 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:12 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:16 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:47 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:51 #: xpack/plugins/orgs/templates/orgs/org_list.html:12 +#: xpack/plugins/orgs/templates/orgs/org_users.html:46 msgid "Name" msgstr "名称" @@ -194,7 +196,7 @@ msgstr "类型" #: applications/templates/applications/database_app_detail.html:56 #: applications/templates/applications/database_app_list.html:25 #: applications/templates/applications/user_database_app_list.html:18 -#: ops/models/adhoc.py:146 templates/index.html:90 +#: ops/models/adhoc.py:146 #: users/templates/users/user_granted_database_app.html:36 msgid "Host" msgstr "主机" @@ -260,13 +262,13 @@ msgstr "数据库" #: xpack/plugins/change_auth_plan/models.py:75 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:115 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:19 -#: xpack/plugins/cloud/models.py:53 xpack/plugins/cloud/models.py:136 +#: xpack/plugins/cloud/models.py:53 xpack/plugins/cloud/models.py:139 #: xpack/plugins/cloud/templates/cloud/account_detail.html:67 #: xpack/plugins/cloud/templates/cloud/account_list.html:15 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:102 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:128 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:18 #: xpack/plugins/gathered_user/models.py:26 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:59 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:63 #: xpack/plugins/orgs/templates/orgs/org_list.html:23 msgid "Comment" msgstr "备注" @@ -323,7 +325,7 @@ msgstr "参数" #: users/templates/users/user_detail.html:97 #: xpack/plugins/change_auth_plan/models.py:79 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:111 -#: xpack/plugins/cloud/models.py:56 xpack/plugins/cloud/models.py:142 +#: xpack/plugins/cloud/models.py:56 xpack/plugins/cloud/models.py:145 #: xpack/plugins/gathered_user/models.py:30 msgid "Created by" msgstr "创建者" @@ -350,10 +352,10 @@ msgstr "创建者" #: tickets/templates/tickets/ticket_detail.html:52 users/models/group.py:18 #: users/templates/users/user_group_detail.html:58 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:103 -#: xpack/plugins/cloud/models.py:59 xpack/plugins/cloud/models.py:145 +#: xpack/plugins/cloud/models.py:59 xpack/plugins/cloud/models.py:148 #: xpack/plugins/cloud/templates/cloud/account_detail.html:63 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:98 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:55 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:108 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:59 msgid "Date created" msgstr "创建日期" @@ -405,7 +407,7 @@ msgstr "远程应用" #: users/templates/users/user_pubkey_update.html:80 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:65 #: xpack/plugins/cloud/templates/cloud/account_create_update.html:29 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:49 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:52 #: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:40 #: xpack/plugins/interface/templates/interface/interface.html:72 #: xpack/plugins/orgs/templates/orgs/org_create_update.html:29 @@ -536,7 +538,7 @@ msgstr "详情" #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:26 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:60 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:46 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:20 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:24 #: xpack/plugins/orgs/templates/orgs/org_list.html:93 msgid "Update" msgstr "更新" @@ -588,7 +590,7 @@ msgstr "更新" #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:30 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:61 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:47 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:24 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:28 #: xpack/plugins/orgs/templates/orgs/org_list.html:95 msgid "Delete" msgstr "删除" @@ -648,6 +650,7 @@ msgstr "创建数据库应用" #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:19 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:20 #: xpack/plugins/orgs/templates/orgs/org_list.html:24 +#: xpack/plugins/orgs/templates/orgs/org_users.html:47 msgid "Action" msgstr "动作" @@ -871,6 +874,7 @@ msgstr "用户名" #: ops/templates/ops/task_detail.html:95 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:82 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:72 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:82 msgid "Yes" msgstr "是" @@ -878,6 +882,7 @@ msgstr "是" #: ops/templates/ops/task_detail.html:97 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:84 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:74 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:84 msgid "No" msgstr "否" @@ -1318,7 +1323,7 @@ msgstr "默认资产组" #: perms/templates/perms/database_app_permission_list.html:15 #: perms/templates/perms/remote_app_permission_create_update.html:41 #: perms/templates/perms/remote_app_permission_list.html:15 -#: templates/index.html:86 terminal/backends/command/models.py:18 +#: templates/index.html:78 terminal/backends/command/models.py:18 #: terminal/models.py:185 terminal/templates/terminal/command_list.html:30 #: terminal/templates/terminal/command_list.html:105 #: terminal/templates/terminal/session_detail.html:48 @@ -1338,7 +1343,6 @@ msgstr "默认资产组" #: users/templates/users/user_remote_app_permission.html:37 #: users/templates/users/user_remote_app_permission.html:58 #: users/views/profile/base.py:46 xpack/plugins/orgs/forms.py:27 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:108 #: xpack/plugins/orgs/templates/orgs/org_list.html:15 msgid "User" msgstr "用户" @@ -1407,7 +1411,7 @@ msgstr "资产管理" #: assets/models/user.py:111 assets/templates/assets/system_user_users.html:76 #: templates/_nav.html:17 users/views/group.py:28 users/views/group.py:45 #: users/views/group.py:63 users/views/group.py:82 users/views/group.py:99 -#: users/views/login.py:164 users/views/profile/password.py:40 +#: users/views/login.py:163 users/views/profile/password.py:40 #: users/views/profile/pubkey.py:36 users/views/user.py:50 #: users/views/user.py:67 users/views/user.py:111 users/views/user.py:178 #: users/views/user.py:206 users/views/user.py:220 users/views/user.py:234 @@ -1728,7 +1732,7 @@ msgstr "资产列表" #: ops/templates/ops/command_execution_create.html:112 #: settings/templates/settings/_ldap_list_users_modal.html:41 #: users/templates/users/_granted_assets.html:7 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:62 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:65 msgid "Loading" msgstr "加载中" @@ -1888,7 +1892,7 @@ msgstr "自动生成密钥" #: perms/templates/perms/remote_app_permission_create_update.html:51 #: terminal/templates/terminal/terminal_update.html:38 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:61 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:44 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:47 #: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:35 msgid "Other" msgstr "其它" @@ -1957,7 +1961,7 @@ msgstr "选择节点" #: users/templates/users/user_list.html:184 #: users/templates/users/user_password_verify.html:20 #: xpack/plugins/cloud/templates/cloud/account_create_update.html:30 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:50 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:53 #: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:41 #: xpack/plugins/interface/templates/interface/interface.html:103 #: xpack/plugins/orgs/templates/orgs/org_create_update.html:30 @@ -1997,7 +2001,7 @@ msgstr "资产用户" #: users/templates/users/user_detail.html:126 #: users/templates/users/user_profile.html:150 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:126 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:129 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:139 #: xpack/plugins/license/templates/license/license_detail.html:80 msgid "Quick modify" msgstr "快速修改" @@ -2530,7 +2534,7 @@ msgstr "启用" msgid "-" msgstr "" -#: audits/models.py:78 xpack/plugins/cloud/models.py:201 +#: audits/models.py:78 xpack/plugins/cloud/models.py:204 msgid "Failed" msgstr "失败" @@ -2563,7 +2567,7 @@ msgstr "多因子认证" #: audits/models.py:87 audits/templates/audits/login_log_list.html:63 #: xpack/plugins/change_auth_plan/models.py:286 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:15 -#: xpack/plugins/cloud/models.py:214 +#: xpack/plugins/cloud/models.py:217 msgid "Reason" msgstr "原因" @@ -2571,7 +2575,7 @@ msgstr "原因" #: tickets/templates/tickets/ticket_detail.html:34 #: tickets/templates/tickets/ticket_list.html:36 #: tickets/templates/tickets/ticket_list.html:104 -#: xpack/plugins/cloud/models.py:211 xpack/plugins/cloud/models.py:269 +#: xpack/plugins/cloud/models.py:214 xpack/plugins/cloud/models.py:272 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:50 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:48 msgid "Status" @@ -2603,6 +2607,7 @@ msgstr "开始日期" #: perms/templates/perms/asset_permission_user.html:74 #: perms/templates/perms/database_app_permission_user.html:74 #: perms/templates/perms/remote_app_permission_user.html:83 +#: xpack/plugins/orgs/templates/orgs/org_users.html:67 msgid "Select user" msgstr "选择用户" @@ -3007,7 +3012,7 @@ msgstr "字段必须唯一" msgid "

Flow service unavailable, check it

" msgstr "" -#: jumpserver/views/index.py:257 templates/_nav.html:7 +#: jumpserver/views/index.py:23 templates/_nav.html:7 msgid "Dashboard" msgstr "仪表盘" @@ -3044,13 +3049,13 @@ msgstr "没有该主机 {} 权限" #: ops/mixin.py:29 ops/mixin.py:92 ops/mixin.py:162 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:98 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:88 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:98 msgid "Cycle perform" msgstr "周期执行" #: ops/mixin.py:33 ops/mixin.py:90 ops/mixin.py:111 ops/mixin.py:150 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:90 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:80 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:90 msgid "Regularly perform" msgstr "定期执行" @@ -3058,8 +3063,8 @@ msgstr "定期执行" #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:54 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:79 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:17 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:37 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:69 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:40 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:79 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:16 #: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:28 msgid "Periodic perform" @@ -3120,7 +3125,7 @@ msgstr "Become" #: ops/models/adhoc.py:150 users/templates/users/user_group_detail.html:54 #: xpack/plugins/cloud/templates/cloud/account_detail.html:59 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:51 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:55 msgid "Create by" msgstr "创建者" @@ -3180,7 +3185,7 @@ msgstr "{} 任务结束" #: ops/models/command.py:24 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:56 -#: xpack/plugins/cloud/models.py:209 +#: xpack/plugins/cloud/models.py:212 msgid "Result" msgstr "结果" @@ -3398,7 +3403,7 @@ msgstr "内容" #: ops/templates/ops/task_list.html:73 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:135 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:54 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:138 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:148 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:58 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:44 msgid "Run" @@ -3573,8 +3578,9 @@ msgstr "添加资产" #: perms/templates/perms/remote_app_permission_user.html:120 #: users/templates/users/user_group_detail.html:87 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:76 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:88 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:125 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:89 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:123 +#: xpack/plugins/orgs/templates/orgs/org_users.html:73 msgid "Add" msgstr "添加" @@ -3673,6 +3679,7 @@ msgstr "刷新成功" #: perms/templates/perms/asset_permission_user.html:31 #: perms/templates/perms/database_app_permission_user.html:31 #: perms/templates/perms/remote_app_permission_user.html:30 +#: xpack/plugins/orgs/templates/orgs/org_users.html:24 msgid "User list of " msgstr "用户列表" @@ -4624,8 +4631,8 @@ msgid "Total users" msgstr "用户总数" #: templates/index.html:23 -msgid "Total hosts" -msgstr "主机总数" +msgid "Total assets" +msgstr "资产总数" #: templates/index.html:36 msgid "Online users" @@ -4647,120 +4654,124 @@ msgstr " 位用户登录 " msgid " times asset." msgstr " 次资产." -#: templates/index.html:66 -msgid " times/week" -msgstr " 次/周" - -#: templates/index.html:77 +#: templates/index.html:69 msgid "Active user asset ratio" msgstr "活跃用户资产占比" -#: templates/index.html:80 +#: templates/index.html:72 msgid "" "The following graphs describe the percentage of active users per month and " "assets per user host per month, respectively." msgstr "以下图形分别描述一个月活跃用户和资产占所有用户主机的百分比" -#: templates/index.html:105 templates/index.html:120 +#: templates/index.html:97 templates/index.html:112 msgid "Top 10 assets in a week" msgstr "一周Top10资产" -#: templates/index.html:121 +#: templates/index.html:113 msgid "Login frequency and last login record." msgstr "登录次数及最近一次登录记录." -#: templates/index.html:132 templates/index.html:218 -msgid " times" -msgstr " 次" - -#: templates/index.html:135 templates/index.html:221 -msgid "The time last logged in" -msgstr "最近一次登录日期" - -#: templates/index.html:136 templates/index.html:222 -msgid "At" -msgstr "于" - -#: templates/index.html:142 templates/index.html:180 templates/index.html:228 -msgid "(No)" -msgstr "(暂无)" - -#: templates/index.html:150 +#: templates/index.html:122 msgid "Last 10 login" msgstr "最近十次登录" -#: templates/index.html:156 +#: templates/index.html:128 msgid "Login record" msgstr "登录记录" -#: templates/index.html:157 +#: templates/index.html:129 msgid "Last 10 login records." msgstr "最近十次登录记录." -#: templates/index.html:170 templates/index.html:172 -msgid "Before" -msgstr "前" - -#: templates/index.html:174 -msgid "Login in " -msgstr "登录了" - -#: templates/index.html:191 templates/index.html:206 +#: templates/index.html:143 templates/index.html:158 msgid "Top 10 users in a week" msgstr "一周Top10用户" -#: templates/index.html:207 +#: templates/index.html:159 msgid "User login frequency and last login record." msgstr "用户登录次数及最近一次登录记录" -#: templates/index.html:264 +#: templates/index.html:184 msgid "Monthly data overview" msgstr "月数据总览" -#: templates/index.html:265 +#: templates/index.html:185 msgid "History summary in one month" msgstr "一个月内历史汇总" -#: templates/index.html:273 templates/index.html:297 +#: templates/index.html:193 templates/index.html:217 msgid "Login count" msgstr "登录次数" -#: templates/index.html:273 templates/index.html:304 +#: templates/index.html:193 templates/index.html:224 msgid "Active users" msgstr "活跃用户" -#: templates/index.html:273 templates/index.html:311 +#: templates/index.html:193 templates/index.html:231 msgid "Active assets" msgstr "活跃资产" -#: templates/index.html:338 templates/index.html:388 +#: templates/index.html:262 templates/index.html:313 msgid "Monthly active users" msgstr "月活跃用户" -#: templates/index.html:338 templates/index.html:389 +#: templates/index.html:262 templates/index.html:314 msgid "Disable user" msgstr "禁用用户" -#: templates/index.html:338 templates/index.html:390 +#: templates/index.html:262 templates/index.html:315 msgid "Month not logged in user" msgstr "月未登录用户" -#: templates/index.html:364 templates/index.html:440 +#: templates/index.html:288 templates/index.html:368 msgid "Access to the source" msgstr "访问来源" -#: templates/index.html:414 templates/index.html:464 -msgid "Month is logged into the host" -msgstr "月被登录主机" +#: templates/index.html:342 +msgid "Month is logged into the asset" +msgstr "月被登录资产" -#: templates/index.html:414 templates/index.html:465 +#: templates/index.html:342 templates/index.html:393 msgid "Disable host" msgstr "禁用主机" -#: templates/index.html:414 templates/index.html:466 +#: templates/index.html:342 templates/index.html:394 msgid "Month not logged on host" msgstr "月未登录主机" +#: templates/index.html:392 +msgid "Month is logged into the host" +msgstr "月被登录主机" + +#: templates/index.html:466 +msgid " times/week" +msgstr " 次/周" + +#: templates/index.html:491 templates/index.html:527 +msgid " times" +msgstr " 次" + +#: templates/index.html:494 templates/index.html:530 +msgid "The time last logged in" +msgstr "最近一次登录日期" + +#: templates/index.html:495 templates/index.html:531 +msgid "At" +msgstr "于" + +#: templates/index.html:510 templates/index.html:545 templates/index.html:580 +msgid "(No)" +msgstr "(暂无)" + +#: templates/index.html:561 +msgid "Before" +msgstr "前" + +#: templates/index.html:562 +msgid "Login in " +msgstr "登录了" + #: templates/rest_framework/base.html:128 msgid "Filters" msgstr "过滤" @@ -4880,9 +4891,9 @@ msgid "" " " msgstr "" -#: terminal/forms/storage.py:136 xpack/plugins/cloud/models.py:263 +#: terminal/forms/storage.py:136 xpack/plugins/cloud/models.py:266 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:29 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:106 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:112 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:46 msgid "Region" msgstr "地域" @@ -5401,7 +5412,7 @@ msgstr "复制用户公钥到这里" msgid "Join user groups" msgstr "添加到用户组" -#: users/forms/user.py:103 users/views/login.py:124 +#: users/forms/user.py:103 users/views/login.py:123 #: users/views/profile/password.py:57 msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" @@ -5427,6 +5438,7 @@ msgid "Administrator" msgstr "管理员" #: users/models/user.py:145 xpack/plugins/orgs/forms.py:29 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:109 #: xpack/plugins/orgs/templates/orgs/org_list.html:14 msgid "Auditor" msgstr "审计员" @@ -5781,7 +5793,6 @@ msgid "User group detail" msgstr "用户组详情" #: users/templates/users/user_group_detail.html:81 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:116 msgid "Add user" msgstr "添加用户" @@ -5921,7 +5932,7 @@ msgid "Update user" msgstr "更新用户" #: users/templates/users/user_update.html:22 users/views/login.py:49 -#: users/views/login.py:117 +#: users/views/login.py:116 msgid "User auth from {}, go there change password" msgstr "用户认证源来自 {}, 请去相应系统修改密码" @@ -6143,28 +6154,28 @@ msgstr "用户组授权资产" msgid "Email address invalid, please input again" msgstr "邮箱地址错误,重新输入" -#: users/views/login.py:63 +#: users/views/login.py:62 msgid "Send reset password message" msgstr "发送重置密码邮件" -#: users/views/login.py:64 +#: users/views/login.py:63 msgid "Send reset password mail success, login your mail box and follow it " msgstr "" "发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)" -#: users/views/login.py:77 +#: users/views/login.py:76 msgid "Reset password success" msgstr "重置密码成功" -#: users/views/login.py:78 +#: users/views/login.py:77 msgid "Reset password success, return to login page" msgstr "重置密码成功,返回到登录页面" -#: users/views/login.py:102 users/views/login.py:112 +#: users/views/login.py:101 users/views/login.py:111 msgid "Token invalid or expired" msgstr "Token错误或失效" -#: users/views/login.py:164 +#: users/views/login.py:163 msgid "First login" msgstr "首次登录" @@ -6420,6 +6431,10 @@ msgstr "选择节点" msgid "Select admins" msgstr "选择管理员" +#: xpack/plugins/cloud/forms.py:85 +msgid "Tips: The asset information is always covered" +msgstr "提示:资产信息总是被覆盖" + #: xpack/plugins/cloud/meta.py:9 xpack/plugins/cloud/views.py:27 #: xpack/plugins/cloud/views.py:44 xpack/plugins/cloud/views.py:62 #: xpack/plugins/cloud/views.py:78 xpack/plugins/cloud/views.py:92 @@ -6464,48 +6479,52 @@ msgstr "地域" msgid "Instances" msgstr "实例" -#: xpack/plugins/cloud/models.py:139 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:94 +#: xpack/plugins/cloud/models.py:136 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:69 +msgid "Covered always" +msgstr "总是覆盖" + +#: xpack/plugins/cloud/models.py:142 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:104 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:17 msgid "Date last sync" msgstr "最后同步日期" -#: xpack/plugins/cloud/models.py:150 xpack/plugins/cloud/models.py:207 +#: xpack/plugins/cloud/models.py:153 xpack/plugins/cloud/models.py:210 msgid "Sync instance task" msgstr "同步实例任务" -#: xpack/plugins/cloud/models.py:202 +#: xpack/plugins/cloud/models.py:205 msgid "Succeed" msgstr "成功" -#: xpack/plugins/cloud/models.py:217 xpack/plugins/cloud/models.py:272 +#: xpack/plugins/cloud/models.py:220 xpack/plugins/cloud/models.py:275 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:51 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:49 msgid "Date sync" msgstr "同步日期" -#: xpack/plugins/cloud/models.py:245 +#: xpack/plugins/cloud/models.py:248 msgid "Unsync" msgstr "未同步" -#: xpack/plugins/cloud/models.py:246 xpack/plugins/cloud/models.py:247 +#: xpack/plugins/cloud/models.py:249 xpack/plugins/cloud/models.py:250 msgid "Synced" msgstr "已同步" -#: xpack/plugins/cloud/models.py:248 +#: xpack/plugins/cloud/models.py:251 msgid "Released" msgstr "已释放" -#: xpack/plugins/cloud/models.py:253 +#: xpack/plugins/cloud/models.py:256 msgid "Sync task" msgstr "同步任务" -#: xpack/plugins/cloud/models.py:257 +#: xpack/plugins/cloud/models.py:260 msgid "Sync instance task history" msgstr "同步实例任务历史" -#: xpack/plugins/cloud/models.py:260 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:114 +#: xpack/plugins/cloud/models.py:263 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:45 msgid "Instance" msgstr "实例" @@ -6584,7 +6603,7 @@ msgstr "创建账户" msgid "Node & AdminUser" msgstr "节点 & 管理用户" -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:63 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:66 msgid "Load failed" msgstr "加载失败" @@ -6609,11 +6628,11 @@ msgstr "同步历史列表" msgid "Sync instance list" msgstr "同步实例列表" -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:135 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:145 msgid "Run task manually" msgstr "手动执行任务" -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:178 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:188 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:102 msgid "Sync success" msgstr "同步成功" @@ -6650,7 +6669,7 @@ msgstr "执行次数" msgid "Instance count" msgstr "实例个数" -#: xpack/plugins/cloud/utils.py:37 +#: xpack/plugins/cloud/utils.py:38 msgid "Account unavailable" msgstr "账户无效" @@ -6887,42 +6906,60 @@ msgid "Select auditor" msgstr "选择审计员" #: xpack/plugins/orgs/forms.py:28 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:71 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:75 #: xpack/plugins/orgs/templates/orgs/org_list.html:13 msgid "Admin" msgstr "管理员" -#: xpack/plugins/orgs/meta.py:8 xpack/plugins/orgs/views.py:26 -#: xpack/plugins/orgs/views.py:43 xpack/plugins/orgs/views.py:61 -#: xpack/plugins/orgs/views.py:79 +#: xpack/plugins/orgs/meta.py:8 xpack/plugins/orgs/views.py:27 +#: xpack/plugins/orgs/views.py:44 xpack/plugins/orgs/views.py:62 +#: xpack/plugins/orgs/views.py:85 xpack/plugins/orgs/views.py:116 msgid "Organizations" msgstr "组织管理" #: xpack/plugins/orgs/templates/orgs/org_detail.html:17 -#: xpack/plugins/orgs/views.py:80 +#: xpack/plugins/orgs/templates/orgs/org_users.html:13 +#: xpack/plugins/orgs/views.py:86 msgid "Org detail" msgstr "组织详情" -#: xpack/plugins/orgs/templates/orgs/org_detail.html:79 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:20 +#: xpack/plugins/orgs/templates/orgs/org_users.html:16 +msgid "Org users" +msgstr "组织用户" + +#: xpack/plugins/orgs/templates/orgs/org_detail.html:83 msgid "Add admin" msgstr "添加管理员" +#: xpack/plugins/orgs/templates/orgs/org_detail.html:117 +msgid "Add auditor" +msgstr "添加审计员" + #: xpack/plugins/orgs/templates/orgs/org_list.html:5 msgid "Create organization " msgstr "创建组织" -#: xpack/plugins/orgs/views.py:27 +#: xpack/plugins/orgs/templates/orgs/org_users.html:59 +msgid "Add user to organization" +msgstr "添加用户" + +#: xpack/plugins/orgs/views.py:28 msgid "Org list" msgstr "组织列表" -#: xpack/plugins/orgs/views.py:44 +#: xpack/plugins/orgs/views.py:45 msgid "Create org" msgstr "创建组织" -#: xpack/plugins/orgs/views.py:62 +#: xpack/plugins/orgs/views.py:63 msgid "Update org" msgstr "更新组织" +#: xpack/plugins/orgs/views.py:117 +msgid "Org user list" +msgstr "组织用户列表" + #: xpack/plugins/vault/meta.py:11 xpack/plugins/vault/views.py:23 #: xpack/plugins/vault/views.py:38 msgid "Vault" @@ -6944,11 +6981,8 @@ msgstr "密码匣子" msgid "vault create" msgstr "创建" -#~ msgid "Tips: The asset information is always covered" -#~ msgstr "提示:资产信息总是被覆盖" - -#~ msgid "Covered always" -#~ msgstr "总是覆盖" +#~ msgid "Total hosts" +#~ msgstr "主机总数" #~ msgid "* For security, do not change {}'s password" #~ msgstr "* 为了安全,不能修改 {} 的密码" From 331cfe2aede2ee13f54d38d3501044da5609f602 Mon Sep 17 00:00:00 2001 From: ibuler Date: Fri, 8 May 2020 15:53:58 +0800 Subject: [PATCH 25/30] =?UTF-8?q?[Bugfix]=20=E4=BF=AE=E5=A4=8D=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E6=95=B0=E9=87=8F=E7=BC=93=E5=AD=98=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E4=B8=8D=E5=AF=B9=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/utils.py | 8 ++------ apps/perms/utils/asset_permission.py | 4 +++- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/assets/utils.py b/apps/assets/utils.py index 8ec20388b..6b4d8111a 100644 --- a/apps/assets/utils.py +++ b/apps/assets/utils.py @@ -76,14 +76,9 @@ class TreeService(Tree): ancestor_ids.pop(0) return ancestor_ids - def ancestors(self, nid, with_self=False, deep=False, with_assets=True): + def ancestors(self, nid, with_self=False, deep=False): ancestor_ids = self.ancestors_ids(nid, with_self=with_self) ancestors = [self.get_node(i, deep=deep) for i in ancestor_ids] - if with_assets: - return ancestors - for n in ancestors: - n.data['assets'] = set() - n.data['all_assets'] = None return ancestors def get_node_full_tag(self, nid): @@ -108,6 +103,7 @@ class TreeService(Tree): node = super().get_node(nid) if deep: node = self.copy_node(node) + node.data = {} return node def parent(self, nid, deep=False): diff --git a/apps/perms/utils/asset_permission.py b/apps/perms/utils/asset_permission.py index 3a1f81a90..eb7b3e56c 100644 --- a/apps/perms/utils/asset_permission.py +++ b/apps/perms/utils/asset_permission.py @@ -301,7 +301,6 @@ class AssetPermissionUtil(AssetPermissionUtilCacheMixin): continue ancestors = self.full_tree.ancestors( child.identifier, with_self=False, deep=True, - with_assets=False, ) if not ancestors: continue @@ -350,6 +349,9 @@ class AssetPermissionUtil(AssetPermissionUtilCacheMixin): self.add_favorite_node_if_need(user_tree) self.set_user_tree_to_cache_if_need(user_tree) self.set_user_tree_to_local(user_tree) + for n in user_tree.all_nodes(): + if n.identifier in ['3', '3:0']: + logger.info('{} - {}'.format(n.tag, n.data)) return user_tree # Todo: 是否可以获取多个资产的系统用户 From c6ed6d8acbb012d96078097d3a70dd4bb2665f4a Mon Sep 17 00:00:00 2001 From: ibuler Date: Fri, 8 May 2020 15:58:00 +0800 Subject: [PATCH 26/30] =?UTF-8?q?[Update]=20=E5=8E=BB=E6=8E=89debug?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/perms/utils/asset_permission.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/perms/utils/asset_permission.py b/apps/perms/utils/asset_permission.py index eb7b3e56c..cd6e1805e 100644 --- a/apps/perms/utils/asset_permission.py +++ b/apps/perms/utils/asset_permission.py @@ -349,9 +349,6 @@ class AssetPermissionUtil(AssetPermissionUtilCacheMixin): self.add_favorite_node_if_need(user_tree) self.set_user_tree_to_cache_if_need(user_tree) self.set_user_tree_to_local(user_tree) - for n in user_tree.all_nodes(): - if n.identifier in ['3', '3:0']: - logger.info('{} - {}'.format(n.tag, n.data)) return user_tree # Todo: 是否可以获取多个资产的系统用户 From 11b3c57c927f96bba0ab5cde27e96df28f718de6 Mon Sep 17 00:00:00 2001 From: Bai Date: Fri, 8 May 2020 20:02:00 +0800 Subject: [PATCH 27/30] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9dashboard?= =?UTF-8?q?=E4=B8=AD=E4=BD=BF=E7=94=A8=E7=9A=84=E5=8F=98=E9=87=8F=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/api.py | 4 ++-- apps/templates/index.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index e272c9ff3..02fb8ba8e 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -219,7 +219,7 @@ class TotalCountMixin: return count @staticmethod - def get_total_count_online_assets(): + def get_total_count_online_sessions(): return Session.objects.filter(is_finished=False).count() @@ -239,7 +239,7 @@ class IndexApi(TotalCountMixin, WeekSessionMetricMixin, MonthLoginMetricMixin, A 'total_count_assets': self.get_total_count_assets(), 'total_count_users': self.get_total_count_users(), 'total_count_online_users': self.get_total_count_online_users(), - 'total_count_online_assets': self.get_total_count_online_assets(), + 'total_count_online_sessions': self.get_total_count_online_sessions(), }) if _all or query_params.get('month_metrics'): diff --git a/apps/templates/index.html b/apps/templates/index.html index 36f57137c..1942ad2cb 100644 --- a/apps/templates/index.html +++ b/apps/templates/index.html @@ -50,7 +50,7 @@
-

+

Online sessions
@@ -422,7 +422,7 @@ function renderTotalCount(){ $('#total_count_assets').html(data['total_count_assets']); $('#total_count_users').html(data['total_count_users']); $('#total_count_online_users').html(data['total_count_online_users']); - $('#total_count_online_assets').html(data['total_count_online_assets']); + $('#total_count_online_sessions').html(data['total_count_online_sessions']); }; renderRequestApi('total_count=1', success); } From 4cebfc7f6ae7aa024c1d684532df15eb09628d58 Mon Sep 17 00:00:00 2001 From: BaiJiangJie <32935519+BaiJiangJie@users.noreply.github.com> Date: Sat, 9 May 2020 13:29:39 +0800 Subject: [PATCH 28/30] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9=E7=BF=BB?= =?UTF-8?q?=E8=AF=91=20(#3981)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 修改翻译 * [Update] 修改翻译2 --- apps/locale/zh/LC_MESSAGES/django.mo | Bin 90004 -> 90124 bytes apps/locale/zh/LC_MESSAGES/django.po | 151 ++++++++++++++------------- 2 files changed, 81 insertions(+), 70 deletions(-) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index a466cf2e82b4c775389f7513945b211882ed2112..cfe9268ebd28e44b73fd8f22be9545edca554db9 100644 GIT binary patch delta 26502 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 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 2a2de30c3..c4f7e78c2 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-05-08 15:42+0800\n" +"POT-Creation-Date: 2020-05-09 13:17+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -26,7 +26,7 @@ msgstr "自定义" #: applications/templates/applications/remote_app_list.html:27 #: applications/templates/applications/user_remote_app_list.html:18 #: assets/forms/domain.py:15 assets/forms/label.py:13 -#: assets/models/asset.py:353 assets/models/authbook.py:27 +#: assets/models/asset.py:352 assets/models/authbook.py:27 #: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:32 #: assets/serializers/asset_user.py:47 assets/serializers/asset_user.py:84 #: assets/serializers/system_user.py:44 assets/serializers/system_user.py:176 @@ -112,7 +112,7 @@ msgstr "运行参数" #: applications/templates/applications/user_database_app_list.html:16 #: applications/templates/applications/user_remote_app_list.html:16 #: assets/forms/asset.py:21 assets/forms/domain.py:77 assets/forms/user.py:74 -#: assets/forms/user.py:96 assets/models/asset.py:146 assets/models/base.py:232 +#: assets/forms/user.py:96 assets/models/asset.py:145 assets/models/base.py:232 #: assets/models/cluster.py:18 assets/models/cmd_filter.py:21 #: assets/models/domain.py:20 assets/models/group.py:20 #: assets/models/label.py:18 assets/templates/assets/_node_detail_modal.html:27 @@ -204,7 +204,7 @@ msgstr "主机" #: applications/models/database_app.py:27 #: applications/templates/applications/database_app_detail.html:60 #: applications/templates/applications/database_app_list.html:26 -#: assets/forms/asset.py:25 assets/models/asset.py:192 +#: assets/forms/asset.py:25 assets/models/asset.py:191 #: assets/models/domain.py:50 #: assets/templates/assets/domain_gateway_list.html:64 msgid "Port" @@ -227,7 +227,7 @@ msgstr "数据库" #: applications/templates/applications/remote_app_list.html:28 #: applications/templates/applications/user_database_app_list.html:20 #: applications/templates/applications/user_remote_app_list.html:19 -#: assets/models/asset.py:151 assets/models/asset.py:227 +#: assets/models/asset.py:150 assets/models/asset.py:226 #: assets/models/base.py:237 assets/models/cluster.py:29 #: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:56 #: assets/models/domain.py:21 assets/models/domain.py:53 @@ -309,7 +309,7 @@ msgstr "参数" #: applications/models/remote_app.py:39 #: applications/templates/applications/database_app_detail.html:72 #: applications/templates/applications/remote_app_detail.html:68 -#: assets/models/asset.py:225 assets/models/base.py:240 +#: assets/models/asset.py:224 assets/models/base.py:240 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:26 #: assets/models/cmd_filter.py:59 assets/models/group.py:21 #: assets/templates/assets/admin_user_detail.html:63 @@ -335,7 +335,7 @@ msgstr "创建者" #: applications/models/remote_app.py:42 #: applications/templates/applications/database_app_detail.html:68 #: applications/templates/applications/remote_app_detail.html:64 -#: assets/models/asset.py:226 assets/models/base.py:238 +#: assets/models/asset.py:225 assets/models/base.py:238 #: assets/models/cluster.py:26 assets/models/domain.py:23 #: assets/models/gathered_user.py:19 assets/models/group.py:22 #: assets/models/label.py:25 assets/templates/assets/admin_user_detail.html:59 @@ -737,7 +737,7 @@ msgstr "不能移除资产的管理用户账号" msgid "Latest version could not be delete" msgstr "最新版本的不能被删除" -#: assets/forms/asset.py:83 assets/models/asset.py:196 +#: assets/forms/asset.py:83 assets/models/asset.py:195 #: assets/models/user.py:109 assets/templates/assets/asset_detail.html:186 #: assets/templates/assets/asset_detail.html:194 #: assets/templates/assets/system_user_assets.html:118 @@ -748,7 +748,7 @@ msgstr "最新版本的不能被删除" msgid "Nodes" msgstr "节点" -#: assets/forms/asset.py:86 assets/models/asset.py:200 +#: assets/forms/asset.py:86 assets/models/asset.py:199 #: assets/models/cluster.py:19 assets/models/user.py:65 #: assets/templates/assets/admin_user_list.html:62 #: assets/templates/assets/asset_detail.html:72 templates/_nav.html:44 @@ -766,7 +766,7 @@ msgstr "管理用户" msgid "Label" msgstr "标签" -#: assets/forms/asset.py:92 assets/models/asset.py:195 +#: assets/forms/asset.py:92 assets/models/asset.py:194 #: assets/models/domain.py:26 assets/models/domain.py:52 #: assets/templates/assets/asset_detail.html:76 #: assets/templates/assets/user_asset_list.html:80 @@ -774,8 +774,8 @@ msgstr "标签" msgid "Domain" msgstr "网域" -#: assets/forms/asset.py:95 assets/models/asset.py:170 -#: assets/models/asset.py:194 assets/serializers/asset.py:67 +#: assets/forms/asset.py:95 assets/models/asset.py:169 +#: assets/models/asset.py:193 assets/serializers/asset.py:67 #: assets/templates/assets/asset_detail.html:100 #: assets/templates/assets/user_asset_list.html:78 msgid "Platform" @@ -980,24 +980,24 @@ msgstr "SFTP的起始路径,tmp目录, 用户home目录或者自定义" msgid "Username is dynamic, When connect asset, using current user's username" msgstr "用户名是动态的,登录资产时使用当前用户的用户名登录" -#: assets/models/asset.py:147 xpack/plugins/cloud/providers/base.py:16 +#: assets/models/asset.py:146 xpack/plugins/cloud/providers/base.py:16 msgid "Base" msgstr "基础" -#: assets/models/asset.py:148 assets/templates/assets/platform_detail.html:56 +#: assets/models/asset.py:147 assets/templates/assets/platform_detail.html:56 msgid "Charset" msgstr "编码" -#: assets/models/asset.py:149 assets/templates/assets/platform_detail.html:60 +#: assets/models/asset.py:148 assets/templates/assets/platform_detail.html:60 #: tickets/models/ticket.py:38 msgid "Meta" msgstr "元数据" -#: assets/models/asset.py:150 +#: assets/models/asset.py:149 msgid "Internal" msgstr "内部的" -#: assets/models/asset.py:187 assets/models/domain.py:49 +#: assets/models/asset.py:186 assets/models/domain.py:49 #: assets/serializers/asset_user.py:46 #: assets/templates/assets/_asset_list_modal.html:47 #: assets/templates/assets/_asset_user_list.html:20 @@ -1014,7 +1014,7 @@ msgstr "内部的" msgid "IP" msgstr "IP" -#: assets/models/asset.py:188 assets/serializers/asset_user.py:45 +#: assets/models/asset.py:187 assets/serializers/asset_user.py:45 #: assets/serializers/gathered_user.py:20 #: assets/templates/assets/_asset_list_modal.html:46 #: assets/templates/assets/_asset_user_auth_update_modal.html:9 @@ -1031,7 +1031,7 @@ msgstr "IP" msgid "Hostname" msgstr "主机名" -#: assets/models/asset.py:191 assets/models/domain.py:51 +#: assets/models/asset.py:190 assets/models/domain.py:51 #: assets/models/user.py:114 assets/templates/assets/asset_detail.html:68 #: assets/templates/assets/domain_gateway_list.html:65 #: assets/templates/assets/system_user_detail.html:78 @@ -1043,84 +1043,84 @@ msgstr "主机名" msgid "Protocol" msgstr "协议" -#: assets/models/asset.py:193 assets/serializers/asset.py:69 +#: assets/models/asset.py:192 assets/serializers/asset.py:69 #: assets/templates/assets/asset_create.html:24 #: assets/templates/assets/user_asset_list.html:77 #: perms/serializers/user_permission.py:60 msgid "Protocols" msgstr "协议组" -#: assets/models/asset.py:197 assets/models/cmd_filter.py:22 +#: assets/models/asset.py:196 assets/models/cmd_filter.py:22 #: assets/models/domain.py:54 assets/models/label.py:22 #: assets/templates/assets/asset_detail.html:108 authentication/models.py:45 msgid "Is active" msgstr "激活" -#: assets/models/asset.py:203 assets/templates/assets/asset_detail.html:64 +#: assets/models/asset.py:202 assets/templates/assets/asset_detail.html:64 msgid "Public IP" msgstr "公网IP" -#: assets/models/asset.py:204 assets/templates/assets/asset_detail.html:116 +#: assets/models/asset.py:203 assets/templates/assets/asset_detail.html:116 msgid "Asset number" msgstr "资产编号" -#: assets/models/asset.py:207 assets/templates/assets/asset_detail.html:80 +#: assets/models/asset.py:206 assets/templates/assets/asset_detail.html:80 msgid "Vendor" msgstr "制造商" -#: assets/models/asset.py:208 assets/templates/assets/asset_detail.html:84 +#: assets/models/asset.py:207 assets/templates/assets/asset_detail.html:84 msgid "Model" msgstr "型号" -#: assets/models/asset.py:209 assets/templates/assets/asset_detail.html:112 +#: assets/models/asset.py:208 assets/templates/assets/asset_detail.html:112 msgid "Serial number" msgstr "序列号" -#: assets/models/asset.py:211 +#: assets/models/asset.py:210 msgid "CPU model" msgstr "CPU型号" -#: assets/models/asset.py:212 +#: assets/models/asset.py:211 msgid "CPU count" msgstr "CPU数量" -#: assets/models/asset.py:213 +#: assets/models/asset.py:212 msgid "CPU cores" msgstr "CPU核数" -#: assets/models/asset.py:214 +#: assets/models/asset.py:213 msgid "CPU vcpus" msgstr "CPU总数" -#: assets/models/asset.py:215 assets/templates/assets/asset_detail.html:92 +#: assets/models/asset.py:214 assets/templates/assets/asset_detail.html:92 msgid "Memory" msgstr "内存" -#: assets/models/asset.py:216 +#: assets/models/asset.py:215 msgid "Disk total" msgstr "硬盘大小" -#: assets/models/asset.py:217 +#: assets/models/asset.py:216 msgid "Disk info" msgstr "硬盘信息" -#: assets/models/asset.py:219 assets/templates/assets/asset_detail.html:104 +#: assets/models/asset.py:218 assets/templates/assets/asset_detail.html:104 msgid "OS" msgstr "操作系统" -#: assets/models/asset.py:220 +#: assets/models/asset.py:219 msgid "OS version" msgstr "系统版本" -#: assets/models/asset.py:221 +#: assets/models/asset.py:220 msgid "OS arch" msgstr "系统架构" -#: assets/models/asset.py:222 +#: assets/models/asset.py:221 msgid "Hostname raw" msgstr "主机名原始" -#: assets/models/asset.py:224 assets/templates/assets/asset_create.html:46 +#: assets/models/asset.py:223 assets/templates/assets/asset_create.html:46 #: assets/templates/assets/asset_detail.html:220 templates/_nav.html:46 msgid "Labels" msgstr "标签管理" @@ -2881,8 +2881,8 @@ msgid "More login options" msgstr "更多登录方式" #: authentication/templates/authentication/login.html:61 -msgid "Keycloak" -msgstr "" +msgid "OpenID" +msgstr "OpenID" #: authentication/templates/authentication/login_otp.html:17 msgid "One-time password" @@ -6529,11 +6529,11 @@ msgstr "同步实例任务历史" msgid "Instance" msgstr "实例" -#: xpack/plugins/cloud/providers/aliyun.py:16 +#: xpack/plugins/cloud/providers/aliyun.py:19 msgid "Alibaba Cloud" msgstr "阿里云" -#: xpack/plugins/cloud/providers/aws.py:14 +#: xpack/plugins/cloud/providers/aws.py:15 msgid "AWS (International)" msgstr "AWS (国际)" @@ -6541,51 +6541,59 @@ msgstr "AWS (国际)" msgid "AWS (China)" msgstr "AWS (中国)" -#: xpack/plugins/cloud/providers/huaweicloud.py:13 +#: xpack/plugins/cloud/providers/huaweicloud.py:17 msgid "Huawei Cloud" msgstr "华为云" -#: xpack/plugins/cloud/providers/huaweicloud.py:16 -msgid "CN North-Beijing4" -msgstr "华北-北京4" - -#: xpack/plugins/cloud/providers/huaweicloud.py:17 -msgid "CN East-Shanghai1" -msgstr "华东-上海1" - -#: xpack/plugins/cloud/providers/huaweicloud.py:18 -msgid "CN East-Shanghai2" -msgstr "华东-上海2" - -#: xpack/plugins/cloud/providers/huaweicloud.py:19 -msgid "CN South-Guangzhou" -msgstr "华南-广州" - #: xpack/plugins/cloud/providers/huaweicloud.py:20 -msgid "CN Southwest-Guiyang1" -msgstr "西南-贵阳1" +msgid "AF-Johannesburg" +msgstr "非洲-约翰内斯堡" #: xpack/plugins/cloud/providers/huaweicloud.py:21 -msgid "AP-Hong-Kong" -msgstr "亚太-香港" - -#: xpack/plugins/cloud/providers/huaweicloud.py:22 msgid "AP-Bangkok" msgstr "亚太-曼谷" +#: xpack/plugins/cloud/providers/huaweicloud.py:22 +msgid "AP-Hong Kong" +msgstr "亚太-香港" + #: xpack/plugins/cloud/providers/huaweicloud.py:23 msgid "AP-Singapore" msgstr "亚太-新加坡" #: xpack/plugins/cloud/providers/huaweicloud.py:24 -msgid "AF-Johannesburg" -msgstr "非洲-约翰内斯堡" +msgid "CN East-Shanghai1" +msgstr "华东-上海1" #: xpack/plugins/cloud/providers/huaweicloud.py:25 -msgid "LA-Santiago" -msgstr "拉美-圣地亚哥" +msgid "CN East-Shanghai2" +msgstr "华东-上海2" + +#: xpack/plugins/cloud/providers/huaweicloud.py:26 +msgid "CN North-Beijing1" +msgstr "华北-北京1" + +#: xpack/plugins/cloud/providers/huaweicloud.py:27 +msgid "CN North-Beijing4" +msgstr "华北-北京4" + +#: xpack/plugins/cloud/providers/huaweicloud.py:28 +msgid "CN Northeast-Dalian" +msgstr "东北-大连" + +#: xpack/plugins/cloud/providers/huaweicloud.py:29 +msgid "CN South-Guangzhou" +msgstr "华南-广州" -#: xpack/plugins/cloud/providers/qcloud.py:14 +#: xpack/plugins/cloud/providers/huaweicloud.py:30 +msgid "CN Southwest-Guiyang1" +msgstr "西南-贵阳1" + +#: xpack/plugins/cloud/providers/huaweicloud.py:31 +msgid "EU-Paris" +msgstr "欧洲-巴黎" + +#: xpack/plugins/cloud/providers/qcloud.py:17 msgid "Tencent Cloud" msgstr "腾讯云" @@ -6981,6 +6989,9 @@ msgstr "密码匣子" msgid "vault create" msgstr "创建" +#~ msgid "LA-Santiago" +#~ msgstr "拉美-圣地亚哥" + #~ msgid "Total hosts" #~ msgstr "主机总数" From 0a8eeca62969da0fe36f111f97f2c4ee5e8c8953 Mon Sep 17 00:00:00 2001 From: Bai Date: Mon, 11 May 2020 15:12:42 +0800 Subject: [PATCH 29/30] =?UTF-8?q?[Update]=20=E4=BB=AA=E8=A1=A8=E7=9B=98?= =?UTF-8?q?=E7=BC=93=E5=AD=98key=E6=B7=BB=E5=8A=A0=E7=BB=84=E7=BB=87id?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index e272c9ff3..e98a44d6b 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -36,7 +36,7 @@ class MonthLoginMetricMixin: @staticmethod def get_cache_key(date, tp): date_str = date.strftime("%Y%m%d") - key = "SESSION_MONTH_{}_{}".format(tp, date_str) + key = "SESSION_MONTH_{}_{}_{}".format(current_org.id, tp, date_str) return key def __get_data_from_cache(self, date, tp): From 7b3647e78afcd6e7b9a304def196cfdd73f8e21b Mon Sep 17 00:00:00 2001 From: Bai Date: Mon, 11 May 2020 16:56:52 +0800 Subject: [PATCH 30/30] =?UTF-8?q?[Bugfix]=20=E4=BF=AE=E5=A4=8D=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E9=A1=B5=E9=9D=A2=E6=9B=B4=E6=96=B0MFA=E6=97=B6?= =?UTF-8?q?=E8=A7=A3=E9=99=A4=E7=AE=A1=E7=90=86=E5=91=98=E5=BC=BA=E5=88=B6?= =?UTF-8?q?=E5=90=AF=E7=94=A8=E7=9A=84Bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/locale/zh/LC_MESSAGES/django.mo | Bin 90004 -> 90055 bytes apps/locale/zh/LC_MESSAGES/django.po | 129 ++++++++++-------- ..._disable_mfa.html => user_verify_mfa.html} | 0 apps/users/views/profile/otp.py | 20 ++- 4 files changed, 87 insertions(+), 62 deletions(-) rename apps/users/templates/users/{user_disable_mfa.html => user_verify_mfa.html} (100%) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index a466cf2e82b4c775389f7513945b211882ed2112..954523412d8c88bcbd3686c3002b6b992e61039a 100644 GIT binary patch delta 26441 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 delta 26373 zcmYk^1#}hH7RK>Oun?R8!AX#y!M#Xvmm)=qySv-PwPGT&knz)erEz&`zE=p150Q&l`&G@G=hS z!d%qX@9KF;0zID>(Va=C_^PMp#m2b3JTECGL)PKtz<8Js<6tRFhSe|}TbZLV9r2II zwRlG{51zzG{Df*B+1vB7Vhv2o{9b!QrS2nu*%!1(<~Sy$7d7xR^Al>Ixc%Kg zVW@tYEzX77se-5lmc>x4gBqt3M#KJ?9tWc@jKU%c+WG^il^sWI@qJ8;pHK@*%#9Di zw3rGrV^S<*`39(gTcE}nh1&8Z7XO0DiEp6B|8D^MuYnQ`boVS0bwznGAr{3XSOsHX zGt`#1F}q_T;z8yl)Riy77PtYmBO!x4F9O3+_r4SK*@ z?NXT8QJ*Wt$FTppcQr{w!#1c1JD9!9A?8?f8tUH9M=fYM7Qt;86aT?j_!_kXpHS@+ zjCJjkqi$u^vFyKAUY3Lw(gd}DUYHYypaxotxo|6LXYQbO>;bBO;5c`SQll1-9(C)o zqMofnsBy}o?tN8@>-i|8C(#u1<8UmG+fiHl5p_!vj&}>pjJlV_Ev}0CJZNF|L2dnb z)RiwqEnu_7`%#~$r!fe9cPMCOPf+*d6>8D8 zG-~2$sPPw?D^cUEcd^ggOCg-Zuc#H>$JqEUYJhhb2ZO$M?UJBg%YvwZI-s_^Kk5R8 zqMq(3)PgsnK9CNgcIq0c-!p%n`yVjTZFL%qOGPf!y)A}XNFDS)8>ok-7wVQw#(1~_ zHQ*Lh|3j#SoW@|hh1#Jfs9PFslDnX!=zssiDQJQ`R#5@Z;E@d}H# zpxW;@FQF!Sff_gFWVevis2$0S+OfjuOF*F{1r1mOb;XU%wirg-9d&D_p>D-IRQr{v ziMESA#TbTrP_NxJ)E3`D zE$APM-=Y>4bE38BYgq_ZTWiC0(PR>{f5c#8miq()Iy@qanDjb)Hvx-7myj% zE++;tzgL`suCP3+VO3PaI;b7^4)s>FM?I`#P!mi?wOfGdw+wa5)>wWAYGM0O?ay2N zb@L(m@>B7ef*zh6bKL~h(LXUJBHtdhl><>bFab5;4Ag=apmt&vYG?MMZq4tgaW0{5 z!F^P}cbEdB&13)7G0i-;(mbfAyCUjgs)o9v4ycLypsr-7ISw_!H1yvh)I{qoK8PCc z5A!_cBEF0oKhAviUk$R%cN0XS;zFp2i(wd6L`~ctb;UhUx6FsynNg?(&anE0sE2F~ z2I3j>JZixgQ9t(YX#f?17w{ZlBIq;oyz%xyXU1*S6m6Tp!%2=TjMGm zi32d#Lig;fLS4X7)Ghb}{TFbNg0A?wRou3UN2slOhT4hri`)P?Q4<$JU2%ETGg1?^ z^({~n^)`oKR^l>4CPO&E@vDAMx9EMEaNL2b;AjZj-X z7B#_o)CKKAJ&gOX3!X-O$1J(Tjb8@+zyH^uke-Sz=2X-IwxG6lKWZT-QCoHeHPKDf z>-QLSPhX&3x4@JK(jR@%DEKTf0=43Cs4JR|x}qhhiFTp} zI)~cw8>sd{%iS#uL0w@Qiz86=xls%J3bnB6sD(9N&iz+|4pz|{b!8(l6lbG$VuRIh z$8h3bQ47D1n(z_oA$y5hV7e7s` zi81kC^9^c(PZ%BJtaMwM5cN5b3^i~B>UGS6X|N_H$1dg=OhUZKMT{qnX23?MXJ#mBq4QDWu0i$Ni1hP$2PtSv zPht$bh}!b&r~&VyI=n$XFud5SoEfn)aY@ud$73vK|fk z;ujc4@Bc>%!5F;84Up7Ki+T++qdFA8xLDHiRZ%#ejy=FHkMBrQ0QyR9`ZFv#1GHQpuL$&XS`k)$tVYmXdwMQ@~o=4rf80(y&sQy_| z&qNWk?mG5gUj(|7&{j<_qfl2c4|VUCp{{hR`7`QD4xko%4z)v1Q2pMZCJb2bt~?Ry z*-3|b+lr!YNzL`_zbaai$c#fUBd$OVd>pmF+Zc%-Q3K@I;4Fn8UBQw!q~_b`1JpZI&!EtrQ|z+%)xw;Humn@|^U z9Cc-9Fd^PR?eH^HyLg-30wa*|d|p8c8mJg*fHIf}t607<>PlOo7Sh=qgxa|Ys0GbJ zUHM|v#2ZoL97Odyi;3|TY6o6n2EG4rwz#J<7wTSDL4D-5MlEb4>Yh(P{{*N3H=+jI zk6OqX%U?(B=o3``(5<|pm=@Kqzd05|^#0GHpoeQUYKt$TuIM@@!M`yNenbtBdz*VE zN@5P;Z&5#FMp%9^YR7hY>VqdU#5p zwziDb*Fi0~3F;HC6{>$b)HBo-HO?T^IHOUwWU9GzJNvH&Tdmj7R~mttFqg%3QCHjqwPRf{7Y;@(aILvz2m7xT{Y*j~PNJ^xD(YEyg1VBR zo$g*IMb&4)5G;b4pc-mHjZjzG!Q$RlKLXWn8tPeDiW+ylk3u#IyHNw(Lk;{2b&rB~ z@n;##g6h~0_0Ww(4KNF}@MWl5v;~vnPArRmqCRp%e|GmiA8N;|pcd?_OFY$53pJ+JXM43mlCJ z_5M$xpoefVYQ>vT4R=|51ho^VP!n9j%J>-7ukb#XuZ+)$8>1%vX208^PN;T+QR7ZV zJ*4x|rz=`XL07U3HPO$ghDT6abryARucB7|FDm~I)h_x0mrsNmI29@%fqKaDp>A<0 z)Gerhx{$^Pxc_R*F96{wHy zW2hazh8p)V>e=~#;TYlj#jUU+HYU*x+v6!LjfD@p_jn*KCf<+zu*I)DA9xux(aa-! z_~2^HjX_8GLp2t`?6?l~5MIJ{_zYK}Z}Bnr-LL9z?h~&k>fxD)S@1{H7XE<|_!bLc z*m3udT<;yJ}_&Yyfh;w60Jc!zfh||1+I1iiQ2h76!UXwrEKXMs^ z4XHSV`7rCB?r*k@F$VEW)WYVYo|z4(Ej@;Mm~LQPe1p2;7-!tQPlVBl!!QstVKmHw z{`Wrz1$8Kn+M-IRfxbaK^-WM$*bd|4VN|=bmR+b|U^cw-XVlaf+kHscrc# zsAppoYTT%E?7s$FLLwWkN3HA<>UFt;y0VuR2b^~cNrXwsXF#2De3691T zI1jbpt*G&iq85DiJojG%JhF=Ss4ETnpUbC2b;yR=@`7dwEJj=&b&Ez|N1TY7_$}(5 z$G_nEhoBam4t3=PP~(;LS%a$9pgwBLx}bJq9BRv^V{2T3*)iy%`*EBf)vgU{q1{mv z3`SkZ_ZW&lpzi%P%!g+&Ir@SwxqB9lAtVZ;2CRyDHtL}UYKQ7D0JURLs0o)^ydKql zA6CTEsGUi1*XkS@8>KmfpGKTR@{g^ z@ib=oi@%8DEBqQe-*BIh>rfvs|DYE30@XkHP4{rNz?8&&(Wfh%MnN5ZL@i_uYQ>u| zbpW>j^AexC*rYDXHQw!AHBf^O))wWwP#AN7o_!4bIsHv6xwE_=sq zZEe&*O;Pu%z1bDDb-hs!=Wx_RIT6)=p~dSkBMaV-)yUtut0#(4~_W@5j9S=U_`x$Y{$4r9LaSw(*;mM}{9R7zh{^5He`6<6DThDeX6*jTz2=!-yVtRp>8ngZ_o%)l+M~9*uQ?L+OiV?+p0h2#+VVRr z-e;aLFPeAF=csYKH_lkdt>*qy(1gj&OqiTFuff zya5yI={%+aUct8b6xFfbf9_dmj#-Gm$9lLG+hDwR?vt*Uxy5{8=6~-#PkN!oTZdZk z4s##+GL!g~f_!2X$v(J`)-4AQ-bPLIFRJ|;Gww(C@P(o3 zbAIIht5Coa#ZUv6F>7KNabt^zm=jR9Y&Po3enjoiCab?}-naZqi_?B`<7daTv@iUL z{nv!`Na&ulLfwkN<`l~>MYY>)`D5l4%Re>WqZSt13-C{n0@bfDYN6%K8mM*+eAb{n zYT`bqkK%Et4ihmW&O-kJP&;~Y`QjE=K`p!?YN94)JF|y5$Q+GY;1r8z;n&RXEw+lAR`ECL;dpNO zXfbT-QCAXbW;2VU7ElxQtTaSTtCQ28W79HNl+IQj=E)e%nGpreEx=wNu;8p8)|^>Q3K4e zc#Xw7Ek1!-*hP!~viPz2!t(D>69)ykag(F^WkTh11o_;S6eFQ4uWJpOnr$rK8MUy! zmLG}9h$mRQ#Oi-Cw^{xWYKMNq3i!>1_H2SYeDcm~Qb(bBpB*#C)6XyO-&jk`Ms3-T=3dn6a@OL6Ai{Ihw~JSX-3-=v_edTIv7cO8OUDO0EEN*XcAJhlcVAPdPu>3sK zLYAP$U5`E`j#|Yz)XHy~(Gvvt|ArfZny8NX9nK+cVexy^IMEV16QbIuK`kJo#rduN zE3;xk-v10Vs7pdy*x#IiMTj@zTzrU{V00okaFjV0m0ygSXr0A-t^PO`CI2Vt*2YWh z+Gj9xC+7Xv))yy{8*8HCVHQtEUCCkdgn0(_ef|<^#~z{j#Sd{4grj~o71u7qAaU^O7@}v5dw|q@hyLzYzTUp!})o!FY z3E3H+H`^7wh2{#>)4A5-Ur_^|G%uk(QtzR5CP6Y+pWiHndX}nN+yQk9`kF&5KMMUn z|EE#VN>^LOUh}x+FQ5i^h-vT{YJmwu-B+)ysQR*~TT%tJ;NhrWRwtvbd@icpGOJ&W z@%8@iu*4D6Q+yWH@V5C7^^iS7O`IgT8!!^pu8>&*HE}sihqX`>_dtz12-W_3)WaBs zKCN&W1vOlcnqVjL+p+hjc@FiB=mO@%duFl}0seo8Tn_b1=WHyE*RTzSr*u0z7B%ra z)Iye^7P=`V?|%vk`$#Ooi`W?lrE;&y15`)-6+*@` zo@nv()V%*{xR``?V4XGCf%O#(-e*u<%h58*a zTDWVU17kD4SA>G@aT%+qYBoR()W+ie=4dm@T!ea^)|z`Q|2wMx71Xoz6t$p~>D>iq z!-9JM%TUnP3_yKyO+kIeud(M0 z09MlzG{_k6QRki-WSd@sgt2r?xn&)92;3L?N>T>Z7q7szZCT4{G2M zsD;c#EpUb94_f{l>LI*?>i-e-$r&e`yM^gcAHfAMuHOH86x5-WKf%{#)CA)#o{O4j zrFjU|{yJ)>-k6_I6UNN$3^Bt{?IJDCkGhajiuL~2v5Mwq7jq!$p&N_p@Pp+yq87Lp z_0XO|UD0Dy`vf`M6=y$wr#~YqUjQ{uMO6D*s88Ick-Yz^XlE5YP*>!$_y^R2R-*>mg!S<- z=D@T$-Cw(^pa$+@4n&PJ7S(?xYMyl#??BzceK~pm)!+gN4R9Sb(IeCVpDa$B%hl&W zZDk2mzv^aPt8Zj+d(?#8%mG$E(wu7k;G>`kSDSlL_wWyk@0tIZ@p8LQv@p~alrx*4 z7Tg!Lu$icZEmzpe6C$`Go2ZM z+VV(L|3a28i<-EK)i<$xJHNbtT`8zTA9I8`#aw_IV3oPY@~2VX{jTEI_z;6JM}B7^ zv$R z|I|mJB!xspT!(K^6E!hgTfURU{ZZd=hFUxl^{d$gi|3-gXe_q)XVgLtqAuunjE;9K z?|VQ&Tl3lsF6vgA#w>^$poZB9)xNzs2z5o1%{izGSc>Ym3U%e%QUCDjG^&4=ul)MF zuPFRWqB^F=uZy_`jZq!Dp#~a3f0)-WT<`xw3Thayg!?BP zNl?G39W~c?d zF3tO|9}+=j0=zIRiILb0^?FP~UGWB-hR3icHY)3W_?$*v(IfMdnYf($tvHie3@g&E zHtG)*^HJZ7ZkF@8dl_8bU1<{3fZ-PBH%p=aS+KY+cBNelJdA&$ZpG{h?pL#|n1lE( zYKKB92KfJff(@}Q@f0ksdLIRSLS?Mvwx$x+A^r_>$G%L*1&^{(9cOBsJVXnNbszM}4ztgc_h9YQPER zY;!s4ZP;Y-IrFCZ6xIK|#R+Pvhx?hRd+`Tqiym8j z%sTd)Ly9_XpbTa{)J-g7aRbys+gsez9D(}Cib5^yqMTIp|C3~!{h8>j@TeHAQ&O;9&u32LD~q29C2sD*8}{4W-t zLbbo_v&2pFsa3o;qt$Z_gUv)3LC56S2}`0Dx(2nN!>EZbpcZlkwSc>*3ww^5xMY3z z5$7vUArpzdsIRFDP#;mpa4BYN5a1obqu3ayG;|-rPf$Bk=sP!IFVsEmhuZq-s0AIg z{5gyNMJ+ITBmcsDUJwPXC@Cfj;7mmbr;D4{D;X%<8Bs zZjQRgU2rxILG=r5>gvNWw*IY*oD?)cVXG*HT2Lj6Yoea&#;5_iqb3}Ntk?gh#HY}b zb7xe%YN5V(d^elSxgwB%EWprRDKDmW5b9Wsx5=mBjLUhNx;yygahUi&`nSY-v@e9a zX{VzYaUL5hHMvu?%V04NgOC3+gKN0$i$V1hw;m03)W)G!zQX`r$&KLrhxQ5Rdz11E z&I7brhik1ZMgMVuKCvjVrQV;MtEvBs_#yVyx3MpeiMEJ6RBoiaj|mpwR2*P+ZK=ym zoQ}aJX)wyAL{Y%Pe$sMHKH<%lj(tZNx8P1gCbabPuru zUku!x2LDr%V-F4HVF^=3?+LXu7#|Z0NiQHS- z&$3wc^K7hnl-pV#eY)$&VC_Qm-|kC8G`Z<`jlMr%6lY0tI;zob73DX2TD~F3^0_0G8#oiIgEgMT z05geSk&jJTpI3Z5`Hw%WO$hZ>DbFL{+;VkHHH^jhGi(mNfBKK_sc)UzmA!!^E7O4g zoT2x@I=22|fDhz56K9S}^i4Y7Y)Y%Gr-(-UgT>`3KjsXyX})4<`mUlwA1pflrM^D- zjf`3duhaIVtMcYhE@FKx&Q_F*MBQi{;%oE8a2cp6MV#Y{A>%XkSLFIwyCbyG@y5(d{T0f+u^;`Ka~7oh z2W?V)F-~=I>B#j(9b>2)t%qDk6@u1GkeE|n&v$7628~7hfioAmk@!}PIrdY|LZ3Lc zh!*&YTz>kMrhJL=IjehsiO3hFeFgfSvIX!>-|G_1&F>%dC;oRUjj~V?%{p%+)=`vr zGaYjh|Bd}m!zip-?znJ`H;=PnRQCF0F z6LN1j+mrvE_y_Vju96!Y)wpgdUjQW?pJ<^Yv%}j*T#(j3TfNf1P)G+2;f_NWoeq@|Q#G@#m$C9`lbv)(t|J&HTtYdl-2{?PuDIVus z&Z-PlgPi}%6SN|iowF|kC8F*D<(1?MQ0_`jhd!i(ZOjhTSG4kZ+US_Z`G9jZbsKyn zS6W3}1{lHFnR5u|7V^btG?=qEgA5@Kx52Vgj!QWqb-8JKjW{1R+&kX9_wT;VjJgj=^=@!ZXzA_{*Ga15Z+0SLF@HMZ_m* z^NRBj(wXLOp*u@N&_wJLhC# z9m{Z`l?Tv%Kj*41`VFNVo%&yCJ6#`k&8+b^B<3?<3*rg3n9IcaBSw7U5KTtsQJhma z8eoHH}AS7ic+3T`JBbYiKG0V zOBZa2QPdoy9ENpmq~BqWlN@Zy- zQO5z|?&NjcV2s{+X!?_^#Hrt+bo@j`dOGxTp?AVMEBuen!>QL%pFTR`aO%;|O}vM4 zKH7i8W!CR0bw#PWXA{>X|A6u<^1TANX|+fkA}GY!fQCJ+;UNY&K;17mk~;l|qIG;_ z4#xeoe`awt%ExIRo%jjuI^b~P@iyKV>I29P!Ks`&`uJ2Jd6-j2P8vVQCY+Cy;0Uz= zx?m34m!j<)&Q1*e`G`rr7{Lr{+l2Z8#F@w+AXfypS)KYcqVF7X`qwl3M++*;*?^lY z*NX-^%9&Gak;Q1clw4urY@CycFIt`2H|NYneOAt2qo#fr;@eK`QL@K4kJGxk&D5B3 z7-tvay0jjRvuNwXW5i2nUxs)CZMIpTQ1TbZO(nO0TvghSNi6`@o##DcgJ=c^INPsAT7-yk=Z z@)2@Xh<_yxBHqIoIzp{|e;0aXsn_uy`&iw#miw80rP2^2;v7ijQAtuqxPyOf!4+nh zsc18g@=L2<>i;&KoJ?A3bS!72{8rvfT*As@DPOZu)UFHd7BgEpt50CwB%hdeHK;33 zj?jPPu)Y8ybBaTnqILc$`lg-+7 zqO4<&#kXAOeTVt2+>X9it-PCdE9vu_?&5hSU11d&D7Ue}W?L>L9mjF5rBg~ucS>G`u&gge{lZHd4>2W=U&bUoHsaiRHm)|xZvZD?f(D# zehRCI!1(?qd^%$+8czFSl3lddQIXtC%Lft9CcnU6!|!h7HQZftf7-aJibuSe@mg~x zCeEv$(gi8pqrT~mXHPoMwz`Sr({t)5`R@FV9Hk*>ptgta^q=x)ar7PTM$^14@{CbX;_cMb~;58XQcf3 z_?o(WoRciKoB^)WW;*9_?qP_pl@0kjnMSk<#NRlJaO&ubSs0-b<$E}kGm<`Yt=~fO zGbnGR-PfF%znDFeatYe&*ho2s)g`6fK5{9vhyN0cx53mf1!r6p>_|m@Ejo5%ptg9J zGdb=1P`|>}?HJl8njOm_e^t!*P^f zQx2!ipPa>sgKW~#wC%;2mik+m2Y*5x9dQ%wqgmXH1qWMM_1_cM!ez|wKi>1d?Kz(_ z_;wnuvmN;bhmp_88Ok}5xF+7BU0upcDbG@lI5iXA);{v{{1_$iL9+C&>DUt-Wwob5QH z6MuPBw|2^HSF`uAl8wQ0TWW*!TFl= z36oB*jt-9ybrjzkNjX lfOIKC?oJqYZ||hYJM)*`nZM!ouDN%jmTzC2HXx$Z{{dSK4UYf- diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 2a2de30c3..9ea83491f 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-05-08 15:42+0800\n" +"POT-Creation-Date: 2020-05-11 16:53+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -2769,7 +2769,7 @@ msgid "" msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)" #: authentication/errors.py:48 users/views/profile/otp.py:63 -#: users/views/profile/otp.py:102 +#: users/views/profile/otp.py:100 users/views/profile/otp.py:116 msgid "MFA code invalid, or ntp sync server time" msgstr "MFA验证码不正确,或者服务器端时间不对" @@ -2832,10 +2832,10 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:360 users/templates/users/user_disable_mfa.html:32 -#: users/templates/users/user_profile.html:94 +#: users/models/user.py:360 users/templates/users/user_profile.html:94 #: users/templates/users/user_profile.html:163 #: users/templates/users/user_profile.html:166 +#: users/templates/users/user_verify_mfa.html:32 msgid "Disable" msgstr "禁用" @@ -2894,10 +2894,10 @@ msgstr "请打开 Google Authenticator,输入6位动态码" #: authentication/templates/authentication/login_otp.html:26 #: users/templates/users/first_login.html:100 -#: users/templates/users/user_disable_mfa.html:26 #: users/templates/users/user_otp_check_password.html:15 #: users/templates/users/user_otp_enable_bind.html:24 #: users/templates/users/user_otp_enable_install_app.html:29 +#: users/templates/users/user_verify_mfa.html:26 msgid "Next" msgstr "下一步" @@ -5766,26 +5766,6 @@ msgstr "解除用户登录限制后,此用户即可正常登录" msgid "Reset user MFA success" msgstr "重置用户多因子认证成功" -#: users/templates/users/user_disable_mfa.html:6 -#: users/templates/users/user_otp_check_password.html:6 -msgid "Authenticate" -msgstr "验证身份" - -#: users/templates/users/user_disable_mfa.html:11 -msgid "" -"The account protection has been opened, please complete the following " -"operations according to the prompts" -msgstr "账号保护已开启,请根据提示完成以下操作" - -#: users/templates/users/user_disable_mfa.html:13 -msgid "Open Authenticator and enter the 6-bit dynamic code" -msgstr "请打开 验证器,输入6位动态码" - -#: users/templates/users/user_disable_mfa.html:23 -#: users/templates/users/user_otp_enable_bind.html:22 -msgid "Six figures" -msgstr "6位数字" - #: users/templates/users/user_group_detail.html:17 #: users/templates/users/user_group_granted_asset.html:18 #: users/views/group.py:83 @@ -5844,6 +5824,11 @@ msgstr "用户已失效" msgid "User is inactive" msgstr "用户已禁用" +#: users/templates/users/user_otp_check_password.html:6 +#: users/templates/users/user_verify_mfa.html:6 +msgid "Authenticate" +msgstr "验证身份" + #: users/templates/users/user_otp_enable_bind.html:6 msgid "Bind one-time password authenticator" msgstr "绑定一次性密码验证器" @@ -5854,6 +5839,11 @@ msgid "" "code for a 6-bit verification code" msgstr "使用手机 Google Authenticator 应用扫描以下二维码,获取6位验证码" +#: users/templates/users/user_otp_enable_bind.html:22 +#: users/templates/users/user_verify_mfa.html:23 +msgid "Six figures" +msgstr "6位数字" + #: users/templates/users/user_otp_enable_install_app.html:6 msgid "Install app" msgstr "安装应用" @@ -5936,6 +5926,16 @@ msgstr "更新用户" msgid "User auth from {}, go there change password" msgstr "用户认证源来自 {}, 请去相应系统修改密码" +#: users/templates/users/user_verify_mfa.html:11 +msgid "" +"The account protection has been opened, please complete the following " +"operations according to the prompts" +msgstr "账号保护已开启,请根据提示完成以下操作" + +#: users/templates/users/user_verify_mfa.html:13 +msgid "Open Authenticator and enter the 6-bit dynamic code" +msgstr "请打开 验证器,输入6位动态码" + # msgid "Update user" # msgstr "更新用户" #: users/utils.py:24 @@ -6183,19 +6183,19 @@ msgstr "首次登录" msgid "Profile setting" msgstr "个人信息设置" -#: users/views/profile/otp.py:130 +#: users/views/profile/otp.py:144 msgid "MFA enable success" msgstr "多因子认证启用成功" -#: users/views/profile/otp.py:131 +#: users/views/profile/otp.py:145 msgid "MFA enable success, return login page" msgstr "多因子认证启用成功,返回到登录页面" -#: users/views/profile/otp.py:133 +#: users/views/profile/otp.py:147 msgid "MFA disable success" msgstr "多因子认证禁用成功" -#: users/views/profile/otp.py:134 +#: users/views/profile/otp.py:148 msgid "MFA disable success, return login page" msgstr "多因子认证禁用成功,返回登录页面" @@ -6529,11 +6529,11 @@ msgstr "同步实例任务历史" msgid "Instance" msgstr "实例" -#: xpack/plugins/cloud/providers/aliyun.py:16 +#: xpack/plugins/cloud/providers/aliyun.py:19 msgid "Alibaba Cloud" msgstr "阿里云" -#: xpack/plugins/cloud/providers/aws.py:14 +#: xpack/plugins/cloud/providers/aws.py:15 msgid "AWS (International)" msgstr "AWS (国际)" @@ -6541,51 +6541,59 @@ msgstr "AWS (国际)" msgid "AWS (China)" msgstr "AWS (中国)" -#: xpack/plugins/cloud/providers/huaweicloud.py:13 +#: xpack/plugins/cloud/providers/huaweicloud.py:17 msgid "Huawei Cloud" msgstr "华为云" -#: xpack/plugins/cloud/providers/huaweicloud.py:16 -msgid "CN North-Beijing4" -msgstr "华北-北京4" - -#: xpack/plugins/cloud/providers/huaweicloud.py:17 -msgid "CN East-Shanghai1" -msgstr "华东-上海1" - -#: xpack/plugins/cloud/providers/huaweicloud.py:18 -msgid "CN East-Shanghai2" -msgstr "华东-上海2" - -#: xpack/plugins/cloud/providers/huaweicloud.py:19 -msgid "CN South-Guangzhou" -msgstr "华南-广州" - #: xpack/plugins/cloud/providers/huaweicloud.py:20 -msgid "CN Southwest-Guiyang1" -msgstr "西南-贵阳1" +msgid "AF-Johannesburg" +msgstr "非洲-约翰内斯堡" #: xpack/plugins/cloud/providers/huaweicloud.py:21 -msgid "AP-Hong-Kong" -msgstr "亚太-香港" - -#: xpack/plugins/cloud/providers/huaweicloud.py:22 msgid "AP-Bangkok" msgstr "亚太-曼谷" +#: xpack/plugins/cloud/providers/huaweicloud.py:22 +msgid "AP-Hong Kong" +msgstr "亚太-香港" + #: xpack/plugins/cloud/providers/huaweicloud.py:23 msgid "AP-Singapore" msgstr "亚太-新加坡" #: xpack/plugins/cloud/providers/huaweicloud.py:24 -msgid "AF-Johannesburg" -msgstr "非洲-约翰内斯堡" +msgid "CN East-Shanghai1" +msgstr "华东-上海1" #: xpack/plugins/cloud/providers/huaweicloud.py:25 -msgid "LA-Santiago" -msgstr "拉美-圣地亚哥" +msgid "CN East-Shanghai2" +msgstr "华东-上海2" + +#: xpack/plugins/cloud/providers/huaweicloud.py:26 +msgid "CN North-Beijing1" +msgstr "华北-北京1" + +#: xpack/plugins/cloud/providers/huaweicloud.py:27 +msgid "CN North-Beijing4" +msgstr "华北-北京4" + +#: xpack/plugins/cloud/providers/huaweicloud.py:28 +msgid "CN Northeast-Dalian" +msgstr "东北-大连" -#: xpack/plugins/cloud/providers/qcloud.py:14 +#: xpack/plugins/cloud/providers/huaweicloud.py:29 +msgid "CN South-Guangzhou" +msgstr "华南-广州" + +#: xpack/plugins/cloud/providers/huaweicloud.py:30 +msgid "CN Southwest-Guiyang1" +msgstr "西南-贵阳1" + +#: xpack/plugins/cloud/providers/huaweicloud.py:31 +msgid "EU-Paris" +msgstr "" + +#: xpack/plugins/cloud/providers/qcloud.py:17 msgid "Tencent Cloud" msgstr "腾讯云" @@ -6981,6 +6989,9 @@ msgstr "密码匣子" msgid "vault create" msgstr "创建" +#~ msgid "LA-Santiago" +#~ msgstr "拉美-圣地亚哥" + #~ msgid "Total hosts" #~ msgstr "主机总数" diff --git a/apps/users/templates/users/user_disable_mfa.html b/apps/users/templates/users/user_verify_mfa.html similarity index 100% rename from apps/users/templates/users/user_disable_mfa.html rename to apps/users/templates/users/user_verify_mfa.html diff --git a/apps/users/views/profile/otp.py b/apps/users/views/profile/otp.py index 7bae70c58..2d823f5ab 100644 --- a/apps/users/views/profile/otp.py +++ b/apps/users/views/profile/otp.py @@ -83,12 +83,26 @@ class UserOtpEnableBindView(TemplateView, FormView): return super().get_context_data(**kwargs) -class UserDisableMFAView(FormView): - template_name = 'users/user_disable_mfa.html' +class UserVerifyMFAView(FormView): + template_name = 'users/user_verify_mfa.html' form_class = forms.UserCheckOtpCodeForm success_url = reverse_lazy('users:user-otp-settings-success') permission_classes = [IsValidUser] + def form_valid(self, form): + user = self.request.user + otp_code = form.cleaned_data.get('otp_code') + + valid = user.check_mfa(otp_code) + if valid: + return super().form_valid(form) + else: + error = _('MFA code invalid, or ntp sync server time') + form.add_error('otp_code', error) + return super().form_invalid(form) + + +class UserDisableMFAView(UserVerifyMFAView): def form_valid(self, form): user = self.request.user otp_code = form.cleaned_data.get('otp_code') @@ -104,7 +118,7 @@ class UserDisableMFAView(FormView): return super().form_invalid(form) -class UserOtpUpdateView(UserDisableMFAView): +class UserOtpUpdateView(UserVerifyMFAView): success_url = reverse_lazy('users:user-otp-enable-bind')