diff --git a/README.md b/README.md index 70dac07d1..59c4e55cf 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ -JumpServer 是全球首款开源的堡垒机,使用 GPLv3 开源协议,是符合 4A 规范的运维安全审计系统。 +JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。 JumpServer 使用 Python 开发,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。 @@ -95,11 +95,15 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向 ### 案例研究 -- [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=3704) +- [万华化学:通过JumpServer管理全球化分布式IT资产,并且实现与云管平台的联动](https://blog.fit2cloud.com/?p=3504) +- [雪花啤酒:JumpServer堡垒机使用体会](https://blog.fit2cloud.com/?p=3412) +- [顺丰科技:JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147) +- [沐瞳游戏:通过JumpServer管控多项目分布式资产](https://blog.fit2cloud.com/?p=3213) +- [携程:JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851) +- [大智慧:JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882) +- [小红书:的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) diff --git a/apps/acls/models/login_acl.py b/apps/acls/models/login_acl.py index 1887ecd33..6fcff0231 100644 --- a/apps/acls/models/login_acl.py +++ b/apps/acls/models/login_acl.py @@ -44,58 +44,29 @@ class LoginACL(BaseACL): def __str__(self): return self.name - @property - def action_reject(self): - return self.action == self.ActionChoices.reject - - @property - def action_allow(self): - return self.action == self.ActionChoices.allow + def is_action(self, action): + return self.action == action @classmethod def filter_acl(cls, user): return user.login_acls.all().valid().distinct() @staticmethod - def allow_user_confirm_if_need(user, ip): - acl = LoginACL.filter_acl(user).filter( - action=LoginACL.ActionChoices.confirm - ).first() - acl = acl if acl and acl.reviewers.exists() else None - if not acl: - return False, acl - ip_group = acl.rules.get('ip_group') - time_periods = acl.rules.get('time_period') - is_contain_ip = contains_ip(ip, ip_group) - is_contain_time_period = contains_time_period(time_periods) - return is_contain_ip and is_contain_time_period, acl + def match(user, ip): + acls = LoginACL.filter_acl(user) + if not acls: + return - @staticmethod - def allow_user_to_login(user, ip): - acl = LoginACL.filter_acl(user).exclude( - action=LoginACL.ActionChoices.confirm - ).first() - if not acl: - return True, '' - ip_group = acl.rules.get('ip_group') - time_periods = acl.rules.get('time_period') - is_contain_ip = contains_ip(ip, ip_group) - is_contain_time_period = contains_time_period(time_periods) - - reject_type = '' - if is_contain_ip and is_contain_time_period: - # 满足条件 - allow = acl.action_allow - if not allow: - reject_type = 'ip' if is_contain_ip else 'time' - else: - # 不满足条件 - # 如果acl本身允许,那就拒绝;如果本身拒绝,那就允许 - allow = not acl.action_allow - if not allow: - reject_type = 'ip' if not is_contain_ip else 'time' - - return allow, reject_type + for acl in acls: + if acl.is_action(LoginACL.ActionChoices.confirm) and not acl.reviewers.exists(): + continue + ip_group = acl.rules.get('ip_group') + time_periods = acl.rules.get('time_period') + is_contain_ip = contains_ip(ip, ip_group) + is_contain_time_period = contains_time_period(time_periods) + if is_contain_ip and is_contain_time_period: + # 满足条件,则返回 + return acl def create_confirm_ticket(self, request): from tickets import const diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index 3f4b7a209..f8c2ab179 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -6,7 +6,7 @@ from django.shortcuts import get_object_or_404 from django.db.models import Q from common.utils import get_logger, get_object_or_none -from common.mixins.api import SuggestionMixin +from common.mixins.api import SuggestionMixin, RenderToJsonMixin from users.models import User, UserGroup from users.serializers import UserSerializer, UserGroupSerializer from users.filters import UserFilter @@ -88,7 +88,7 @@ class AssetPlatformRetrieveApi(RetrieveAPIView): return asset.platform -class AssetPlatformViewSet(ModelViewSet): +class AssetPlatformViewSet(ModelViewSet, RenderToJsonMixin): queryset = Platform.objects.all() serializer_class = serializers.PlatformSerializer filterset_fields = ['name', 'base'] diff --git a/apps/assets/api/mixin.py b/apps/assets/api/mixin.py index 7f485bf29..43220a1a5 100644 --- a/apps/assets/api/mixin.py +++ b/apps/assets/api/mixin.py @@ -24,7 +24,7 @@ class SerializeToTreeNodeMixin: 'title': _name(node), 'pId': node.parent_key, 'isParent': True, - 'open': node.is_org_root(), + 'open': True, 'meta': { 'data': { "id": node.id, diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index 2bb77b60c..0ef3aecc6 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -101,6 +101,8 @@ class NodeListAsTreeApi(generics.ListAPIView): class NodeChildrenApi(generics.ListCreateAPIView): serializer_class = serializers.NodeSerializer + search_fields = ('value',) + instance = None is_initial = False @@ -179,8 +181,15 @@ class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi): """ model = Node + def filter_queryset(self, queryset): + if not self.request.GET.get('search'): + return queryset + queryset = super().filter_queryset(queryset) + queryset = self.model.get_ancestor_queryset(queryset) + return queryset + def list(self, request, *args, **kwargs): - nodes = self.get_queryset().order_by('value') + nodes = self.filter_queryset(self.get_queryset()).order_by('value') nodes = self.serialize_nodes(nodes, with_asset_amount=True) assets = self.get_assets() data = [*nodes, *assets] diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index dcebab3eb..0e98bce14 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -25,7 +25,6 @@ from orgs.mixins.models import OrgModelMixin, OrgManager from orgs.utils import get_current_org, tmp_to_org, tmp_to_root_org from orgs.models import Organization - __all__ = ['Node', 'FamilyMixin', 'compute_parent_key', 'NodeQuerySet'] logger = get_logger(__name__) @@ -98,6 +97,14 @@ class FamilyMixin: q |= Q(key=self.key) return Node.objects.filter(q) + @classmethod + def get_ancestor_queryset(cls, queryset, with_self=True): + parent_keys = set() + for i in queryset: + parent_keys.update(set(i.get_ancestor_keys(with_self=with_self))) + queryset = queryset.model.objects.filter(key__in=list(parent_keys)).distinct() + return queryset + @property def children(self): return self.get_children(with_self=False) @@ -396,7 +403,7 @@ class NodeAllAssetsMappingMixin: mapping[ancestor_key].update(asset_ids) t3 = time.time() - logger.info('t1-t2(DB Query): {} s, t3-t2(Generate mapping): {} s'.format(t2-t1, t3-t2)) + logger.info('t1-t2(DB Query): {} s, t3-t2(Generate mapping): {} s'.format(t2 - t1, t3 - t2)) return mapping diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py index 427d0e470..5211cfef6 100644 --- a/apps/assets/serializers/asset.py +++ b/apps/assets/serializers/asset.py @@ -189,6 +189,9 @@ class PlatformSerializer(serializers.ModelSerializer): 'id', 'name', 'base', 'charset', 'internal', 'meta', 'comment' ] + extra_kwargs = { + 'internal': {'read_only': True}, + } class AssetSimpleSerializer(serializers.ModelSerializer): diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index ecd329af0..2b4ce7e2b 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -22,7 +22,6 @@ from ..serializers import ( ) from ..models import ConnectionToken - __all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet'] @@ -174,9 +173,8 @@ class ConnectionTokenMixin: rdp_options['remoteapplicationname:s'] = name else: name = '*' - - filename = "{}-{}-jumpserver".format(token.user.username, name) - filename = urllib.parse.quote(filename) + prefix_name = f'{token.user.username}-{name}' + filename = self.get_connect_filename(prefix_name) content = '' for k, v in rdp_options.items(): @@ -184,6 +182,15 @@ class ConnectionTokenMixin: return filename, content + @staticmethod + def get_connect_filename(prefix_name): + prefix_name = prefix_name.replace('/', '_') + prefix_name = prefix_name.replace('\\', '_') + prefix_name = prefix_name.replace('.', '_') + filename = f'{prefix_name}-jumpserver' + filename = urllib.parse.quote(filename) + return filename + def get_ssh_token(self, token: ConnectionToken): if token.asset: name = token.asset.hostname @@ -191,7 +198,8 @@ class ConnectionTokenMixin: name = token.application.name else: name = '*' - filename = f'{token.user.username}-{name}-jumpserver' + prefix_name = f'{token.user.username}-{name}' + filename = self.get_connect_filename(prefix_name) endpoint = self.get_smart_endpoint( protocol='ssh', asset=token.asset, application=token.application @@ -326,4 +334,3 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet): 'msg': f'Token is renewed, date expired: {date_expired}' } return Response(data=data, status=status.HTTP_200_OK) - diff --git a/apps/authentication/api/login_confirm.py b/apps/authentication/api/login_confirm.py index 22594b88e..71613348c 100644 --- a/apps/authentication/api/login_confirm.py +++ b/apps/authentication/api/login_confirm.py @@ -17,8 +17,10 @@ class TicketStatusApi(mixins.AuthMixin, APIView): def get(self, request, *args, **kwargs): try: self.check_user_login_confirm() + self.request.session['auth_third_party_done'] = 1 return Response({"msg": "ok"}) except errors.NeedMoreInfoError as e: + self.send_auth_signal(success=False, reason=e.as_data().get('msg')) return Response(e.as_data(), status=200) def delete(self, request, *args, **kwargs): diff --git a/apps/authentication/backends/base.py b/apps/authentication/backends/base.py index 84cdeab27..13e99f2c6 100644 --- a/apps/authentication/backends/base.py +++ b/apps/authentication/backends/base.py @@ -49,7 +49,7 @@ class JMSBaseAuthBackend: if not allow: info = 'User {} skip authentication backend {}, because it not in {}' info = info.format(username, backend_name, ','.join(allowed_backend_names)) - logger.debug(info) + logger.info(info) return allow diff --git a/apps/authentication/backends/oauth2/__init__.py b/apps/authentication/backends/oauth2/__init__.py new file mode 100644 index 000000000..448096520 --- /dev/null +++ b/apps/authentication/backends/oauth2/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# + +from .backends import * diff --git a/apps/authentication/backends/oauth2/backends.py b/apps/authentication/backends/oauth2/backends.py new file mode 100644 index 000000000..755d5ef54 --- /dev/null +++ b/apps/authentication/backends/oauth2/backends.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +# +import requests + +from django.contrib.auth import get_user_model +from django.utils.http import urlencode +from django.conf import settings +from django.urls import reverse + +from common.utils import get_logger +from users.utils import construct_user_email +from authentication.utils import build_absolute_uri +from common.exceptions import JMSException + +from .signals import ( + oauth2_create_or_update_user, oauth2_user_login_failed, + oauth2_user_login_success +) +from ..base import JMSModelBackend + + +__all__ = ['OAuth2Backend'] + +logger = get_logger(__name__) + + +class OAuth2Backend(JMSModelBackend): + @staticmethod + def is_enabled(): + return settings.AUTH_OAUTH2 + + def get_or_create_user_from_userinfo(self, request, userinfo): + log_prompt = "Get or Create user [OAuth2Backend]: {}" + logger.debug(log_prompt.format('start')) + + # Construct user attrs value + user_attrs = {} + for field, attr in settings.AUTH_OAUTH2_USER_ATTR_MAP.items(): + user_attrs[field] = userinfo.get(attr, '') + + username = user_attrs.get('username') + if not username: + error_msg = 'username is missing' + logger.error(log_prompt.format(error_msg)) + raise JMSException(error_msg) + + email = user_attrs.get('email', '') + email = construct_user_email(user_attrs.get('username'), email) + user_attrs.update({'email': email}) + + logger.debug(log_prompt.format(user_attrs)) + user, created = get_user_model().objects.get_or_create( + username=username, defaults=user_attrs + ) + logger.debug(log_prompt.format("user: {}|created: {}".format(user, created))) + logger.debug(log_prompt.format("Send signal => oauth2 create or update user")) + oauth2_create_or_update_user.send( + sender=self.__class__, request=request, user=user, created=created, + attrs=user_attrs + ) + return user, created + + @staticmethod + def get_response_data(response_data): + if response_data.get('data') is not None: + response_data = response_data['data'] + return response_data + + @staticmethod + def get_query_dict(response_data, query_dict): + query_dict.update({ + 'uid': response_data.get('uid', ''), + 'access_token': response_data.get('access_token', '') + }) + return query_dict + + def authenticate(self, request, code=None, **kwargs): + log_prompt = "Process authenticate [OAuth2Backend]: {}" + logger.debug(log_prompt.format('Start')) + if code is None: + logger.error(log_prompt.format('code is missing')) + return None + + query_dict = { + 'client_id': settings.AUTH_OAUTH2_CLIENT_ID, + 'client_secret': settings.AUTH_OAUTH2_CLIENT_SECRET, + 'grant_type': 'authorization_code', + 'code': code, + 'redirect_uri': build_absolute_uri( + request, path=reverse(settings.AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME) + ) + } + access_token_url = '{url}?{query}'.format( + url=settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT, query=urlencode(query_dict) + ) + token_method = settings.AUTH_OAUTH2_ACCESS_TOKEN_METHOD.lower() + requests_func = getattr(requests, token_method, requests.get) + logger.debug(log_prompt.format('Call the access token endpoint[method: %s]' % token_method)) + headers = { + 'Accept': 'application/json' + } + access_token_response = requests_func(access_token_url, headers=headers) + try: + access_token_response.raise_for_status() + access_token_response_data = access_token_response.json() + response_data = self.get_response_data(access_token_response_data) + except Exception as e: + error = "Json access token response error, access token response " \ + "content is: {}, error is: {}".format(access_token_response.content, str(e)) + logger.error(log_prompt.format(error)) + return None + + query_dict = self.get_query_dict(response_data, query_dict) + + headers = { + 'Accept': 'application/json', + 'Authorization': 'token {}'.format(response_data.get('access_token', '')) + } + + logger.debug(log_prompt.format('Get userinfo endpoint')) + userinfo_url = '{url}?{query}'.format( + url=settings.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT, + query=urlencode(query_dict) + ) + userinfo_response = requests.get(userinfo_url, headers=headers) + try: + userinfo_response.raise_for_status() + userinfo_response_data = userinfo_response.json() + if 'data' in userinfo_response_data: + userinfo = userinfo_response_data['data'] + else: + userinfo = userinfo_response_data + except Exception as e: + error = "Json userinfo response error, userinfo response " \ + "content is: {}, error is: {}".format(userinfo_response.content, str(e)) + logger.error(log_prompt.format(error)) + return None + + try: + logger.debug(log_prompt.format('Update or create oauth2 user')) + user, created = self.get_or_create_user_from_userinfo(request, userinfo) + except JMSException: + return None + + if self.user_can_authenticate(user): + logger.debug(log_prompt.format('OAuth2 user login success')) + logger.debug(log_prompt.format('Send signal => oauth2 user login success')) + oauth2_user_login_success.send(sender=self.__class__, request=request, user=user) + return user + else: + logger.debug(log_prompt.format('OAuth2 user login failed')) + logger.debug(log_prompt.format('Send signal => oauth2 user login failed')) + oauth2_user_login_failed.send( + sender=self.__class__, request=request, username=user.username, + reason=_('User invalid, disabled or expired') + ) + return None diff --git a/apps/authentication/backends/oauth2/signals.py b/apps/authentication/backends/oauth2/signals.py new file mode 100644 index 000000000..50c7837f8 --- /dev/null +++ b/apps/authentication/backends/oauth2/signals.py @@ -0,0 +1,9 @@ +from django.dispatch import Signal + + +oauth2_create_or_update_user = Signal( + providing_args=['request', 'user', 'created', 'name', 'username', 'email'] +) +oauth2_user_login_success = Signal(providing_args=['request', 'user']) +oauth2_user_login_failed = Signal(providing_args=['request', 'username', 'reason']) + diff --git a/apps/authentication/backends/oauth2/urls.py b/apps/authentication/backends/oauth2/urls.py new file mode 100644 index 000000000..94c044a7d --- /dev/null +++ b/apps/authentication/backends/oauth2/urls.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# +from django.urls import path + +from . import views + + +urlpatterns = [ + path('login/', views.OAuth2AuthRequestView.as_view(), name='login'), + path('callback/', views.OAuth2AuthCallbackView.as_view(), name='login-callback') +] diff --git a/apps/authentication/backends/oauth2/views.py b/apps/authentication/backends/oauth2/views.py new file mode 100644 index 000000000..dd295fe86 --- /dev/null +++ b/apps/authentication/backends/oauth2/views.py @@ -0,0 +1,58 @@ +from django.views import View +from django.conf import settings +from django.contrib.auth import login +from django.http import HttpResponseRedirect +from django.urls import reverse +from django.utils.http import urlencode + +from authentication.utils import build_absolute_uri +from common.utils import get_logger +from authentication.mixins import authenticate + +logger = get_logger(__file__) + + +class OAuth2AuthRequestView(View): + + def get(self, request): + log_prompt = "Process OAuth2 GET requests: {}" + logger.debug(log_prompt.format('Start')) + + query_dict = { + 'client_id': settings.AUTH_OAUTH2_CLIENT_ID, 'response_type': 'code', + 'scope': settings.AUTH_OAUTH2_SCOPE, + 'redirect_uri': build_absolute_uri( + request, path=reverse(settings.AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME) + ) + } + + redirect_url = '{url}?{query}'.format( + url=settings.AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT, + query=urlencode(query_dict) + ) + logger.debug(log_prompt.format('Redirect login url')) + return HttpResponseRedirect(redirect_url) + + +class OAuth2AuthCallbackView(View): + http_method_names = ['get', ] + + def get(self, request): + """ Processes GET requests. """ + log_prompt = "Process GET requests [OAuth2AuthCallbackView]: {}" + logger.debug(log_prompt.format('Start')) + callback_params = request.GET + + if 'code' in callback_params: + logger.debug(log_prompt.format('Process authenticate')) + user = authenticate(code=callback_params['code'], request=request) + if user and user.is_valid: + logger.debug(log_prompt.format('Login: {}'.format(user))) + login(self.request, user) + logger.debug(log_prompt.format('Redirect')) + return HttpResponseRedirect( + settings.AUTH_OAUTH2_AUTHENTICATION_REDIRECT_URI + ) + + logger.debug(log_prompt.format('Redirect')) + return HttpResponseRedirect(settings.AUTH_OAUTH2_AUTHENTICATION_FAILURE_REDIRECT_URI) diff --git a/apps/authentication/backends/oidc/backends.py b/apps/authentication/backends/oidc/backends.py index 9866e84f3..03b71334f 100644 --- a/apps/authentication/backends/oidc/backends.py +++ b/apps/authentication/backends/oidc/backends.py @@ -9,6 +9,7 @@ import base64 import requests + from rest_framework.exceptions import ParseError from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend @@ -18,10 +19,11 @@ from django.urls import reverse from django.conf import settings from common.utils import get_logger +from authentication.utils import build_absolute_uri_for_oidc from users.utils import construct_user_email from ..base import JMSBaseAuthBackend -from .utils import validate_and_return_id_token, build_absolute_uri +from .utils import validate_and_return_id_token from .decorator import ssl_verification from .signals import ( openid_create_or_update_user, openid_user_login_failed, openid_user_login_success @@ -127,7 +129,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend): token_payload = { 'grant_type': 'authorization_code', 'code': code, - 'redirect_uri': build_absolute_uri( + 'redirect_uri': build_absolute_uri_for_oidc( request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME) ) } diff --git a/apps/authentication/backends/oidc/utils.py b/apps/authentication/backends/oidc/utils.py index 2a0f0609e..31cca6f03 100644 --- a/apps/authentication/backends/oidc/utils.py +++ b/apps/authentication/backends/oidc/utils.py @@ -8,7 +8,7 @@ import datetime as dt from calendar import timegm -from urllib.parse import urlparse, urljoin +from urllib.parse import urlparse from django.core.exceptions import SuspiciousOperation from django.utils.encoding import force_bytes, smart_bytes @@ -110,17 +110,3 @@ def _validate_claims(id_token, nonce=None, validate_nonce=True): raise SuspiciousOperation('Incorrect id_token: nonce') logger.debug(log_prompt.format('End')) - - -def build_absolute_uri(request, path=None): - """ - Build absolute redirect uri - """ - if path is None: - path = '/' - - if settings.BASE_SITE_URL: - redirect_uri = urljoin(settings.BASE_SITE_URL, path) - else: - redirect_uri = request.build_absolute_uri(path) - return redirect_uri diff --git a/apps/authentication/backends/oidc/views.py b/apps/authentication/backends/oidc/views.py index 1c9442ef2..78019ac33 100644 --- a/apps/authentication/backends/oidc/views.py +++ b/apps/authentication/backends/oidc/views.py @@ -20,7 +20,8 @@ from django.utils.crypto import get_random_string from django.utils.http import is_safe_url, urlencode from django.views.generic import View -from .utils import get_logger, build_absolute_uri +from authentication.utils import build_absolute_uri_for_oidc +from .utils import get_logger logger = get_logger(__file__) @@ -50,7 +51,7 @@ class OIDCAuthRequestView(View): 'scope': settings.AUTH_OPENID_SCOPES, 'response_type': 'code', 'client_id': settings.AUTH_OPENID_CLIENT_ID, - 'redirect_uri': build_absolute_uri( + 'redirect_uri': build_absolute_uri_for_oidc( request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME) ) }) @@ -216,7 +217,7 @@ class OIDCEndSessionView(View): """ Returns the end-session URL. """ q = QueryDict(mutable=True) q[settings.AUTH_OPENID_PROVIDER_END_SESSION_REDIRECT_URI_PARAMETER] = \ - build_absolute_uri(self.request, path=settings.LOGOUT_REDIRECT_URL or '/') + build_absolute_uri_for_oidc(self.request, path=settings.LOGOUT_REDIRECT_URL or '/') q[settings.AUTH_OPENID_PROVIDER_END_SESSION_ID_TOKEN_PARAMETER] = \ self.request.session['oidc_auth_id_token'] return '{}?{}'.format(settings.AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT, q.urlencode()) diff --git a/apps/authentication/backends/saml2/backends.py b/apps/authentication/backends/saml2/backends.py index 0ac0efe1c..70667cd94 100644 --- a/apps/authentication/backends/saml2/backends.py +++ b/apps/authentication/backends/saml2/backends.py @@ -39,7 +39,7 @@ class SAML2Backend(JMSModelBackend): return user, created def authenticate(self, request, saml_user_data=None, **kwargs): - log_prompt = "Process authenticate [SAML2AuthCodeBackend]: {}" + log_prompt = "Process authenticate [SAML2Backend]: {}" logger.debug(log_prompt.format('Start')) if saml_user_data is None: logger.error(log_prompt.format('saml_user_data is missing')) @@ -48,7 +48,7 @@ class SAML2Backend(JMSModelBackend): logger.debug(log_prompt.format('saml data, {}'.format(saml_user_data))) username = saml_user_data.get('username') if not username: - logger.debug(log_prompt.format('username is missing')) + logger.warning(log_prompt.format('username is missing')) return None user, created = self.get_or_create_from_saml_data(request, **saml_user_data) diff --git a/apps/authentication/errors/failed.py b/apps/authentication/errors/failed.py index 118fd6d6e..ec43d1b73 100644 --- a/apps/authentication/errors/failed.py +++ b/apps/authentication/errors/failed.py @@ -12,12 +12,13 @@ class AuthFailedNeedLogMixin: username = '' request = None error = '' + msg = '' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) post_auth_failed.send( sender=self.__class__, username=self.username, - request=self.request, reason=self.error + request=self.request, reason=self.msg ) @@ -138,18 +139,11 @@ class ACLError(AuthFailedNeedLogMixin, AuthFailedError): } -class LoginIPNotAllowed(ACLError): +class LoginACLIPAndTimePeriodNotAllowed(ACLError): def __init__(self, username, request, **kwargs): self.username = username self.request = request - super().__init__(_("IP is not allowed"), **kwargs) - - -class TimePeriodNotAllowed(ACLError): - def __init__(self, username, request, **kwargs): - self.username = username - self.request = request - super().__init__(_("Time Period is not allowed"), **kwargs) + super().__init__(_("Current IP and Time period is not allowed"), **kwargs) class MFACodeRequiredError(AuthFailedError): diff --git a/apps/authentication/middleware.py b/apps/authentication/middleware.py index b4241f12d..5b6d7c06f 100644 --- a/apps/authentication/middleware.py +++ b/apps/authentication/middleware.py @@ -1,11 +1,16 @@ import base64 -from django.shortcuts import redirect, reverse +from django.shortcuts import redirect, reverse, render from django.utils.deprecation import MiddlewareMixin from django.http import HttpResponse from django.conf import settings +from django.utils.translation import ugettext as _ +from django.contrib.auth import logout as auth_logout +from apps.authentication import mixins from common.utils import gen_key_pair +from common.utils import get_request_ip +from .signals import post_auth_failed class MFAMiddleware: @@ -13,6 +18,7 @@ class MFAMiddleware: 这个 中间件 是用来全局拦截开启了 MFA 却没有认证的,如 OIDC, CAS,使用第三方库做的登录,直接 login 了, 所以只能在 Middleware 中控制 """ + def __init__(self, get_response): self.get_response = get_response @@ -42,6 +48,50 @@ class MFAMiddleware: return redirect(url) +class ThirdPartyLoginMiddleware(mixins.AuthMixin): + """OpenID、CAS、SAML2登录规则设置验证""" + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + # 没有认证过,证明不是从 第三方 来的 + if request.user.is_anonymous: + return response + if not request.session.get('auth_third_party_required'): + return response + ip = get_request_ip(request) + try: + self.request = request + self._check_login_acl(request.user, ip) + except Exception as e: + post_auth_failed.send( + sender=self.__class__, username=request.user.username, + request=self.request, reason=e.msg + ) + auth_logout(request) + context = { + 'title': _('Authentication failed'), + 'message': _('Authentication failed (before login check failed): {}').format(e), + 'interval': 10, + 'redirect_url': reverse('authentication:login'), + 'auto_redirect': True, + } + response = render(request, 'authentication/auth_fail_flash_message_standalone.html', context) + else: + if not self.request.session['auth_confirm_required']: + return response + guard_url = reverse('authentication:login-guard') + args = request.META.get('QUERY_STRING', '') + if args: + guard_url = "%s?%s" % (guard_url, args) + response = redirect(guard_url) + finally: + request.session.pop('auth_third_party_required', '') + return response + + class SessionCookieMiddleware(MiddlewareMixin): @staticmethod diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 403f7186d..739048d75 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -328,13 +328,56 @@ class AuthACLMixin: def _check_login_acl(self, user, ip): # ACL 限制用户登录 - is_allowed, limit_type = LoginACL.allow_user_to_login(user, ip) - if is_allowed: + acl = LoginACL.match(user, ip) + if not acl: return - if limit_type == 'ip': - raise errors.LoginIPNotAllowed(username=user.username, request=self.request) - elif limit_type == 'time': - raise errors.TimePeriodNotAllowed(username=user.username, request=self.request) + + acl: LoginACL + if acl.is_action(acl.ActionChoices.allow): + return + + if acl.is_action(acl.ActionChoices.reject): + raise errors.LoginACLIPAndTimePeriodNotAllowed(user.username, request=self.request) + + if acl.is_action(acl.ActionChoices.confirm): + self.request.session['auth_confirm_required'] = '1' + self.request.session['auth_acl_id'] = str(acl.id) + return + + def check_user_login_confirm_if_need(self, user): + if not self.request.session.get("auth_confirm_required"): + return + acl_id = self.request.session.get('auth_acl_id') + logger.debug('Login confirm acl id: {}'.format(acl_id)) + if not acl_id: + return + acl = LoginACL.filter_acl(user).filter(id=acl_id).first() + if not acl: + return + if not acl.is_action(acl.ActionChoices.confirm): + return + self.get_ticket_or_create(acl) + self.check_user_login_confirm() + + def get_ticket_or_create(self, acl): + ticket = self.get_ticket() + if not ticket or ticket.is_state(ticket.State.closed): + ticket = acl.create_confirm_ticket(self.request) + self.request.session['auth_ticket_id'] = str(ticket.id) + return ticket + + def check_user_login_confirm(self): + ticket = self.get_ticket() + if not ticket: + raise errors.LoginConfirmOtherError('', "Not found") + elif ticket.is_state(ticket.State.approved): + self.request.session["auth_confirm_required"] = '' + return + elif ticket.is_status(ticket.Status.open): + raise errors.LoginConfirmWaitError(ticket.id) + else: + # rejected, closed + raise errors.LoginConfirmOtherError(ticket.id, ticket.get_state_display()) def get_ticket(self): from tickets.models import ApplyLoginTicket @@ -346,44 +389,6 @@ class AuthACLMixin: ticket = ApplyLoginTicket.all().filter(id=ticket_id).first() return ticket - def get_ticket_or_create(self, confirm_setting): - ticket = self.get_ticket() - if not ticket or ticket.is_status(ticket.Status.closed): - ticket = confirm_setting.create_confirm_ticket(self.request) - self.request.session['auth_ticket_id'] = str(ticket.id) - return ticket - - def check_user_login_confirm(self): - ticket = self.get_ticket() - if not ticket: - raise errors.LoginConfirmOtherError('', "Not found") - - if ticket.is_status(ticket.Status.open): - raise errors.LoginConfirmWaitError(ticket.id) - elif ticket.is_state(ticket.State.approved): - self.request.session["auth_confirm"] = "1" - return - elif ticket.is_state(ticket.State.rejected): - raise errors.LoginConfirmOtherError( - ticket.id, ticket.get_state_display() - ) - elif ticket.is_state(ticket.State.closed): - raise errors.LoginConfirmOtherError( - ticket.id, ticket.get_state_display() - ) - else: - raise errors.LoginConfirmOtherError( - ticket.id, ticket.get_status_display() - ) - - def check_user_login_confirm_if_need(self, user): - ip = self.get_request_ip() - is_allowed, confirm_setting = LoginACL.allow_user_confirm_if_need(user, ip) - if self.request.session.get('auth_confirm') or not is_allowed: - return - self.get_ticket_or_create(confirm_setting) - self.check_user_login_confirm() - class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPostCheckMixin): request = None @@ -482,7 +487,9 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost return self.check_user_auth(valid_data) def clear_auth_mark(self): - keys = ['auth_password', 'user_id', 'auth_confirm', 'auth_ticket_id'] + keys = [ + 'auth_password', 'user_id', 'auth_confirm_required', 'auth_ticket_id', 'auth_acl_id' + ] for k in keys: self.request.session.pop(k, '') diff --git a/apps/authentication/signal_handlers.py b/apps/authentication/signal_handlers.py index ac155dcf0..e7be3465c 100644 --- a/apps/authentication/signal_handlers.py +++ b/apps/authentication/signal_handlers.py @@ -6,12 +6,16 @@ from django.core.cache import cache from django.dispatch import receiver from django_cas_ng.signals import cas_user_authenticated +from apps.jumpserver.settings.auth import AUTHENTICATION_BACKENDS_THIRD_PARTY from authentication.backends.oidc.signals import ( openid_user_login_failed, openid_user_login_success ) from authentication.backends.saml2.signals import ( saml2_user_authenticated, saml2_user_authentication_failed ) +from authentication.backends.oauth2.signals import ( + oauth2_user_login_failed, oauth2_user_login_success +) from .signals import post_auth_success, post_auth_failed @@ -25,7 +29,8 @@ def on_user_auth_login_success(sender, user, request, **kwargs): and user.mfa_enabled \ and not request.session.get('auth_mfa'): request.session['auth_mfa_required'] = 1 - + if not request.session.get("auth_third_party_done") and request.session.get('auth_backend') in AUTHENTICATION_BACKENDS_THIRD_PARTY: + request.session['auth_third_party_required'] = 1 # 单点登录,超过了自动退出 if settings.USER_LOGIN_SINGLE_MACHINE_ENABLED: lock_key = 'single_machine_login_' + str(user.id) @@ -67,3 +72,15 @@ def on_saml2_user_login_success(sender, request, user, **kwargs): def on_saml2_user_login_failed(sender, request, username, reason, **kwargs): request.session['auth_backend'] = settings.AUTH_BACKEND_SAML2 post_auth_failed.send(sender, username=username, request=request, reason=reason) + + +@receiver(oauth2_user_login_success) +def on_oauth2_user_login_success(sender, request, user, **kwargs): + request.session['auth_backend'] = settings.AUTH_BACKEND_OAUTH2 + post_auth_success.send(sender, user=user, request=request) + + +@receiver(oauth2_user_login_failed) +def on_oauth2_user_login_failed(sender, username, request, reason, **kwargs): + request.session['auth_backend'] = settings.AUTH_BACKEND_OAUTH2 + post_auth_failed.send(sender, username=username, request=request, reason=reason) diff --git a/apps/authentication/templates/authentication/auth_fail_flash_message_standalone.html b/apps/authentication/templates/authentication/auth_fail_flash_message_standalone.html new file mode 100644 index 000000000..61b431b9a --- /dev/null +++ b/apps/authentication/templates/authentication/auth_fail_flash_message_standalone.html @@ -0,0 +1,70 @@ +{% extends '_base_only_content.html' %} +{% load static %} +{% load i18n %} +{% block html_title %} {{ title }} {% endblock %} +{% block title %} {{ title }}{% endblock %} + +{% block content %} + +
+

