From 61407331bcc19ac5310d200dfc69f8690cb9d18e Mon Sep 17 00:00:00 2001 From: ibuler Date: Mon, 20 Apr 2020 10:37:07 +0800 Subject: [PATCH 01/42] =?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/42] =?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 44db0e8a5de7e814b5f4dc499472a0dd8cfff6e1 Mon Sep 17 00:00:00 2001 From: ibuler Date: Mon, 20 Apr 2020 16:56:47 +0800 Subject: [PATCH 03/42] =?UTF-8?q?[Bugfix]=20=E4=BF=AE=E5=A4=8D=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E6=8E=88=E6=9D=83=E6=97=B6=E9=A2=9D=E5=A4=96=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2platform=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/models/asset.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index 39ea8420f..6b5e716e0 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -41,11 +41,10 @@ def default_node(): class AssetManager(OrgManager): - # def get_queryset(self): - # return super().get_queryset().annotate( - # platform_base=models.F('platform__base') - # ) - pass + def get_queryset(self): + return super().get_queryset().annotate( + platform_base=models.F('platform__base') + ) class AssetQuerySet(models.QuerySet): From c8c6ba1c194d443cdc7d6f9b1363fcc99a55f314 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 21 Apr 2020 17:37:29 +0800 Subject: [PATCH 04/42] [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 05/42] 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 06/42] =?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 07/42] =?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 08/42] =?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 09/42] 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 | 58 +----- .../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, 241 insertions(+), 906 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 10/42] =?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 11/42] =?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 | 40 +++++++++++++++++-- 2 files changed, 36 insertions(+), 4 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_IGNORE_SSL_VERIFICATION: True # 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 12/42] =?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 13/42] =?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 14/42] =?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 15/42] =?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 16/42] =?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 17/42] =?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 18/42] =?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 19/42] =?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 8323de1c07a7638c18d5e8e83ed63d263aa1927b Mon Sep 17 00:00:00 2001 From: Bai Date: Wed, 6 May 2020 21:05:23 +0800 Subject: [PATCH 20/42] =?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 11b3c57c927f96bba0ab5cde27e96df28f718de6 Mon Sep 17 00:00:00 2001 From: Bai Date: Fri, 8 May 2020 20:02:00 +0800 Subject: [PATCH 21/42] =?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 22/42] =?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/qcloud.py:14 +#: 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/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 d50ea83f405d2eff5a04d3c1a101a444418d4631 Mon Sep 17 00:00:00 2001 From: wojiushixiaobai <296015668@qq.com> Date: Wed, 13 May 2020 15:21:04 +0800 Subject: [PATCH 23/42] =?UTF-8?q?[Fix]=E4=BF=AE=E6=94=B9=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 052952256..388186ddf 100644 --- a/README.md +++ b/README.md @@ -174,20 +174,19 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向 ## 安装及使用指南 -- [Docker 快速安装文档](http://docs.jumpserver.org/zh/docs/dockerinstall.html) -- [Step by Step 安装文档](http://docs.jumpserver.org/zh/docs/step_by_step.html) +- [快速安装文档](https://docs.jumpserver.org/zh/master/install/setup_by_fast/) +- [快速入门文档](https://docs.jumpserver.org/zh/master/admin-guide/quick_start/) - [完整文档](http://docs.jumpserver.org) -## 演示视频和截屏 +## 演示视频 我们提供了演示视频和系统截图可以让你快速了解 JumpServer: - [演示视频](https://jumpserver.oss-cn-hangzhou.aliyuncs.com/jms-media/%E3%80%90%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91%E3%80%91Jumpserver%20%E5%A0%A1%E5%9E%92%E6%9C%BA%20V1.5.0%20%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91%20-%20final.mp4) -- [系统截图](http://docs.JumpServer.org/zh/docs/snapshot.html) ## License & Copyright -Copyright (c) 2014-2019 飞致云 FIT2CLOUD, All rights reserved. +Copyright (c) 2014-2020 飞致云 FIT2CLOUD, All rights reserved. Licensed under The GNU General Public License version 2 (GPLv2) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at From 4658a4c90f948c7750ac4701936a5ca80511bcd9 Mon Sep 17 00:00:00 2001 From: Bai Date: Thu, 14 May 2020 11:00:24 +0800 Subject: [PATCH 24/42] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9=E5=90=91?= =?UTF-8?q?=E5=AF=BC=E6=96=87=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/locale/zh/LC_MESSAGES/django.mo | Bin 90002 -> 89992 bytes apps/locale/zh/LC_MESSAGES/django.po | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 72d6cdb438c46b07dbdd455422190ad8acaefa63..acc82a3552390f50429dc0e117e050835b453ab8 100644 GIT binary patch delta 350 zcmXZWy-Gp>6u|Lw2nA6n5+iWYKpHCS0g@mbqFf{cHBAUEIVi*hTODk0b`pvxdV!_{ zxn)x!^c2Aje1nFD{)c;~ALsjwW|846^6VY=vm$a^RjK1EKH+*zY{smxKYdv)Tt*N&U6u|Lw5-&u8kLqQQV7ItxR@U!~@LDUcg!5vzf8e9r08hio4 z;ev{{Ng>*N2R|U_75X3Uoqn9}^AslAVX`~C@OMPyt*TPT33f1FQ<>lxTlid8Imcf- z!GnfM0&j5}?{E*>xPf1Bej3MrX#Fcql@vPYV5X_QY^g|=#3_mWmP!S0&<4h6kA34R z7A_-;XorqsU9`m>rtu12@d0ga(~Igkw0IY7|Djim3dbbuqqDfNj2Yqv+QMtJgZ|^v h4VJEs)?V&Qx}IJHz1c@Nd+*PCgZbby7*_Si`3Dh6JM;hm diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index db84b6b14..5ad5d3edc 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -5598,7 +5598,7 @@ msgstr "上一步" #: users/templates/users/first_login_done.html:31 msgid "Welcome to use jumpserver, visit " -msgstr "欢迎使用JumpServer开源跳板机系统" +msgstr "欢迎使用 JumpServer 堡垒机" #: users/templates/users/first_login_done.html:32 msgid "Use guide" From d878089ebd7648fa307ef0486566009c93451c85 Mon Sep 17 00:00:00 2001 From: Bai Date: Thu, 14 May 2020 14:26:00 +0800 Subject: [PATCH 25/42] =?UTF-8?q?[Update]=20=E7=94=A8=E6=88=B7=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E8=AF=A6=E6=83=85Modal=E6=B7=BB=E5=8A=A0comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/templates/assets/user_asset_list.html | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/assets/templates/assets/user_asset_list.html b/apps/assets/templates/assets/user_asset_list.html index fa2c9830c..3c5cdc4a1 100644 --- a/apps/assets/templates/assets/user_asset_list.html +++ b/apps/assets/templates/assets/user_asset_list.html @@ -78,6 +78,7 @@ $(document).ready(function () { 'platform': "{% trans 'Platform' %}", 'system_users_join': "{% trans 'System user' %}", 'domain': "{% trans 'Domain' %}", + 'comment': "{% trans 'Comment' %}", }; var value; for (var i = 0; i < data.results.length; i++) { From e951b64b0a268928d8cca82003e56769bf2a1ef1 Mon Sep 17 00:00:00 2001 From: Bai Date: Thu, 14 May 2020 16:14:17 +0800 Subject: [PATCH 26/42] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8F=B71.5.9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/jumpserver/const.py b/apps/jumpserver/const.py index 2a97a00d2..b7caa6114 100644 --- a/apps/jumpserver/const.py +++ b/apps/jumpserver/const.py @@ -7,6 +7,6 @@ __all__ = ['BASE_DIR', 'PROJECT_DIR', 'VERSION', 'CONFIG', 'DYNAMIC'] BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.dirname(BASE_DIR) -VERSION = '1.5.8' +VERSION = '1.5.9' CONFIG = ConfigManager.load_user_config() DYNAMIC = ConfigManager.get_dynamic_config(CONFIG) From 5ae6e81a1d770638ed69267553afc358af2a5b46 Mon Sep 17 00:00:00 2001 From: maninhill <41712985+maninhill@users.noreply.github.com> Date: Sun, 17 May 2020 00:02:35 +0800 Subject: [PATCH 27/42] Update README.md --- README.md | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 388186ddf..43803762c 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,28 @@ # JumpServer 多云环境下更好用的堡垒机 [![Python3](https://img.shields.io/badge/python-3.6-green.svg?style=plastic)](https://www.python.org/) -[![Django](https://img.shields.io/badge/django-2.1-brightgreen.svg?style=plastic)](https://www.djangoproject.com/) -[![Ansible](https://img.shields.io/badge/ansible-2.4.2.0-blue.svg?style=plastic)](https://www.ansible.com/) -[![Paramiko](https://img.shields.io/badge/paramiko-2.4.1-green.svg?style=plastic)](http://www.paramiko.org/) +[![Django](https://img.shields.io/badge/django-2.2-brightgreen.svg?style=plastic)](https://www.djangoproject.com/) -JumpServer 是全球首款开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 机制的运维安全审计系统。 +JumpServer 是全球首款开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 规范的运维安全审计系统。 -JumpServer 使用 Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。 +JumpServer 使用 Python / Django 为主进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。 JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。 改变世界,从一点点开始。 -注: [KubeOperator](https://github.com/KubeOperator/KubeOperator) 是 JumpServer 团队在 Kubernetes 领域的的又一全新力作,欢迎关注和使用。 +> 注: [KubeOperator](https://github.com/KubeOperator/KubeOperator) 是 JumpServer 团队在 Kubernetes 领域的的又一全新力作,欢迎关注和使用。 -## 核心功能列表 +## 特色优势 + +- 开源: 零门槛,线上快速获取和安装; +- 分布式: 轻松支持大规模并发访问; +- 无插件: 仅需浏览器,极致的 Web Terminal 使用体验; +- 多云支持: 一套系统,同时管理不同云上面的资产; +- 云端存储: 审计录像云端存储,永不丢失; +- 多租户: 一套系统,多个子公司和部门同时使用。 + +## 功能列表 @@ -172,18 +179,23 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
-## 安装及使用指南 - -- [快速安装文档](https://docs.jumpserver.org/zh/master/install/setup_by_fast/) -- [快速入门文档](https://docs.jumpserver.org/zh/master/admin-guide/quick_start/) -- [完整文档](http://docs.jumpserver.org) - -## 演示视频 - -我们提供了演示视频和系统截图可以让你快速了解 JumpServer: +## 快速开始 +- [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/) +- [完整文档](https://docs.jumpserver.org) - [演示视频](https://jumpserver.oss-cn-hangzhou.aliyuncs.com/jms-media/%E3%80%90%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91%E3%80%91Jumpserver%20%E5%A0%A1%E5%9E%92%E6%9C%BA%20V1.5.0%20%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91%20-%20final.mp4) +## 案例研究 + +- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147); +- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882); +- [携程 JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851); +- [小红书的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516); +- [JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732); +- [中通快递:JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708); +- [东方明珠:JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687); +- [江苏农信:JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)。 + ## License & Copyright Copyright (c) 2014-2020 飞致云 FIT2CLOUD, All rights reserved. From 2f7b1694054fb60cccb85523685ab38a5480432d Mon Sep 17 00:00:00 2001 From: Bai Date: Mon, 18 May 2020 15:24:55 +0800 Subject: [PATCH 28/42] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E7=94=A8=E6=88=B7/=E7=94=A8=E6=88=B7=E7=BB=84?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=88=A0=E9=99=A4=E7=94=A8=E6=88=B7=E7=BB=84?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/perms/templates/perms/asset_permission_user.html | 2 +- apps/perms/templates/perms/database_app_permission_user.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/perms/templates/perms/asset_permission_user.html b/apps/perms/templates/perms/asset_permission_user.html index bb9ca375a..088d80c75 100644 --- a/apps/perms/templates/perms/asset_permission_user.html +++ b/apps/perms/templates/perms/asset_permission_user.html @@ -218,7 +218,7 @@ function addGroups(groupsId) { } function removeGroup(groupId) { - var theUrl = "{% url 'api-perms:asset-permissions-user-groups-relation-list' %}?assetpermission={{ object.id }}"; + var theUrl = "{% url 'api-perms:asset-permissions-user-groups-relation-list' %}?assetpermission={{ object.id }}&usergroup=groupId"; theUrl = theUrl.replace("groupId", groupId); var success = function(data) { location.reload(); diff --git a/apps/perms/templates/perms/database_app_permission_user.html b/apps/perms/templates/perms/database_app_permission_user.html index 603f60a6e..8b109fa1a 100644 --- a/apps/perms/templates/perms/database_app_permission_user.html +++ b/apps/perms/templates/perms/database_app_permission_user.html @@ -218,7 +218,7 @@ function addGroups(groupsId) { } function removeGroup(groupId) { - var theUrl = "{% url 'api-perms:database-app-permissions-user-groups-relation-list' %}?databaseapppermission={{ object.id }}"; + var theUrl = "{% url 'api-perms:database-app-permissions-user-groups-relation-list' %}?databaseapppermission={{ object.id }}&usergroup=groupId"; theUrl = theUrl.replace("groupId", groupId); var success = function(data) { location.reload(); From 75edc26a10d7a667387e1dab83dbfdfd61b8684b Mon Sep 17 00:00:00 2001 From: Bai Date: Mon, 18 May 2020 15:25:24 +0800 Subject: [PATCH 29/42] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9=E7=BF=BB?= =?UTF-8?q?=E8=AF=91=EF=BC=8C=E7=A6=81=E7=94=A8=E8=B5=84=E4=BA=A7=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E4=B8=8D=E6=94=AF=E6=8C=81ansible=E7=9A=84=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/locale/zh/LC_MESSAGES/django.mo | Bin 89992 -> 89985 bytes apps/locale/zh/LC_MESSAGES/django.po | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index acc82a3552390f50429dc0e117e050835b453ab8..8ce97ff575ac8bb7ed55c47ded03104bd285200d 100644 GIT binary patch delta 8833 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 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 5ad5d3edc..562154404 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -1671,7 +1671,7 @@ msgstr "定期测试系统用户可连接性: {}" #: assets/tasks/utils.py:17 msgid "Asset has been disabled, skipped: {}" -msgstr "资产或许不支持ansible, 跳过: {}" +msgstr "资产已经被禁用, 跳过: {}" #: assets/tasks/utils.py:21 msgid "Asset may not be support ansible, skipped: {}" From 041d99f0beb168eafbcd2d76d56caeabb8989f56 Mon Sep 17 00:00:00 2001 From: Bai Date: Mon, 18 May 2020 15:40:54 +0800 Subject: [PATCH 30/42] =?UTF-8?q?[Update]=20=E6=9B=B4=E6=96=B0remote-app?= =?UTF-8?q?=E7=94=A8=E6=88=B7/=E7=94=A8=E6=88=B7=E7=BB=84=E6=97=B6?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=88=B7=E6=96=B0=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/perms/templates/perms/remote_app_permission_user.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/perms/templates/perms/remote_app_permission_user.html b/apps/perms/templates/perms/remote_app_permission_user.html index d222dfb7c..9fa623585 100644 --- a/apps/perms/templates/perms/remote_app_permission_user.html +++ b/apps/perms/templates/perms/remote_app_permission_user.html @@ -179,9 +179,13 @@ var body = { user_groups: groups }; + var success = function(data) { + location.reload(); + }; requestApi({ url: the_url, - body: JSON.stringify(body) + body: JSON.stringify(body), + success: success }); } $(document).ready(function () { From 01a52812f07c4502f10f99b0c2ba6d5ed5753dec Mon Sep 17 00:00:00 2001 From: Bai Date: Mon, 18 May 2020 15:49:53 +0800 Subject: [PATCH 31/42] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9=E7=94=A8?= =?UTF-8?q?=E6=88=B7MFA=E7=A6=81=E7=94=A8/=E6=9B=B4=E6=96=B0=E7=9A=84View?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/users/views/profile/otp.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/apps/users/views/profile/otp.py b/apps/users/views/profile/otp.py index 2d823f5ab..83918114e 100644 --- a/apps/users/views/profile/otp.py +++ b/apps/users/views/profile/otp.py @@ -83,26 +83,12 @@ class UserOtpEnableBindView(TemplateView, FormView): return super().get_context_data(**kwargs) -class UserVerifyMFAView(FormView): +class UserDisableMFAView(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') @@ -118,8 +104,23 @@ class UserDisableMFAView(UserVerifyMFAView): return super().form_invalid(form) -class UserOtpUpdateView(UserVerifyMFAView): +class UserOtpUpdateView(FormView): + template_name = 'users/user_verify_mfa.html' + form_class = forms.UserCheckOtpCodeForm success_url = reverse_lazy('users:user-otp-enable-bind') + 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 UserOtpSettingsSuccessView(TemplateView): From e98626988bac4d085eda5e738d4f6564fb5cf841 Mon Sep 17 00:00:00 2001 From: Bai Date: Mon, 18 May 2020 18:02:37 +0800 Subject: [PATCH 32/42] =?UTF-8?q?[Update]=20=E8=A7=A3=E5=86=B3=E5=BC=80?= =?UTF-8?q?=E5=90=AFOpenID=EF=BC=8C=E4=B8=BA=E7=94=A8=E6=88=B7=E5=BC=BA?= =?UTF-8?q?=E5=88=B6=E5=90=AF=E7=94=A8MFA=E5=AF=BC=E8=87=B4=EF=BC=8C?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E6=97=B6=E6=8A=A5=E9=94=99=E7=9A=84=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/users/views/profile/password.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/users/views/profile/password.py b/apps/users/views/profile/password.py index c9bb97f38..bb7caa9a1 100644 --- a/apps/users/views/profile/password.py +++ b/apps/users/views/profile/password.py @@ -67,7 +67,7 @@ class UserVerifyPasswordView(FormView): def form_valid(self, form): user = get_user_or_pre_auth_user(self.request) password = form.cleaned_data.get('password') - user = authenticate(username=user.username, password=password) + user = authenticate(request=self.request, username=user.username, password=password) if not user: form.add_error("password", _("Password invalid")) return self.form_invalid(form) From 73c3de97b86fd8ddbb29d856ebc3455138ef40ad Mon Sep 17 00:00:00 2001 From: Bai Date: Mon, 18 May 2020 18:14:19 +0800 Subject: [PATCH 33/42] =?UTF-8?q?[Update]=20=E5=8F=96=E6=B6=88=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E8=B5=84=E4=BA=A7=E9=A1=B5=E9=9D=A2=E4=B8=AD=EF=BC=8C?= =?UTF-8?q?=E8=B5=84=E4=BA=A7=E8=AF=A6=E6=83=85=E7=B3=BB=E7=BB=9F=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E5=80=BC=E4=B8=BAundefine=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/templates/assets/user_asset_list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/assets/templates/assets/user_asset_list.html b/apps/assets/templates/assets/user_asset_list.html index 3c5cdc4a1..2a8c8cf64 100644 --- a/apps/assets/templates/assets/user_asset_list.html +++ b/apps/assets/templates/assets/user_asset_list.html @@ -76,7 +76,7 @@ $(document).ready(function () { 'ip': "{% trans 'IP' %}", 'protocols': "{% trans 'Protocols' %}", 'platform': "{% trans 'Platform' %}", - 'system_users_join': "{% trans 'System user' %}", + {#'system_users_join': "{% trans 'System user' %}",#} 'domain': "{% trans 'Domain' %}", 'comment': "{% trans 'Comment' %}", }; From 728e4b7edd57fb8a37d89c102360a6b41531cf9d Mon Sep 17 00:00:00 2001 From: Bai Date: Tue, 19 May 2020 11:47:49 +0800 Subject: [PATCH 34/42] =?UTF-8?q?[Update]=20=E4=BF=AE=E5=A4=8D=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E6=A8=A1=E5=9D=97=E7=9A=84=E4=BF=A1=E5=8F=B7=E5=A4=84?= =?UTF-8?q?=E7=90=86=E7=9B=91=E5=90=AC=E4=B8=8D=E5=88=B0=E7=9A=84=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/perms/apps.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/perms/apps.py b/apps/perms/apps.py index d40373e08..5bb7420bb 100644 --- a/apps/perms/apps.py +++ b/apps/perms/apps.py @@ -5,3 +5,7 @@ from django.apps import AppConfig class PermsConfig(AppConfig): name = 'perms' + + def ready(self): + super().ready() + from . import signals_handler From 96afd82341199a431f9b25c7b2ebb8e6829e0199 Mon Sep 17 00:00:00 2001 From: Bai Date: Tue, 19 May 2020 15:15:17 +0800 Subject: [PATCH 35/42] =?UTF-8?q?[Update]=20=E4=BF=AE=E5=A4=8D=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E5=88=97=E8=A1=A8=EF=BC=8C=E6=89=A7=E8=A1=8C=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E4=B8=ADstat=E6=B1=87=E6=80=BB=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/ops/serializers/adhoc.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/ops/serializers/adhoc.py b/apps/ops/serializers/adhoc.py index afdd659fc..8d9e18e24 100644 --- a/apps/ops/serializers/adhoc.py +++ b/apps/ops/serializers/adhoc.py @@ -19,10 +19,13 @@ class AdHocExecutionSerializer(serializers.ModelSerializer): @staticmethod def get_stat(obj): + count_failed_hosts = len(obj.failed_hosts) + count_success_hosts = len(obj.success_hosts) + count_total = count_success_hosts + count_failed_hosts return { - "total": obj.hosts_amount, - "success": len(obj.summary.get("contacted", [])), - "failed": len(obj.summary.get("dark", [])), + "total": count_total, + "success": count_success_hosts, + "failed": count_failed_hosts } def get_field_names(self, declared_fields, info): From be3fdac8a95aac5c432ff830b1f70bf80abd00e8 Mon Sep 17 00:00:00 2001 From: Bai Date: Tue, 19 May 2020 15:28:31 +0800 Subject: [PATCH 36/42] =?UTF-8?q?[Update]=20=E4=BF=AE=E5=A4=8DAPI-key?= =?UTF-8?q?=E4=B8=8D=E8=83=BD=E5=88=A0=E9=99=A4=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../templates/authentication/_access_key_modal.html | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/authentication/templates/authentication/_access_key_modal.html b/apps/authentication/templates/authentication/_access_key_modal.html index f0b34cf30..71b1f67f5 100644 --- a/apps/authentication/templates/authentication/_access_key_modal.html +++ b/apps/authentication/templates/authentication/_access_key_modal.html @@ -135,6 +135,19 @@ $(document).ready(function () { } }; requestApi(data) +}).on('click', '.btn-api-keydel', function (){ + var url = "{% url "api-auth:access-key-detail" pk=DEFAULT_PK %}"; + url = url.replace("{{ DEFAULT_PK }}", $(this).data("id")) ; + var data = { + url: url, + method: "DELETE", + success: function () { + ak_table.ajax.reload(); + }, + success_message: "{% trans 'Delete success' %}" + + }; + requestApi(data) }) {% endblock %} From b97deec1de68bfaf3847fa7afda89036994ff571 Mon Sep 17 00:00:00 2001 From: Bai Date: Tue, 19 May 2020 18:24:26 +0800 Subject: [PATCH 37/42] =?UTF-8?q?[Update]=20openid=20=E7=BB=A7=E7=BB=AD?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E9=85=8D=E7=BD=AE=E9=A1=B9=20base=5Fsite=5Fu?= =?UTF-8?q?rl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/conf.py | 4 ++-- apps/jumpserver/settings/auth.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index dc0e9eae6..5da97cfb7 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -173,11 +173,12 @@ class Config(dict): # OpenID 配置参数 # OpenID 公有配置参数 (version <= 1.5.8 或 version >= 1.5.8) 'AUTH_OPENID': False, + 'BASE_SITE_URL': None, '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) + # OpenID 新配置参数 (version >= 1.5.9) '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', @@ -193,7 +194,6 @@ class Config(dict): 'AUTH_OPENID_USE_NONCE': 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', 'AUTH_OPENID_REALM_NAME': None, diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 516671980..b7633ace7 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -48,6 +48,7 @@ AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS = CONFIG.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS # 参考: https://django-oidc-rp.readthedocs.io/en/stable/settings.html # ============================================================================== AUTH_OPENID = CONFIG.AUTH_OPENID +BASE_SITE_URL = CONFIG.BASE_SITE_URL AUTH_OPENID_CLIENT_ID = CONFIG.AUTH_OPENID_CLIENT_ID AUTH_OPENID_CLIENT_SECRET = CONFIG.AUTH_OPENID_CLIENT_SECRET AUTH_OPENID_PROVIDER_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_ENDPOINT From d704a35ead5b83edc03ac46e9e4b2365e87c31c7 Mon Sep 17 00:00:00 2001 From: Bai Date: Tue, 19 May 2020 18:33:33 +0800 Subject: [PATCH 38/42] =?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=E5=8F=B7=20jumpserver-django-oidc-r?= =?UTF-8?q?p=3D=3D0.3.7.4?= 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 50e4a1862..59e86f1ac 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.3 +jumpserver-django-oidc-rp==0.3.7.4 From 216e0f28b955600573046c50a43b09ad232e4514 Mon Sep 17 00:00:00 2001 From: Bai Date: Tue, 19 May 2020 18:36:56 +0800 Subject: [PATCH 39/42] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=96=87=E4=BB=B6demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config_example.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config_example.yml b/config_example.yml index 30cfabc3e..fd8710c78 100644 --- a/config_example.yml +++ b/config_example.yml @@ -56,6 +56,7 @@ REDIS_PORT: 6379 # Use OpenID Authorization # 使用 OpenID 进行认证设置 # AUTH_OPENID: False # True or False +# BASE_SITE_URL: None # AUTH_OPENID_CLIENT_ID: client-id # AUTH_OPENID_CLIENT_SECRET: client-secret # AUTH_OPENID_PROVIDER_ENDPOINT: https://op-example.com/ From 63ff868553346917d8ffa80e2442d012e8a2da1b Mon Sep 17 00:00:00 2001 From: Bai Date: Tue, 19 May 2020 19:36:19 +0800 Subject: [PATCH 40/42] =?UTF-8?q?[Update]=20=E6=8E=88=E6=9D=83=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E6=A0=91=E8=BF=94=E5=9B=9Eorg=5Fid=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/perms/utils/asset_permission.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/perms/utils/asset_permission.py b/apps/perms/utils/asset_permission.py index cd6e1805e..80b1c19e5 100644 --- a/apps/perms/utils/asset_permission.py +++ b/apps/perms/utils/asset_permission.py @@ -477,6 +477,7 @@ class ParserNode: 'platform': asset.platform_base, 'domain': asset.domain_id, 'org_name': asset.org_name, + 'org_id': asset.org_id }, } } From 7c44f7406864cbc3c7d53f6a7259583220633385 Mon Sep 17 00:00:00 2001 From: Bai Date: Wed, 20 May 2020 18:20:01 +0800 Subject: [PATCH 41/42] =?UTF-8?q?[Bugfix]=20=E4=BF=AE=E5=A4=8D=E5=8F=AA?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E5=BD=93=E5=89=8D=E8=8A=82=E7=82=B9=E4=B8=8B?= =?UTF-8?q?=E8=B5=84=E4=BA=A7=E4=B8=8D=E7=94=9F=E6=95=88=E7=9A=84=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/assets/filters.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/assets/filters.py b/apps/assets/filters.py index b68d9d127..94a49a3f5 100644 --- a/apps/assets/filters.py +++ b/apps/assets/filters.py @@ -58,7 +58,9 @@ class AssetByNodeFilterBackend(filters.BaseFilterBackend): if query_all: pattern = node.get_all_children_pattern(with_self=True) else: - pattern = node.get_children_key_pattern(with_self=True) + # pattern = node.get_children_key_pattern(with_self=True) + # 只显示当前节点下资产 + pattern = r"^{}$".format(node.key) return self.perform_query(pattern, queryset) From 2e0eab9289c6987270bc40592796fd1c71c93c70 Mon Sep 17 00:00:00 2001 From: Bai Date: Wed, 20 May 2020 19:35:35 +0800 Subject: [PATCH 42/42] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9=E7=BF=BB?= =?UTF-8?q?=E8=AF=91=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/locale/zh/LC_MESSAGES/django.mo | Bin 89985 -> 90157 bytes apps/locale/zh/LC_MESSAGES/django.po | 116 +++++++++++++++------------ 2 files changed, 63 insertions(+), 53 deletions(-) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 8ce97ff575ac8bb7ed55c47ded03104bd285200d..c51d13e46ca681bcb48fee10eafab7ee7fe40e4d 100644 GIT binary patch delta 26542 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 Ir%aLm0~=T#zyJUM delta 26358 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 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 562154404..8207c4b7a 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-11 16:53+0800\n" +"POT-Creation-Date: 2020-05-20 19:25+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 @@ -244,7 +244,8 @@ msgstr "数据库" #: assets/templates/assets/platform_detail.html:64 #: assets/templates/assets/platform_list.html:18 #: assets/templates/assets/system_user_detail.html:112 -#: assets/templates/assets/system_user_list.html:29 ops/models/adhoc.py:37 +#: assets/templates/assets/system_user_list.html:29 +#: assets/templates/assets/user_asset_list.html:81 ops/models/adhoc.py:37 #: orgs/models.py:18 perms/models/base.py:56 #: perms/templates/perms/asset_permission_detail.html:97 #: perms/templates/perms/database_app_permission_detail.html:93 @@ -309,7 +310,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 +336,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 +738,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 +749,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 +767,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 +775,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 +981,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 +1015,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 +1032,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 +1044,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 "标签管理" @@ -1449,8 +1450,7 @@ msgid "SFTP Root" msgstr "SFTP根路径" #: assets/models/user.py:195 assets/templates/assets/system_user_list.html:73 -#: assets/templates/assets/user_asset_list.html:79 audits/models.py:21 -#: audits/templates/audits/ftp_log_list.html:53 +#: audits/models.py:21 audits/templates/audits/ftp_log_list.html:53 #: audits/templates/audits/ftp_log_list.html:76 #: perms/forms/asset_permission.py:95 perms/forms/remote_app_permission.py:49 #: perms/models/asset_permission.py:82 @@ -1771,7 +1771,7 @@ msgstr "获取认证信息错误" #: assets/templates/assets/_asset_user_auth_view_modal.html:101 #: assets/templates/assets/_node_detail_modal.html:67 #: assets/templates/assets/_user_asset_detail_modal.html:23 -#: authentication/templates/authentication/_access_key_modal.html:142 +#: authentication/templates/authentication/_access_key_modal.html:155 #: authentication/templates/authentication/_mfa_confirm_modal.html:53 #: settings/templates/settings/_ldap_list_users_modal.html:171 #: templates/_modal.html:22 tickets/models/ticket.py:68 @@ -1817,6 +1817,7 @@ msgid "Push" msgstr "推送" #: assets/templates/assets/_asset_user_list.html:167 +#: authentication/templates/authentication/_access_key_modal.html:147 msgid "Delete success" msgstr "删除成功" @@ -2769,7 +2770,7 @@ msgid "" msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)" #: authentication/errors.py:48 users/views/profile/otp.py:63 -#: users/views/profile/otp.py:100 users/views/profile/otp.py:116 +#: users/views/profile/otp.py:102 users/views/profile/otp.py:121 msgid "MFA code invalid, or ntp sync server time" msgstr "MFA验证码不正确,或者服务器端时间不对" @@ -2881,8 +2882,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" @@ -6183,19 +6184,19 @@ msgstr "首次登录" msgid "Profile setting" msgstr "个人信息设置" -#: users/views/profile/otp.py:144 +#: users/views/profile/otp.py:145 msgid "MFA enable success" msgstr "多因子认证启用成功" -#: users/views/profile/otp.py:145 +#: users/views/profile/otp.py:146 msgid "MFA enable success, return login page" msgstr "多因子认证启用成功,返回到登录页面" -#: users/views/profile/otp.py:147 +#: users/views/profile/otp.py:148 msgid "MFA disable success" msgstr "多因子认证禁用成功" -#: users/views/profile/otp.py:148 +#: users/views/profile/otp.py:149 msgid "MFA disable success, return login page" msgstr "多因子认证禁用成功,返回登录页面" @@ -6585,7 +6586,19 @@ msgstr "东北-大连" msgid "CN South-Guangzhou" msgstr "华南-广州" -#: xpack/plugins/cloud/providers/qcloud.py:14 +#: xpack/plugins/cloud/providers/huaweicloud.py:30 +msgid "CN Southwest-Guiyang1" +msgstr "西南-贵阳一" + +#: xpack/plugins/cloud/providers/huaweicloud.py:31 +msgid "EU-Paris" +msgstr "欧洲-巴黎" + +#: xpack/plugins/cloud/providers/huaweicloud.py:32 +msgid "LA-Santiago" +msgstr "拉美-圣地亚哥" + +#: xpack/plugins/cloud/providers/qcloud.py:17 msgid "Tencent Cloud" msgstr "腾讯云" @@ -6981,9 +6994,6 @@ msgstr "密码匣子" msgid "vault create" msgstr "创建" -#~ msgid "LA-Santiago" -#~ msgstr "拉美-圣地亚哥" - #~ msgid "Total hosts" #~ msgstr "主机总数"