+

+ {% if error %} + {{ error }} + {% else %} + {{ message|safe }} + {% endif %} +
+

+ +
+ {% if has_cancel %} + + {% endif %} + +
+
+{% endblock %} + +{% block custom_foot_js %} + +{% endblock %} + diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py index 9abd61e3b..2c97fe1ea 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -56,9 +56,11 @@ urlpatterns = [ path('profile/otp/disable/', users_view.UserOtpDisableView.as_view(), name='user-otp-disable'), - # openid + # other authentication protocol path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')), path('openid/', include(('authentication.backends.oidc.urls', 'authentication'), namespace='openid')), path('saml2/', include(('authentication.backends.saml2.urls', 'authentication'), namespace='saml2')), + path('oauth2/', include(('authentication.backends.oauth2.urls', 'authentication'), namespace='oauth2')), + path('captcha/', include('captcha.urls')), ] diff --git a/apps/authentication/utils.py b/apps/authentication/utils.py index c0588b206..836cb55bf 100644 --- a/apps/authentication/utils.py +++ b/apps/authentication/utils.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # +from urllib.parse import urljoin from django.conf import settings @@ -29,3 +30,23 @@ def check_different_city_login_if_need(user, request): if last_user_login and last_user_login.city != city: DifferentCityLoginMessage(user, ip, city).publish_async() + + +def build_absolute_uri(request, path=None): + """ Build absolute redirect """ + if path is None: + path = '/' + redirect_uri = request.build_absolute_uri(path) + return redirect_uri + + +def build_absolute_uri_for_oidc(request, path=None): + """ Build absolute redirect uri for OIDC """ + if path is None: + path = '/' + if settings.BASE_SITE_URL: + # OIDC 专用配置项 + redirect_uri = urljoin(settings.BASE_SITE_URL, path) + else: + redirect_uri = request.build_absolute_uri(path) + return redirect_uri diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index 09c842d88..88f580279 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -21,7 +21,7 @@ from django.conf import settings from django.urls import reverse_lazy from django.contrib.auth import BACKEND_SESSION_KEY -from common.utils import FlashMessageUtil +from common.utils import FlashMessageUtil, static_or_direct from users.utils import ( redirect_user_first_login_or_index ) @@ -39,8 +39,7 @@ class UserLoginContextMixin: get_user_mfa_context: Callable request: HttpRequest - @staticmethod - def get_support_auth_methods(): + def get_support_auth_methods(self): auth_methods = [ { 'name': 'OpenID', @@ -63,6 +62,13 @@ class UserLoginContextMixin: 'logo': static('img/login_saml2_logo.png'), 'auto_redirect': True }, + { + 'name': settings.AUTH_OAUTH2_PROVIDER, + 'enabled': settings.AUTH_OAUTH2, + 'url': reverse('authentication:oauth2:login'), + 'logo': static_or_direct(settings.AUTH_OAUTH2_LOGO_PATH), + 'auto_redirect': True + }, { 'name': _('WeCom'), 'enabled': settings.AUTH_WECOM, diff --git a/apps/common/hashers/__init__.py b/apps/common/hashers/__init__.py new file mode 100644 index 000000000..6313fa88b --- /dev/null +++ b/apps/common/hashers/__init__.py @@ -0,0 +1 @@ +from .sm3 import PBKDF2SM3PasswordHasher diff --git a/apps/common/hashers/sm3.py b/apps/common/hashers/sm3.py new file mode 100644 index 000000000..62f811e94 --- /dev/null +++ b/apps/common/hashers/sm3.py @@ -0,0 +1,23 @@ +from gmssl import sm3, func + +from django.contrib.auth.hashers import PBKDF2PasswordHasher + + +class Hasher: + name = 'sm3' + + def __init__(self, key): + self.key = key + + def hexdigest(self): + return sm3.sm3_hash(func.bytes_to_list(self.key)) + + @staticmethod + def hash(msg): + return Hasher(msg) + + +class PBKDF2SM3PasswordHasher(PBKDF2PasswordHasher): + algorithm = "pbkdf2_sm3" + digest = Hasher.hash + diff --git a/apps/common/sdk/sms/base.py b/apps/common/sdk/sms/base.py index 4d02370b1..77dcc669a 100644 --- a/apps/common/sdk/sms/base.py +++ b/apps/common/sdk/sms/base.py @@ -17,4 +17,8 @@ class BaseSMSClient: def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs): raise NotImplementedError + @staticmethod + def need_pre_check(): + return True + diff --git a/apps/common/sdk/sms/cmpp2.py b/apps/common/sdk/sms/cmpp2.py new file mode 100644 index 000000000..4d81ee1a0 --- /dev/null +++ b/apps/common/sdk/sms/cmpp2.py @@ -0,0 +1,329 @@ +import hashlib +import socket +import struct +import time + +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ + +from common.utils import get_logger +from common.exceptions import JMSException +from .base import BaseSMSClient + + +logger = get_logger(__file__) + + +CMPP_CONNECT = 0x00000001 # 请求连接 +CMPP_CONNECT_RESP = 0x80000001 # 请求连接应答 +CMPP_TERMINATE = 0x00000002 # 终止连接 +CMPP_TERMINATE_RESP = 0x80000002 # 终止连接应答 +CMPP_SUBMIT = 0x00000004 # 提交短信 +CMPP_SUBMIT_RESP = 0x80000004 # 提交短信应答 +CMPP_DELIVER = 0x00000005 # 短信下发 +CMPP_DELIVER_RESP = 0x80000005 # 下发短信应答 + + +class CMPPBaseRequestInstance(object): + def __init__(self): + self.command_id = '' + self.body = b'' + self.length = 0 + + def get_header(self, sequence_id): + length = struct.pack('!L', 12 + self.length) + command_id = struct.pack('!L', self.command_id) + sequence_id = struct.pack('!L', sequence_id) + return length + command_id + sequence_id + + def get_message(self, sequence_id): + return self.get_header(sequence_id) + self.body + + +class CMPPConnectRequestInstance(CMPPBaseRequestInstance): + def __init__(self, sp_id, sp_secret): + if len(sp_id) != 6: + raise ValueError(_("sp_id is 6 bits")) + + super().__init__() + + source_addr = sp_id.encode('utf-8') + sp_secret = sp_secret.encode('utf-8') + version = struct.pack('!B', 0x02) + timestamp = struct.pack('!L', int(self.get_now())) + authenticator_source = source_addr + 9 * b'\x00' + sp_secret + self.get_now().encode('utf-8') + auth_source_md5 = hashlib.md5(authenticator_source).digest() + self.body = source_addr + auth_source_md5 + version + timestamp + self.length = len(self.body) + self.command_id = CMPP_CONNECT + + @staticmethod + def get_now(): + return time.strftime('%m%d%H%M%S', time.localtime(time.time())) + + +class CMPPSubmitRequestInstance(CMPPBaseRequestInstance): + def __init__(self, msg_src, dest_terminal_id, msg_content, src_id, + service_id='', dest_usr_tl=1): + if len(msg_content) >= 70: + raise JMSException('The message length should be within 70 characters') + if len(dest_terminal_id) > 100: + raise JMSException('The number of users receiving information should be less than 100') + + super().__init__() + + msg_id = 8 * b'\x00' + pk_total = struct.pack('!B', 1) + pk_number = struct.pack('!B', 1) + registered_delivery = struct.pack('!B', 0) + msg_level = struct.pack('!B', 0) + service_id = ((10 - len(service_id)) * '\x00' + service_id).encode('utf-8') + fee_user_type = struct.pack('!B', 2) + fee_terminal_id = ('0' * 21).encode('utf-8') + tp_pid = struct.pack('!B', 0) + tp_udhi = struct.pack('!B', 0) + msg_fmt = struct.pack('!B', 8) + fee_type = '01'.encode('utf-8') + fee_code = '000000'.encode('utf-8') + valid_time = ('\x00' * 17).encode('utf-8') + at_time = ('\x00' * 17).encode('utf-8') + src_id = ((21 - len(src_id)) * '\x00' + src_id).encode('utf-8') + reserve = b'\x00' * 8 + _msg_length = struct.pack('!B', len(msg_content) * 2) + _msg_src = msg_src.encode('utf-8') + _dest_usr_tl = struct.pack('!B', dest_usr_tl) + _msg_content = msg_content.encode('utf-16-be') + _dest_terminal_id = b''.join([ + (i + (21 - len(i)) * '\x00').encode('utf-8') for i in dest_terminal_id + ]) + self.length = 126 + 21 * dest_usr_tl + len(_msg_content) + self.command_id = CMPP_SUBMIT + self.body = msg_id + pk_total + pk_number + registered_delivery \ + + msg_level + service_id + fee_user_type + fee_terminal_id \ + + tp_pid + tp_udhi + msg_fmt + _msg_src + fee_type + fee_code \ + + valid_time + at_time + src_id + _dest_usr_tl + _dest_terminal_id \ + + _msg_length + _msg_content + reserve + + +class CMPPTerminateRequestInstance(CMPPBaseRequestInstance): + def __init__(self): + super().__init__() + self.body = b'' + self.command_id = CMPP_TERMINATE + + +class CMPPDeliverRespRequestInstance(CMPPBaseRequestInstance): + def __init__(self, msg_id, result=0): + super().__init__() + msg_id = struct.pack('!Q', msg_id) + result = struct.pack('!B', result) + self.length = len(self.body) + self.body = msg_id + result + + +class CMPPResponseInstance(object): + def __init__(self): + self.command_id = None + self.length = None + self.response_handler_map = { + CMPP_CONNECT_RESP: self.connect_response_parse, + CMPP_SUBMIT_RESP: self.submit_response_parse, + CMPP_DELIVER: self.deliver_request_parse, + } + + @staticmethod + def connect_response_parse(body): + status, = struct.unpack('!B', body[0:1]) + authenticator_ISMG = body[1:17] + version, = struct.unpack('!B', body[17:18]) + return { + 'Status': status, + 'AuthenticatorISMG': authenticator_ISMG, + 'Version': version + } + + @staticmethod + def submit_response_parse(body): + msg_id = body[:8] + result = struct.unpack('!B', body[8:9]) + return { + 'Msg_Id': msg_id, 'Result': result[0] + } + + @staticmethod + def deliver_request_parse(body): + msg_id, = struct.unpack('!Q', body[0:8]) + dest_id = body[8:29] + service_id = body[29:39] + tp_pid = struct.unpack('!B', body[39:40]) + tp_udhi = struct.unpack('!B', body[40:41]) + msg_fmt = struct.unpack('!B', body[41:42]) + src_terminal_id = body[42:63] + registered_delivery = struct.unpack('!B', body[63:64]) + msg_length = struct.unpack('!B', body[64:65]) + msg_content = body[65:msg_length[0]+65] + return { + 'Msg_Id': msg_id, 'Dest_Id': dest_id, 'Service_Id': service_id, + 'TP_pid': tp_pid, 'TP_udhi': tp_udhi, 'Msg_Fmt': msg_fmt, + 'Src_terminal_Id': src_terminal_id, 'Registered_Delivery': registered_delivery, + 'Msg_Length': msg_length, 'Msg_content': msg_content + } + + def parse_header(self, data): + self.command_id, = struct.unpack('!L', data[4:8]) + sequence_id, = struct.unpack('!L', data[8:12]) + return { + 'length': self.length, + 'command_id': hex(self.command_id), + 'sequence_id': sequence_id + } + + def parse_body(self, body): + response_body_func = self.response_handler_map.get(self.command_id) + if response_body_func is None: + raise JMSException('Unable to parse the returned result: %s' % body) + return response_body_func(body) + + def parse(self, data): + self.length, = struct.unpack('!L', data[0:4]) + header = self.parse_header(data) + body = self.parse_body(data[12:self.length]) + return header, body + + +class CMPPClient(object): + def __init__(self, host, port, sp_id, sp_secret, src_id, service_id): + self.ip = host + self.port = port + self.sp_id = sp_id + self.sp_secret = sp_secret + self.src_id = src_id + self.service_id = service_id + self._sequence_id = 0 + self._is_connect = False + self._times = 3 + self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._connect() + + @property + def sequence_id(self): + s = self._sequence_id + self._sequence_id += 1 + return s + + def _connect(self): + self.__socket.settimeout(5) + error_msg = _('Failed to connect to the CMPP gateway server, err: {}') + for i in range(self._times): + try: + self.__socket.connect((self.ip, self.port)) + except Exception as err: + error_msg = error_msg.format(str(err)) + logger.warning(error_msg) + time.sleep(1) + else: + self._is_connect = True + break + else: + raise JMSException(error_msg) + + def send(self, instance): + if isinstance(instance, CMPPBaseRequestInstance): + message = instance.get_message(sequence_id=self.sequence_id) + else: + message = instance + self.__socket.send(message) + + def recv(self): + raw_length = self.__socket.recv(4) + length, = struct.unpack('!L', raw_length) + header, body = CMPPResponseInstance().parse( + raw_length + self.__socket.recv(length - 4) + ) + return header, body + + def close(self): + if self._is_connect: + terminate_request = CMPPTerminateRequestInstance() + self.send(terminate_request) + self.__socket.close() + + def _cmpp_connect(self): + connect_request = CMPPConnectRequestInstance(self.sp_id, self.sp_secret) + self.send(connect_request) + header, body = self.recv() + if body['Status'] != 0: + raise JMSException('CMPPv2.0 authentication failed: %s' % body) + + def _cmpp_send_sms(self, dest, sign_name, template_code, template_param): + """ + 优先发送template_param中message的信息 + 若该内容不存在,则根据template_code构建验证码发送 + """ + message = template_param.get('message') + if message is None: + code = template_param.get('code') + message = template_code.replace('{code}', code) + msg = '【%s】 %s' % (sign_name, message) + submit_request = CMPPSubmitRequestInstance( + msg_src=self.sp_id, src_id=self.src_id, msg_content=msg, + dest_usr_tl=len(dest), dest_terminal_id=dest, + service_id=self.service_id + ) + self.send(submit_request) + header, body = self.recv() + command_id = header.get('command_id') + if command_id == CMPP_DELIVER: + deliver_request = CMPPDeliverRespRequestInstance( + msg_id=body['Msg_Id'], result=body['Result'] + ) + self.send(deliver_request) + + def send_sms(self, dest, sign_name, template_code, template_param): + try: + self._cmpp_connect() + self._cmpp_send_sms(dest, sign_name, template_code, template_param) + except Exception as e: + logger.error('CMPPv2.0 Error: %s', e) + self.close() + raise JMSException(e) + + +class CMPP2SMS(BaseSMSClient): + SIGN_AND_TMPL_SETTING_FIELD_PREFIX = 'CMPP2' + + @classmethod + def new_from_settings(cls): + return cls( + host=settings.CMPP2_HOST, port=settings.CMPP2_PORT, + sp_id=settings.CMPP2_SP_ID, sp_secret=settings.CMPP2_SP_SECRET, + service_id=settings.CMPP2_SERVICE_ID, src_id=getattr(settings, 'CMPP2_SRC_ID', ''), + ) + + def __init__(self, host: str, port: int, sp_id: str, sp_secret: str, service_id: str, src_id=''): + try: + self.client = CMPPClient( + host=host, port=port, sp_id=sp_id, sp_secret=sp_secret, src_id=src_id, service_id=service_id + ) + except Exception as err: + self.client = None + logger.warning(err) + raise JMSException(err) + + @staticmethod + def need_pre_check(): + return False + + def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs): + try: + logger.info(f'CMPPv2.0 sms send: ' + f'phone_numbers={phone_numbers} ' + f'sign_name={sign_name} ' + f'template_code={template_code} ' + f'template_param={template_param}') + self.client.send_sms(phone_numbers, sign_name, template_code, template_param) + except Exception as e: + raise JMSException(e) + + +client = CMPP2SMS diff --git a/apps/common/sdk/sms/endpoint.py b/apps/common/sdk/sms/endpoint.py index 610bf2d99..3bcaa8559 100644 --- a/apps/common/sdk/sms/endpoint.py +++ b/apps/common/sdk/sms/endpoint.py @@ -15,6 +15,7 @@ logger = get_logger(__name__) class BACKENDS(TextChoices): ALIBABA = 'alibaba', _('Alibaba cloud') TENCENT = 'tencent', _('Tencent cloud') + CMPP2 = 'cmpp2', _('CMPP v2.0') class SMS: @@ -43,7 +44,7 @@ class SMS: sign_name = getattr(settings, f'{self.client.SIGN_AND_TMPL_SETTING_FIELD_PREFIX}_VERIFY_SIGN_NAME') template_code = getattr(settings, f'{self.client.SIGN_AND_TMPL_SETTING_FIELD_PREFIX}_VERIFY_TEMPLATE_CODE') - if not (sign_name and template_code): + if self.client.need_pre_check() and not (sign_name and template_code): raise JMSException( code='verify_code_sign_tmpl_invalid', detail=_('SMS verification code signature or template invalid') diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index 5b2180ec0..44920a339 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # import re +import socket +from django.templatetags.static import static from collections import OrderedDict from itertools import chain import logging @@ -365,3 +367,25 @@ def pretty_string(data: str, max_length=128, ellipsis_str='...'): def group_by_count(it, count): return [it[i:i+count] for i in range(0, len(it), count)] + + +def test_ip_connectivity(host, port, timeout=0.5): + """ + timeout: seconds + """ + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + result = sock.connect_ex((host, int(port))) + sock.close() + if result == 0: + connectivity = True + else: + connectivity = False + return connectivity + + +def static_or_direct(logo_path): + if logo_path.startswith('img/'): + return static(logo_path) + else: + return logo_path diff --git a/apps/common/utils/crypto.py b/apps/common/utils/crypto.py index 0c818ec5b..5943ad9e3 100644 --- a/apps/common/utils/crypto.py +++ b/apps/common/utils/crypto.py @@ -1,7 +1,7 @@ import base64 import logging +import re from Cryptodome.Cipher import AES, PKCS1_v1_5 -from Cryptodome.Util.Padding import pad from Cryptodome.Random import get_random_bytes from Cryptodome.PublicKey import RSA from Cryptodome import Random @@ -11,21 +11,25 @@ from django.conf import settings from django.core.exceptions import ImproperlyConfigured -def process_key(key): +secret_pattern = re.compile(r'password|secret|key|token', re.IGNORECASE) + + +def padding_key(key, max_length=32): """ 返回32 bytes 的key """ if not isinstance(key, bytes): key = bytes(key, encoding='utf-8') - if len(key) >= 32: - return key[:32] + if len(key) >= max_length: + return key[:max_length] - return pad(key, 32) + while len(key) % 16 != 0: + key += b'\0' + return key class BaseCrypto: - def encrypt(self, text): return base64.urlsafe_b64encode( self._encrypt(bytes(text, encoding='utf8')) @@ -45,7 +49,7 @@ class BaseCrypto: class GMSM4EcbCrypto(BaseCrypto): def __init__(self, key): - self.key = process_key(key) + self.key = padding_key(key, 16) self.sm4_encryptor = CryptSM4() self.sm4_encryptor.set_key(self.key, SM4_ENCRYPT) @@ -70,9 +74,8 @@ class AESCrypto: """ def __init__(self, key): - if len(key) > 32: - key = key[:32] - self.key = self.to_16(key) + self.key = padding_key(key, 32) + self.aes = AES.new(self.key, AES.MODE_ECB) @staticmethod def to_16(key): @@ -87,17 +90,15 @@ class AESCrypto: return key # 返回bytes def aes(self): - return AES.new(self.key, AES.MODE_ECB) # 初始化加密器 + return AES.new(self.key, AES.MODE_ECB) def encrypt(self, text): - aes = self.aes() - cipher = base64.encodebytes(aes.encrypt(self.to_16(text))) + cipher = base64.encodebytes(self.aes.encrypt(self.to_16(text))) return str(cipher, encoding='utf8').replace('\n', '') # 加密 def decrypt(self, text): - aes = self.aes() text_decoded = base64.decodebytes(bytes(text, encoding='utf8')) - return str(aes.decrypt(text_decoded).rstrip(b'\0').decode("utf8")) + return str(self.aes.decrypt(text_decoded).rstrip(b'\0').decode("utf8")) class AESCryptoGCM: @@ -106,7 +107,7 @@ class AESCryptoGCM: """ def __init__(self, key): - self.key = process_key(key) + self.key = padding_key(key) def encrypt(self, text): """ @@ -133,7 +134,6 @@ class AESCryptoGCM: nonce = base64.b64decode(metadata[24:48]) tag = base64.b64decode(metadata[48:]) ciphertext = base64.b64decode(text[72:]) - cipher = AES.new(self.key, AES.MODE_GCM, nonce=nonce) cipher.update(header) @@ -144,11 +144,10 @@ class AESCryptoGCM: def get_aes_crypto(key=None, mode='GCM'): if key is None: key = settings.SECRET_KEY - if mode == 'ECB': - a = AESCrypto(key) - elif mode == 'GCM': - a = AESCryptoGCM(key) - return a + if mode == 'GCM': + return AESCryptoGCM(key) + else: + return AESCrypto(key) def get_gm_sm4_ecb_crypto(key=None): @@ -162,34 +161,42 @@ gm_sm4_ecb_crypto = get_gm_sm4_ecb_crypto() class Crypto: - cryptoes = { + cryptor_map = { 'aes_ecb': aes_ecb_crypto, 'aes_gcm': aes_crypto, 'aes': aes_crypto, 'gm_sm4_ecb': gm_sm4_ecb_crypto, 'gm': gm_sm4_ecb_crypto, } + cryptos = [] def __init__(self): - cryptoes = self.__class__.cryptoes.copy() - crypto = cryptoes.pop(settings.SECURITY_DATA_CRYPTO_ALGO, None) - if crypto is None: + crypt_algo = settings.SECURITY_DATA_CRYPTO_ALGO + if not crypt_algo: + if settings.GMSSL_ENABLED: + crypt_algo = 'gm' + else: + crypt_algo = 'aes' + + cryptor = self.cryptor_map.get(crypt_algo, None) + if cryptor is None: raise ImproperlyConfigured( f'Crypto method not supported {settings.SECURITY_DATA_CRYPTO_ALGO}' ) - self.cryptoes = [crypto, *cryptoes.values()] + others = set(self.cryptor_map.values()) - {cryptor} + self.cryptos = [cryptor, *others] @property def encryptor(self): - return self.cryptoes[0] + return self.cryptos[0] def encrypt(self, text): return self.encryptor.encrypt(text) def decrypt(self, text): - for decryptor in self.cryptoes: + for cryptor in self.cryptos: try: - origin_text = decryptor.decrypt(text) + origin_text = cryptor.decrypt(text) if origin_text: # 有时不同算法解密不报错,但是返回空字符串 return origin_text @@ -255,6 +262,8 @@ def decrypt_password(value): if len(cipher) != 2: return value key_cipher, password_cipher = cipher + if not all([key_cipher, password_cipher]): + return value aes_key = rsa_decrypt_by_session_pkey(key_cipher) aes = get_aes_crypto(aes_key, 'ECB') try: diff --git a/apps/common/utils/encode.py b/apps/common/utils/encode.py index 4178e4a0d..2bf02ac4c 100644 --- a/apps/common/utils/encode.py +++ b/apps/common/utils/encode.py @@ -196,7 +196,8 @@ def encrypt_password(password, salt=None, algorithm='sha512'): return des_crypt.hash(password, salt=salt[:2]) support_algorithm = { - 'sha512': sha512, 'des': des + 'sha512': sha512, + 'des': des } if isinstance(algorithm, str): @@ -222,9 +223,6 @@ def ensure_last_char_is_ascii(data): remain = '' -secret_pattern = re.compile(r'password|secret|key', re.IGNORECASE) - - def data_to_json(data, sort_keys=True, indent=2, cls=None): if cls is None: cls = DjangoJSONEncoder diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 551076192..7b26b4ee2 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -15,18 +15,23 @@ import errno import json import yaml import copy +import base64 +import logging from importlib import import_module from urllib.parse import urljoin, urlparse +from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT from django.urls import reverse_lazy -from django.conf import settings from django.utils.translation import ugettext_lazy as _ + BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.dirname(BASE_DIR) XPACK_DIR = os.path.join(BASE_DIR, 'xpack') HAS_XPACK = os.path.isdir(XPACK_DIR) +logger = logging.getLogger('jumpserver.conf') + def import_string(dotted_path): try: @@ -39,9 +44,9 @@ def import_string(dotted_path): try: return getattr(module, class_name) except AttributeError as err: - raise ImportError('Module "%s" does not define a "%s" attribute/class' % ( - module_path, class_name) - ) from err + raise ImportError( + 'Module "%s" does not define a "%s" attribute/class' % + (module_path, class_name)) from err def is_absolute_uri(uri): @@ -80,6 +85,59 @@ class DoesNotExist(Exception): pass +class ConfigCrypto: + secret_keys = [ + 'SECRET_KEY', 'DB_PASSWORD', 'REDIS_PASSWORD', + ] + + def __init__(self, key): + self.safe_key = self.process_key(key) + self.sm4_encryptor = CryptSM4() + self.sm4_encryptor.set_key(self.safe_key, SM4_ENCRYPT) + + self.sm4_decryptor = CryptSM4() + self.sm4_decryptor.set_key(self.safe_key, SM4_DECRYPT) + + @staticmethod + def process_key(secret_encrypt_key): + key = secret_encrypt_key.encode() + if len(key) >= 16: + key = key[:16] + else: + key += b'\0' * (16 - len(key)) + return key + + def encrypt(self, data): + data = bytes(data, encoding='utf8') + return base64.b64encode(self.sm4_encryptor.crypt_ecb(data)).decode('utf8') + + def decrypt(self, data): + data = base64.urlsafe_b64decode(bytes(data, encoding='utf8')) + return self.sm4_decryptor.crypt_ecb(data).decode('utf8') + + def decrypt_if_need(self, value, item): + if item not in self.secret_keys: + return value + + try: + plaintext = self.decrypt(value) + if plaintext: + value = plaintext + except Exception as e: + logger.error('decrypt %s error: %s', item, e) + return value + + @classmethod + def get_secret_encryptor(cls): + # 使用 SM4 加密配置文件敏感信息 + # https://the-x.cn/cryptography/Sm4.aspx + secret_encrypt_key = os.environ.get('SECRET_ENCRYPT_KEY', '') + if not secret_encrypt_key: + return None + print('Info: Using SM4 to encrypt config secret value') + return cls(secret_encrypt_key) + + class Config(dict): """Works exactly like a dict but provides ways to fill it from files or special dictionaries. There are two common patterns to populate the @@ -160,7 +218,7 @@ class Config(dict): 'SESSION_COOKIE_DOMAIN': None, 'CSRF_COOKIE_DOMAIN': None, 'SESSION_COOKIE_NAME_PREFIX': None, - 'SESSION_COOKIE_AGE': 3600, + 'SESSION_COOKIE_AGE': 3600 * 24, 'SESSION_EXPIRE_AT_BROWSER_CLOSE': False, 'LOGIN_URL': reverse_lazy('authentication:login'), 'CONNECTION_TOKEN_EXPIRATION': 5 * 60, @@ -265,6 +323,22 @@ class Config(dict): 'AUTH_SAML2_PROVIDER_AUTHORIZATION_ENDPOINT': '/', 'AUTH_SAML2_AUTHENTICATION_FAILURE_REDIRECT_URI': '/', + # OAuth2 认证 + 'AUTH_OAUTH2': False, + 'AUTH_OAUTH2_LOGO_PATH': 'img/login_oauth2_logo.png', + 'AUTH_OAUTH2_PROVIDER': 'OAuth2', + 'AUTH_OAUTH2_ALWAYS_UPDATE_USER': True, + 'AUTH_OAUTH2_CLIENT_ID': 'client-id', + 'AUTH_OAUTH2_SCOPE': '', + 'AUTH_OAUTH2_CLIENT_SECRET': '', + 'AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT': 'https://oauth2.example.com/authorize', + 'AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT': 'https://oauth2.example.com/userinfo', + 'AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT': 'https://oauth2.example.com/access_token', + 'AUTH_OAUTH2_ACCESS_TOKEN_METHOD': 'GET', + 'AUTH_OAUTH2_USER_ATTR_MAP': { + 'name': 'name', 'username': 'username', 'email': 'email' + }, + 'AUTH_TEMP_TOKEN': False, # 企业微信 @@ -302,6 +376,15 @@ class Config(dict): 'TENCENT_VERIFY_SIGN_NAME': '', 'TENCENT_VERIFY_TEMPLATE_CODE': '', + 'CMPP2_HOST': '', + 'CMPP2_PORT': 7890, + 'CMPP2_SP_ID': '', + 'CMPP2_SP_SECRET': '', + 'CMPP2_SRC_ID': '', + 'CMPP2_SERVICE_ID': '', + 'CMPP2_VERIFY_SIGN_NAME': '', + 'CMPP2_VERIFY_TEMPLATE_CODE': '{code}', + # Email 'EMAIL_CUSTOM_USER_CREATED_SUBJECT': _('Create account successfully'), 'EMAIL_CUSTOM_USER_CREATED_HONORIFIC': _('Hello'), @@ -387,7 +470,8 @@ class Config(dict): 'SESSION_SAVE_EVERY_REQUEST': True, 'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False, 'SERVER_REPLAY_STORAGE': {}, - 'SECURITY_DATA_CRYPTO_ALGO': 'aes', + 'SECURITY_DATA_CRYPTO_ALGO': None, + 'GMSSL_ENABLED': False, # 记录清理清理 'LOGIN_LOG_KEEP_DAYS': 200, @@ -405,6 +489,7 @@ class Config(dict): 'CONNECTION_TOKEN_ENABLED': False, 'PERM_SINGLE_ASSET_TO_UNGROUP_NODE': False, + 'TICKET_AUTHORIZE_DEFAULT_TIME': 7, 'WINDOWS_SSH_DEFAULT_SHELL': 'cmd', 'PERIOD_TASK_ENABLED': True, @@ -416,6 +501,10 @@ class Config(dict): 'HEALTH_CHECK_TOKEN': '', } + def __init__(self, *args): + super().__init__(*args) + self.secret_encryptor = ConfigCrypto.get_secret_encryptor() + @staticmethod def convert_keycloak_to_openid(keycloak_config): """ @@ -427,7 +516,6 @@ class Config(dict): """ openid_config = copy.deepcopy(keycloak_config) - auth_openid = openid_config.get('AUTH_OPENID') auth_openid_realm_name = openid_config.get('AUTH_OPENID_REALM_NAME') auth_openid_server_url = openid_config.get('AUTH_OPENID_SERVER_URL') @@ -556,13 +644,12 @@ class Config(dict): def get(self, item): # 再从配置文件中获取 value = self.get_from_config(item) - if value is not None: - return value - # 其次从环境变量来 - value = self.get_from_env(item) - if value is not None: - return value - value = self.defaults.get(item) + if value is None: + value = self.get_from_env(item) + if value is None: + value = self.defaults.get(item) + if self.secret_encryptor: + value = self.secret_encryptor.decrypt_if_need(value, item) return value def __getitem__(self, item): diff --git a/apps/jumpserver/routing.py b/apps/jumpserver/routing.py index 1d1de2230..773baee99 100644 --- a/apps/jumpserver/routing.py +++ b/apps/jumpserver/routing.py @@ -4,10 +4,10 @@ from django.core.asgi import get_asgi_application from ops.urls.ws_urls import urlpatterns as ops_urlpatterns from notifications.urls.ws_urls import urlpatterns as notifications_urlpatterns +from settings.urls.ws_urls import urlpatterns as setting_urlpatterns urlpatterns = [] -urlpatterns += ops_urlpatterns \ - + notifications_urlpatterns +urlpatterns += ops_urlpatterns + notifications_urlpatterns + setting_urlpatterns application = ProtocolTypeRouter({ 'websocket': AuthMiddlewareStack( diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 95a12d01f..de43b03be 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -24,9 +24,15 @@ AUTH_LDAP_GLOBAL_OPTIONS = { ldap.OPT_X_TLS_REQUIRE_CERT: ldap.OPT_X_TLS_NEVER, ldap.OPT_REFERRALS: CONFIG.AUTH_LDAP_OPTIONS_OPT_REFERRALS } -LDAP_CERT_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_ca.pem") +LDAP_CACERT_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_ca.pem") +if os.path.isfile(LDAP_CACERT_FILE): + AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_CACERTFILE] = LDAP_CACERT_FILE +LDAP_CERT_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_cert.pem") if os.path.isfile(LDAP_CERT_FILE): - AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_CACERTFILE] = LDAP_CERT_FILE + AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_CERTFILE] = LDAP_CERT_FILE +LDAP_KEY_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_cert.key") +if os.path.isfile(LDAP_KEY_FILE): + AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_KEYFILE] = LDAP_KEY_FILE # AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU # AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER # AUTH_LDAP_GROUP_SEARCH = LDAPSearch( @@ -143,6 +149,23 @@ SAML2_SP_ADVANCED_SETTINGS = CONFIG.SAML2_SP_ADVANCED_SETTINGS SAML2_LOGIN_URL_NAME = "authentication:saml2:saml2-login" SAML2_LOGOUT_URL_NAME = "authentication:saml2:saml2-logout" +# OAuth2 auth +AUTH_OAUTH2 = CONFIG.AUTH_OAUTH2 +AUTH_OAUTH2_LOGO_PATH = CONFIG.AUTH_OAUTH2_LOGO_PATH +AUTH_OAUTH2_PROVIDER = CONFIG.AUTH_OAUTH2_PROVIDER +AUTH_OAUTH2_ALWAYS_UPDATE_USER = CONFIG.AUTH_OAUTH2_ALWAYS_UPDATE_USER +AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT = CONFIG.AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT +AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT = CONFIG.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT +AUTH_OAUTH2_ACCESS_TOKEN_METHOD = CONFIG.AUTH_OAUTH2_ACCESS_TOKEN_METHOD +AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT = CONFIG.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT +AUTH_OAUTH2_CLIENT_SECRET = CONFIG.AUTH_OAUTH2_CLIENT_SECRET +AUTH_OAUTH2_CLIENT_ID = CONFIG.AUTH_OAUTH2_CLIENT_ID +AUTH_OAUTH2_SCOPE = CONFIG.AUTH_OAUTH2_SCOPE +AUTH_OAUTH2_USER_ATTR_MAP = CONFIG.AUTH_OAUTH2_USER_ATTR_MAP +AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME = 'authentication:oauth2:login-callback' +AUTH_OAUTH2_AUTHENTICATION_REDIRECT_URI = '/' +AUTH_OAUTH2_AUTHENTICATION_FAILURE_REDIRECT_URI = '/' + # 临时 token AUTH_TEMP_TOKEN = CONFIG.AUTH_TEMP_TOKEN @@ -170,6 +193,7 @@ AUTH_BACKEND_DINGTALK = 'authentication.backends.sso.DingTalkAuthentication' AUTH_BACKEND_FEISHU = 'authentication.backends.sso.FeiShuAuthentication' AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.sso.AuthorizationTokenAuthentication' AUTH_BACKEND_SAML2 = 'authentication.backends.saml2.SAML2Backend' +AUTH_BACKEND_OAUTH2 = 'authentication.backends.oauth2.OAuth2Backend' AUTH_BACKEND_TEMP_TOKEN = 'authentication.backends.token.TempTokenAuthBackend' @@ -180,12 +204,14 @@ AUTHENTICATION_BACKENDS = [ AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY, AUTH_BACKEND_LDAP, AUTH_BACKEND_RADIUS, # 跳转形式 AUTH_BACKEND_CAS, AUTH_BACKEND_OIDC_PASSWORD, AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_SAML2, + AUTH_BACKEND_OAUTH2, # 扫码模式 AUTH_BACKEND_WECOM, AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU, # Token模式 AUTH_BACKEND_AUTH_TOKEN, AUTH_BACKEND_SSO, AUTH_BACKEND_TEMP_TOKEN ] +AUTHENTICATION_BACKENDS_THIRD_PARTY = [AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_CAS, AUTH_BACKEND_SAML2, AUTH_BACKEND_OAUTH2] ONLY_ALLOW_EXIST_USER_AUTH = CONFIG.ONLY_ALLOW_EXIST_USER_AUTH ONLY_ALLOW_AUTH_FROM_SOURCE = CONFIG.ONLY_ALLOW_AUTH_FROM_SOURCE diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index 519623ac9..a65266a1f 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -43,6 +43,9 @@ DEBUG_DEV = CONFIG.DEBUG_DEV # Absolute url for some case, for example email link SITE_URL = CONFIG.SITE_URL +# https://docs.djangoproject.com/en/4.1/ref/settings/ +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + # LOG LEVEL LOG_LEVEL = CONFIG.LOG_LEVEL @@ -106,6 +109,7 @@ MIDDLEWARE = [ 'authentication.backends.oidc.middleware.OIDCRefreshIDTokenMiddleware', 'authentication.backends.cas.middleware.CASMiddleware', 'authentication.middleware.MFAMiddleware', + 'authentication.middleware.ThirdPartyLoginMiddleware', 'authentication.middleware.SessionCookieMiddleware', 'simple_history.middleware.HistoryRequestMiddleware', ] @@ -307,6 +311,21 @@ CSRF_COOKIE_SECURE = CONFIG.CSRF_COOKIE_SECURE DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +PASSWORD_HASHERS = [ + 'django.contrib.auth.hashers.PBKDF2PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', + 'django.contrib.auth.hashers.Argon2PasswordHasher', + 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', +] + + +GMSSL_ENABLED = CONFIG.GMSSL_ENABLED +GM_HASHER = 'common.hashers.PBKDF2SM3PasswordHasher' +if GMSSL_ENABLED: + PASSWORD_HASHERS.insert(0, GM_HASHER) +else: + PASSWORD_HASHERS.append(GM_HASHER) + # For Debug toolbar INTERNAL_IPS = ["127.0.0.1"] if os.environ.get('DEBUG_TOOLBAR', False): @@ -315,3 +334,4 @@ if os.environ.get('DEBUG_TOOLBAR', False): DEBUG_TOOLBAR_PANELS = [ 'debug_toolbar.panels.profiling.ProfilingPanel', ] + diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index b6f6e9863..0bcdc77d9 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -85,6 +85,7 @@ TERMINAL_TELNET_REGEX = CONFIG.TERMINAL_TELNET_REGEX BACKEND_ASSET_USER_AUTH_VAULT = False PERM_SINGLE_ASSET_TO_UNGROUP_NODE = CONFIG.PERM_SINGLE_ASSET_TO_UNGROUP_NODE +TICKET_AUTHORIZE_DEFAULT_TIME = CONFIG.TICKET_AUTHORIZE_DEFAULT_TIME PERM_EXPIRED_CHECK_PERIODIC = CONFIG.PERM_EXPIRED_CHECK_PERIODIC WINDOWS_SSH_DEFAULT_SHELL = CONFIG.WINDOWS_SSH_DEFAULT_SHELL FLOWER_URL = CONFIG.FLOWER_URL diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo index 7dbdfa16d..66532110b 100644 --- a/apps/locale/ja/LC_MESSAGES/django.mo +++ b/apps/locale/ja/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f2fdd3a7bd34a26d068fc6ce521d0ea9983c477b13536ba3f51700a554d4ae3 -size 128706 +oid sha256:62879a50d21ad41c43bf748f6045bf30a933d5d08021d97dc1c68e23f6bf8e65 +size 130015 diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po index 2d5151877..655e05db0 100644 --- a/apps/locale/ja/LC_MESSAGES/django.po +++ b/apps/locale/ja/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-07-20 13:51+0800\n" +"POT-Creation-Date: 2022-08-10 19:03+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -27,12 +27,12 @@ msgstr "Acls" #: assets/models/base.py:175 assets/models/cluster.py:18 #: assets/models/cmd_filter.py:27 assets/models/domain.py:23 #: assets/models/group.py:20 assets/models/label.py:18 ops/mixin.py:24 -#: orgs/models.py:65 perms/models/base.py:83 rbac/models/role.py:29 -#: settings/models.py:29 settings/serializers/sms.py:6 +#: orgs/models.py:70 perms/models/base.py:83 rbac/models/role.py:29 +#: settings/models.py:33 settings/serializers/sms.py:6 #: terminal/models/endpoint.py:10 terminal/models/endpoint.py:86 #: terminal/models/storage.py:26 terminal/models/task.py:16 #: terminal/models/terminal.py:100 users/forms/profile.py:33 -#: users/models/group.py:15 users/models/user.py:661 +#: users/models/group.py:15 users/models/user.py:665 #: xpack/plugins/cloud/models.py:28 msgid "Name" msgstr "名前" @@ -59,12 +59,12 @@ msgstr "アクティブ" #: assets/models/cluster.py:29 assets/models/cmd_filter.py:48 #: assets/models/cmd_filter.py:96 assets/models/domain.py:24 #: assets/models/domain.py:65 assets/models/group.py:23 -#: assets/models/label.py:23 ops/models/adhoc.py:38 orgs/models.py:68 -#: perms/models/base.py:93 rbac/models/role.py:37 settings/models.py:34 +#: assets/models/label.py:23 ops/models/adhoc.py:38 orgs/models.py:73 +#: perms/models/base.py:93 rbac/models/role.py:37 settings/models.py:38 #: terminal/models/endpoint.py:23 terminal/models/endpoint.py:96 #: terminal/models/storage.py:29 terminal/models/terminal.py:114 #: tickets/models/comment.py:32 tickets/models/ticket/general.py:288 -#: users/models/group.py:16 users/models/user.py:698 +#: users/models/group.py:16 users/models/user.py:702 #: xpack/plugins/change_auth_plan/models/base.py:44 #: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:116 #: xpack/plugins/gathered_user/models.py:26 @@ -80,7 +80,7 @@ msgstr "拒否" msgid "Allow" msgstr "許可" -#: acls/models/login_acl.py:20 acls/models/login_acl.py:104 +#: acls/models/login_acl.py:20 acls/models/login_acl.py:75 #: acls/models/login_asset_acl.py:17 tickets/const.py:9 msgid "Login confirm" msgstr "ログイン確認" @@ -88,13 +88,13 @@ msgstr "ログイン確認" #: acls/models/login_acl.py:24 acls/models/login_asset_acl.py:20 #: assets/models/cmd_filter.py:30 assets/models/label.py:15 audits/models.py:37 #: audits/models.py:62 audits/models.py:87 audits/serializers.py:100 -#: authentication/models.py:54 authentication/models.py:78 orgs/models.py:215 +#: authentication/models.py:54 authentication/models.py:78 orgs/models.py:220 #: perms/models/base.py:84 rbac/builtin.py:120 rbac/models/rolebinding.py:41 #: terminal/backends/command/models.py:20 #: terminal/backends/command/serializers.py:13 terminal/models/session.py:44 #: terminal/models/sharing.py:33 terminal/notifications.py:91 #: terminal/notifications.py:139 tickets/models/comment.py:21 users/const.py:14 -#: users/models/user.py:890 users/models/user.py:921 +#: users/models/user.py:894 users/models/user.py:925 #: users/serializers/group.py:19 msgid "User" msgstr "ユーザー" @@ -160,11 +160,11 @@ msgstr "コンマ区切り文字列の形式。* はすべて一致すること #: authentication/models.py:260 #: authentication/templates/authentication/_msg_different_city.html:9 #: authentication/templates/authentication/_msg_oauth_bind.html:9 -#: ops/models/adhoc.py:159 users/forms/profile.py:32 users/models/user.py:659 +#: ops/models/adhoc.py:159 users/forms/profile.py:32 users/models/user.py:663 #: users/templates/users/_msg_user_created.html:12 #: xpack/plugins/change_auth_plan/models/asset.py:34 #: xpack/plugins/change_auth_plan/models/asset.py:195 -#: xpack/plugins/cloud/serializers/account_attrs.py:24 +#: xpack/plugins/cloud/serializers/account_attrs.py:26 msgid "Username" msgstr "ユーザー名" @@ -304,7 +304,7 @@ msgstr "アプリケーションアカウントの秘密を変更できます" #: applications/serializers/application.py:99 assets/models/label.py:21 #: perms/models/application_permission.py:21 #: perms/serializers/application/user_permission.py:33 -#: tickets/models/ticket/apply_application.py:14 +#: tickets/models/ticket/apply_application.py:15 #: xpack/plugins/change_auth_plan/models/app.py:25 msgid "Category" msgstr "カテゴリ" @@ -316,7 +316,7 @@ msgstr "カテゴリ" #: perms/serializers/application/user_permission.py:34 #: terminal/models/storage.py:58 terminal/models/storage.py:143 #: tickets/models/comment.py:26 tickets/models/flow.py:57 -#: tickets/models/ticket/apply_application.py:17 +#: tickets/models/ticket/apply_application.py:18 #: tickets/models/ticket/general.py:273 #: xpack/plugins/change_auth_plan/models/app.py:28 #: xpack/plugins/change_auth_plan/models/app.py:153 @@ -329,7 +329,7 @@ msgid "Domain" msgstr "ドメイン" #: applications/models/application.py:231 xpack/plugins/cloud/models.py:33 -#: xpack/plugins/cloud/serializers/account.py:60 +#: xpack/plugins/cloud/serializers/account.py:61 msgid "Attrs" msgstr "ツールバーの" @@ -363,8 +363,8 @@ msgstr "タイプ表示" #: assets/serializers/account.py:18 assets/serializers/cmd_filter.py:28 #: assets/serializers/cmd_filter.py:48 common/db/models.py:114 #: common/mixins/models.py:50 ops/models/adhoc.py:39 ops/models/command.py:30 -#: orgs/models.py:67 orgs/models.py:218 perms/models/base.py:92 -#: users/models/group.py:18 users/models/user.py:922 +#: orgs/models.py:72 orgs/models.py:223 perms/models/base.py:92 +#: users/models/group.py:18 users/models/user.py:926 #: xpack/plugins/cloud/models.py:125 msgid "Date created" msgstr "作成された日付" @@ -373,7 +373,7 @@ msgstr "作成された日付" #: assets/models/gathered_user.py:20 assets/serializers/account.py:21 #: assets/serializers/cmd_filter.py:29 assets/serializers/cmd_filter.py:49 #: common/db/models.py:115 common/mixins/models.py:51 ops/models/adhoc.py:40 -#: orgs/models.py:219 +#: orgs/models.py:224 msgid "Date updated" msgstr "更新日" @@ -393,8 +393,8 @@ msgstr "クラスター" #: applications/serializers/attrs/application_category/db.py:11 #: ops/models/adhoc.py:157 settings/serializers/auth/radius.py:14 -#: terminal/models/endpoint.py:11 -#: xpack/plugins/cloud/serializers/account_attrs.py:70 +#: settings/serializers/auth/sms.py:52 terminal/models/endpoint.py:11 +#: xpack/plugins/cloud/serializers/account_attrs.py:72 msgid "Host" msgstr "ホスト" @@ -407,8 +407,8 @@ msgstr "ホスト" #: applications/serializers/attrs/application_type/redis.py:10 #: applications/serializers/attrs/application_type/sqlserver.py:10 #: assets/models/asset.py:214 assets/models/domain.py:62 -#: settings/serializers/auth/radius.py:15 -#: xpack/plugins/cloud/serializers/account_attrs.py:71 +#: settings/serializers/auth/radius.py:15 settings/serializers/auth/sms.py:53 +#: xpack/plugins/cloud/serializers/account_attrs.py:73 msgid "Port" msgstr "ポート" @@ -425,13 +425,13 @@ msgstr "アプリケーションパス" #: applications/serializers/attrs/application_category/remote_app.py:44 #: assets/serializers/system_user.py:167 -#: tickets/serializers/ticket/apply_application.py:35 +#: tickets/serializers/ticket/apply_application.py:38 #: tickets/serializers/ticket/common.py:59 #: xpack/plugins/change_auth_plan/serializers/asset.py:67 #: xpack/plugins/change_auth_plan/serializers/asset.py:70 #: xpack/plugins/change_auth_plan/serializers/asset.py:73 #: xpack/plugins/change_auth_plan/serializers/asset.py:104 -#: xpack/plugins/cloud/serializers/account_attrs.py:54 +#: xpack/plugins/cloud/serializers/account_attrs.py:56 msgid "This field is required." msgstr "このフィールドは必須です。" @@ -529,6 +529,7 @@ msgstr "内部" #: assets/models/asset.py:162 assets/models/asset.py:216 #: assets/serializers/account.py:15 assets/serializers/asset.py:63 #: perms/serializers/asset/user_permission.py:43 +#: xpack/plugins/cloud/serializers/account_attrs.py:162 msgid "Platform" msgstr "プラットフォーム" @@ -627,8 +628,8 @@ msgstr "ラベル" #: assets/models/asset.py:229 assets/models/base.py:183 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:52 #: assets/models/cmd_filter.py:99 assets/models/group.py:21 -#: common/db/models.py:112 common/mixins/models.py:49 orgs/models.py:66 -#: orgs/models.py:220 perms/models/base.py:91 users/models/user.py:706 +#: common/db/models.py:112 common/mixins/models.py:49 orgs/models.py:71 +#: orgs/models.py:225 perms/models/base.py:91 users/models/user.py:710 #: users/serializers/group.py:33 #: xpack/plugins/change_auth_plan/models/base.py:48 #: xpack/plugins/cloud/models.py:122 xpack/plugins/gathered_user/models.py:30 @@ -711,7 +712,7 @@ msgstr "タイミングトリガー" #: assets/models/backup.py:105 audits/models.py:44 ops/models/command.py:31 #: perms/models/base.py:89 terminal/models/session.py:58 -#: tickets/models/ticket/apply_application.py:25 +#: tickets/models/ticket/apply_application.py:29 #: tickets/models/ticket/apply_asset.py:23 #: xpack/plugins/change_auth_plan/models/base.py:112 #: xpack/plugins/change_auth_plan/models/base.py:203 @@ -773,7 +774,7 @@ msgstr "OK" #: assets/models/base.py:32 audits/models.py:118 #: xpack/plugins/change_auth_plan/serializers/app.py:88 #: xpack/plugins/change_auth_plan/serializers/asset.py:199 -#: xpack/plugins/cloud/const.py:32 +#: xpack/plugins/cloud/const.py:33 msgid "Failed" msgstr "失敗しました" @@ -799,7 +800,7 @@ msgstr "確認済みの日付" #: xpack/plugins/change_auth_plan/models/base.py:196 #: xpack/plugins/change_auth_plan/serializers/base.py:21 #: xpack/plugins/change_auth_plan/serializers/base.py:73 -#: xpack/plugins/cloud/serializers/account_attrs.py:26 +#: xpack/plugins/cloud/serializers/account_attrs.py:28 msgid "Password" msgstr "パスワード" @@ -824,7 +825,7 @@ msgstr "帯域幅" msgid "Contact" msgstr "連絡先" -#: assets/models/cluster.py:22 users/models/user.py:681 +#: assets/models/cluster.py:22 users/models/user.py:685 msgid "Phone" msgstr "電話" @@ -850,7 +851,7 @@ msgid "Default" msgstr "デフォルト" #: assets/models/cluster.py:36 assets/models/label.py:14 rbac/const.py:6 -#: users/models/user.py:907 +#: users/models/user.py:911 msgid "System" msgstr "システム" @@ -859,7 +860,7 @@ msgid "Default Cluster" msgstr "デフォルトクラスター" #: assets/models/cmd_filter.py:34 perms/models/base.py:86 -#: users/models/group.py:31 users/models/user.py:667 +#: users/models/group.py:31 users/models/user.py:671 msgid "User group" msgstr "ユーザーグループ" @@ -907,11 +908,11 @@ msgstr "家を無視する" msgid "Command filter rule" msgstr "コマンドフィルタルール" -#: assets/models/cmd_filter.py:144 +#: assets/models/cmd_filter.py:147 msgid "The generated regular expression is incorrect: {}" msgstr "生成された正規表現が正しくありません: {}" -#: assets/models/cmd_filter.py:170 tickets/const.py:13 +#: assets/models/cmd_filter.py:173 tickets/const.py:13 msgid "Command confirm" msgstr "コマンドの確認" @@ -928,7 +929,8 @@ msgstr "テストゲートウェイ" msgid "Unable to connect to port {port} on {ip}" msgstr "{ip} でポート {port} に接続できません" -#: assets/models/domain.py:134 xpack/plugins/cloud/providers/fc.py:48 +#: assets/models/domain.py:134 authentication/middleware.py:75 +#: xpack/plugins/cloud/providers/fc.py:48 msgid "Authentication failed" msgstr "認証に失敗しました" @@ -960,7 +962,7 @@ msgstr "資産グループ" msgid "Default asset group" msgstr "デフォルトアセットグループ" -#: assets/models/label.py:19 assets/models/node.py:546 settings/models.py:30 +#: assets/models/label.py:19 assets/models/node.py:546 settings/models.py:34 msgid "Value" msgstr "値" @@ -1141,6 +1143,7 @@ msgstr "CPU情報" #: perms/serializers/application/permission.py:42 #: perms/serializers/asset/permission.py:18 #: perms/serializers/asset/permission.py:46 +#: tickets/models/ticket/apply_application.py:27 #: tickets/models/ticket/apply_asset.py:21 msgid "Actions" msgstr "アクション" @@ -1156,7 +1159,7 @@ msgstr "定期的なパフォーマンス" msgid "Currently only mail sending is supported" msgstr "現在、メール送信のみがサポートされています" -#: assets/serializers/base.py:16 users/models/user.py:689 +#: assets/serializers/base.py:16 users/models/user.py:693 msgid "Private key" msgstr "ssh秘密鍵" @@ -1506,7 +1509,7 @@ msgstr "パスワード変更ログ" msgid "Disabled" msgstr "無効" -#: audits/models.py:112 settings/models.py:33 +#: audits/models.py:112 settings/models.py:37 msgid "Enabled" msgstr "有効化" @@ -1534,7 +1537,7 @@ msgstr "ユーザーエージェント" #: audits/models.py:126 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 -#: users/forms/profile.py:65 users/models/user.py:684 +#: users/forms/profile.py:65 users/models/user.py:688 #: users/serializers/profile.py:126 msgid "MFA" msgstr "MFA" @@ -1612,20 +1615,20 @@ msgid "Auth Token" msgstr "認証トークン" #: audits/signal_handlers.py:53 authentication/notifications.py:73 -#: authentication/views/login.py:67 authentication/views/wecom.py:178 -#: notifications/backends/__init__.py:11 users/models/user.py:720 +#: authentication/views/login.py:73 authentication/views/wecom.py:178 +#: notifications/backends/__init__.py:11 users/models/user.py:724 msgid "WeCom" msgstr "企業微信" #: audits/signal_handlers.py:54 authentication/views/feishu.py:144 -#: authentication/views/login.py:79 notifications/backends/__init__.py:14 -#: users/models/user.py:722 +#: authentication/views/login.py:85 notifications/backends/__init__.py:14 +#: users/models/user.py:726 msgid "FeiShu" msgstr "本を飛ばす" #: audits/signal_handlers.py:55 authentication/views/dingtalk.py:179 -#: authentication/views/login.py:73 notifications/backends/__init__.py:12 -#: users/models/user.py:721 +#: authentication/views/login.py:79 notifications/backends/__init__.py:12 +#: users/models/user.py:725 msgid "DingTalk" msgstr "DingTalk" @@ -1866,6 +1869,10 @@ msgstr "" msgid "Invalid token or cache refreshed." msgstr "無効なトークンまたはキャッシュの更新。" +#: authentication/backends/oauth2/backends.py:155 authentication/models.py:158 +msgid "User invalid, disabled or expired" +msgstr "ユーザーが無効、無効、または期限切れです" + #: authentication/confirm/password.py:16 msgid "Authentication failed password incorrect" msgstr "認証に失敗しました (ユーザー名またはパスワードが正しくありません)" @@ -1916,7 +1923,7 @@ msgstr "Authバックエンドが一致しない" #: authentication/errors/const.py:28 msgid "ACL is not allowed" -msgstr "ACLは許可されません" +msgstr "ログイン アクセス制御は許可されません" #: authentication/errors/const.py:29 msgid "Only local users are allowed" @@ -1982,23 +1989,19 @@ msgstr "受け入れのためのログイン確認チケットを待つ" msgid "Login confirm ticket was {}" msgstr "ログイン確認チケットは {} でした" -#: authentication/errors/failed.py:145 -msgid "IP is not allowed" -msgstr "IPは許可されていません" +#: authentication/errors/failed.py:146 +msgid "Current IP and Time period is not allowed" +msgstr "現在の IP と期間はログインを許可されていません" -#: authentication/errors/failed.py:152 -msgid "Time Period is not allowed" -msgstr "期間は許可されていません" - -#: authentication/errors/failed.py:157 +#: authentication/errors/failed.py:151 msgid "Please enter MFA code" msgstr "MFAコードを入力してください" -#: authentication/errors/failed.py:162 +#: authentication/errors/failed.py:156 msgid "Please enter SMS code" msgstr "SMSコードを入力してください" -#: authentication/errors/failed.py:167 users/exceptions.py:15 +#: authentication/errors/failed.py:161 users/exceptions.py:15 msgid "Phone not set" msgstr "電話が設定されていない" @@ -2113,6 +2116,10 @@ msgstr "電話番号を設定して有効にする" msgid "Clear phone number to disable" msgstr "無効にする電話番号をクリアする" +#: authentication/middleware.py:76 settings/utils/ldap.py:652 +msgid "Authentication failed (before login check failed): {}" +msgstr "認証に失敗しました (ログインチェックが失敗する前): {}" + #: authentication/mixins.py:256 msgid "The MFA type ({}) is not enabled" msgstr "MFAタイプ ({}) が有効になっていない" @@ -2144,8 +2151,8 @@ msgid "Secret" msgstr "ひみつ" #: authentication/models.py:74 authentication/models.py:264 -#: perms/models/base.py:90 tickets/models/ticket/apply_application.py:26 -#: tickets/models/ticket/apply_asset.py:24 users/models/user.py:703 +#: perms/models/base.py:90 tickets/models/ticket/apply_application.py:30 +#: tickets/models/ticket/apply_asset.py:24 users/models/user.py:707 msgid "Date expired" msgstr "期限切れの日付" @@ -2169,10 +2176,6 @@ msgstr "接続トークンの有効期限: {}" msgid "User not exists" msgstr "ユーザーは存在しません" -#: authentication/models.py:158 -msgid "User invalid, disabled or expired" -msgstr "ユーザーが無効、無効、または期限切れです" - #: authentication/models.py:163 msgid "System user not exists" msgstr "システムユーザーが存在しません" @@ -2226,7 +2229,7 @@ msgstr "有効性" msgid "Expired time" msgstr "期限切れ時間" -#: authentication/serializers/connection_token.py:74 +#: authentication/serializers/connection_token.py:73 msgid "Asset or application required" msgstr "アセットまたはアプリが必要" @@ -2306,6 +2309,7 @@ msgid "Need MFA for view auth" msgstr "ビューオートのためにMFAが必要" #: authentication/templates/authentication/_mfa_confirm_modal.html:20 +#: authentication/templates/authentication/auth_fail_flash_message_standalone.html:37 #: templates/_modal.html:23 templates/flash_message_standalone.html:37 #: users/templates/users/user_password_verify.html:20 msgid "Confirm" @@ -2320,7 +2324,7 @@ msgstr "コードエラー" #: authentication/templates/authentication/_msg_reset_password.html:3 #: authentication/templates/authentication/_msg_rest_password_success.html:2 #: authentication/templates/authentication/_msg_rest_public_key_success.html:2 -#: jumpserver/conf.py:307 ops/tasks.py:145 ops/tasks.py:148 +#: jumpserver/conf.py:390 ops/tasks.py:145 ops/tasks.py:148 #: perms/templates/perms/_msg_item_permissions_expire.html:3 #: perms/templates/perms/_msg_permed_items_expire.html:3 #: tickets/templates/tickets/approve_check_password.html:33 @@ -2411,14 +2415,19 @@ msgstr "" "公開鍵の更新が開始されなかった場合、アカウントにセキュリティ上の問題がある可" "能性があります" +#: authentication/templates/authentication/auth_fail_flash_message_standalone.html:28 +#: templates/flash_message_standalone.html:28 tickets/const.py:20 +msgid "Cancel" +msgstr "キャンセル" + #: authentication/templates/authentication/login.html:221 msgid "Welcome back, please enter username and password to login" msgstr "" "おかえりなさい、ログインするためにユーザー名とパスワードを入力してください" #: authentication/templates/authentication/login.html:256 -#: users/templates/users/forgot_password.html:15 #: users/templates/users/forgot_password.html:16 +#: users/templates/users/forgot_password.html:17 msgid "Forgot password" msgstr "パスワードを忘れた" @@ -2533,19 +2542,19 @@ msgstr "本を飛ばすからユーザーを取得できませんでした" msgid "Please login with a password and then bind the FeiShu" msgstr "パスワードでログインしてから本を飛ばすをバインドしてください" -#: authentication/views/login.py:175 +#: authentication/views/login.py:181 msgid "Redirecting" msgstr "リダイレクト" -#: authentication/views/login.py:176 +#: authentication/views/login.py:182 msgid "Redirecting to {} authentication" msgstr "{} 認証へのリダイレクト" -#: authentication/views/login.py:199 +#: authentication/views/login.py:205 msgid "Please enable cookies and try again." msgstr "クッキーを有効にして、もう一度お試しください。" -#: authentication/views/login.py:301 +#: authentication/views/login.py:307 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -2553,15 +2562,15 @@ msgstr "" "{} 確認を待ちます。彼女/彼へのリンクをコピーすることもできます
\n" " このページを閉じないでください" -#: authentication/views/login.py:306 +#: authentication/views/login.py:312 msgid "No ticket found" msgstr "チケットが見つかりません" -#: authentication/views/login.py:340 +#: authentication/views/login.py:346 msgid "Logout success" msgstr "ログアウト成功" -#: authentication/views/login.py:341 +#: authentication/views/login.py:347 msgid "Logout success, return login page" msgstr "ログアウト成功、ログインページを返す" @@ -2717,6 +2726,14 @@ msgstr "企業微信エラー、システム管理者に連絡してください msgid "Signature does not match" msgstr "署名が一致しない" +#: common/sdk/sms/cmpp2.py:46 +msgid "sp_id is 6 bits" +msgstr "SP idは6ビット" + +#: common/sdk/sms/cmpp2.py:216 +msgid "Failed to connect to the CMPP gateway server, err: {}" +msgstr "接続ゲートウェイサーバエラー, 非: {}" + #: common/sdk/sms/endpoint.py:16 msgid "Alibaba cloud" msgstr "アリ雲" @@ -2725,11 +2742,15 @@ msgstr "アリ雲" msgid "Tencent cloud" msgstr "テンセント雲" -#: common/sdk/sms/endpoint.py:28 +#: common/sdk/sms/endpoint.py:18 +msgid "CMPP v2.0" +msgstr "CMPP v2.0" + +#: common/sdk/sms/endpoint.py:29 msgid "SMS provider not support: {}" msgstr "SMSプロバイダーはサポートしていません: {}" -#: common/sdk/sms/endpoint.py:49 +#: common/sdk/sms/endpoint.py:50 msgid "SMS verification code signature or template invalid" msgstr "SMS検証コードの署名またはテンプレートが無効" @@ -2745,9 +2766,9 @@ msgstr "確認コードが正しくありません" msgid "Please wait {} seconds before sending" msgstr "{} 秒待ってから送信してください" -#: common/utils/ip/geoip/utils.py:24 +#: common/utils/ip/geoip/utils.py:24 xpack/plugins/cloud/const.py:24 msgid "LAN" -msgstr "LAN" +msgstr "ローカルエリアネットワーク" #: common/utils/ip/geoip/utils.py:26 common/utils/ip/utils.py:78 msgid "Invalid ip" @@ -2765,11 +2786,11 @@ msgstr "特殊文字を含むべきではない" msgid "The mobile phone number format is incorrect" msgstr "携帯電話番号の形式が正しくありません" -#: jumpserver/conf.py:306 +#: jumpserver/conf.py:389 msgid "Create account successfully" msgstr "アカウントを正常に作成" -#: jumpserver/conf.py:308 +#: jumpserver/conf.py:391 msgid "Your account has been created successfully" msgstr "アカウントが正常に作成されました" @@ -2814,7 +2835,7 @@ msgid "Notifications" msgstr "通知" #: notifications/backends/__init__.py:10 users/forms/profile.py:102 -#: users/models/user.py:663 +#: users/models/user.py:667 msgid "Email" msgstr "メール" @@ -3028,27 +3049,27 @@ msgstr "組織のリソース ({}) は削除できません" msgid "App organizations" msgstr "アプリ組織" -#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:80 -#: orgs/models.py:212 rbac/const.py:7 rbac/models/rolebinding.py:48 +#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:85 +#: orgs/models.py:217 rbac/const.py:7 rbac/models/rolebinding.py:48 #: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:62 #: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:71 msgid "Organization" msgstr "組織" -#: orgs/models.py:74 +#: orgs/models.py:79 msgid "GLOBAL" msgstr "グローバル組織" -#: orgs/models.py:82 +#: orgs/models.py:87 msgid "Can view root org" msgstr "グローバル組織を表示できます" -#: orgs/models.py:83 +#: orgs/models.py:88 msgid "Can view all joined org" msgstr "参加しているすべての組織を表示できます" -#: orgs/models.py:217 rbac/models/role.py:46 rbac/models/rolebinding.py:44 -#: users/models/user.py:671 +#: orgs/models.py:222 rbac/models/role.py:46 rbac/models/rolebinding.py:44 +#: users/models/user.py:675 msgid "Role" msgstr "ロール" @@ -3136,27 +3157,32 @@ msgstr "クリップボードコピーペースト" msgid "From ticket" msgstr "チケットから" -#: perms/notifications.py:18 +#: perms/notifications.py:12 perms/notifications.py:44 +#: perms/notifications.py:88 perms/notifications.py:119 +msgid "today" +msgstr "今" + +#: perms/notifications.py:15 msgid "You permed assets is about to expire" msgstr "パーマ資産の有効期限が近づいています" -#: perms/notifications.py:23 +#: perms/notifications.py:20 msgid "permed assets" msgstr "パーマ資産" -#: perms/notifications.py:62 +#: perms/notifications.py:59 msgid "Asset permissions is about to expire" msgstr "資産権限の有効期限が近づいています" -#: perms/notifications.py:67 +#: perms/notifications.py:64 msgid "asset permissions of organization {}" msgstr "組織 {} の資産権限" -#: perms/notifications.py:94 +#: perms/notifications.py:91 msgid "Your permed applications is about to expire" msgstr "パーマアプリケーションの有効期限が近づいています" -#: perms/notifications.py:98 +#: perms/notifications.py:95 msgid "permed applications" msgstr "Permedアプリケーション" @@ -3321,6 +3347,7 @@ msgid "Permission" msgstr "権限" #: rbac/models/role.py:31 rbac/models/rolebinding.py:38 +#: settings/serializers/auth/oauth2.py:35 msgid "Scope" msgstr "スコープ" @@ -3399,7 +3426,7 @@ msgstr "ワークスペースビュー" msgid "Audit view" msgstr "監査ビュー" -#: rbac/tree.py:28 settings/models.py:140 +#: rbac/tree.py:28 settings/models.py:156 msgid "System setting" msgstr "システム設定" @@ -3459,13 +3486,8 @@ msgstr "権限ツリーの表示" msgid "Execute batch command" msgstr "バッチ実行コマンド" -#: settings/api/alibaba_sms.py:31 settings/api/tencent_sms.py:35 -msgid "test_phone is required" -msgstr "携帯番号をテストこのフィールドは必須です" - -#: settings/api/alibaba_sms.py:52 settings/api/dingtalk.py:31 -#: settings/api/feishu.py:36 settings/api/tencent_sms.py:57 -#: settings/api/wecom.py:37 +#: settings/api/dingtalk.py:31 settings/api/feishu.py:36 +#: settings/api/sms.py:131 settings/api/wecom.py:37 msgid "Test success" msgstr "テストの成功" @@ -3493,47 +3515,55 @@ msgstr "Ldapユーザーを取得するにはNone" msgid "Imported {} users successfully (Organization: {})" msgstr "{} 人のユーザーを正常にインポートしました (組織: {})" +#: settings/api/sms.py:113 +msgid "Invalid SMS platform" +msgstr "無効なショートメッセージプラットフォーム" + +#: settings/api/sms.py:119 +msgid "test_phone is required" +msgstr "携帯番号をテストこのフィールドは必須です" + #: settings/apps.py:7 msgid "Settings" msgstr "設定" -#: settings/models.py:142 +#: settings/models.py:158 msgid "Can change email setting" msgstr "メール設定を変更できます" -#: settings/models.py:143 +#: settings/models.py:159 msgid "Can change auth setting" msgstr "資格認定の設定" -#: settings/models.py:144 +#: settings/models.py:160 msgid "Can change system msg sub setting" msgstr "システムmsgサブ设定を変更できます" -#: settings/models.py:145 +#: settings/models.py:161 msgid "Can change sms setting" msgstr "Smsの設定を変えることができます" -#: settings/models.py:146 +#: settings/models.py:162 msgid "Can change security setting" msgstr "セキュリティ設定を変更できます" -#: settings/models.py:147 +#: settings/models.py:163 msgid "Can change clean setting" msgstr "きれいな設定を変えることができます" -#: settings/models.py:148 +#: settings/models.py:164 msgid "Can change interface setting" msgstr "インターフェイスの設定を変えることができます" -#: settings/models.py:149 +#: settings/models.py:165 msgid "Can change license setting" msgstr "ライセンス設定を変更できます" -#: settings/models.py:150 +#: settings/models.py:166 msgid "Can change terminal setting" msgstr "ターミナルの設定を変えることができます" -#: settings/models.py:151 +#: settings/models.py:167 msgid "Can change other setting" msgstr "他の設定を変えることができます" @@ -3646,7 +3676,8 @@ msgstr "ユーザー検索フィルター" msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)" msgstr "選択は (cnまたはuidまたはsAMAccountName)=%(user)s)" -#: settings/serializers/auth/ldap.py:57 settings/serializers/auth/oidc.py:36 +#: settings/serializers/auth/ldap.py:57 settings/serializers/auth/oauth2.py:51 +#: settings/serializers/auth/oidc.py:36 msgid "User attr map" msgstr "ユーザー属性マッピング" @@ -3670,23 +3701,52 @@ msgstr "ページサイズを検索" msgid "Enable LDAP auth" msgstr "LDAP認証の有効化" -#: settings/serializers/auth/oidc.py:15 -msgid "Base site url" -msgstr "ベースサイトのアドレス" +#: settings/serializers/auth/oauth2.py:20 +msgid "Enable OAuth2 Auth" +msgstr "OAuth2認証の有効化" -#: settings/serializers/auth/oidc.py:18 +#: settings/serializers/auth/oauth2.py:23 +msgid "Logo" +msgstr "アイコン" + +#: settings/serializers/auth/oauth2.py:26 +msgid "Service provider" +msgstr "サービスプロバイダー" + +#: settings/serializers/auth/oauth2.py:29 settings/serializers/auth/oidc.py:18 msgid "Client Id" msgstr "クライアントID" -#: settings/serializers/auth/oidc.py:21 -#: xpack/plugins/cloud/serializers/account_attrs.py:36 +#: settings/serializers/auth/oauth2.py:32 settings/serializers/auth/oidc.py:21 +#: xpack/plugins/cloud/serializers/account_attrs.py:38 msgid "Client Secret" msgstr "クライアント秘密" -#: settings/serializers/auth/oidc.py:29 +#: settings/serializers/auth/oauth2.py:38 settings/serializers/auth/oidc.py:62 +msgid "Provider auth endpoint" +msgstr "認証エンドポイントアドレス" + +#: settings/serializers/auth/oauth2.py:41 settings/serializers/auth/oidc.py:65 +msgid "Provider token endpoint" +msgstr "プロバイダートークンエンドポイント" + +#: settings/serializers/auth/oauth2.py:44 settings/serializers/auth/oidc.py:29 msgid "Client authentication method" msgstr "クライアント認証方式" +#: settings/serializers/auth/oauth2.py:48 settings/serializers/auth/oidc.py:71 +msgid "Provider userinfo endpoint" +msgstr "プロバイダーuserinfoエンドポイント" + +#: settings/serializers/auth/oauth2.py:54 settings/serializers/auth/oidc.py:92 +#: settings/serializers/auth/saml2.py:33 +msgid "Always update user" +msgstr "常にユーザーを更新" + +#: settings/serializers/auth/oidc.py:15 +msgid "Base site url" +msgstr "ベースサイトのアドレス" + #: settings/serializers/auth/oidc.py:31 msgid "Share session" msgstr "セッションの共有" @@ -3719,22 +3779,10 @@ msgstr "OIDC認証の有効化" msgid "Provider endpoint" msgstr "プロバイダーエンドポイント" -#: settings/serializers/auth/oidc.py:62 -msgid "Provider auth endpoint" -msgstr "認証エンドポイントアドレス" - -#: settings/serializers/auth/oidc.py:65 -msgid "Provider token endpoint" -msgstr "プロバイダートークンエンドポイント" - #: settings/serializers/auth/oidc.py:68 msgid "Provider jwks endpoint" msgstr "プロバイダーjwksエンドポイント" -#: settings/serializers/auth/oidc.py:71 -msgid "Provider userinfo endpoint" -msgstr "プロバイダーuserinfoエンドポイント" - #: settings/serializers/auth/oidc.py:74 msgid "Provider end session endpoint" msgstr "プロバイダーのセッション終了エンドポイント" @@ -3767,10 +3815,6 @@ msgstr "使用状態" msgid "Use nonce" msgstr "Nonceを使用" -#: settings/serializers/auth/oidc.py:92 settings/serializers/auth/saml2.py:33 -msgid "Always update user" -msgstr "常にユーザーを更新" - #: settings/serializers/auth/radius.py:13 msgid "Enable Radius Auth" msgstr "Radius認証の有効化" @@ -3803,42 +3847,84 @@ msgstr "SP プライベートキー" msgid "SP cert" msgstr "SP 証明書" -#: settings/serializers/auth/sms.py:11 +#: settings/serializers/auth/sms.py:14 msgid "Enable SMS" msgstr "SMSの有効化" -#: settings/serializers/auth/sms.py:13 -msgid "SMS provider" -msgstr "SMSプロバイダ" +#: settings/serializers/auth/sms.py:16 +msgid "SMS provider / Protocol" +msgstr "SMSプロバイダ / プロトコル" -#: settings/serializers/auth/sms.py:18 settings/serializers/auth/sms.py:36 -#: settings/serializers/auth/sms.py:44 settings/serializers/email.py:65 +#: settings/serializers/auth/sms.py:21 settings/serializers/auth/sms.py:39 +#: settings/serializers/auth/sms.py:47 settings/serializers/auth/sms.py:58 +#: settings/serializers/email.py:65 msgid "Signature" msgstr "署名" -#: settings/serializers/auth/sms.py:19 settings/serializers/auth/sms.py:37 -#: settings/serializers/auth/sms.py:45 +#: settings/serializers/auth/sms.py:22 settings/serializers/auth/sms.py:40 +#: settings/serializers/auth/sms.py:48 msgid "Template code" msgstr "テンプレートコード" -#: settings/serializers/auth/sms.py:23 +#: settings/serializers/auth/sms.py:26 msgid "Test phone" msgstr "テスト電話" -#: settings/serializers/auth/sso.py:12 +#: settings/serializers/auth/sms.py:54 +msgid "Enterprise code(SP id)" +msgstr "企業コード(SP id)" + +#: settings/serializers/auth/sms.py:55 +msgid "Shared secret(Shared secret)" +msgstr "パスワードを共有する(Shared secret)" + +#: settings/serializers/auth/sms.py:56 +msgid "Original number(Src id)" +msgstr "元の番号(Src id)" + +#: settings/serializers/auth/sms.py:57 +msgid "Business type(Service id)" +msgstr "ビジネス・タイプ(Service id)" + +#: settings/serializers/auth/sms.py:60 +msgid "Template" +msgstr "テンプレート" + +#: settings/serializers/auth/sms.py:61 +#, python-brace-format +msgid "" +"Template need contain {code} and Signature + template length does not exceed " +"67 words. For example, your verification code is {code}, which is valid for " +"5 minutes. Please do not disclose it to others." +msgstr "" +"テンプレートには{code}を含める必要があり、署名+テンプレートの長さは67ワード未" +"満です。たとえば、認証コードは{code}で、有効期間は5分です。他の人には言わない" +"でください。" + +#: settings/serializers/auth/sms.py:70 +#, python-brace-format +msgid "The template needs to contain {code}" +msgstr "テンプレートには{code}を含める必要があります" + +#: settings/serializers/auth/sms.py:73 +msgid "Signature + Template must not exceed 65 words" +msgstr "署名+テンプレートの長さは65文字以内" + +#: settings/serializers/auth/sso.py:11 msgid "Enable SSO auth" msgstr "SSO Token認証の有効化" -#: settings/serializers/auth/sso.py:13 +#: settings/serializers/auth/sso.py:12 msgid "Other service can using SSO token login to JumpServer without password" msgstr "" "他のサービスはパスワードなしでJumpServerへのSSOトークンログインを使用できます" -#: settings/serializers/auth/sso.py:16 +#: settings/serializers/auth/sso.py:15 msgid "SSO auth key TTL" msgstr "Token有効期間" -#: settings/serializers/auth/sso.py:16 +#: settings/serializers/auth/sso.py:15 +#: xpack/plugins/cloud/serializers/account_attrs.py:159 msgid "Unit: second" msgstr "単位: 秒" @@ -3904,7 +3990,7 @@ msgstr "ログインログは日数を保持します" #: settings/serializers/cleaning.py:10 settings/serializers/cleaning.py:14 #: settings/serializers/cleaning.py:18 settings/serializers/cleaning.py:22 -#: settings/serializers/cleaning.py:26 +#: settings/serializers/cleaning.py:26 settings/serializers/other.py:35 msgid "Unit: day" msgstr "単位: 日" @@ -4077,19 +4163,23 @@ msgstr "" "ノードが表示されないようにしますが、そのノードが許可されていないという質問に" "質問" -#: settings/serializers/other.py:34 +#: settings/serializers/other.py:35 +msgid "Ticket authorize default time" +msgstr "デフォルト製造オーダ承認時間" + +#: settings/serializers/other.py:39 msgid "Help Docs URL" msgstr "ドキュメントリンク" -#: settings/serializers/other.py:35 +#: settings/serializers/other.py:40 msgid "default: http://docs.jumpserver.org" msgstr "デフォルト: http://docs.jumpserver.org" -#: settings/serializers/other.py:39 +#: settings/serializers/other.py:44 msgid "Help Support URL" msgstr "サポートリンク" -#: settings/serializers/other.py:40 +#: settings/serializers/other.py:45 msgid "default: http://www.jumpserver.org/support/" msgstr "デフォルト: http://www.jumpserver.org/support/" @@ -4478,10 +4568,6 @@ msgstr "成功: {} 人のユーザーに一致" msgid "Authentication failed (configuration incorrect): {}" msgstr "認証に失敗しました (設定が正しくありません): {}" -#: settings/utils/ldap.py:652 -msgid "Authentication failed (before login check failed): {}" -msgstr "認証に失敗しました (ログインチェックが失敗する前): {}" - #: settings/utils/ldap.py:654 msgid "Authentication failed (username or password incorrect): {}" msgstr "認証に失敗しました (ユーザー名またはパスワードが正しくありません): {}" @@ -4652,10 +4738,6 @@ msgstr "確認コードが送信されました" msgid "Home page" msgstr "ホームページ" -#: templates/flash_message_standalone.html:28 tickets/const.py:20 -msgid "Cancel" -msgstr "キャンセル" - #: templates/resource_download.html:18 templates/resource_download.html:31 msgid "Client" msgstr "クライアント" @@ -4838,7 +4920,7 @@ msgstr "一括作成非サポート" msgid "Storage is invalid" msgstr "ストレージが無効です" -#: terminal/models/command.py:53 +#: terminal/models/command.py:66 msgid "Command record" msgstr "コマンドレコード" @@ -5134,12 +5216,12 @@ msgid "Bucket" msgstr "バケット" #: terminal/serializers/storage.py:30 -#: xpack/plugins/cloud/serializers/account_attrs.py:15 +#: xpack/plugins/cloud/serializers/account_attrs.py:17 msgid "Access key id" msgstr "アクセスキー" #: terminal/serializers/storage.py:34 -#: xpack/plugins/cloud/serializers/account_attrs.py:18 +#: xpack/plugins/cloud/serializers/account_attrs.py:20 msgid "Access key secret" msgstr "アクセスキーシークレット" @@ -5279,7 +5361,7 @@ msgstr "カスタムユーザー" msgid "Ticket already closed" msgstr "チケットはすでに閉じています" -#: tickets/handlers/apply_application.py:37 +#: tickets/handlers/apply_application.py:38 msgid "" "Created by the ticket, ticket title: {}, ticket applicant: {}, ticket " "processor: {}, ticket ID: {}" @@ -5365,16 +5447,16 @@ msgstr "チケットの流れ" msgid "Ticket session relation" msgstr "チケットセッションの関係" -#: tickets/models/ticket/apply_application.py:11 +#: tickets/models/ticket/apply_application.py:12 #: tickets/models/ticket/apply_asset.py:13 msgid "Permission name" msgstr "認可ルール名" -#: tickets/models/ticket/apply_application.py:20 +#: tickets/models/ticket/apply_application.py:21 msgid "Apply applications" msgstr "アプリケーションの適用" -#: tickets/models/ticket/apply_application.py:23 +#: tickets/models/ticket/apply_application.py:24 #: tickets/models/ticket/apply_asset.py:18 msgid "Apply system users" msgstr "システムユーザーの適用" @@ -5680,7 +5762,7 @@ msgstr "公開鍵は古いものと同じであってはなりません。" msgid "Not a valid ssh public key" msgstr "有効なssh公開鍵ではありません" -#: users/forms/profile.py:161 users/models/user.py:692 +#: users/forms/profile.py:161 users/models/user.py:696 msgid "Public key" msgstr "公開キー" @@ -5692,55 +5774,55 @@ msgstr "強制有効" msgid "Local" msgstr "ローカル" -#: users/models/user.py:673 users/serializers/user.py:149 +#: users/models/user.py:677 users/serializers/user.py:149 msgid "Is service account" msgstr "サービスアカウントです" -#: users/models/user.py:675 +#: users/models/user.py:679 msgid "Avatar" msgstr "アバター" -#: users/models/user.py:678 +#: users/models/user.py:682 msgid "Wechat" msgstr "微信" -#: users/models/user.py:695 +#: users/models/user.py:699 msgid "Secret key" msgstr "秘密キー" -#: users/models/user.py:711 +#: users/models/user.py:715 msgid "Source" msgstr "ソース" -#: users/models/user.py:715 +#: users/models/user.py:719 msgid "Date password last updated" msgstr "最終更新日パスワード" -#: users/models/user.py:718 +#: users/models/user.py:722 msgid "Need update password" msgstr "更新パスワードが必要" -#: users/models/user.py:892 +#: users/models/user.py:896 msgid "Can invite user" msgstr "ユーザーを招待できます" -#: users/models/user.py:893 +#: users/models/user.py:897 msgid "Can remove user" msgstr "ユーザーを削除できます" -#: users/models/user.py:894 +#: users/models/user.py:898 msgid "Can match user" msgstr "ユーザーに一致できます" -#: users/models/user.py:903 +#: users/models/user.py:907 msgid "Administrator" msgstr "管理者" -#: users/models/user.py:906 +#: users/models/user.py:910 msgid "Administrator is the super user of system" msgstr "管理者はシステムのスーパーユーザーです" -#: users/models/user.py:931 +#: users/models/user.py:935 msgid "User password history" msgstr "ユーザーパスワード履歴" @@ -5947,11 +6029,11 @@ msgstr "あなたのssh公開鍵はサイト管理者によってリセットさ msgid "click here to set your password" msgstr "ここをクリックしてパスワードを設定してください" -#: users/templates/users/forgot_password.html:23 +#: users/templates/users/forgot_password.html:24 msgid "Input your email, that will send a mail to your" msgstr "あなたのメールを入力し、それはあなたにメールを送信します" -#: users/templates/users/forgot_password.html:32 +#: users/templates/users/forgot_password.html:33 msgid "Submit" msgstr "送信" @@ -6394,31 +6476,31 @@ msgstr "谷歌雲" msgid "Fusion Compute" msgstr "" -#: xpack/plugins/cloud/const.py:27 +#: xpack/plugins/cloud/const.py:28 msgid "Instance name" msgstr "インスタンス名" -#: xpack/plugins/cloud/const.py:28 +#: xpack/plugins/cloud/const.py:29 msgid "Instance name and Partial IP" msgstr "インスタンス名と部分IP" -#: xpack/plugins/cloud/const.py:33 +#: xpack/plugins/cloud/const.py:34 msgid "Succeed" msgstr "成功" -#: xpack/plugins/cloud/const.py:37 +#: xpack/plugins/cloud/const.py:38 msgid "Unsync" msgstr "同期していません" -#: xpack/plugins/cloud/const.py:38 +#: xpack/plugins/cloud/const.py:39 msgid "New Sync" msgstr "新しい同期" -#: xpack/plugins/cloud/const.py:39 +#: xpack/plugins/cloud/const.py:40 msgid "Synced" msgstr "同期済み" -#: xpack/plugins/cloud/const.py:40 +#: xpack/plugins/cloud/const.py:41 msgid "Released" msgstr "リリース済み" @@ -6688,52 +6770,88 @@ msgstr "華南-広州-友好ユーザー環境" msgid "CN East-Suqian" msgstr "華東-宿遷" -#: xpack/plugins/cloud/serializers/account.py:61 +#: xpack/plugins/cloud/serializers/account.py:62 msgid "Validity display" msgstr "有効表示" -#: xpack/plugins/cloud/serializers/account.py:62 +#: xpack/plugins/cloud/serializers/account.py:63 msgid "Provider display" msgstr "プロバイダ表示" -#: xpack/plugins/cloud/serializers/account_attrs.py:33 +#: xpack/plugins/cloud/serializers/account_attrs.py:35 msgid "Client ID" msgstr "クライアントID" -#: xpack/plugins/cloud/serializers/account_attrs.py:39 +#: xpack/plugins/cloud/serializers/account_attrs.py:41 msgid "Tenant ID" msgstr "テナントID" -#: xpack/plugins/cloud/serializers/account_attrs.py:42 +#: xpack/plugins/cloud/serializers/account_attrs.py:44 msgid "Subscription ID" msgstr "サブスクリプションID" -#: xpack/plugins/cloud/serializers/account_attrs.py:93 -#: xpack/plugins/cloud/serializers/account_attrs.py:98 -#: xpack/plugins/cloud/serializers/account_attrs.py:122 +#: xpack/plugins/cloud/serializers/account_attrs.py:95 +#: xpack/plugins/cloud/serializers/account_attrs.py:100 +#: xpack/plugins/cloud/serializers/account_attrs.py:124 msgid "API Endpoint" msgstr "APIエンドポイント" -#: xpack/plugins/cloud/serializers/account_attrs.py:104 +#: xpack/plugins/cloud/serializers/account_attrs.py:106 msgid "Auth url" msgstr "認証アドレス" -#: xpack/plugins/cloud/serializers/account_attrs.py:105 +#: xpack/plugins/cloud/serializers/account_attrs.py:107 msgid "eg: http://openstack.example.com:5000/v3" msgstr "例えば: http://openstack.example.com:5000/v3" -#: xpack/plugins/cloud/serializers/account_attrs.py:108 +#: xpack/plugins/cloud/serializers/account_attrs.py:110 msgid "User domain" msgstr "ユーザードメイン" -#: xpack/plugins/cloud/serializers/account_attrs.py:115 +#: xpack/plugins/cloud/serializers/account_attrs.py:117 msgid "Service account key" msgstr "サービスアカウントキー" -#: xpack/plugins/cloud/serializers/account_attrs.py:116 +#: xpack/plugins/cloud/serializers/account_attrs.py:118 msgid "The file is in JSON format" msgstr "ファイルはJSON形式です。" +#: xpack/plugins/cloud/serializers/account_attrs.py:131 +msgid "IP address invalid `{}`, {}" +msgstr "IPアドレスが無効: '{}', {}" + +#: xpack/plugins/cloud/serializers/account_attrs.py:137 +msgid "" +"Format for comma-delimited string,Such as: 192.168.1.0/24, " +"10.0.0.0-10.0.0.255" +msgstr "形式はコンマ区切りの文字列です,例:192.168.1.0/24,10.0.0.0-10.0.0.255" + +#: xpack/plugins/cloud/serializers/account_attrs.py:141 +msgid "" +"The port is used to detect the validity of the IP address. When the " +"synchronization task is executed, only the valid IP address will be " +"synchronized.
If the port is 0, all IP addresses are valid." +msgstr "" +"このポートは、 IP アドレスの有効性を検出するために使用されます。同期タスクが" +"実行されると、有効な IP アドレスのみが同期されます。
ポートが0の場合、す" +"べてのIPアドレスが有効です。" + +#: xpack/plugins/cloud/serializers/account_attrs.py:149 +msgid "Hostname prefix" +msgstr "ホスト名プレフィックス" + +#: xpack/plugins/cloud/serializers/account_attrs.py:152 +msgid "IP segment" +msgstr "IP セグメント" + +#: xpack/plugins/cloud/serializers/account_attrs.py:156 +msgid "Test port" +msgstr "テストポート" + +#: xpack/plugins/cloud/serializers/account_attrs.py:159 +msgid "Test timeout" +msgstr "テストタイムアウト" + #: xpack/plugins/cloud/serializers/task.py:29 msgid "" "Only instances matching the IP range will be synced.
If the instance " @@ -6852,6 +6970,3 @@ msgstr "究極のエディション" #: xpack/plugins/license/models.py:77 msgid "Community edition" msgstr "コミュニティ版" - -#~ msgid "User cannot self-update fields: {}" -#~ msgstr "ユーザーは自分のフィールドを更新できません: {}" diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 33d7105d9..034a33cb1 100644 --- a/apps/locale/zh/LC_MESSAGES/django.mo +++ b/apps/locale/zh/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c2b13f7242beec8786179e03de895bd3e9d8d6392b74c2398409c1bfa33d9f8 -size 106088 +oid sha256:adb46a0b7afdddc1b8a399e0fc0a0926012308ddce0ff487b68cb6c209d74039 +size 107133 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 808ca2b08..d36c5f037 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-07-20 13:51+0800\n" +"POT-Creation-Date: 2022-08-10 19:03+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -26,12 +26,12 @@ msgstr "访问控制" #: assets/models/base.py:175 assets/models/cluster.py:18 #: assets/models/cmd_filter.py:27 assets/models/domain.py:23 #: assets/models/group.py:20 assets/models/label.py:18 ops/mixin.py:24 -#: orgs/models.py:65 perms/models/base.py:83 rbac/models/role.py:29 -#: settings/models.py:29 settings/serializers/sms.py:6 +#: orgs/models.py:70 perms/models/base.py:83 rbac/models/role.py:29 +#: settings/models.py:33 settings/serializers/sms.py:6 #: terminal/models/endpoint.py:10 terminal/models/endpoint.py:86 #: terminal/models/storage.py:26 terminal/models/task.py:16 #: terminal/models/terminal.py:100 users/forms/profile.py:33 -#: users/models/group.py:15 users/models/user.py:661 +#: users/models/group.py:15 users/models/user.py:665 #: xpack/plugins/cloud/models.py:28 msgid "Name" msgstr "名称" @@ -58,12 +58,12 @@ msgstr "激活中" #: assets/models/cluster.py:29 assets/models/cmd_filter.py:48 #: assets/models/cmd_filter.py:96 assets/models/domain.py:24 #: assets/models/domain.py:65 assets/models/group.py:23 -#: assets/models/label.py:23 ops/models/adhoc.py:38 orgs/models.py:68 -#: perms/models/base.py:93 rbac/models/role.py:37 settings/models.py:34 +#: assets/models/label.py:23 ops/models/adhoc.py:38 orgs/models.py:73 +#: perms/models/base.py:93 rbac/models/role.py:37 settings/models.py:38 #: terminal/models/endpoint.py:23 terminal/models/endpoint.py:96 #: terminal/models/storage.py:29 terminal/models/terminal.py:114 #: tickets/models/comment.py:32 tickets/models/ticket/general.py:288 -#: users/models/group.py:16 users/models/user.py:698 +#: users/models/group.py:16 users/models/user.py:702 #: xpack/plugins/change_auth_plan/models/base.py:44 #: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:116 #: xpack/plugins/gathered_user/models.py:26 @@ -79,7 +79,7 @@ msgstr "拒绝" msgid "Allow" msgstr "允许" -#: acls/models/login_acl.py:20 acls/models/login_acl.py:104 +#: acls/models/login_acl.py:20 acls/models/login_acl.py:75 #: acls/models/login_asset_acl.py:17 tickets/const.py:9 msgid "Login confirm" msgstr "登录复核" @@ -87,13 +87,13 @@ msgstr "登录复核" #: acls/models/login_acl.py:24 acls/models/login_asset_acl.py:20 #: assets/models/cmd_filter.py:30 assets/models/label.py:15 audits/models.py:37 #: audits/models.py:62 audits/models.py:87 audits/serializers.py:100 -#: authentication/models.py:54 authentication/models.py:78 orgs/models.py:215 +#: authentication/models.py:54 authentication/models.py:78 orgs/models.py:220 #: perms/models/base.py:84 rbac/builtin.py:120 rbac/models/rolebinding.py:41 #: terminal/backends/command/models.py:20 #: terminal/backends/command/serializers.py:13 terminal/models/session.py:44 #: terminal/models/sharing.py:33 terminal/notifications.py:91 #: terminal/notifications.py:139 tickets/models/comment.py:21 users/const.py:14 -#: users/models/user.py:890 users/models/user.py:921 +#: users/models/user.py:894 users/models/user.py:925 #: users/serializers/group.py:19 msgid "User" msgstr "用户" @@ -159,11 +159,11 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " #: authentication/models.py:260 #: authentication/templates/authentication/_msg_different_city.html:9 #: authentication/templates/authentication/_msg_oauth_bind.html:9 -#: ops/models/adhoc.py:159 users/forms/profile.py:32 users/models/user.py:659 +#: ops/models/adhoc.py:159 users/forms/profile.py:32 users/models/user.py:663 #: users/templates/users/_msg_user_created.html:12 #: xpack/plugins/change_auth_plan/models/asset.py:34 #: xpack/plugins/change_auth_plan/models/asset.py:195 -#: xpack/plugins/cloud/serializers/account_attrs.py:24 +#: xpack/plugins/cloud/serializers/account_attrs.py:26 msgid "Username" msgstr "用户名" @@ -299,7 +299,7 @@ msgstr "可以查看应用账号密码" #: applications/serializers/application.py:99 assets/models/label.py:21 #: perms/models/application_permission.py:21 #: perms/serializers/application/user_permission.py:33 -#: tickets/models/ticket/apply_application.py:14 +#: tickets/models/ticket/apply_application.py:15 #: xpack/plugins/change_auth_plan/models/app.py:25 msgid "Category" msgstr "类别" @@ -311,7 +311,7 @@ msgstr "类别" #: perms/serializers/application/user_permission.py:34 #: terminal/models/storage.py:58 terminal/models/storage.py:143 #: tickets/models/comment.py:26 tickets/models/flow.py:57 -#: tickets/models/ticket/apply_application.py:17 +#: tickets/models/ticket/apply_application.py:18 #: tickets/models/ticket/general.py:273 #: xpack/plugins/change_auth_plan/models/app.py:28 #: xpack/plugins/change_auth_plan/models/app.py:153 @@ -324,7 +324,7 @@ msgid "Domain" msgstr "网域" #: applications/models/application.py:231 xpack/plugins/cloud/models.py:33 -#: xpack/plugins/cloud/serializers/account.py:60 +#: xpack/plugins/cloud/serializers/account.py:61 msgid "Attrs" msgstr "属性" @@ -358,8 +358,8 @@ msgstr "类型名称" #: assets/serializers/account.py:18 assets/serializers/cmd_filter.py:28 #: assets/serializers/cmd_filter.py:48 common/db/models.py:114 #: common/mixins/models.py:50 ops/models/adhoc.py:39 ops/models/command.py:30 -#: orgs/models.py:67 orgs/models.py:218 perms/models/base.py:92 -#: users/models/group.py:18 users/models/user.py:922 +#: orgs/models.py:72 orgs/models.py:223 perms/models/base.py:92 +#: users/models/group.py:18 users/models/user.py:926 #: xpack/plugins/cloud/models.py:125 msgid "Date created" msgstr "创建日期" @@ -368,7 +368,7 @@ msgstr "创建日期" #: assets/models/gathered_user.py:20 assets/serializers/account.py:21 #: assets/serializers/cmd_filter.py:29 assets/serializers/cmd_filter.py:49 #: common/db/models.py:115 common/mixins/models.py:51 ops/models/adhoc.py:40 -#: orgs/models.py:219 +#: orgs/models.py:224 msgid "Date updated" msgstr "更新日期" @@ -388,8 +388,8 @@ msgstr "集群" #: applications/serializers/attrs/application_category/db.py:11 #: ops/models/adhoc.py:157 settings/serializers/auth/radius.py:14 -#: terminal/models/endpoint.py:11 -#: xpack/plugins/cloud/serializers/account_attrs.py:70 +#: settings/serializers/auth/sms.py:52 terminal/models/endpoint.py:11 +#: xpack/plugins/cloud/serializers/account_attrs.py:72 msgid "Host" msgstr "主机" @@ -402,8 +402,8 @@ msgstr "主机" #: applications/serializers/attrs/application_type/redis.py:10 #: applications/serializers/attrs/application_type/sqlserver.py:10 #: assets/models/asset.py:214 assets/models/domain.py:62 -#: settings/serializers/auth/radius.py:15 -#: xpack/plugins/cloud/serializers/account_attrs.py:71 +#: settings/serializers/auth/radius.py:15 settings/serializers/auth/sms.py:53 +#: xpack/plugins/cloud/serializers/account_attrs.py:73 msgid "Port" msgstr "端口" @@ -420,13 +420,13 @@ msgstr "应用路径" #: applications/serializers/attrs/application_category/remote_app.py:44 #: assets/serializers/system_user.py:167 -#: tickets/serializers/ticket/apply_application.py:35 +#: tickets/serializers/ticket/apply_application.py:38 #: tickets/serializers/ticket/common.py:59 #: xpack/plugins/change_auth_plan/serializers/asset.py:67 #: xpack/plugins/change_auth_plan/serializers/asset.py:70 #: xpack/plugins/change_auth_plan/serializers/asset.py:73 #: xpack/plugins/change_auth_plan/serializers/asset.py:104 -#: xpack/plugins/cloud/serializers/account_attrs.py:54 +#: xpack/plugins/cloud/serializers/account_attrs.py:56 msgid "This field is required." msgstr "该字段是必填项。" @@ -524,6 +524,7 @@ msgstr "内部的" #: assets/models/asset.py:162 assets/models/asset.py:216 #: assets/serializers/account.py:15 assets/serializers/asset.py:63 #: perms/serializers/asset/user_permission.py:43 +#: xpack/plugins/cloud/serializers/account_attrs.py:162 msgid "Platform" msgstr "系统平台" @@ -622,8 +623,8 @@ msgstr "标签管理" #: assets/models/asset.py:229 assets/models/base.py:183 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:52 #: assets/models/cmd_filter.py:99 assets/models/group.py:21 -#: common/db/models.py:112 common/mixins/models.py:49 orgs/models.py:66 -#: orgs/models.py:220 perms/models/base.py:91 users/models/user.py:706 +#: common/db/models.py:112 common/mixins/models.py:49 orgs/models.py:71 +#: orgs/models.py:225 perms/models/base.py:91 users/models/user.py:710 #: users/serializers/group.py:33 #: xpack/plugins/change_auth_plan/models/base.py:48 #: xpack/plugins/cloud/models.py:122 xpack/plugins/gathered_user/models.py:30 @@ -706,7 +707,7 @@ msgstr "定时触发" #: assets/models/backup.py:105 audits/models.py:44 ops/models/command.py:31 #: perms/models/base.py:89 terminal/models/session.py:58 -#: tickets/models/ticket/apply_application.py:25 +#: tickets/models/ticket/apply_application.py:29 #: tickets/models/ticket/apply_asset.py:23 #: xpack/plugins/change_auth_plan/models/base.py:112 #: xpack/plugins/change_auth_plan/models/base.py:203 @@ -768,7 +769,7 @@ msgstr "成功" #: assets/models/base.py:32 audits/models.py:118 #: xpack/plugins/change_auth_plan/serializers/app.py:88 #: xpack/plugins/change_auth_plan/serializers/asset.py:199 -#: xpack/plugins/cloud/const.py:32 +#: xpack/plugins/cloud/const.py:33 msgid "Failed" msgstr "失败" @@ -794,7 +795,7 @@ msgstr "校验日期" #: xpack/plugins/change_auth_plan/models/base.py:196 #: xpack/plugins/change_auth_plan/serializers/base.py:21 #: xpack/plugins/change_auth_plan/serializers/base.py:73 -#: xpack/plugins/cloud/serializers/account_attrs.py:26 +#: xpack/plugins/cloud/serializers/account_attrs.py:28 msgid "Password" msgstr "密码" @@ -819,7 +820,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:681 +#: assets/models/cluster.py:22 users/models/user.py:685 msgid "Phone" msgstr "手机" @@ -845,7 +846,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 rbac/const.py:6 -#: users/models/user.py:907 +#: users/models/user.py:911 msgid "System" msgstr "系统" @@ -854,7 +855,7 @@ msgid "Default Cluster" msgstr "默认Cluster" #: assets/models/cmd_filter.py:34 perms/models/base.py:86 -#: users/models/group.py:31 users/models/user.py:667 +#: users/models/group.py:31 users/models/user.py:671 msgid "User group" msgstr "用户组" @@ -902,11 +903,11 @@ msgstr "忽略大小写" msgid "Command filter rule" msgstr "命令过滤规则" -#: assets/models/cmd_filter.py:144 +#: assets/models/cmd_filter.py:147 msgid "The generated regular expression is incorrect: {}" msgstr "生成的正则表达式有误" -#: assets/models/cmd_filter.py:170 tickets/const.py:13 +#: assets/models/cmd_filter.py:173 tickets/const.py:13 msgid "Command confirm" msgstr "命令复核" @@ -923,7 +924,8 @@ msgstr "测试网关" msgid "Unable to connect to port {port} on {ip}" msgstr "无法连接到 {ip} 上的端口 {port}" -#: assets/models/domain.py:134 xpack/plugins/cloud/providers/fc.py:48 +#: assets/models/domain.py:134 authentication/middleware.py:75 +#: xpack/plugins/cloud/providers/fc.py:48 msgid "Authentication failed" msgstr "认证失败" @@ -955,7 +957,7 @@ msgstr "资产组" msgid "Default asset group" msgstr "默认资产组" -#: assets/models/label.py:19 assets/models/node.py:546 settings/models.py:30 +#: assets/models/label.py:19 assets/models/node.py:546 settings/models.py:34 msgid "Value" msgstr "值" @@ -1133,6 +1135,7 @@ msgstr "CPU信息" #: perms/serializers/application/permission.py:42 #: perms/serializers/asset/permission.py:18 #: perms/serializers/asset/permission.py:46 +#: tickets/models/ticket/apply_application.py:27 #: tickets/models/ticket/apply_asset.py:21 msgid "Actions" msgstr "动作" @@ -1148,7 +1151,7 @@ msgstr "定时执行" msgid "Currently only mail sending is supported" msgstr "当前只支持邮件发送" -#: assets/serializers/base.py:16 users/models/user.py:689 +#: assets/serializers/base.py:16 users/models/user.py:693 msgid "Private key" msgstr "ssh私钥" @@ -1494,7 +1497,7 @@ msgstr "改密日志" msgid "Disabled" msgstr "禁用" -#: audits/models.py:112 settings/models.py:33 +#: audits/models.py:112 settings/models.py:37 msgid "Enabled" msgstr "启用" @@ -1522,7 +1525,7 @@ msgstr "用户代理" #: audits/models.py:126 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 -#: users/forms/profile.py:65 users/models/user.py:684 +#: users/forms/profile.py:65 users/models/user.py:688 #: users/serializers/profile.py:126 msgid "MFA" msgstr "MFA" @@ -1600,20 +1603,20 @@ msgid "Auth Token" msgstr "认证令牌" #: audits/signal_handlers.py:53 authentication/notifications.py:73 -#: authentication/views/login.py:67 authentication/views/wecom.py:178 -#: notifications/backends/__init__.py:11 users/models/user.py:720 +#: authentication/views/login.py:73 authentication/views/wecom.py:178 +#: notifications/backends/__init__.py:11 users/models/user.py:724 msgid "WeCom" msgstr "企业微信" #: audits/signal_handlers.py:54 authentication/views/feishu.py:144 -#: authentication/views/login.py:79 notifications/backends/__init__.py:14 -#: users/models/user.py:722 +#: authentication/views/login.py:85 notifications/backends/__init__.py:14 +#: users/models/user.py:726 msgid "FeiShu" msgstr "飞书" #: audits/signal_handlers.py:55 authentication/views/dingtalk.py:179 -#: authentication/views/login.py:73 notifications/backends/__init__.py:12 -#: users/models/user.py:721 +#: authentication/views/login.py:79 notifications/backends/__init__.py:12 +#: users/models/user.py:725 msgid "DingTalk" msgstr "钉钉" @@ -1852,6 +1855,10 @@ msgstr "无效的令牌头。符号字符串不应包含无效字符。" msgid "Invalid token or cache refreshed." msgstr "刷新的令牌或缓存无效。" +#: authentication/backends/oauth2/backends.py:155 authentication/models.py:158 +msgid "User invalid, disabled or expired" +msgstr "用户无效,已禁用或已过期" + #: authentication/confirm/password.py:16 msgid "Authentication failed password incorrect" msgstr "认证失败 (用户名或密码不正确)" @@ -1902,7 +1909,7 @@ msgstr "没有匹配到认证后端" #: authentication/errors/const.py:28 msgid "ACL is not allowed" -msgstr "ACL 不被允许" +msgstr "登录访问控制不被允许" #: authentication/errors/const.py:29 msgid "Only local users are allowed" @@ -1962,23 +1969,19 @@ msgstr "等待登录复核处理" msgid "Login confirm ticket was {}" msgstr "登录复核: {}" -#: authentication/errors/failed.py:145 -msgid "IP is not allowed" -msgstr "来源 IP 不被允许登录" +#: authentication/errors/failed.py:146 +msgid "Current IP and Time period is not allowed" +msgstr "当前 IP 和时间段不被允许登录" -#: authentication/errors/failed.py:152 -msgid "Time Period is not allowed" -msgstr "该 时间段 不被允许登录" - -#: authentication/errors/failed.py:157 +#: authentication/errors/failed.py:151 msgid "Please enter MFA code" msgstr "请输入 MFA 验证码" -#: authentication/errors/failed.py:162 +#: authentication/errors/failed.py:156 msgid "Please enter SMS code" msgstr "请输入短信验证码" -#: authentication/errors/failed.py:167 users/exceptions.py:15 +#: authentication/errors/failed.py:161 users/exceptions.py:15 msgid "Phone not set" msgstr "手机号没有设置" @@ -2092,6 +2095,10 @@ msgstr "设置手机号码启用" msgid "Clear phone number to disable" msgstr "清空手机号码禁用" +#: authentication/middleware.py:76 settings/utils/ldap.py:652 +msgid "Authentication failed (before login check failed): {}" +msgstr "认证失败(登录前检查失败): {}" + #: authentication/mixins.py:256 msgid "The MFA type ({}) is not enabled" msgstr "该 MFA ({}) 方式没有启用" @@ -2123,8 +2130,8 @@ msgid "Secret" msgstr "密钥" #: authentication/models.py:74 authentication/models.py:264 -#: perms/models/base.py:90 tickets/models/ticket/apply_application.py:26 -#: tickets/models/ticket/apply_asset.py:24 users/models/user.py:703 +#: perms/models/base.py:90 tickets/models/ticket/apply_application.py:30 +#: tickets/models/ticket/apply_asset.py:24 users/models/user.py:707 msgid "Date expired" msgstr "失效日期" @@ -2148,10 +2155,6 @@ msgstr "连接令牌过期: {}" msgid "User not exists" msgstr "用户不存在" -#: authentication/models.py:158 -msgid "User invalid, disabled or expired" -msgstr "用户无效,已禁用或已过期" - #: authentication/models.py:163 msgid "System user not exists" msgstr "系统用户不存在" @@ -2201,7 +2204,7 @@ msgstr "有效" msgid "Expired time" msgstr "过期时间" -#: authentication/serializers/connection_token.py:74 +#: authentication/serializers/connection_token.py:73 msgid "Asset or application required" msgstr "资产或应用必填" @@ -2281,6 +2284,7 @@ msgid "Need MFA for view auth" msgstr "需要 MFA 认证来查看账号信息" #: authentication/templates/authentication/_mfa_confirm_modal.html:20 +#: authentication/templates/authentication/auth_fail_flash_message_standalone.html:37 #: templates/_modal.html:23 templates/flash_message_standalone.html:37 #: users/templates/users/user_password_verify.html:20 msgid "Confirm" @@ -2295,7 +2299,7 @@ msgstr "代码错误" #: authentication/templates/authentication/_msg_reset_password.html:3 #: authentication/templates/authentication/_msg_rest_password_success.html:2 #: authentication/templates/authentication/_msg_rest_public_key_success.html:2 -#: jumpserver/conf.py:307 ops/tasks.py:145 ops/tasks.py:148 +#: jumpserver/conf.py:390 ops/tasks.py:145 ops/tasks.py:148 #: perms/templates/perms/_msg_item_permissions_expire.html:3 #: perms/templates/perms/_msg_permed_items_expire.html:3 #: tickets/templates/tickets/approve_check_password.html:33 @@ -2378,13 +2382,18 @@ msgid "" "security issues" msgstr "如果这次公钥更新不是由你发起的,那么你的账号可能存在安全问题" +#: authentication/templates/authentication/auth_fail_flash_message_standalone.html:28 +#: templates/flash_message_standalone.html:28 tickets/const.py:20 +msgid "Cancel" +msgstr "取消" + #: authentication/templates/authentication/login.html:221 msgid "Welcome back, please enter username and password to login" msgstr "欢迎回来,请输入用户名和密码登录" #: authentication/templates/authentication/login.html:256 -#: users/templates/users/forgot_password.html:15 #: users/templates/users/forgot_password.html:16 +#: users/templates/users/forgot_password.html:17 msgid "Forgot password" msgstr "忘记密码" @@ -2499,19 +2508,19 @@ msgstr "从飞书获取用户失败" msgid "Please login with a password and then bind the FeiShu" msgstr "请使用密码登录,然后绑定飞书" -#: authentication/views/login.py:175 +#: authentication/views/login.py:181 msgid "Redirecting" msgstr "跳转中" -#: authentication/views/login.py:176 +#: authentication/views/login.py:182 msgid "Redirecting to {} authentication" msgstr "正在跳转到 {} 认证" -#: authentication/views/login.py:199 +#: authentication/views/login.py:205 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:301 +#: authentication/views/login.py:307 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -2519,15 +2528,15 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
\n" " 不要关闭本页面" -#: authentication/views/login.py:306 +#: authentication/views/login.py:312 msgid "No ticket found" msgstr "没有发现工单" -#: authentication/views/login.py:340 +#: authentication/views/login.py:346 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:341 +#: authentication/views/login.py:347 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" @@ -2683,6 +2692,14 @@ msgstr "企业微信错误,请联系系统管理员" msgid "Signature does not match" msgstr "签名不匹配" +#: common/sdk/sms/cmpp2.py:46 +msgid "sp_id is 6 bits" +msgstr "SP_id 为6位" + +#: common/sdk/sms/cmpp2.py:216 +msgid "Failed to connect to the CMPP gateway server, err: {}" +msgstr "连接网关服务器错误,错误:{}" + #: common/sdk/sms/endpoint.py:16 msgid "Alibaba cloud" msgstr "阿里云" @@ -2691,11 +2708,15 @@ msgstr "阿里云" msgid "Tencent cloud" msgstr "腾讯云" -#: common/sdk/sms/endpoint.py:28 +#: common/sdk/sms/endpoint.py:18 +msgid "CMPP v2.0" +msgstr "CMPP v2.0" + +#: common/sdk/sms/endpoint.py:29 msgid "SMS provider not support: {}" msgstr "短信服务商不支持:{}" -#: common/sdk/sms/endpoint.py:49 +#: common/sdk/sms/endpoint.py:50 msgid "SMS verification code signature or template invalid" msgstr "短信验证码签名或模版无效" @@ -2711,9 +2732,9 @@ msgstr "验证码错误" msgid "Please wait {} seconds before sending" msgstr "请在 {} 秒后发送" -#: common/utils/ip/geoip/utils.py:24 +#: common/utils/ip/geoip/utils.py:24 xpack/plugins/cloud/const.py:24 msgid "LAN" -msgstr "LAN" +msgstr "局域网" #: common/utils/ip/geoip/utils.py:26 common/utils/ip/utils.py:78 msgid "Invalid ip" @@ -2731,11 +2752,11 @@ msgstr "不能包含特殊字符" msgid "The mobile phone number format is incorrect" msgstr "手机号格式不正确" -#: jumpserver/conf.py:306 +#: jumpserver/conf.py:389 msgid "Create account successfully" msgstr "创建账号成功" -#: jumpserver/conf.py:308 +#: jumpserver/conf.py:391 msgid "Your account has been created successfully" msgstr "你的账号已创建成功" @@ -2775,7 +2796,7 @@ msgid "Notifications" msgstr "通知" #: notifications/backends/__init__.py:10 users/forms/profile.py:102 -#: users/models/user.py:663 +#: users/models/user.py:667 msgid "Email" msgstr "邮件" @@ -2988,27 +3009,27 @@ msgstr "组织存在资源 ({}) 不能被删除" msgid "App organizations" msgstr "组织管理" -#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:80 -#: orgs/models.py:212 rbac/const.py:7 rbac/models/rolebinding.py:48 +#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:85 +#: orgs/models.py:217 rbac/const.py:7 rbac/models/rolebinding.py:48 #: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:62 #: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:71 msgid "Organization" msgstr "组织" -#: orgs/models.py:74 +#: orgs/models.py:79 msgid "GLOBAL" msgstr "全局组织" -#: orgs/models.py:82 +#: orgs/models.py:87 msgid "Can view root org" msgstr "可以查看全局组织" -#: orgs/models.py:83 +#: orgs/models.py:88 msgid "Can view all joined org" msgstr "可以查看所有加入的组织" -#: orgs/models.py:217 rbac/models/role.py:46 rbac/models/rolebinding.py:44 -#: users/models/user.py:671 +#: orgs/models.py:222 rbac/models/role.py:46 rbac/models/rolebinding.py:44 +#: users/models/user.py:675 msgid "Role" msgstr "角色" @@ -3096,27 +3117,32 @@ msgstr "剪贴板复制粘贴" msgid "From ticket" msgstr "来自工单" -#: perms/notifications.py:18 +#: perms/notifications.py:12 perms/notifications.py:44 +#: perms/notifications.py:88 perms/notifications.py:119 +msgid "today" +msgstr "今" + +#: perms/notifications.py:15 msgid "You permed assets is about to expire" msgstr "你授权的资产即将到期" -#: perms/notifications.py:23 +#: perms/notifications.py:20 msgid "permed assets" msgstr "授权的资产" -#: perms/notifications.py:62 +#: perms/notifications.py:59 msgid "Asset permissions is about to expire" msgstr "资产授权规则将要过期" -#: perms/notifications.py:67 +#: perms/notifications.py:64 msgid "asset permissions of organization {}" msgstr "组织 ({}) 的资产授权" -#: perms/notifications.py:94 +#: perms/notifications.py:91 msgid "Your permed applications is about to expire" msgstr "你授权的应用即将过期" -#: perms/notifications.py:98 +#: perms/notifications.py:95 msgid "permed applications" msgstr "授权的应用" @@ -3279,6 +3305,7 @@ msgid "Permission" msgstr "权限" #: rbac/models/role.py:31 rbac/models/rolebinding.py:38 +#: settings/serializers/auth/oauth2.py:35 msgid "Scope" msgstr "范围" @@ -3356,7 +3383,7 @@ msgstr "工作台" msgid "Audit view" msgstr "审计台" -#: rbac/tree.py:28 settings/models.py:140 +#: rbac/tree.py:28 settings/models.py:156 msgid "System setting" msgstr "系统设置" @@ -3416,13 +3443,8 @@ msgstr "查看授权树" msgid "Execute batch command" msgstr "执行批量命令" -#: settings/api/alibaba_sms.py:31 settings/api/tencent_sms.py:35 -msgid "test_phone is required" -msgstr "测试手机号 该字段是必填项。" - -#: settings/api/alibaba_sms.py:52 settings/api/dingtalk.py:31 -#: settings/api/feishu.py:36 settings/api/tencent_sms.py:57 -#: settings/api/wecom.py:37 +#: settings/api/dingtalk.py:31 settings/api/feishu.py:36 +#: settings/api/sms.py:131 settings/api/wecom.py:37 msgid "Test success" msgstr "测试成功" @@ -3450,47 +3472,55 @@ msgstr "获取 LDAP 用户为 None" msgid "Imported {} users successfully (Organization: {})" msgstr "成功导入 {} 个用户 ( 组织: {} )" +#: settings/api/sms.py:113 +msgid "Invalid SMS platform" +msgstr "无效的短信平台" + +#: settings/api/sms.py:119 +msgid "test_phone is required" +msgstr "测试手机号 该字段是必填项。" + #: settings/apps.py:7 msgid "Settings" msgstr "系统设置" -#: settings/models.py:142 +#: settings/models.py:158 msgid "Can change email setting" msgstr "邮件设置" -#: settings/models.py:143 +#: settings/models.py:159 msgid "Can change auth setting" msgstr "认证设置" -#: settings/models.py:144 +#: settings/models.py:160 msgid "Can change system msg sub setting" msgstr "消息订阅设置" -#: settings/models.py:145 +#: settings/models.py:161 msgid "Can change sms setting" msgstr "短信设置" -#: settings/models.py:146 +#: settings/models.py:162 msgid "Can change security setting" msgstr "安全设置" -#: settings/models.py:147 +#: settings/models.py:163 msgid "Can change clean setting" msgstr "定期清理" -#: settings/models.py:148 +#: settings/models.py:164 msgid "Can change interface setting" msgstr "界面设置" -#: settings/models.py:149 +#: settings/models.py:165 msgid "Can change license setting" msgstr "许可证设置" -#: settings/models.py:150 +#: settings/models.py:166 msgid "Can change terminal setting" msgstr "终端设置" -#: settings/models.py:151 +#: settings/models.py:167 msgid "Can change other setting" msgstr "其它设置" @@ -3603,7 +3633,8 @@ msgstr "用户过滤器" msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)" msgstr "可能的选项是(cn或uid或sAMAccountName=%(user)s)" -#: settings/serializers/auth/ldap.py:57 settings/serializers/auth/oidc.py:36 +#: settings/serializers/auth/ldap.py:57 settings/serializers/auth/oauth2.py:51 +#: settings/serializers/auth/oidc.py:36 msgid "User attr map" msgstr "用户属性映射" @@ -3627,23 +3658,52 @@ msgstr "搜索分页数量" msgid "Enable LDAP auth" msgstr "启用 LDAP 认证" -#: settings/serializers/auth/oidc.py:15 -msgid "Base site url" -msgstr "JumpServer 地址" +#: settings/serializers/auth/oauth2.py:20 +msgid "Enable OAuth2 Auth" +msgstr "启用 OAuth2 认证" -#: settings/serializers/auth/oidc.py:18 +#: settings/serializers/auth/oauth2.py:23 +msgid "Logo" +msgstr "图标" + +#: settings/serializers/auth/oauth2.py:26 +msgid "Service provider" +msgstr "服务提供商" + +#: settings/serializers/auth/oauth2.py:29 settings/serializers/auth/oidc.py:18 msgid "Client Id" msgstr "客户端 ID" -#: settings/serializers/auth/oidc.py:21 -#: xpack/plugins/cloud/serializers/account_attrs.py:36 +#: settings/serializers/auth/oauth2.py:32 settings/serializers/auth/oidc.py:21 +#: xpack/plugins/cloud/serializers/account_attrs.py:38 msgid "Client Secret" msgstr "客户端密钥" -#: settings/serializers/auth/oidc.py:29 +#: settings/serializers/auth/oauth2.py:38 settings/serializers/auth/oidc.py:62 +msgid "Provider auth endpoint" +msgstr "授权端点地址" + +#: settings/serializers/auth/oauth2.py:41 settings/serializers/auth/oidc.py:65 +msgid "Provider token endpoint" +msgstr "token 端点地址" + +#: settings/serializers/auth/oauth2.py:44 settings/serializers/auth/oidc.py:29 msgid "Client authentication method" msgstr "客户端认证方式" +#: settings/serializers/auth/oauth2.py:48 settings/serializers/auth/oidc.py:71 +msgid "Provider userinfo endpoint" +msgstr "用户信息端点地址" + +#: settings/serializers/auth/oauth2.py:54 settings/serializers/auth/oidc.py:92 +#: settings/serializers/auth/saml2.py:33 +msgid "Always update user" +msgstr "总是更新用户信息" + +#: settings/serializers/auth/oidc.py:15 +msgid "Base site url" +msgstr "JumpServer 地址" + #: settings/serializers/auth/oidc.py:31 msgid "Share session" msgstr "共享会话" @@ -3676,22 +3736,10 @@ msgstr "启用 OIDC 认证" msgid "Provider endpoint" msgstr "端点地址" -#: settings/serializers/auth/oidc.py:62 -msgid "Provider auth endpoint" -msgstr "授权端点地址" - -#: settings/serializers/auth/oidc.py:65 -msgid "Provider token endpoint" -msgstr "token 端点地址" - #: settings/serializers/auth/oidc.py:68 msgid "Provider jwks endpoint" msgstr "jwks 端点地址" -#: settings/serializers/auth/oidc.py:71 -msgid "Provider userinfo endpoint" -msgstr "用户信息端点地址" - #: settings/serializers/auth/oidc.py:74 msgid "Provider end session endpoint" msgstr "注销会话端点地址" @@ -3724,10 +3772,6 @@ msgstr "使用状态" msgid "Use nonce" msgstr "临时使用" -#: settings/serializers/auth/oidc.py:92 settings/serializers/auth/saml2.py:33 -msgid "Always update user" -msgstr "总是更新用户信息" - #: settings/serializers/auth/radius.py:13 msgid "Enable Radius Auth" msgstr "启用 Radius 认证" @@ -3760,41 +3804,82 @@ msgstr "SP 密钥" msgid "SP cert" msgstr "SP 证书" -#: settings/serializers/auth/sms.py:11 +#: settings/serializers/auth/sms.py:14 msgid "Enable SMS" msgstr "启用 SMS" -#: settings/serializers/auth/sms.py:13 -msgid "SMS provider" -msgstr "短信服务商" +#: settings/serializers/auth/sms.py:16 +msgid "SMS provider / Protocol" +msgstr "短信服务商 / 协议" -#: settings/serializers/auth/sms.py:18 settings/serializers/auth/sms.py:36 -#: settings/serializers/auth/sms.py:44 settings/serializers/email.py:65 +#: settings/serializers/auth/sms.py:21 settings/serializers/auth/sms.py:39 +#: settings/serializers/auth/sms.py:47 settings/serializers/auth/sms.py:58 +#: settings/serializers/email.py:65 msgid "Signature" msgstr "签名" -#: settings/serializers/auth/sms.py:19 settings/serializers/auth/sms.py:37 -#: settings/serializers/auth/sms.py:45 +#: settings/serializers/auth/sms.py:22 settings/serializers/auth/sms.py:40 +#: settings/serializers/auth/sms.py:48 msgid "Template code" msgstr "模板" -#: settings/serializers/auth/sms.py:23 +#: settings/serializers/auth/sms.py:26 msgid "Test phone" msgstr "测试手机号" -#: settings/serializers/auth/sso.py:12 +#: settings/serializers/auth/sms.py:54 +msgid "Enterprise code(SP id)" +msgstr "企业代码(SP id)" + +#: settings/serializers/auth/sms.py:55 +msgid "Shared secret(Shared secret)" +msgstr "共享密码(Shared secret)" + +#: settings/serializers/auth/sms.py:56 +msgid "Original number(Src id)" +msgstr "原始号码(Src id)" + +#: settings/serializers/auth/sms.py:57 +msgid "Business type(Service id)" +msgstr "业务类型(Service id)" + +#: settings/serializers/auth/sms.py:60 +msgid "Template" +msgstr "模板" + +#: settings/serializers/auth/sms.py:61 +#, python-brace-format +msgid "" +"Template need contain {code} and Signature + template length does not exceed " +"67 words. For example, your verification code is {code}, which is valid for " +"5 minutes. Please do not disclose it to others." +msgstr "" +"模板需要包含 {code},并且模板+签名长度不能超过67个字。例如, 您的验证码是 " +"{code}, 有效期为5分钟。请不要泄露给其他人。" + +#: settings/serializers/auth/sms.py:70 +#, python-brace-format +msgid "The template needs to contain {code}" +msgstr "模板需要包含 {code}" + +#: settings/serializers/auth/sms.py:73 +msgid "Signature + Template must not exceed 65 words" +msgstr "模板+签名不能超过65个字" + +#: settings/serializers/auth/sso.py:11 msgid "Enable SSO auth" msgstr "启用 SSO Token 认证" -#: settings/serializers/auth/sso.py:13 +#: settings/serializers/auth/sso.py:12 msgid "Other service can using SSO token login to JumpServer without password" msgstr "其它系统可以使用 SSO Token 对接 JumpServer, 免去登录的过程" -#: settings/serializers/auth/sso.py:16 +#: settings/serializers/auth/sso.py:15 msgid "SSO auth key TTL" msgstr "Token 有效期" -#: settings/serializers/auth/sso.py:16 +#: settings/serializers/auth/sso.py:15 +#: xpack/plugins/cloud/serializers/account_attrs.py:159 msgid "Unit: second" msgstr "单位: 秒" @@ -3860,7 +3945,7 @@ msgstr "登录日志" #: settings/serializers/cleaning.py:10 settings/serializers/cleaning.py:14 #: settings/serializers/cleaning.py:18 settings/serializers/cleaning.py:22 -#: settings/serializers/cleaning.py:26 +#: settings/serializers/cleaning.py:26 settings/serializers/other.py:35 msgid "Unit: day" msgstr "单位: 天" @@ -4025,19 +4110,23 @@ msgstr "" "放置单独授权的资产到未分组节点, 避免能看到资产所在节点,但该节点未被授权的问" "题" -#: settings/serializers/other.py:34 +#: settings/serializers/other.py:35 +msgid "Ticket authorize default time" +msgstr "默认工单授权时间" + +#: settings/serializers/other.py:39 msgid "Help Docs URL" msgstr "文档链接" -#: settings/serializers/other.py:35 +#: settings/serializers/other.py:40 msgid "default: http://docs.jumpserver.org" msgstr "默认: http://dev.jumpserver.org:8080" -#: settings/serializers/other.py:39 +#: settings/serializers/other.py:44 msgid "Help Support URL" msgstr "支持链接" -#: settings/serializers/other.py:40 +#: settings/serializers/other.py:45 msgid "default: http://www.jumpserver.org/support/" msgstr "默认: http://www.jumpserver.org/support/" @@ -4261,7 +4350,7 @@ msgstr "会话分享" #: settings/serializers/security.py:180 msgid "Enabled, Allows user active session to be shared with other users" -msgstr "开启后允许用户分享已连接的资产会话给它人,协同工作" +msgstr "开启后允许用户分享已连接的资产会话给他人,协同工作" #: settings/serializers/security.py:183 msgid "Remote Login Protection" @@ -4412,10 +4501,6 @@ msgstr "成功匹配 {} 个用户" msgid "Authentication failed (configuration incorrect): {}" msgstr "认证失败(配置错误): {}" -#: settings/utils/ldap.py:652 -msgid "Authentication failed (before login check failed): {}" -msgstr "认证失败(登录前检查失败): {}" - #: settings/utils/ldap.py:654 msgid "Authentication failed (username or password incorrect): {}" msgstr "认证失败 (用户名或密码不正确): {}" @@ -4581,10 +4666,6 @@ msgstr "验证码已发送" msgid "Home page" msgstr "首页" -#: templates/flash_message_standalone.html:28 tickets/const.py:20 -msgid "Cancel" -msgstr "取消" - #: templates/resource_download.html:18 templates/resource_download.html:31 msgid "Client" msgstr "客户端" @@ -4762,7 +4843,7 @@ msgstr "不支持批量创建" msgid "Storage is invalid" msgstr "存储无效" -#: terminal/models/command.py:53 +#: terminal/models/command.py:66 msgid "Command record" msgstr "命令记录" @@ -5056,12 +5137,12 @@ msgid "Bucket" msgstr "桶名称" #: terminal/serializers/storage.py:30 -#: xpack/plugins/cloud/serializers/account_attrs.py:15 +#: xpack/plugins/cloud/serializers/account_attrs.py:17 msgid "Access key id" msgstr "访问密钥 ID(AK)" #: terminal/serializers/storage.py:34 -#: xpack/plugins/cloud/serializers/account_attrs.py:18 +#: xpack/plugins/cloud/serializers/account_attrs.py:20 msgid "Access key secret" msgstr "访问密钥密文(SK)" @@ -5201,7 +5282,7 @@ msgstr "自定义用户" msgid "Ticket already closed" msgstr "工单已经关闭" -#: tickets/handlers/apply_application.py:37 +#: tickets/handlers/apply_application.py:38 msgid "" "Created by the ticket, ticket title: {}, ticket applicant: {}, ticket " "processor: {}, ticket ID: {}" @@ -5285,16 +5366,16 @@ msgstr "工单流程" msgid "Ticket session relation" msgstr "工单会话" -#: tickets/models/ticket/apply_application.py:11 +#: tickets/models/ticket/apply_application.py:12 #: tickets/models/ticket/apply_asset.py:13 msgid "Permission name" msgstr "授权规则名称" -#: tickets/models/ticket/apply_application.py:20 +#: tickets/models/ticket/apply_application.py:21 msgid "Apply applications" msgstr "申请应用" -#: tickets/models/ticket/apply_application.py:23 +#: tickets/models/ticket/apply_application.py:24 #: tickets/models/ticket/apply_asset.py:18 msgid "Apply system users" msgstr "申请的系统用户" @@ -5598,7 +5679,7 @@ msgstr "不能和原来的密钥相同" msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" -#: users/forms/profile.py:161 users/models/user.py:692 +#: users/forms/profile.py:161 users/models/user.py:696 msgid "Public key" msgstr "SSH公钥" @@ -5610,55 +5691,55 @@ msgstr "强制启用" msgid "Local" msgstr "数据库" -#: users/models/user.py:673 users/serializers/user.py:149 +#: users/models/user.py:677 users/serializers/user.py:149 msgid "Is service account" msgstr "服务账号" -#: users/models/user.py:675 +#: users/models/user.py:679 msgid "Avatar" msgstr "头像" -#: users/models/user.py:678 +#: users/models/user.py:682 msgid "Wechat" msgstr "微信" -#: users/models/user.py:695 +#: users/models/user.py:699 msgid "Secret key" msgstr "Secret key" -#: users/models/user.py:711 +#: users/models/user.py:715 msgid "Source" msgstr "来源" -#: users/models/user.py:715 +#: users/models/user.py:719 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:718 +#: users/models/user.py:722 msgid "Need update password" msgstr "需要更新密码" -#: users/models/user.py:892 +#: users/models/user.py:896 msgid "Can invite user" msgstr "可以邀请用户" -#: users/models/user.py:893 +#: users/models/user.py:897 msgid "Can remove user" msgstr "可以移除用户" -#: users/models/user.py:894 +#: users/models/user.py:898 msgid "Can match user" msgstr "可以匹配用户" -#: users/models/user.py:903 +#: users/models/user.py:907 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:906 +#: users/models/user.py:910 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" -#: users/models/user.py:931 +#: users/models/user.py:935 msgid "User password history" msgstr "用户密码历史" @@ -5863,11 +5944,11 @@ msgstr "你的 SSH 密钥已经被管理员重置" msgid "click here to set your password" msgstr "点击这里设置密码" -#: users/templates/users/forgot_password.html:23 +#: users/templates/users/forgot_password.html:24 msgid "Input your email, that will send a mail to your" msgstr "输入您的邮箱, 将会发一封重置邮件到您的邮箱中" -#: users/templates/users/forgot_password.html:32 +#: users/templates/users/forgot_password.html:33 msgid "Submit" msgstr "提交" @@ -6299,31 +6380,31 @@ msgstr "谷歌云" msgid "Fusion Compute" msgstr "" -#: xpack/plugins/cloud/const.py:27 +#: xpack/plugins/cloud/const.py:28 msgid "Instance name" msgstr "实例名称" -#: xpack/plugins/cloud/const.py:28 +#: xpack/plugins/cloud/const.py:29 msgid "Instance name and Partial IP" msgstr "实例名称和部分IP" -#: xpack/plugins/cloud/const.py:33 +#: xpack/plugins/cloud/const.py:34 msgid "Succeed" msgstr "成功" -#: xpack/plugins/cloud/const.py:37 +#: xpack/plugins/cloud/const.py:38 msgid "Unsync" msgstr "未同步" -#: xpack/plugins/cloud/const.py:38 +#: xpack/plugins/cloud/const.py:39 msgid "New Sync" msgstr "新同步" -#: xpack/plugins/cloud/const.py:39 +#: xpack/plugins/cloud/const.py:40 msgid "Synced" msgstr "已同步" -#: xpack/plugins/cloud/const.py:40 +#: xpack/plugins/cloud/const.py:41 msgid "Released" msgstr "已释放" @@ -6593,52 +6674,87 @@ msgstr "华南-广州-友好用户环境" msgid "CN East-Suqian" msgstr "华东-宿迁" -#: xpack/plugins/cloud/serializers/account.py:61 +#: xpack/plugins/cloud/serializers/account.py:62 msgid "Validity display" msgstr "有效性显示" -#: xpack/plugins/cloud/serializers/account.py:62 +#: xpack/plugins/cloud/serializers/account.py:63 msgid "Provider display" msgstr "服务商显示" -#: xpack/plugins/cloud/serializers/account_attrs.py:33 +#: xpack/plugins/cloud/serializers/account_attrs.py:35 msgid "Client ID" msgstr "客户端 ID" -#: xpack/plugins/cloud/serializers/account_attrs.py:39 +#: xpack/plugins/cloud/serializers/account_attrs.py:41 msgid "Tenant ID" msgstr "租户 ID" -#: xpack/plugins/cloud/serializers/account_attrs.py:42 +#: xpack/plugins/cloud/serializers/account_attrs.py:44 msgid "Subscription ID" msgstr "订阅 ID" -#: xpack/plugins/cloud/serializers/account_attrs.py:93 -#: xpack/plugins/cloud/serializers/account_attrs.py:98 -#: xpack/plugins/cloud/serializers/account_attrs.py:122 +#: xpack/plugins/cloud/serializers/account_attrs.py:95 +#: xpack/plugins/cloud/serializers/account_attrs.py:100 +#: xpack/plugins/cloud/serializers/account_attrs.py:124 msgid "API Endpoint" msgstr "API 端点" -#: xpack/plugins/cloud/serializers/account_attrs.py:104 +#: xpack/plugins/cloud/serializers/account_attrs.py:106 msgid "Auth url" msgstr "认证地址" -#: xpack/plugins/cloud/serializers/account_attrs.py:105 +#: xpack/plugins/cloud/serializers/account_attrs.py:107 msgid "eg: http://openstack.example.com:5000/v3" msgstr "如: http://openstack.example.com:5000/v3" -#: xpack/plugins/cloud/serializers/account_attrs.py:108 +#: xpack/plugins/cloud/serializers/account_attrs.py:110 msgid "User domain" msgstr "用户域" -#: xpack/plugins/cloud/serializers/account_attrs.py:115 +#: xpack/plugins/cloud/serializers/account_attrs.py:117 msgid "Service account key" msgstr "服务账号密钥" -#: xpack/plugins/cloud/serializers/account_attrs.py:116 +#: xpack/plugins/cloud/serializers/account_attrs.py:118 msgid "The file is in JSON format" msgstr "JSON 格式的文件" +#: xpack/plugins/cloud/serializers/account_attrs.py:131 +msgid "IP address invalid `{}`, {}" +msgstr "IP 地址无效: `{}`, {}" + +#: xpack/plugins/cloud/serializers/account_attrs.py:137 +msgid "" +"Format for comma-delimited string,Such as: 192.168.1.0/24, " +"10.0.0.0-10.0.0.255" +msgstr "格式为逗号分隔的字符串,如:192.168.1.0/24,10.0.0.0-10.0.0.255" + +#: xpack/plugins/cloud/serializers/account_attrs.py:141 +msgid "" +"The port is used to detect the validity of the IP address. When the " +"synchronization task is executed, only the valid IP address will be " +"synchronized.
If the port is 0, all IP addresses are valid." +msgstr "" +"端口用来检测 IP 地址的有效性,在同步任务执行时,只会同步有效的 IP 地址。
" +"如果端口为 0,则表示所有 IP 地址均有效。" + +#: xpack/plugins/cloud/serializers/account_attrs.py:149 +msgid "Hostname prefix" +msgstr "主机名前缀" + +#: xpack/plugins/cloud/serializers/account_attrs.py:152 +msgid "IP segment" +msgstr "IP 网段" + +#: xpack/plugins/cloud/serializers/account_attrs.py:156 +msgid "Test port" +msgstr "测试端口" + +#: xpack/plugins/cloud/serializers/account_attrs.py:159 +msgid "Test timeout" +msgstr "测试超时时间" + #: xpack/plugins/cloud/serializers/task.py:29 msgid "" "Only instances matching the IP range will be synced.
If the instance " @@ -6755,6 +6871,3 @@ msgstr "旗舰版" #: xpack/plugins/license/models.py:77 msgid "Community edition" msgstr "社区版" - -#~ msgid "User cannot self-update fields: {}" -#~ msgstr "用户不能更新自己的字段: {}" diff --git a/apps/perms/api/application/application_permission.py b/apps/perms/api/application/application_permission.py index 798455053..bd8fb3452 100644 --- a/apps/perms/api/application/application_permission.py +++ b/apps/perms/api/application/application_permission.py @@ -1,8 +1,12 @@ # -*- coding: utf-8 -*- # -from applications.models import Application -from perms.models import ApplicationPermission +from rest_framework.response import Response +from rest_framework.generics import RetrieveAPIView + from perms import serializers +from perms.models import ApplicationPermission +from applications.models import Application +from common.permissions import IsValidUser from ..base import BasePermissionViewSet @@ -23,7 +27,7 @@ class ApplicationPermissionViewSet(BasePermissionViewSet): 'application_id', 'application', 'app', 'app_name' ] ordering_fields = ('name',) - ordering = ('name', ) + ordering = ('name',) def get_queryset(self): queryset = super().get_queryset().prefetch_related( @@ -53,3 +57,11 @@ class ApplicationPermissionViewSet(BasePermissionViewSet): queryset = self.filter_application(queryset) return queryset + +class ApplicationPermissionActionsApi(RetrieveAPIView): + permission_classes = (IsValidUser,) + + def retrieve(self, request, *args, **kwargs): + category = request.GET.get('category') + actions = ApplicationPermission.get_include_actions_choices(category=category) + return Response(data=actions) diff --git a/apps/perms/notifications.py b/apps/perms/notifications.py index 088fa107a..b40ccab7c 100644 --- a/apps/perms/notifications.py +++ b/apps/perms/notifications.py @@ -9,7 +9,7 @@ class PermedAssetsWillExpireUserMsg(UserMessage): def __init__(self, user, assets, day_count=0): super().__init__(user) self.assets = assets - self.day_count = day_count + self.day_count = _('today') if day_count == 0 else day_count def get_html_msg(self) -> dict: subject = _("You permed assets is about to expire") @@ -41,7 +41,7 @@ class AssetPermsWillExpireForOrgAdminMsg(UserMessage): super().__init__(user) self.perms = perms self.org = org - self.day_count = day_count + self.day_count = _('today') if day_count == 0 else day_count def get_items_with_url(self): items_with_url = [] @@ -59,7 +59,7 @@ class AssetPermsWillExpireForOrgAdminMsg(UserMessage): subject = _("Asset permissions is about to expire") context = { 'name': self.user.name, - 'count': self.day_count, + 'count': str(self.day_count), 'items_with_url': items_with_url, 'item_type': _('asset permissions of organization {}').format(self.org) } @@ -85,13 +85,13 @@ class PermedAppsWillExpireUserMsg(UserMessage): def __init__(self, user, apps, day_count=0): super().__init__(user) self.apps = apps - self.day_count = day_count + self.day_count = _('today') if day_count == 0 else day_count def get_html_msg(self) -> dict: subject = _("Your permed applications is about to expire") context = { 'name': self.user.name, - 'count': self.day_count, + 'count': str(self.day_count), 'item_type': _('permed applications'), 'items': [str(app) for app in self.apps] } @@ -116,7 +116,7 @@ class AppPermsWillExpireForOrgAdminMsg(UserMessage): super().__init__(user) self.perms = perms self.org = org - self.day_count = day_count + self.day_count = _('today') if day_count == 0 else day_count def get_items_with_url(self): items_with_url = [] @@ -134,7 +134,7 @@ class AppPermsWillExpireForOrgAdminMsg(UserMessage): subject = _('Application permissions is about to expire') context = { 'name': self.user.name, - 'count': self.day_count, + 'count': str(self.day_count), 'item_type': _('application permissions of organization {}').format(self.org), 'items_with_url': items } diff --git a/apps/perms/urls/application_permission.py b/apps/perms/urls/application_permission.py index 4ed9e6d37..50772a8d5 100644 --- a/apps/perms/urls/application_permission.py +++ b/apps/perms/urls/application_permission.py @@ -37,6 +37,8 @@ permission_urlpatterns = [ # 验证用户是否有某个应用的权限 path('user/validate/', api.ValidateUserApplicationPermissionApi.as_view(), name='validate-user-application-permission'), + + path('applications/actions/', api.ApplicationPermissionActionsApi.as_view(), name='application-actions'), ] application_permission_urlpatterns = [ diff --git a/apps/settings/api/__init__.py b/apps/settings/api/__init__.py index 65438dda1..176a6c2c6 100644 --- a/apps/settings/api/__init__.py +++ b/apps/settings/api/__init__.py @@ -5,6 +5,4 @@ from .dingtalk import * from .feishu import * from .public import * from .email import * -from .alibaba_sms import * -from .tencent_sms import * from .sms import * diff --git a/apps/settings/api/alibaba_sms.py b/apps/settings/api/alibaba_sms.py deleted file mode 100644 index 8240ba0e0..000000000 --- a/apps/settings/api/alibaba_sms.py +++ /dev/null @@ -1,58 +0,0 @@ -from rest_framework.views import Response -from rest_framework.generics import GenericAPIView -from rest_framework.exceptions import APIException -from rest_framework import status -from django.utils.translation import gettext_lazy as _ - -from common.sdk.sms.alibaba import AlibabaSMS -from settings.models import Setting -from common.exceptions import JMSException - -from .. import serializers - - -class AlibabaSMSTestingAPI(GenericAPIView): - serializer_class = serializers.AlibabaSMSSettingSerializer - rbac_perms = { - 'POST': 'settings.change_sms' - } - - def post(self, request): - serializer = self.serializer_class(data=request.data) - serializer.is_valid(raise_exception=True) - - alibaba_access_key_id = serializer.validated_data['ALIBABA_ACCESS_KEY_ID'] - alibaba_access_key_secret = serializer.validated_data.get('ALIBABA_ACCESS_KEY_SECRET') - alibaba_verify_sign_name = serializer.validated_data['ALIBABA_VERIFY_SIGN_NAME'] - alibaba_verify_template_code = serializer.validated_data['ALIBABA_VERIFY_TEMPLATE_CODE'] - test_phone = serializer.validated_data.get('SMS_TEST_PHONE') - - if not test_phone: - raise JMSException(code='test_phone_required', detail=_('test_phone is required')) - - if not alibaba_access_key_secret: - secret = Setting.objects.filter(name='ALIBABA_ACCESS_KEY_SECRET').first() - if secret: - alibaba_access_key_secret = secret.cleaned_value - - alibaba_access_key_secret = alibaba_access_key_secret or '' - - try: - client = AlibabaSMS( - access_key_id=alibaba_access_key_id, - access_key_secret=alibaba_access_key_secret - ) - - client.send_sms( - phone_numbers=[test_phone], - sign_name=alibaba_verify_sign_name, - template_code=alibaba_verify_template_code, - template_param={'code': 'test'} - ) - return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')}) - except APIException as e: - try: - error = e.detail['errmsg'] - except: - error = e.detail - return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error}) diff --git a/apps/settings/api/settings.py b/apps/settings/api/settings.py index 3e1d9336d..0f487c280 100644 --- a/apps/settings/api/settings.py +++ b/apps/settings/api/settings.py @@ -34,11 +34,13 @@ class SettingsApi(generics.RetrieveUpdateAPIView): 'cas': serializers.CASSettingSerializer, 'sso': serializers.SSOSettingSerializer, 'saml2': serializers.SAML2SettingSerializer, + 'oauth2': serializers.OAuth2SettingSerializer, 'clean': serializers.CleaningSerializer, 'other': serializers.OtherSettingSerializer, 'sms': serializers.SMSSettingSerializer, 'alibaba': serializers.AlibabaSMSSettingSerializer, 'tencent': serializers.TencentSMSSettingSerializer, + 'cmpp2': serializers.CMPP2SMSSettingSerializer, } rbac_category_permissions = { @@ -113,9 +115,12 @@ class SettingsApi(generics.RetrieveUpdateAPIView): return data def perform_update(self, serializer): + post_data_names = list(self.request.data.keys()) settings_items = self.parse_serializer_data(serializer) serializer_data = getattr(serializer, 'data', {}) for item in settings_items: + if item['name'] not in post_data_names: + continue changed, setting = Setting.update_or_create(**item) if not changed: continue diff --git a/apps/settings/api/sms.py b/apps/settings/api/sms.py index bb30fa3aa..668b5ec56 100644 --- a/apps/settings/api/sms.py +++ b/apps/settings/api/sms.py @@ -1,8 +1,19 @@ -from rest_framework.generics import ListAPIView +import importlib + +from collections import OrderedDict + +from rest_framework.generics import ListAPIView, GenericAPIView from rest_framework.response import Response +from rest_framework.exceptions import APIException +from rest_framework import status +from django.utils.translation import gettext_lazy as _ from common.sdk.sms import BACKENDS +from common.exceptions import JMSException from settings.serializers.sms import SMSBackendSerializer +from settings.models import Setting + +from .. import serializers class SMSBackendAPI(ListAPIView): @@ -21,3 +32,111 @@ class SMSBackendAPI(ListAPIView): ] return Response(data) + + +class SMSTestingAPI(GenericAPIView): + backends_serializer = { + 'alibaba': serializers.AlibabaSMSSettingSerializer, + 'tencent': serializers.TencentSMSSettingSerializer, + 'cmpp2': serializers.CMPP2SMSSettingSerializer + } + rbac_perms = { + 'POST': 'settings.change_sms' + } + + @staticmethod + def get_or_from_setting(key, value=''): + if not value: + secret = Setting.objects.filter(name=key).first() + if secret: + value = secret.cleaned_value + + return value or '' + + def get_alibaba_params(self, data): + init_params = { + 'access_key_id': data['ALIBABA_ACCESS_KEY_ID'], + 'access_key_secret': self.get_or_from_setting( + 'ALIBABA_ACCESS_KEY_SECRET', data.get('ALIBABA_ACCESS_KEY_SECRET') + ) + } + send_sms_params = { + 'sign_name': data['ALIBABA_VERIFY_SIGN_NAME'], + 'template_code': data['ALIBABA_VERIFY_TEMPLATE_CODE'], + 'template_param': {'code': '666666'} + } + return init_params, send_sms_params + + def get_tencent_params(self, data): + init_params = { + 'secret_id': data['TENCENT_SECRET_ID'], + 'secret_key': self.get_or_from_setting( + 'TENCENT_SECRET_KEY', data.get('TENCENT_SECRET_KEY') + ), + 'sdkappid': data['TENCENT_SDKAPPID'] + } + send_sms_params = { + 'sign_name': data['TENCENT_VERIFY_SIGN_NAME'], + 'template_code': data['TENCENT_VERIFY_TEMPLATE_CODE'], + 'template_param': OrderedDict(code='666666') + } + return init_params, send_sms_params + + def get_cmpp2_params(self, data): + init_params = { + 'host': data['CMPP2_HOST'], 'port': data['CMPP2_PORT'], + 'sp_id': data['CMPP2_SP_ID'], 'src_id': data['CMPP2_SRC_ID'], + 'sp_secret': self.get_or_from_setting( + 'CMPP2_SP_SECRET', data.get('CMPP2_SP_SECRET') + ), + 'service_id': data['CMPP2_SERVICE_ID'], + } + send_sms_params = { + 'sign_name': data['CMPP2_VERIFY_SIGN_NAME'], + 'template_code': data['CMPP2_VERIFY_TEMPLATE_CODE'], + 'template_param': OrderedDict(code='666666') + } + return init_params, send_sms_params + + def get_params_by_backend(self, backend, data): + """ + 返回两部分参数 + 1、实例化参数 + 2、发送测试短信参数 + """ + get_params_func = getattr(self, 'get_%s_params' % backend) + return get_params_func(data) + + def post(self, request, backend): + serializer_class = self.backends_serializer.get(backend) + if serializer_class is None: + raise JMSException(_('Invalid SMS platform')) + serializer = serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + + test_phone = serializer.validated_data.get('SMS_TEST_PHONE') + if not test_phone: + raise JMSException(code='test_phone_required', detail=_('test_phone is required')) + + init_params, send_sms_params = self.get_params_by_backend(backend, serializer.validated_data) + + m = importlib.import_module(f'common.sdk.sms.{backend}', __package__) + try: + client = m.client(**init_params) + client.send_sms( + phone_numbers=[test_phone], + **send_sms_params + ) + status_code = status.HTTP_200_OK + data = {'msg': _('Test success')} + except APIException as e: + try: + error = e.detail['errmsg'] + except: + error = e.detail + status_code = status.HTTP_400_BAD_REQUEST + data = {'error': error} + except Exception as e: + status_code = status.HTTP_400_BAD_REQUEST + data = {'error': str(e)} + return Response(status=status_code, data=data) diff --git a/apps/settings/api/tencent_sms.py b/apps/settings/api/tencent_sms.py deleted file mode 100644 index 83a87a474..000000000 --- a/apps/settings/api/tencent_sms.py +++ /dev/null @@ -1,63 +0,0 @@ -from collections import OrderedDict - -from rest_framework.views import Response -from rest_framework.generics import GenericAPIView -from rest_framework.exceptions import APIException -from rest_framework import status -from django.utils.translation import gettext_lazy as _ - -from common.sdk.sms.tencent import TencentSMS -from settings.models import Setting -from common.exceptions import JMSException - -from .. import serializers - - -class TencentSMSTestingAPI(GenericAPIView): - serializer_class = serializers.TencentSMSSettingSerializer - rbac_perms = { - 'POST': 'settings.change_sms' - } - - def post(self, request): - serializer = self.serializer_class(data=request.data) - serializer.is_valid(raise_exception=True) - - tencent_secret_id = serializer.validated_data['TENCENT_SECRET_ID'] - tencent_secret_key = serializer.validated_data.get('TENCENT_SECRET_KEY') - tencent_verify_sign_name = serializer.validated_data['TENCENT_VERIFY_SIGN_NAME'] - tencent_verify_template_code = serializer.validated_data['TENCENT_VERIFY_TEMPLATE_CODE'] - tencent_sdkappid = serializer.validated_data.get('TENCENT_SDKAPPID') - - test_phone = serializer.validated_data.get('SMS_TEST_PHONE') - - if not test_phone: - raise JMSException(code='test_phone_required', detail=_('test_phone is required')) - - if not tencent_secret_key: - secret = Setting.objects.filter(name='TENCENT_SECRET_KEY').first() - if secret: - tencent_secret_key = secret.cleaned_value - - tencent_secret_key = tencent_secret_key or '' - - try: - client = TencentSMS( - secret_id=tencent_secret_id, - secret_key=tencent_secret_key, - sdkappid=tencent_sdkappid - ) - - client.send_sms( - phone_numbers=[test_phone], - sign_name=tencent_verify_sign_name, - template_code=tencent_verify_template_code, - template_param=OrderedDict(code='666666') - ) - return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')}) - except APIException as e: - try: - error = e.detail['errmsg'] - except: - error = e.detail - return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error}) diff --git a/apps/settings/models.py b/apps/settings/models.py index 4546f9624..8b91765cf 100644 --- a/apps/settings/models.py +++ b/apps/settings/models.py @@ -1,9 +1,13 @@ +import os import json from django.db import models from django.db.utils import ProgrammingError, OperationalError from django.utils.translation import ugettext_lazy as _ from django.conf import settings +from django.core.files.storage import default_storage +from django.core.files.base import ContentFile +from django.core.files.uploadedfile import InMemoryUploadedFile from common.utils import signer, get_logger @@ -118,6 +122,14 @@ class Setting(models.Model): setattr(settings, key, value) self.__class__.update_or_create(key, value, encrypted=False, category=self.category) + @classmethod + def save_to_file(cls, value: InMemoryUploadedFile): + filename = value.name + filepath = f'settings/{filename}' + path = default_storage.save(filepath, ContentFile(value.read())) + url = default_storage.url(path) + return url + @classmethod def update_or_create(cls, name='', value='', encrypted=False, category=''): """ @@ -128,6 +140,10 @@ class Setting(models.Model): changed = False if not setting: setting = Setting(name=name, encrypted=encrypted, category=category) + + if isinstance(value, InMemoryUploadedFile): + value = cls.save_to_file(value) + if setting.cleaned_value != value: setting.encrypted = encrypted setting.cleaned_value = value diff --git a/apps/settings/serializers/auth/__init__.py b/apps/settings/serializers/auth/__init__.py index c675f4070..1f9f360f9 100644 --- a/apps/settings/serializers/auth/__init__.py +++ b/apps/settings/serializers/auth/__init__.py @@ -9,3 +9,4 @@ from .sso import * from .base import * from .sms import * from .saml2 import * +from .oauth2 import * diff --git a/apps/settings/serializers/auth/oauth2.py b/apps/settings/serializers/auth/oauth2.py new file mode 100644 index 000000000..2c620d331 --- /dev/null +++ b/apps/settings/serializers/auth/oauth2.py @@ -0,0 +1,55 @@ + +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from common.drf.fields import EncryptedField +from common.utils import static_or_direct + +__all__ = [ + 'OAuth2SettingSerializer', +] + + +class SettingImageField(serializers.ImageField): + def to_representation(self, value): + return static_or_direct(value) + + +class OAuth2SettingSerializer(serializers.Serializer): + AUTH_OAUTH2 = serializers.BooleanField( + default=False, required=False, label=_('Enable OAuth2 Auth') + ) + AUTH_OAUTH2_LOGO_PATH = SettingImageField( + allow_null=True, required=False, label=_('Logo') + ) + AUTH_OAUTH2_PROVIDER = serializers.CharField( + required=False, max_length=16, label=_('Service provider') + ) + AUTH_OAUTH2_CLIENT_ID = serializers.CharField( + required=False, max_length=1024, label=_('Client Id') + ) + AUTH_OAUTH2_CLIENT_SECRET = EncryptedField( + required=False, max_length=1024, label=_('Client Secret') + ) + AUTH_OAUTH2_SCOPE = serializers.CharField( + required=False, max_length=1024, label=_('Scope'), allow_blank=True + ) + AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT = serializers.CharField( + required=False, max_length=1024, label=_('Provider auth endpoint') + ) + AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT = serializers.CharField( + required=False, max_length=1024, label=_('Provider token endpoint') + ) + AUTH_OAUTH2_ACCESS_TOKEN_METHOD = serializers.ChoiceField( + default='GET', label=_('Client authentication method'), + choices=(('GET', 'GET'), ('POST', 'POST')) + ) + AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT = serializers.CharField( + required=False, max_length=1024, label=_('Provider userinfo endpoint') + ) + AUTH_OAUTH2_USER_ATTR_MAP = serializers.DictField( + required=False, label=_('User attr map') + ) + AUTH_OAUTH2_ALWAYS_UPDATE_USER = serializers.BooleanField( + required=False, label=_('Always update user') + ) diff --git a/apps/settings/serializers/auth/sms.py b/apps/settings/serializers/auth/sms.py index cd3bef74c..d3f96b33f 100644 --- a/apps/settings/serializers/auth/sms.py +++ b/apps/settings/serializers/auth/sms.py @@ -4,13 +4,16 @@ from rest_framework import serializers from common.drf.fields import EncryptedField from common.sdk.sms import BACKENDS -__all__ = ['SMSSettingSerializer', 'AlibabaSMSSettingSerializer', 'TencentSMSSettingSerializer'] +__all__ = [ + 'SMSSettingSerializer', 'AlibabaSMSSettingSerializer', 'TencentSMSSettingSerializer', + 'CMPP2SMSSettingSerializer' +] class SMSSettingSerializer(serializers.Serializer): SMS_ENABLED = serializers.BooleanField(default=False, label=_('Enable SMS')) SMS_BACKEND = serializers.ChoiceField( - choices=BACKENDS.choices, default=BACKENDS.ALIBABA, label=_('SMS provider') + choices=BACKENDS.choices, default=BACKENDS.ALIBABA, label=_('SMS provider / Protocol') ) @@ -43,3 +46,29 @@ class TencentSMSSettingSerializer(BaseSMSSettingSerializer): TENCENT_SDKAPPID = serializers.CharField(max_length=256, required=True, label='SDK app id') TENCENT_VERIFY_SIGN_NAME = serializers.CharField(max_length=256, required=True, label=_('Signature')) TENCENT_VERIFY_TEMPLATE_CODE = serializers.CharField(max_length=256, required=True, label=_('Template code')) + + +class CMPP2SMSSettingSerializer(BaseSMSSettingSerializer): + CMPP2_HOST = serializers.CharField(max_length=256, required=True, label=_('Host')) + CMPP2_PORT = serializers.IntegerField(default=7890, label=_('Port')) + CMPP2_SP_ID = serializers.CharField(max_length=128, required=True, label=_('Enterprise code(SP id)')) + CMPP2_SP_SECRET = EncryptedField(max_length=256, required=False, label=_('Shared secret(Shared secret)')) + CMPP2_SRC_ID = serializers.CharField(max_length=256, required=False, label=_('Original number(Src id)')) + CMPP2_SERVICE_ID = serializers.CharField(max_length=256, required=True, label=_('Business type(Service id)')) + CMPP2_VERIFY_SIGN_NAME = serializers.CharField(max_length=256, required=True, label=_('Signature')) + CMPP2_VERIFY_TEMPLATE_CODE = serializers.CharField( + max_length=69, required=True, label=_('Template'), + help_text=_('Template need contain {code} and Signature + template length does not exceed 67 words. ' + 'For example, your verification code is {code}, which is valid for 5 minutes. ' + 'Please do not disclose it to others.') + ) + + def validate(self, attrs): + sign_name = attrs.get('CMPP2_VERIFY_SIGN_NAME', '') + template_code = attrs.get('CMPP2_VERIFY_TEMPLATE_CODE', '') + if template_code.find('{code}') == -1: + raise serializers.ValidationError(_('The template needs to contain {code}')) + if len(sign_name + template_code) > 65: + # 保证验证码内容在一条短信中(长度小于70字), 签名两边的括号和空格占3个字,再减去2个即可(验证码占用4个但占位符6个 + raise serializers.ValidationError(_('Signature + Template must not exceed 65 words')) + return attrs diff --git a/apps/settings/serializers/other.py b/apps/settings/serializers/other.py index 7d701756c..775e26dd7 100644 --- a/apps/settings/serializers/other.py +++ b/apps/settings/serializers/other.py @@ -30,6 +30,11 @@ class OtherSettingSerializer(serializers.Serializer): help_text=_("Perm single to ungroup node") ) + TICKET_AUTHORIZE_DEFAULT_TIME = serializers.IntegerField( + min_value=7, max_value=9999, required=False, + label=_("Ticket authorize default time"), help_text=_("Unit: day") + ) + HELP_DOCUMENT_URL = serializers.URLField( required=False, allow_blank=True, allow_null=True, label=_("Help Docs URL"), help_text=_('default: http://docs.jumpserver.org') diff --git a/apps/settings/serializers/public.py b/apps/settings/serializers/public.py index 73b61a3bc..04e2b85af 100644 --- a/apps/settings/serializers/public.py +++ b/apps/settings/serializers/public.py @@ -14,6 +14,7 @@ class PublicSettingSerializer(serializers.Serializer): class PrivateSettingSerializer(PublicSettingSerializer): WINDOWS_SKIP_ALL_MANUAL_PASSWORD = serializers.BooleanField() OLD_PASSWORD_HISTORY_LIMIT_COUNT = serializers.IntegerField() + TICKET_AUTHORIZE_DEFAULT_TIME = serializers.IntegerField() SECURITY_MAX_IDLE_TIME = serializers.IntegerField() SECURITY_VIEW_AUTH_NEED_MFA = serializers.BooleanField() SECURITY_MFA_VERIFY_TTL = serializers.IntegerField() diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py index 7baa19196..3152a5ef5 100644 --- a/apps/settings/serializers/settings.py +++ b/apps/settings/serializers/settings.py @@ -7,7 +7,7 @@ from .auth import ( LDAPSettingSerializer, OIDCSettingSerializer, KeycloakSettingSerializer, CASSettingSerializer, RadiusSettingSerializer, FeiShuSettingSerializer, WeComSettingSerializer, DingTalkSettingSerializer, AlibabaSMSSettingSerializer, - TencentSMSSettingSerializer, + TencentSMSSettingSerializer, CMPP2SMSSettingSerializer ) from .terminal import TerminalSettingSerializer from .security import SecuritySettingSerializer @@ -37,6 +37,7 @@ class SettingsSerializer( CleaningSerializer, AlibabaSMSSettingSerializer, TencentSMSSettingSerializer, + CMPP2SMSSettingSerializer, ): # encrypt_fields 现在使用 write_only 来判断了 pass diff --git a/apps/settings/urls/api_urls.py b/apps/settings/urls/api_urls.py index 728baf0ae..ba04f2a4b 100644 --- a/apps/settings/urls/api_urls.py +++ b/apps/settings/urls/api_urls.py @@ -16,8 +16,7 @@ urlpatterns = [ path('wecom/testing/', api.WeComTestingAPI.as_view(), name='wecom-testing'), path('dingtalk/testing/', api.DingTalkTestingAPI.as_view(), name='dingtalk-testing'), path('feishu/testing/', api.FeiShuTestingAPI.as_view(), name='feishu-testing'), - path('alibaba/testing/', api.AlibabaSMSTestingAPI.as_view(), name='alibaba-sms-testing'), - path('tencent/testing/', api.TencentSMSTestingAPI.as_view(), name='tencent-sms-testing'), + path('sms//testing/', api.SMSTestingAPI.as_view(), name='sms-testing'), path('sms/backend/', api.SMSBackendAPI.as_view(), name='sms-backend'), path('setting/', api.SettingsApi.as_view(), name='settings-setting'), diff --git a/apps/settings/urls/ws_urls.py b/apps/settings/urls/ws_urls.py new file mode 100644 index 000000000..b1555c957 --- /dev/null +++ b/apps/settings/urls/ws_urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from .. import ws + +app_name = 'common' + +urlpatterns = [ + path('ws/setting/tools/', ws.ToolsWebsocket.as_asgi(), name='setting-tools-ws'), +] diff --git a/apps/settings/utils/__init__.py b/apps/settings/utils/__init__.py index e17c4e43c..0927bde18 100644 --- a/apps/settings/utils/__init__.py +++ b/apps/settings/utils/__init__.py @@ -3,3 +3,5 @@ from .ldap import * from .common import * +from .ping import * +from .telnet import * diff --git a/apps/settings/utils/ping.py b/apps/settings/utils/ping.py new file mode 100644 index 000000000..409edc83a --- /dev/null +++ b/apps/settings/utils/ping.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +# + +import os +import select +import socket +import struct +import time + +# From /usr/include/linux/icmp.h; your milage may vary. +ICMP_ECHO_REQUEST = 8 # Seems to be the same on Solaris. + + +def checksum(source_string): + """ + I'm not too confident that this is right but testing seems + to suggest that it gives the same answers as in_cksum in ping.c + """ + sum = 0 + count_to = int((len(source_string) / 2) * 2) + for count in range(0, count_to, 2): + this = source_string[count + 1] * 256 + source_string[count] + sum = sum + this + sum = sum & 0xffffffff # Necessary? + + if count_to < len(source_string): + sum = sum + ord(source_string[len(source_string) - 1]) + sum = sum & 0xffffffff # Necessary? + + sum = (sum >> 16) + (sum & 0xffff) + sum = sum + (sum >> 16) + answer = ~sum + answer = answer & 0xffff + + # Swap bytes. Bugger me if I know why. + answer = answer >> 8 | (answer << 8 & 0xff00) + + return answer + + +def receive_one_ping(my_socket, id, timeout): + """ + Receive the ping from the socket. + """ + time_left = timeout + while True: + started_select = time.time() + what_ready = select.select([my_socket], [], [], time_left) + how_long_in_select = time.time() - started_select + if not what_ready[0]: # Timeout + return + + time_received = time.time() + received_packet, addr = my_socket.recvfrom(1024) + icmpHeader = received_packet[20:28] + type, code, checksum, packet_id, sequence = struct.unpack("bbHHh", icmpHeader) + if packet_id == id: + bytes = struct.calcsize("d") + time_sent = struct.unpack("d", received_packet[28: 28 + bytes])[0] + return time_received - time_sent + + time_left = time_left - how_long_in_select + if time_left <= 0: + return + + +def send_one_ping(my_socket, dest_addr, id, psize): + """ + Send one ping to the given >dest_addr<. + """ + dest_addr = socket.gethostbyname(dest_addr) + + # Remove header size from packet size + # psize = psize - 8 + # laixintao edit: + # Do not need to remove header here. From BSD ping man: + # The default is 56, which translates into 64 ICMP data + # bytes when combined with the 8 bytes of ICMP header data. + + # Header is type (8), code (8), checksum (16), id (16), sequence (16) + my_checksum = 0 + + # Make a dummy heder with a 0 checksum. + header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, my_checksum, id, 1) + bytes = struct.calcsize("d") + data = (psize - bytes) * b"Q" + data = struct.pack("d", time.time()) + data + + # Calculate the checksum on the data and the dummy header. + my_checksum = checksum(header + data) + + # Now that we have the right checksum, we put that in. It's just easier + # to make up a new header than to stuff it into the dummy. + header = struct.pack( + "bbHHh", ICMP_ECHO_REQUEST, 0, socket.htons(my_checksum), id, 1 + ) + packet = header + data + my_socket.sendto(packet, (dest_addr, 1)) # Don't know about the 1 + + +def ping(dest_addr, timeout, psize, flag=0): + """ + Returns either the delay (in seconds) or none on timeout. + """ + icmp = socket.getprotobyname("icmp") + try: + if os.getuid() != 0: + my_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, icmp) + else: + my_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp) + except socket.error as e: + if e.errno == 1: + # Operation not permitted + msg = str(e) + raise socket.error(msg) + raise # raise the original error + + process_pre = os.getpid() & 0xFF00 + flag = flag & 0x00FF + my_id = process_pre | flag + + send_one_ping(my_socket, dest_addr, my_id, psize) + delay = receive_one_ping(my_socket, my_id, timeout) + + my_socket.close() + return delay + + +def verbose_ping(dest_addr, timeout=2, count=5, psize=64): + """ + Send `count' ping with `psize' size to `dest_addr' with + the given `timeout' and display the result. + """ + for i in range(count): + print("ping %s with ..." % dest_addr, end="") + try: + delay = ping(dest_addr, timeout, psize) + except socket.gaierror as e: + print("failed. (socket error: '%s')" % str(e)) + break + + if delay is None: + print("failed. (timeout within %ssec.)" % timeout) + else: + delay = delay * 1000 + print("get ping in %0.4fms" % delay) + print() + + +if __name__ == "__main__": + verbose_ping("google.com") + verbose_ping("192.168.4.1") + verbose_ping("www.baidu.com") + verbose_ping("sssssss") diff --git a/apps/settings/utils/telnet.py b/apps/settings/utils/telnet.py new file mode 100644 index 000000000..9785b43ae --- /dev/null +++ b/apps/settings/utils/telnet.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# +import socket +import telnetlib + +PROMPT_REGEX = r'[\<|\[](.*)[\>|\]]' + + +def telnet(dest_addr, port_number=23, timeout=10): + try: + connection = telnetlib.Telnet(dest_addr, port_number, timeout) + except (ConnectionRefusedError, socket.timeout, socket.gaierror) as e: + return False, str(e) + expected_regexes = [bytes(PROMPT_REGEX, encoding='ascii')] + index, prompt_regex, output = connection.expect(expected_regexes, timeout=3) + return True, output.decode('ascii') + + +if __name__ == "__main__": + print(telnet(dest_addr='1.1.1.1', port_number=2222)) + print(telnet(dest_addr='baidu.com', port_number=80)) + print(telnet(dest_addr='baidu.com', port_number=8080)) + print(telnet(dest_addr='192.168.4.1', port_number=2222)) + print(telnet(dest_addr='192.168.4.1', port_number=2223)) + print(telnet(dest_addr='ssssss', port_number=-1)) diff --git a/apps/settings/ws.py b/apps/settings/ws.py new file mode 100644 index 000000000..3455abe2b --- /dev/null +++ b/apps/settings/ws.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# + +import json + +from channels.generic.websocket import JsonWebsocketConsumer + +from common.db.utils import close_old_connections +from common.utils import get_logger +from .utils import ping, telnet + +logger = get_logger(__name__) + + +class ToolsWebsocket(JsonWebsocketConsumer): + + def connect(self): + user = self.scope["user"] + if user.is_authenticated: + self.accept() + else: + self.close() + + def imitate_ping(self, dest_addr, timeout=3, count=5, psize=64): + """ + Send `count' ping with `psize' size to `dest_addr' with + the given `timeout' and display the result. + """ + logger.info('receive request ping {}'.format(dest_addr)) + self.send_json({'msg': 'Trying {0}...\r\n'.format(dest_addr)}) + for i in range(count): + msg = 'ping {0} with ...{1}\r\n' + try: + delay = ping(dest_addr, timeout, psize) + except Exception as e: + msg = msg.format(dest_addr, 'failed. (socket error: {})'.format(str(e))) + logger.error(msg) + self.send_json({'msg': msg}) + break + if delay is None: + msg = msg.format(dest_addr, 'failed. (timeout within {}sec.)'.format(timeout)) + else: + delay = delay * 1000 + msg = msg.format(dest_addr, 'get ping in %0.4fms' % delay) + self.send_json({'msg': msg}) + + def imitate_telnet(self, dest_addr, port_num=23, timeout=10): + logger.info('receive request telnet {}'.format(dest_addr)) + self.send_json({'msg': 'Trying {0} {1}...\r\n'.format(dest_addr, port_num)}) + msg = 'Telnet: {}' + try: + is_connective, resp = telnet(dest_addr, port_num, timeout) + if is_connective: + msg = msg.format('Connected to {0} {1}\r\n{2}'.format(dest_addr, port_num, resp)) + else: + msg = msg.format('Connect to {0} {1} {2}\r\nTelnet: Unable to connect to remote host' + .format(dest_addr, port_num, resp)) + except Exception as e: + logger.error(msg) + msg = msg.format(str(e)) + finally: + self.send_json({'msg': msg}) + + def receive(self, text_data=None, bytes_data=None, **kwargs): + data = json.loads(text_data) + tool_type = data.get('tool_type', 'Ping') + dest_addr = data.get('dest_addr') + if tool_type == 'Ping': + self.imitate_ping(dest_addr) + else: + port_num = data.get('port_num') + self.imitate_telnet(dest_addr, port_num) + self.close() + + def disconnect(self, code): + self.close() + close_old_connections() diff --git a/apps/static/img/login_oauth2_logo.png b/apps/static/img/login_oauth2_logo.png new file mode 100644 index 000000000..b1cf562b0 Binary files /dev/null and b/apps/static/img/login_oauth2_logo.png differ diff --git a/apps/static/js/jumpserver.js b/apps/static/js/jumpserver.js index e1e6707b2..3edf445b0 100644 --- a/apps/static/js/jumpserver.js +++ b/apps/static/js/jumpserver.js @@ -1504,17 +1504,11 @@ function getStatusIcon(status, mapping, title) { function fillKey(key) { - let keySize = 128 - // 如果超过 key 16 位, 最大取 32 位,需要更改填充 - if (key.length > 16) { - key = key.slice(0, 32) - keySize = keySize * 2 + const KeyLength = 16 + if (key.length > KeyLength) { + key = key.slice(0, KeyLength) } - const filledKeyLength = keySize / 8 - if (key.length >= filledKeyLength) { - return key.slice(0, filledKeyLength) - } - const filledKey = Buffer.alloc(keySize / 8) + const filledKey = Buffer.alloc(KeyLength) const keys = Buffer.from(key) for (let i = 0; i < keys.length; i++) { filledKey[i] = keys[i] diff --git a/apps/tickets/api/super_ticket.py b/apps/tickets/api/super_ticket.py index ea186bd1b..32c4a56c0 100644 --- a/apps/tickets/api/super_ticket.py +++ b/apps/tickets/api/super_ticket.py @@ -20,4 +20,4 @@ class SuperTicketStatusAPI(RetrieveDestroyAPIView): return Ticket.objects.all() def perform_destroy(self, instance): - instance.close(processor=instance.applicant) + instance.close() diff --git a/apps/tickets/handlers/apply_application.py b/apps/tickets/handlers/apply_application.py index 5b7712553..287c70c4a 100644 --- a/apps/tickets/handlers/apply_application.py +++ b/apps/tickets/handlers/apply_application.py @@ -1,6 +1,6 @@ from django.utils.translation import ugettext as _ -from orgs.utils import tmp_to_org, tmp_to_root_org +from orgs.utils import tmp_to_org from perms.models import ApplicationPermission from tickets.models import ApplyApplicationTicket from .base import BaseHandler @@ -26,6 +26,7 @@ class Handler(BaseHandler): apply_system_users = self.ticket.apply_system_users.all() apply_permission_name = self.ticket.apply_permission_name + apply_actions = self.ticket.apply_actions apply_category = self.ticket.apply_category apply_type = self.ticket.apply_type apply_date_start = self.ticket.apply_date_start @@ -50,6 +51,7 @@ class Handler(BaseHandler): 'name': apply_permission_name, 'from_ticket': True, 'category': apply_category, + 'actions': apply_actions, 'type': apply_type, 'comment': str(permission_comment), 'created_by': permission_created_by, diff --git a/apps/tickets/migrations/0018_applyapplicationticket_apply_actions.py b/apps/tickets/migrations/0018_applyapplicationticket_apply_actions.py new file mode 100644 index 000000000..74f5ed7a3 --- /dev/null +++ b/apps/tickets/migrations/0018_applyapplicationticket_apply_actions.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.14 on 2022-07-22 08:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0017_auto_20220623_1027'), + ] + + operations = [ + migrations.AddField( + model_name='applyapplicationticket', + name='apply_actions', + field=models.IntegerField(choices=[(255, 'All'), (1, 'Connect'), (2, 'Upload file'), (4, 'Download file'), (6, 'Upload download'), (8, 'Clipboard copy'), (16, 'Clipboard paste'), (24, 'Clipboard copy paste')], default=255, verbose_name='Actions'), + ), + ] diff --git a/apps/tickets/models/ticket/apply_application.py b/apps/tickets/models/ticket/apply_application.py index 6bd721677..378047db2 100644 --- a/apps/tickets/models/ticket/apply_application.py +++ b/apps/tickets/models/ticket/apply_application.py @@ -1,8 +1,9 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from .general import Ticket +from perms.models import Action from applications.const import AppCategory, AppType +from .general import Ticket __all__ = ['ApplyApplicationTicket'] @@ -22,6 +23,9 @@ class ApplyApplicationTicket(Ticket): apply_system_users = models.ManyToManyField( 'assets.SystemUser', verbose_name=_('Apply system users'), ) + apply_actions = models.IntegerField( + choices=Action.DB_CHOICES, default=Action.ALL, verbose_name=_('Actions') + ) apply_date_start = models.DateTimeField(verbose_name=_('Date start'), null=True) apply_date_expired = models.DateTimeField(verbose_name=_('Date expired'), null=True) @@ -32,3 +36,10 @@ class ApplyApplicationTicket(Ticket): @property def apply_type_display(self): return AppType.get_label(self.apply_type) + + @property + def apply_actions_display(self): + return Action.value_to_choices_display(self.apply_actions) + + def get_apply_actions_display(self): + return ', '.join(self.apply_actions_display) diff --git a/apps/tickets/serializers/ticket/apply_application.py b/apps/tickets/serializers/ticket/apply_application.py index c713f21d6..12b3f230d 100644 --- a/apps/tickets/serializers/ticket/apply_application.py +++ b/apps/tickets/serializers/ticket/apply_application.py @@ -2,6 +2,7 @@ from django.utils.translation import ugettext as _ from rest_framework import serializers from perms.models import ApplicationPermission +from perms.serializers.base import ActionsField from orgs.utils import tmp_to_org from applications.models import Application from tickets.models import ApplyApplicationTicket @@ -12,6 +13,7 @@ __all__ = ['ApplyApplicationSerializer', 'ApplyApplicationDisplaySerializer', 'A class ApplyApplicationSerializer(BaseApplyAssetApplicationSerializer, TicketApplySerializer): + apply_actions = ActionsField(required=True, allow_empty=False) permission_model = ApplicationPermission class Meta: @@ -19,9 +21,10 @@ class ApplyApplicationSerializer(BaseApplyAssetApplicationSerializer, TicketAppl writeable_fields = [ 'id', 'title', 'type', 'apply_category', 'apply_type', 'apply_applications', 'apply_system_users', - 'apply_date_start', 'apply_date_expired', 'org_id' + 'apply_actions', 'apply_date_start', 'apply_date_expired', 'org_id' ] - fields = TicketApplySerializer.Meta.fields + writeable_fields + ['apply_permission_name'] + fields = TicketApplySerializer.Meta.fields + \ + writeable_fields + ['apply_permission_name', 'apply_actions_display'] read_only_fields = list(set(fields) - set(writeable_fields)) ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs extra_kwargs = { diff --git a/apps/tickets/serializers/ticket/apply_asset.py b/apps/tickets/serializers/ticket/apply_asset.py index b8a007e8d..93a4026c1 100644 --- a/apps/tickets/serializers/ticket/apply_asset.py +++ b/apps/tickets/serializers/ticket/apply_asset.py @@ -23,10 +23,11 @@ class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySeria model = ApplyAssetTicket writeable_fields = [ 'id', 'title', 'type', 'apply_nodes', 'apply_assets', - 'apply_system_users', 'apply_actions', 'apply_actions_display', + 'apply_system_users', 'apply_actions', 'apply_date_start', 'apply_date_expired', 'org_id' ] - fields = TicketApplySerializer.Meta.fields + writeable_fields + ['apply_permission_name'] + fields = TicketApplySerializer.Meta.fields + \ + writeable_fields + ['apply_permission_name', 'apply_actions_display'] read_only_fields = list(set(fields) - set(writeable_fields)) ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs extra_kwargs = { diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 2fd33aa08..9fd6ea0b9 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -628,6 +628,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): radius = 'radius', 'Radius' cas = 'cas', 'CAS' saml2 = 'saml2', 'SAML2' + oauth2 = 'oauth2', 'OAuth2' SOURCE_BACKEND_MAPPING = { Source.local: [ @@ -652,6 +653,9 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): Source.saml2: [ settings.AUTH_BACKEND_SAML2 ], + Source.oauth2: [ + settings.AUTH_BACKEND_OAUTH2 + ], } id = models.UUIDField(default=uuid.uuid4, primary_key=True) diff --git a/apps/users/signal_handlers.py b/apps/users/signal_handlers.py index bd9b01845..bcf50173a 100644 --- a/apps/users/signal_handlers.py +++ b/apps/users/signal_handlers.py @@ -9,6 +9,7 @@ from django.db.models.signals import post_save from authentication.backends.oidc.signals import openid_create_or_update_user from authentication.backends.saml2.signals import saml2_create_or_update_user +from authentication.backends.oauth2.signals import oauth2_create_or_update_user from common.utils import get_logger from common.decorator import on_transaction_commit from .signals import post_user_create @@ -26,16 +27,18 @@ def user_authenticated_handle(user, created, source, attrs=None, **kwargs): user.source = source user.save() - if not created and settings.AUTH_SAML2_ALWAYS_UPDATE_USER: + if not attrs: + return + + always_update = getattr(settings, 'AUTH_%s_ALWAYS_UPDATE_USER' % source.upper(), False) + if not created and always_update: attr_whitelist = ('user', 'username', 'email', 'phone', 'comment') logger.debug( - "Receive saml2 user updated signal: {}, " + "Receive {} user updated signal: {}, " "Update user info: {}," "(Update only properties in the whitelist. [{}])" - "".format(user, str(attrs), ','.join(attr_whitelist)) + "".format(source, user, str(attrs), ','.join(attr_whitelist)) ) - if not attrs: - return for key, value in attrs.items(): if key in attr_whitelist and value: setattr(user, key, value) @@ -103,6 +106,12 @@ def on_saml2_create_or_update_user(sender, user, created, attrs, **kwargs): user_authenticated_handle(user, created, source, attrs, **kwargs) +@receiver(oauth2_create_or_update_user) +def on_oauth2_create_or_update_user(sender, user, created, attrs, **kwargs): + source = user.Source.oauth2.value + user_authenticated_handle(user, created, source, attrs, **kwargs) + + @receiver(populate_user) def on_ldap_create_user(sender, user, ldap_user, **kwargs): if user and user.username not in ['admin']: diff --git a/apps/users/templates/users/forgot_password.html b/apps/users/templates/users/forgot_password.html index 8289726f3..16c784250 100644 --- a/apps/users/templates/users/forgot_password.html +++ b/apps/users/templates/users/forgot_password.html @@ -6,6 +6,7 @@