From 3e541162e34e7a3e007b1d0f41481c13535008c2 Mon Sep 17 00:00:00 2001 From: Bai Date: Mon, 20 Jul 2020 18:49:05 +0800 Subject: [PATCH 01/40] =?UTF-8?q?fix(authentication):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=AE=A4=E8=AF=81Radius=E8=AE=A4=E8=AF=81?= =?UTF-8?q?=E4=B8=AD=E5=8F=82=E6=95=B0=E4=BC=A0=E9=80=92=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E9=97=AE=E9=A2=98=20*kwargs=20->=20**kwargs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/backends/radius.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/authentication/backends/radius.py b/apps/authentication/backends/radius.py index 6e39b2b79..e45fd8033 100644 --- a/apps/authentication/backends/radius.py +++ b/apps/authentication/backends/radius.py @@ -31,7 +31,7 @@ class CreateUserMixin: # 校验用户时,会传入public_key参数,父类authentication中不接受public_key参数,所以要pop掉 # TODO:需要优化各backend的authenticate方法,django进行调用前会检测各authenticate的参数 kwargs.pop('public_key', None) - return super().authenticate(*args, *kwargs) + return super().authenticate(*args, **kwargs) class RadiusBackend(CreateUserMixin, RADIUSBackend): From 31ba0564e42e0e38831f0b08757587738a361bda Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 21 Jul 2020 13:13:02 +0800 Subject: [PATCH 02/40] =?UTF-8?q?ci(github):=20=E6=B7=BB=E5=8A=A0=E9=80=9A?= =?UTF-8?q?=E7=94=A8action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/jms-generic-action-handler.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/workflows/jms-generic-action-handler.yml diff --git a/.github/workflows/jms-generic-action-handler.yml b/.github/workflows/jms-generic-action-handler.yml new file mode 100644 index 000000000..3f499cfb9 --- /dev/null +++ b/.github/workflows/jms-generic-action-handler.yml @@ -0,0 +1,12 @@ +on: [push, pull_request, release] + +name: JumpServer repos generic handler + +jobs: + generic_handler: + name: Run generic handler + runs-on: ubuntu-latest + steps: + - uses: jumpserver/action-generic-handler@master + env: + GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }} From 5d08438dade4ff765c41ac8b44f4e6dc079283e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=B9=BF?= Date: Tue, 21 Jul 2020 15:33:33 +0800 Subject: [PATCH 03/40] =?UTF-8?q?feat(ops):=20=E9=A1=B9=E7=9B=AE=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E6=97=B6=EF=BC=8C=E6=B8=85=E9=99=A4=E6=8C=87=E5=AE=9A?= =?UTF-8?q?=E7=9A=84celery=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1;=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E8=8E=B7=E5=8F=96celery=E5=AE=9A=E6=97=B6=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E7=9A=84=E5=87=BD=E6=95=B0=20(#4378)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(ops): 添加获取celery定时任务的函数 * feat(ops): 项目启动时,清除指定的celery定时任务 * feat(ops): 项目启动时,清除指定的celery定时任务 2 Co-authored-by: Bai --- apps/ops/celery/utils.py | 6 ++++++ apps/ops/tasks.py | 28 +++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/apps/ops/celery/utils.py b/apps/ops/celery/utils.py index 55ce4a9d1..0c758b70e 100644 --- a/apps/ops/celery/utils.py +++ b/apps/ops/celery/utils.py @@ -93,6 +93,12 @@ def delete_celery_periodic_task(task_name): PeriodicTasks.update_changed() +def get_celery_periodic_task(task_name): + from django_celery_beat.models import PeriodicTask + task = PeriodicTask.objects.filter(name=task_name).first() + return task + + def get_celery_task_log_path(task_id): task_id = str(task_id) rel_path = os.path.join(task_id[0], task_id[1], task_id + '.log') diff --git a/apps/ops/tasks.py b/apps/ops/tasks.py index 7ea11a3e4..3419c8976 100644 --- a/apps/ops/tasks.py +++ b/apps/ops/tasks.py @@ -15,7 +15,10 @@ from .celery.decorator import ( register_as_period_task, after_app_shutdown_clean_periodic, after_app_ready_start ) -from .celery.utils import create_or_update_celery_periodic_tasks +from .celery.utils import ( + create_or_update_celery_periodic_tasks, get_celery_periodic_task, + disable_celery_periodic_task, delete_celery_periodic_task +) from .models import Task, CommandExecution, CeleryTask from .utils import send_server_performance_mail @@ -95,6 +98,29 @@ def clean_celery_tasks_period(): subprocess.call(command, shell=True) +@shared_task +@after_app_ready_start +def clean_celery_periodic_tasks(): + """清除celery定时任务""" + need_cleaned_tasks = [ + 'handle_be_interrupted_change_auth_task_periodic', + ] + logger.info('Start clean celery periodic tasks: {}'.format(need_cleaned_tasks)) + for task_name in need_cleaned_tasks: + logger.info('Start clean task: {}'.format(task_name)) + task = get_celery_periodic_task(task_name) + if task is None: + logger.info('Task does not exist: {}'.format(task_name)) + continue + disable_celery_periodic_task(task_name) + delete_celery_periodic_task(task_name) + task = get_celery_periodic_task(task_name) + if task is None: + logger.info('Clean task success: {}'.format(task_name)) + else: + logger.info('Clean task failure: {}'.format(task)) + + @shared_task @after_app_ready_start def create_or_update_registered_periodic_tasks(): From 1b71350199af74cdd8d33c87297fda8f49e76763 Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 22 Jul 2020 16:41:56 +0800 Subject: [PATCH 04/40] =?UTF-8?q?fix(es):=20=E4=BF=AE=E5=A4=8Des7=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E7=BB=93=E6=9E=84=E5=BC=95=E8=B5=B7=E7=9A=84=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E6=97=A0=E6=B3=95=E6=9F=A5=E8=AF=A2=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新了 jms-storage版本依赖 --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 932ab3b09..3a4897457 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -61,7 +61,7 @@ pytz==2018.3 PyYAML==5.1 redis==3.2.0 requests==2.22.0 -jms-storage==0.0.29 +jms-storage==0.0.31 s3transfer==0.3.3 simplejson==3.13.2 six==1.11.0 From 78089e01a36233bd662a08c7c9445aeb23f61db1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 22 Jul 2020 10:46:58 +0800 Subject: [PATCH 05/40] =?UTF-8?q?fix(cas):=20=E4=BF=AE=E5=A4=8Dcas?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E4=B8=8D=E5=90=8C=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/settings/auth.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index f96498e0e..ae31ba10d 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -92,6 +92,7 @@ CAS_LOGGED_MSG = None CAS_LOGOUT_COMPLETELY = CONFIG.CAS_LOGOUT_COMPLETELY CAS_VERSION = CONFIG.CAS_VERSION CAS_ROOT_PROXIED_AS = CONFIG.CAS_ROOT_PROXIED_AS +CAS_CHECK_NEXT = lambda: lambda _next_page: True # Other setting From 674ad40f67e5d14f9e9041adb2edf03804a37814 Mon Sep 17 00:00:00 2001 From: Bai Date: Thu, 23 Jul 2020 10:55:01 +0800 Subject: [PATCH 06/40] =?UTF-8?q?fix(perms):=20=E4=BF=AE=E5=A4=8Dperms=20a?= =?UTF-8?q?pi=20UserPermissionMixin=20=E4=B8=AD=20kwargs=20=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E4=BC=A0=E9=80=92=EF=BC=88=E6=9C=AA=E5=8F=91=E7=8E=B0?= =?UTF-8?q?=E5=BC=95=E5=87=BA=E5=85=B6=E4=BB=96=E9=97=AE=E9=A2=98=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/perms/api/mixin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/perms/api/mixin.py b/apps/perms/api/mixin.py index cbbfa825f..43c96dc01 100644 --- a/apps/perms/api/mixin.py +++ b/apps/perms/api/mixin.py @@ -21,7 +21,7 @@ class UserPermissionMixin: obj = None def initial(self, *args, **kwargs): - super().initial(*args, *kwargs) + super().initial(*args, **kwargs) self.obj = self.get_obj() def get_obj(self): From 2a53a20808792fdce86b74d79e18358cbc3c2be6 Mon Sep 17 00:00:00 2001 From: ibuler Date: Fri, 24 Jul 2020 10:25:19 +0800 Subject: [PATCH 07/40] =?UTF-8?q?perf(assets):=20=E4=BF=AE=E6=94=B9=20?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E7=94=A8=E6=88=B7=20=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E7=94=A8=E6=88=B7=20=E7=AD=89=E7=9A=84=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=90=8D=E9=95=BF=E5=BA=A6=E5=88=B0128?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0053_auto_20200723_1232.py | 34 +++++++++++++++++++ apps/assets/models/base.py | 2 +- .../migrations/0002_auto_20200723_1232.py | 18 ++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 apps/assets/migrations/0053_auto_20200723_1232.py create mode 100644 apps/tickets/migrations/0002_auto_20200723_1232.py diff --git a/apps/assets/migrations/0053_auto_20200723_1232.py b/apps/assets/migrations/0053_auto_20200723_1232.py new file mode 100644 index 000000000..7d1ba220d --- /dev/null +++ b/apps/assets/migrations/0053_auto_20200723_1232.py @@ -0,0 +1,34 @@ +# Generated by Django 2.2.10 on 2020-07-23 04:32 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0052_auto_20200715_1535'), + ] + + operations = [ + migrations.AlterField( + model_name='adminuser', + name='username', + field=models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'), + ), + migrations.AlterField( + model_name='authbook', + name='username', + field=models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'), + ), + migrations.AlterField( + model_name='gateway', + name='username', + field=models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'), + ), + migrations.AlterField( + model_name='systemuser', + name='username', + field=models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'), + ), + ] diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index 3c9a481a5..282c9b928 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -230,7 +230,7 @@ class AuthMixin: class BaseUser(OrgModelMixin, AuthMixin, ConnectivityMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, verbose_name=_('Name')) - username = models.CharField(max_length=32, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True) + username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True) password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password')) private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key')) public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key')) diff --git a/apps/tickets/migrations/0002_auto_20200723_1232.py b/apps/tickets/migrations/0002_auto_20200723_1232.py new file mode 100644 index 000000000..26d980a4a --- /dev/null +++ b/apps/tickets/migrations/0002_auto_20200723_1232.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2020-07-23 04:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='ticket', + name='type', + field=models.CharField(choices=[('general', 'General'), ('login_confirm', 'Login confirm'), ('request_asset', 'Request asset permission')], default='general', max_length=16, verbose_name='Type'), + ), + ] From c277aec561594235411622ce232028872c0e9ac8 Mon Sep 17 00:00:00 2001 From: xinwen Date: Fri, 24 Jul 2020 15:47:01 +0800 Subject: [PATCH 08/40] =?UTF-8?q?feat(authenticaion):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E9=A1=B5=E9=9D=A2=E9=AA=8C=E8=AF=81=E7=A0=81?= =?UTF-8?q?=E4=B8=8EMFA=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/forms.py | 22 +++++++-- apps/authentication/mixins.py | 48 ++++++++++++++----- .../templates/authentication/login.html | 10 ++++ .../templates/authentication/xpack_login.html | 16 +++++-- apps/authentication/utils.py | 33 ------------- apps/authentication/views/login.py | 12 ++--- apps/jumpserver/conf.py | 2 + apps/jumpserver/settings/custom.py | 2 + 8 files changed, 87 insertions(+), 58 deletions(-) diff --git a/apps/authentication/forms.py b/apps/authentication/forms.py index 84e923a8e..2f03d935b 100644 --- a/apps/authentication/forms.py +++ b/apps/authentication/forms.py @@ -2,6 +2,7 @@ # from django import forms +from django.conf import settings from django.utils.translation import gettext_lazy as _ from captcha.fields import CaptchaField @@ -21,9 +22,24 @@ class UserLoginForm(forms.Form): ) -class UserLoginCaptchaForm(UserLoginForm): +class UserCheckOtpCodeForm(forms.Form): + otp_code = forms.CharField(label=_('MFA code'), max_length=6) + + +class CaptchaMixin(forms.Form): captcha = CaptchaField() -class UserCheckOtpCodeForm(forms.Form): - otp_code = forms.CharField(label=_('MFA code'), max_length=6) +class ChallengeMixin(forms.Form): + challenge = forms.CharField(label=_('MFA code'), max_length=6, + required=False) + + +def get_user_login_form_cls(*, captcha=False): + bases = [] + if settings.SECURITY_LOGIN_CAPTCHA_ENABLED and captcha: + bases.append(CaptchaMixin) + if settings.SECURITY_LOGIN_CHALLENGE_ENABLED: + bases.append(ChallengeMixin) + bases.append(UserLoginForm) + return type('UserLoginForm', tuple(bases), {}) diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 5b3738c98..cdc7856bd 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- # +from functools import partial import time from django.conf import settings +from django.contrib.auth import authenticate from common.utils import get_object_or_none, get_request_ip, get_logger from users.models import User @@ -9,7 +11,7 @@ from users.utils import ( is_block_login, clean_failed_count ) from . import errors -from .utils import check_user_valid +from .utils import rsa_decrypt from .signals import post_auth_success, post_auth_failed logger = get_logger(__name__) @@ -54,21 +56,41 @@ class AuthMixin: self.check_is_block() request = self.request if hasattr(request, 'data'): - username = request.data.get('username', '') - password = request.data.get('password', '') - public_key = request.data.get('public_key', '') + data = request.data else: - username = request.POST.get('username', '') - password = request.POST.get('password', '') - public_key = request.POST.get('public_key', '') - user, error = check_user_valid( - request=request, username=username, password=password, public_key=public_key - ) + data = request.POST + username = data.get('username', '') + password = data.get('password', '') + challenge = data.get('challenge', '') + public_key = data.get('public_key', '') ip = self.get_request_ip() + + CredentialError = partial(errors.CredentialError, username=username, ip=ip, request=request) + + # 获取解密密钥,对密码进行解密 + rsa_private_key = request.session.get('rsa_private_key') + if rsa_private_key is not None: + try: + password = rsa_decrypt(password, rsa_private_key) + except Exception as e: + logger.error(e, exc_info=True) + logger.error('Need decrypt password => {}'.format(password)) + raise CredentialError(error=errors.reason_password_decrypt_failed) + + user = authenticate(request, + username=username, + password=password + challenge.strip(), + public_key=public_key) + if not user: - raise errors.CredentialError( - username=username, error=error, ip=ip, request=request - ) + raise CredentialError(error=errors.reason_password_failed) + elif user.is_expired: + raise CredentialError(error=errors.reason_user_inactive) + elif not user.is_active: + raise CredentialError(error=errors.reason_user_inactive) + elif user.password_has_expired: + raise CredentialError(error=errors.reason_password_expired) + clean_failed_count(username, ip) request.session['auth_password'] = 1 request.session['user_id'] = str(user.id) diff --git a/apps/authentication/templates/authentication/login.html b/apps/authentication/templates/authentication/login.html index 14978e426..a6dec7d9d 100644 --- a/apps/authentication/templates/authentication/login.html +++ b/apps/authentication/templates/authentication/login.html @@ -33,6 +33,16 @@ {% endif %} + {% if form.challenge %} +
+ + {% if form.errors.challenge %} +
+

{{ form.errors.challenge.as_text }}

+
+ {% endif %} +
+ {% endif %}
{{ form.captcha }}
diff --git a/apps/authentication/templates/authentication/xpack_login.html b/apps/authentication/templates/authentication/xpack_login.html index 5929650df..77edc3582 100644 --- a/apps/authentication/templates/authentication/xpack_login.html +++ b/apps/authentication/templates/authentication/xpack_login.html @@ -67,16 +67,16 @@
-
+
{{ JMS_TITLE }}
{% trans 'Welcome back, please enter username and password to login' %}
-
+
-
+
{% csrf_token %} {% if form.non_field_errors %} @@ -105,6 +105,16 @@
{% endif %}
+ {% if form.challenge %} +
+ + {% if form.errors.challenge %} +
+

{{ form.errors.challenge.as_text }}

+
+ {% endif %} +
+ {% endif %}
{{ form.captcha }}
diff --git a/apps/authentication/utils.py b/apps/authentication/utils.py index 359778cb6..cb697c237 100644 --- a/apps/authentication/utils.py +++ b/apps/authentication/utils.py @@ -4,12 +4,9 @@ import base64 from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 from Crypto import Random -from django.contrib.auth import authenticate from common.utils import get_logger -from . import errors - logger = get_logger(__file__) @@ -41,33 +38,3 @@ def rsa_decrypt(cipher_text, rsa_private_key=None): cipher = PKCS1_v1_5.new(key) message = cipher.decrypt(base64.b64decode(cipher_text.encode()), 'error').decode() return message - - -def check_user_valid(**kwargs): - password = kwargs.pop('password', None) - public_key = kwargs.pop('public_key', None) - username = kwargs.pop('username', None) - request = kwargs.get('request') - - # 获取解密密钥,对密码进行解密 - rsa_private_key = request.session.get('rsa_private_key') - if rsa_private_key is not None: - try: - password = rsa_decrypt(password, rsa_private_key) - except Exception as e: - logger.error(e, exc_info=True) - logger.error('Need decrypt password => {}'.format(password)) - return None, errors.reason_password_decrypt_failed - - user = authenticate(request, username=username, - password=password, public_key=public_key) - if not user: - return None, errors.reason_password_failed - elif user.is_expired: - return None, errors.reason_user_inactive - elif not user.is_active: - return None, errors.reason_user_inactive - elif user.password_has_expired: - return None, errors.reason_password_expired - - return user, '' diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index 7ef72235b..141d0f6e7 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -22,7 +22,8 @@ from common.utils import get_request_ip, get_object_or_none from users.utils import ( redirect_user_first_login_or_index ) -from .. import forms, mixins, errors, utils +from .. import mixins, errors, utils +from ..forms import get_user_login_form_cls __all__ = [ @@ -35,8 +36,6 @@ __all__ = [ @method_decorator(csrf_protect, name='dispatch') @method_decorator(never_cache, name='dispatch') class UserLoginView(mixins.AuthMixin, FormView): - form_class = forms.UserLoginForm - form_class_captcha = forms.UserLoginCaptchaForm key_prefix_captcha = "_LOGIN_INVALID_{}" redirect_field_name = 'next' @@ -87,7 +86,8 @@ class UserLoginView(mixins.AuthMixin, FormView): form.add_error(None, e.msg) ip = self.get_request_ip() cache.set(self.key_prefix_captcha.format(ip), 1, 3600) - new_form = self.form_class_captcha(data=form.data) + form_cls = get_user_login_form_cls(captcha=True) + new_form = form_cls(data=form.data) new_form._errors = form.errors context = self.get_context_data(form=new_form) return self.render_to_response(context) @@ -103,9 +103,9 @@ class UserLoginView(mixins.AuthMixin, FormView): def get_form_class(self): ip = get_request_ip(self.request) if cache.get(self.key_prefix_captcha.format(ip)): - return self.form_class_captcha + return get_user_login_form_cls(captcha=True) else: - return self.form_class + return get_user_login_form_cls() def get_context_data(self, **kwargs): # 生成加解密密钥对,public_key传递给前端,private_key存入session中供解密使用 diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 173d134b2..6f521c453 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -238,6 +238,8 @@ class Config(dict): 'SECURITY_PASSWORD_LOWER_CASE': False, 'SECURITY_PASSWORD_NUMBER': False, 'SECURITY_PASSWORD_SPECIAL_CHAR': False, + 'SECURITY_LOGIN_CHALLENGE_ENABLED': False, + 'SECURITY_LOGIN_CAPTCHA_ENABLED': True, 'HTTP_BIND_HOST': '0.0.0.0', 'HTTP_LISTEN_PORT': 8080, diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 1e552345b..e7d687dc3 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -41,6 +41,8 @@ SECURITY_PASSWORD_RULES = [ SECURITY_MFA_VERIFY_TTL = CONFIG.SECURITY_MFA_VERIFY_TTL SECURITY_VIEW_AUTH_NEED_MFA = CONFIG.SECURITY_VIEW_AUTH_NEED_MFA SECURITY_SERVICE_ACCOUNT_REGISTRATION = DYNAMIC.SECURITY_SERVICE_ACCOUNT_REGISTRATION +SECURITY_LOGIN_CAPTCHA_ENABLED = CONFIG.SECURITY_LOGIN_CAPTCHA_ENABLED +SECURITY_LOGIN_CHALLENGE_ENABLED = CONFIG.SECURITY_LOGIN_CHALLENGE_ENABLED # Terminal other setting TERMINAL_PASSWORD_AUTH = DYNAMIC.TERMINAL_PASSWORD_AUTH From 2f11a70341263c5935d758f73bc6c3a6c953fe8c Mon Sep 17 00:00:00 2001 From: OrangeM21 Date: Fri, 24 Jul 2020 18:23:28 +0800 Subject: [PATCH 09/40] =?UTF-8?q?fix(authentication):=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E9=A1=B5=E9=9D=A2=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../templates/authentication/xpack_login.html | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/apps/authentication/templates/authentication/xpack_login.html b/apps/authentication/templates/authentication/xpack_login.html index 77edc3582..32f12e9b5 100644 --- a/apps/authentication/templates/authentication/xpack_login.html +++ b/apps/authentication/templates/authentication/xpack_login.html @@ -67,9 +67,13 @@
-
- {{ JMS_TITLE }} -
+ {% if form.challenge %} +
+ {% else %} +
+ {% endif %} + {{ JMS_TITLE }} +
{% trans 'Welcome back, please enter username and password to login' %}
@@ -80,9 +84,13 @@ {% csrf_token %} {% if form.non_field_errors %} -
-

{{ form.non_field_errors.as_text }}

-
+ {% if form.challenge %} +
+ {% else %} +
+ {% endif %} +

{{ form.non_field_errors.as_text }}

+
{% elif form.errors.captcha %}

{% trans 'Captcha invalid' %}

{% else %} From 1bc913ab1367d8c08e47d57e99153c121220717c Mon Sep 17 00:00:00 2001 From: xinwen Date: Tue, 21 Jul 2020 12:37:30 +0800 Subject: [PATCH 10/40] =?UTF-8?q?feat(perms):=20=E8=B5=84=E4=BA=A7?= =?UTF-8?q?=E6=8E=88=E6=9D=83=E6=B7=BB=E5=8A=A0GUI=E5=A4=8D=E5=88=B6?= =?UTF-8?q?=E7=B2=98=E8=B4=B4=E5=8A=A8=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/locale/zh/LC_MESSAGES/django.mo | Bin 55230 -> 55392 bytes apps/locale/zh/LC_MESSAGES/django.po | 106 ++++++++++-------- .../migrations/0011_auto_20200721_1739.py | 28 +++++ apps/perms/models/asset_permission.py | 20 +++- 4 files changed, 105 insertions(+), 49 deletions(-) create mode 100644 apps/perms/migrations/0011_auto_20200721_1739.py diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 0337649da0092020c18e8181d5235a60bebd6803..8fb1a75b49efdaf650b27b6d6e8ee016aab7d2dd 100644 GIT binary patch delta 16748 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 delta 16617 zcmY-02b@jE-^cO8YP&2}Uv1Y~tFB&R5kw8Emms3|-aA*2sL>)(qekyU?=|WwNr~bR&ug04^UmN8#JQ__Ub94=_W~yoH>~b?qwzXk z#a1;u?=0;xH9ao_=cTCSdFMUP=jDj?yo_{sgvl^T9nTBKl$a7jF%#y&Tv*<0g<-^_ zk@b3uu^2ALXuOQ-pR}&$<;T336B}Rw?1P1w-uc7S-`g#*Lpi%}8p*p@mO_aQm=jFqc zm={Z8Qf!Ip*A5F~9}K}osD*7dccXUZ2 zYuJsN=qTz6Pop|sLM`MjYJf+m0iUC8#cR}gDL-(gL!B3jsWCt1z;{qP(-d{SZ#WeV zI2yH8lTi0^zPa8!U|vAovfG#rpJ7G}YUaiXHw&00&3Db3sI6~^W%d4lNJT6E0(FH8 zP%Hl$)$tqD!uFxIG7dxW9%|wMKAm&{p{R#3H)@=Qs4HxNnXxs7;$YOXH3O6B{hv)m zTeuK)PuHOa*oC?UhfoVTWBF?sL3{_bQ^{MnEe=KPOg>cq@@6dRLRzB6?TFgBo{IJU zkEW6VC!p@tSEwsqhoQL7;>(zY_;=KmC;HIc)6A%WOQGJ93aATt-||h(R;ZooWbJ*> zr;o^yRP@v?Lfwk*FdP1aI^ij5V6UaS;#8=e$%*QpA9ck=Q9D@)bzWVw8HN+LL+!*^ z)Ob@`+WS9;L?#mJQ1|Qz>IzPvPWZv%8>k5$payt>nmFl4Zs#(h78Z$Fv53XBPz!2; z8n-oS+#VmX|GI|*NoWhlqXwLYx|fSkSGd*kyHU636z0P#SP&Dp;?=^!s0-+fd$9*< z-2AQG#Kp{aQ46T;qoRo#qgL1jbp`#S6B{{VLi-=O>qql zMBRe$&hARzHLIg0tcyjkF=~P1P~%TWUEo~Qg{(q7J6k%l|LSneI>wooQ4irQ)D{PI zaqn|R)Br_M`54qh)ld^OuzVX#LEIe+;sDDpM(yxg)Ggf9h5Zkpa)^WmI)=L93mAd% zX8NvfM@pl1ssifX)wCu$*MQRmOZmh3}a`4!9m;iIChdt&~J8qn+J zCQ6B#C=@k7QPe_XF$$Yu5c)6>Ct(VlW${-SM7$FHTaJ20j#=J!k&5p5b=247V@!sD z-QAU>#azT;sGX{enxGoy$GVsjhoHt8hg#Si)Px5x4_?F|e2(E5(8IqFpBG6*D=&uH zx^k!m)G`}jO5$dyd)EQAV_nVOs9QD=^>EHYeM@dJPoutF@1Snc3)I5W^wf`9?tcUo zy(YC#3u%G=iBMbL6?Mg-sqr@IO8-KAfF+t_ABURolI8EAcI+=y|5uhz(Z@{~f_g@BqQ z8joN)e2lsUN&3451p26GD?(5cWJPUdKI>4@+RLLJ#%idCttD#fhg$nsi>ILf;l$#! zFToJ}9vk9a%!g$MxUXqnJ1TlO#-O%z8fxH$sC%{wwN)EYJF~;``z(JHHNhFn-#}f^ zuc+}Jp)Tk(YTUqqu77&Wpzr@YR5Vcx>Yi0bZB+xycQCu72JDC0;*U{VI}f$66Q~K| zkb%7)u^2u?ofkI9?O6|!4bxF8Ux~UEoBSO-Z$D~**Ua0fiSDDe`U(1< z8PvT^I@mo+L8yM&Q0M1C?Q|i_mz8?|t5DH|bx;fV5VaE>P%9sR8eptB3AN=jQ2myn z9=5Hh1)aqL_#^5QF<^+BI2USxg;6_L0)17f#8S~~G6OT?denlBq3->6sD)fbJ^lAl zTbp#KTR<69dkxeDG)0})8g<2;EgoR)BTzf`$x!xRE1FC~56N`Yl`gP&8D=5ghWbyW$wsdDT$k#ai6JY=-^?p~mZM@#tafe?AfuNa)1PsC%~EIvhmp#0k_x zc?q-PJygFW!`;NG&5WoE$&Tum548hTP~+FPxCv^f+WDwxpzf9!XdOnO?&W0ER(*+D z$O6=fYcMZv#c;f8zCvALjuGy2qBQCmXoT9qzE}#!pnjnEc2dzpbQQG|4^ZFt8AiGZ zO5;bwt*|TZ#rH63lzYg!B9DVN3&T*qXj@`5M&TsPft#@?o=~cn)=s1IN0Z3NgblBl&1ldll5JsEz6M z{@16X6?Z^w^&x)){}~Onl~+*JxhnQJ)JnEN+hB#9dLJ zh?7tkvIsTb4)kflBUJPZoU(?WP%FKQ-7y}MV9QV4iEU8}>S^&IL$xE!_cgIEwx zqdqU5p(f5h-hEz_8qfY~#Z^gYWld2JOE1&_Q&8=*Q75iOEp!`dtB;`;dI>erEzFIn zCb)c2RKFNZfwfWdG&XxoVE^?xj3c2f-Hl1{3Rc9Mm;%Elx_mxNMqC)RkW!c&tDzQH z2LrJ=cE|P@iRVzCA5Ty_nS7FaCQ|vV5{mEA5QUncBkHN{frW7e>b3jcV{0sOZGHsIBUXy0T%I9w(tb+ZSR= zT#tdc3pL;o)Ga!R>UR~j(EFB8HI*G;0XeY_`6|GlBk%+s#;rK{ zEBCokaiRNQYHD^s|L^~PmKclrpqODUG}oe@iJcZ7Fwa>2ig^olYksr#$L4>i53-bt z-1w2G@rvQ+3qrH_KLZ+BV+*r0YOn#Q-57=Wa5c8Ti>L=Qda>)*88v8M%!@Qm`xookw)aM$4m%5c@MRhD|R=0d3)PVg^6O6F-DX4dQE(YK_RKHEuz8iG`-&!1p zdVa2AL44|?5ZlXThs)@ES`k=)LVwSmAfz*{)j$Z$xTb#L&fnH|BIS9 z(JE(Z%uAdNYhV@BK$B4mnu$7pIqJNPsDH6BV=g z9gC}>p6*!G0$W(V8)`v)%~7ZiqRHlZj3z$kqvF4EoJ%6fS{E-w4Y1tYV)+B8iH=+T zl6f0*laEJzG-p`H-;S{^YW#aXZ0jPPz|k6Thtwk;wXzZT6`FT$^VTS zIMD`oWhqdfBLz_T!e%MdxaBbr-^WO7goX6}522!omZSb&cL}2~c%%F3EQva?Hfo?I zW;@hGz0Ec%i z8fGKk(BfXGg$+kNQ)5sInQ!eI%srS)@BaxZ`Vnys_2uvowG%1$v8M%8#2}2d_ybHs z+yOPw0Lu?YO*p~wOUzZ)z7fM{-;3I*Yv|KJcd2NgrVhy1nT_i7>ad}?=`P8>NO4Gr;ysi&Aevet=#|0G?XHtD;;bN<1C(R@f?d6p;o@m z^4raQSb+RdOo4x*{?_x{+JpI#sE0NKYMuzQ$T#->mnV^qhWAnLX-n%c3bpdd);<&U zlrP8hxEgiE`!F0YqyC=w95qpzZEoD`sD4FI`S&f~)MtsVsL$p>s4M&eGvXrD!nRrd zgn1FQuxqFRZ=3O`{;yHDHtlxT9*+5lqb+WVn#b3kingq$H4HMxq6V67@iKE0>KWN* z###O*s{bFTTk!_9fC4+*Lm7jmi5p=?oP@kJK5sFVq9pdZg!d3j5C`sb9V?*P8(BOK z^}5YNUHNv@P8=~$S^fg*3U65ayS4v=TIegk-2XJYTq43Og<5${vzg_)U^MyRsEJpb z+tI%O)Rmt?P5h7f#thu;^662ZBRMdq-v9TgXhm&N0}U{Tn`5kfB5LawSiBxXi4S5e z{0X(NJbRqQ&5C9$YM$n(3+#bDB}Q0c8ft(=<~r1Zc42-zh$Zp9<-_*6_9)bO(Wonr zv3ym`MqJzC&ZvodnWOgF_y1JuFdy~$Ew%WldD^^)n&=8@;dfC3zCcYBxX;CTFbi=h z)OdAK=k>AtNQ=MRr~6OCGD~bl4Rp}Fh8pNOY9~VWyMeNpdCcOd_KFr)Hyfdz_BIv| zLd`c8wXn~9)?pj!1LZX8%jPC(#mNu2fisxdP+J^f7BWkt`d30Nqz>v?X^h&bj;Qm7 zp!$tLz4pFo)-VTil2~dT4q-FmI4p#D54yjoG)4_D7}b9qYT#w4iPxLopssX}#iz}S zsPnJ5*yr7)q5&SF8lIU+4!Jl4wS_rRS5gu+Q4MRaYkr6tr;D}sv;0uhd7q+g?Obc$ zh8cPOyuDPk(u=51s9#VE2|esij6%KFWl_&U2h4>dQ43jw>c0ZD(5;p~X`V+d>@up~ z&*mewGr#weidLBTi0haU)iKO0WR^vJi&aN0xG`$r4rVWN2x?(t%!#P+rkh`(=39gQ zzyI%}qANRUUPqnq6g5!5QP&=XU5ImAJjGm&TF8D>{}ZV5&!HabYZ!^~md|+1wMQJY z_rC}UZC!b5Xowo9wZ)wOxkb7IX-8t4?D@e2D5FecX*7 zbDaIxM3qSdU`=a?H5;KmVp~`|0JYHJm;`5_CjQb~ftp}DYKxDc7Iew-f1(!l3X@?n z-?#3BV9ZV;tHotd9jaPf+iYfbH2awe<4hsed2>+{Ew=VGsPF$B7=gD;U%&~s!YI@N zDq>b_iTVH;jQMZ^hT&$^6<)CX4b*_iPr7~uusU%gEQ|B-9sC~kc@cby|HOxdFo(YX z$5GL1w-mLtS5RB}Cx+u2)YoqKY5OckZD}8zi4(CuW{@CIP=h=T<;d~NF zaXIQ?S!>=wEilIgcO?Z-1I3^|Sn62Z7B$WQOoO8=o@((Na}{d5ofwVBE^z<#0r9sr zWW4A)M4$#Nf|{@_YG-1xJa$B$UT-OCz=K!?&!Vn8B1V-!Hj||1h7T7Uo@crbqvkp|-vRYN1suU&rDWsPQ^rdhCMq z_jx0z=!0PjYT&iV{QiHMS)7MZyt6{R{GINYud(h^tfQ zqul$E_(SWs5LePRirfW?j`YMuC<`c^D5uC@q4-u4=-7sZDbK0vKCPf}vl=)iQLjSD zOnVLM{}G-er=u11!PK|7(Eo2exydD?G_qWnB%VvZkyLk3pOaYo&#|8JF%4}no`%iT z_fbzc#*iyUFdg*~t>ZHFkMRZ`r08q97Nr~|2j{mZr{gJQpufiLv7Ex;l%F*JIy&d0 zN zu4Au{N>M8B;YV~Ti6yY5znQDRqc-@@wAG^Yq}(9ajkc%w81-RPfKrw?Gj_xM>W?pp zU*ZJJiK)q#LLI&zo&WF8Ry6)frnhxc<6oSVaEu|Jml9{~FKD<&JvZe`>PILwD4mGE zcp!&PnL|Df#S_A(YRQ)bn4&TFc-DPWbSxllGE(M(0hGWaL{Br?vik zLVJIb8--i#oP*RiQtA?aM_EA8$E}Wb)CVxmDeB3o|B1emRPIyJZ@!z9k0?9IeSwJs z_)7+kC)RP;;hkd;9m8-btQercxhggH$HB-0J^O*Rh}Yd&&&@B^+_ot6I>R zdNB1Nlu6W|_~<;0pe^2~jH4b!(XpKlYst;U%b17KpEANV|L>m#$m!TcIZfY7lp>Zp zj+H5ixRn#gYrco{D@I+1uLYH-7R0!mcL5*Mv8v?*iH})bU7k~KMmxVL{KrJ<8!5HP z|BF?~-m?DTIFz^{PNwY<<%&B^f4cooK{DaEXqCS?VF~rE2@Uu!xv`Wq^q-I4Vlj$- zIVBwL66>f>`}-9AuKbVkg8V9C9p$kd*46yOX(&x%Bc%{!FeR3<)CP+7pW^;VL0(63 za|-pl)^8pLQ{JOoA|FJb+muc^;jQCiJD1}yQ$HKmC)##7Hiy)P#x9DS&qp|qvs zvHXdIM*N+!i1S|1*4y$ciFNd%L|Q!*?-3usuQ8F{|1F%9k;F;s^cha1QO8i){;+&n zn|Oh1@=}p+OYRtPI_hPpAF#H=)PE&+lX8^$1Ius2VB%lM@1^+n|1%OgzM^EHR3=_d z8B0BkP8})5{hj$qX42%1p==^PM#)Nlez$rj-fCAL9mi>_hHYr8Lwy%kB+iW0e0IWQ z8rEA~CpEQtaq9VL(~$w6VQ0!o>LcjW3qK)FXML!8q0}4VYy~#XKyve}&ziU9@dXhi zS4WO-t;1aExhNMbzDl3@cG6<k-a;d3zrC!DsLFhm9Hy|BPDCzt)ezs#H`i-LhSJ=|dX^3ga zeN9P3y)63ge_bjgDSuLavq5rDZ$wErLa3ahyhqXhTY4dFBPiDy=niEa^&r|3Q-6aQ zDPd~I5|p##yA%I|D~aEsl&8Ly&!7K2;;A(uspDsyL78fCL34v^_EIuf1G@P;E0scz-5CbU#DXSh`&wM{4fj{J4vH`G&6Uyc9i`+q;dNy-@-=h%saUQOzgRpIDG zy&LUGZNT)_Zv*jk*Tw(mEaJh`KAJp+{}g()Q|IVl|} z4e0n4B|G&=lq%H!VvuW;qSWtEbo}A)j-$_V(eznCyu#v<#Gg=qg()dIx}vYT1xnne zyiYtDBPbW`WUBuEncYr{GvT{$wXLCE)W)ev{WWER#eK}q*jfW|^rJnutJd@O5Hux- zKplgyg{%7iCsJ1Rz9Sz_(9HTJA|V diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index c311e0a7f..b819ee494 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-07-15 17:13+0800\n" +"POT-Creation-Date: 2020-07-21 16:29+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -46,7 +46,7 @@ msgstr "自定义" msgid "Name" msgstr "名称" -#: applications/models/database_app.py:22 assets/models/cmd_filter.py:51 +#: applications/models/database_app.py:22 assets/models/cmd_filter.py:52 #: terminal/models.py:376 terminal/models.py:413 tickets/models/ticket.py:45 #: users/templates/users/user_granted_database_app.html:35 msgid "Type" @@ -72,7 +72,7 @@ msgstr "数据库" #: applications/models/database_app.py:33 applications/models/remote_app.py:45 #: assets/models/asset.py:150 assets/models/asset.py:226 #: assets/models/base.py:237 assets/models/cluster.py:29 -#: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:56 +#: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:57 #: assets/models/domain.py:21 assets/models/domain.py:54 #: assets/models/group.py:23 assets/models/label.py:23 ops/models/adhoc.py:37 #: orgs/models.py:18 perms/models/base.py:56 settings/models.py:32 @@ -104,7 +104,7 @@ msgstr "数据库应用" #: assets/serializers/admin_user.py:32 assets/serializers/asset_user.py:47 #: assets/serializers/asset_user.py:84 assets/serializers/system_user.py:44 #: assets/serializers/system_user.py:176 audits/models.py:38 -#: perms/forms/asset_permission.py:89 perms/models/asset_permission.py:80 +#: perms/forms/asset_permission.py:89 perms/models/asset_permission.py:90 #: templates/index.html:82 terminal/backends/command/models.py:19 #: terminal/backends/command/serializers.py:13 terminal/models.py:187 #: users/templates/users/user_asset_permission.html:40 @@ -130,7 +130,7 @@ msgstr "参数" #: applications/models/remote_app.py:39 assets/models/asset.py:224 #: assets/models/base.py:240 assets/models/cluster.py:28 -#: assets/models/cmd_filter.py:26 assets/models/cmd_filter.py:59 +#: assets/models/cmd_filter.py:26 assets/models/cmd_filter.py:60 #: assets/models/group.py:21 common/mixins/models.py:49 orgs/models.py:16 #: perms/models/base.py:54 users/models/user.py:508 #: users/serializers/group.py:35 users/templates/users/user_detail.html:97 @@ -233,7 +233,7 @@ msgid "Domain" msgstr "网域" #: assets/models/asset.py:195 assets/models/user.py:109 -#: perms/models/asset_permission.py:81 +#: perms/models/asset_permission.py:91 #: xpack/plugins/change_auth_plan/models.py:56 #: xpack/plugins/gathered_user/models.py:24 msgid "Nodes" @@ -247,7 +247,7 @@ msgstr "激活" #: assets/models/asset.py:199 assets/models/cluster.py:19 #: assets/models/user.py:65 templates/_nav.html:44 -#: xpack/plugins/cloud/models.py:133 xpack/plugins/cloud/serializers.py:82 +#: xpack/plugins/cloud/models.py:133 xpack/plugins/cloud/serializers.py:83 msgid "Admin user" msgstr "管理用户" @@ -441,48 +441,48 @@ msgstr "北京电信" msgid "BGP full netcom" msgstr "BGP全网通" -#: assets/models/cmd_filter.py:32 assets/models/user.py:119 +#: assets/models/cmd_filter.py:33 assets/models/user.py:119 msgid "Command filter" msgstr "命令过滤器" -#: assets/models/cmd_filter.py:39 +#: assets/models/cmd_filter.py:40 msgid "Regex" msgstr "正则表达式" -#: assets/models/cmd_filter.py:40 ops/models/command.py:23 +#: assets/models/cmd_filter.py:41 ops/models/command.py:23 #: terminal/backends/command/serializers.py:15 terminal/models.py:196 msgid "Command" msgstr "命令" -#: assets/models/cmd_filter.py:45 +#: assets/models/cmd_filter.py:46 msgid "Deny" msgstr "拒绝" -#: assets/models/cmd_filter.py:46 +#: assets/models/cmd_filter.py:47 msgid "Allow" msgstr "允许" -#: assets/models/cmd_filter.py:50 +#: assets/models/cmd_filter.py:51 msgid "Filter" msgstr "过滤器" -#: assets/models/cmd_filter.py:52 assets/models/user.py:113 +#: assets/models/cmd_filter.py:53 assets/models/user.py:113 msgid "Priority" msgstr "优先级" -#: assets/models/cmd_filter.py:52 +#: assets/models/cmd_filter.py:53 msgid "1-100, the higher will be match first" msgstr "优先级可选范围为1-100,1最低优先级,100最高优先级" -#: assets/models/cmd_filter.py:54 xpack/plugins/license/models.py:29 +#: assets/models/cmd_filter.py:55 xpack/plugins/license/models.py:29 msgid "Content" msgstr "内容" -#: assets/models/cmd_filter.py:54 +#: assets/models/cmd_filter.py:55 msgid "One line one command" msgstr "每行一个命令" -#: assets/models/cmd_filter.py:55 audits/models.py:57 +#: assets/models/cmd_filter.py:56 audits/models.py:57 #: authentication/templates/authentication/_access_key_modal.html:34 #: perms/forms/asset_permission.py:20 #: tickets/serializers/request_asset_perm.py:54 @@ -497,7 +497,7 @@ msgstr "每行一个命令" msgid "Action" msgstr "动作" -#: assets/models/cmd_filter.py:63 +#: assets/models/cmd_filter.py:64 msgid "Command filter rule" msgstr "命令过滤规则" @@ -590,7 +590,7 @@ msgstr "键" #: users/templates/users/user_asset_permission.html:41 #: users/templates/users/user_asset_permission.html:73 #: users/templates/users/user_asset_permission.html:158 -#: xpack/plugins/cloud/models.py:129 xpack/plugins/cloud/serializers.py:83 +#: xpack/plugins/cloud/models.py:129 xpack/plugins/cloud/serializers.py:84 msgid "Node" msgstr "节点" @@ -643,7 +643,7 @@ msgstr "SFTP根路径" #: assets/models/user.py:195 audits/models.py:39 #: perms/forms/asset_permission.py:95 perms/forms/remote_app_permission.py:49 -#: perms/models/asset_permission.py:82 +#: perms/models/asset_permission.py:92 #: perms/models/database_app_permission.py:22 #: perms/models/remote_app_permission.py:16 templates/_nav.html:45 #: terminal/backends/command/models.py:20 @@ -1000,7 +1000,7 @@ msgstr "Agent" #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: authentication/templates/authentication/login_otp.html:6 #: users/forms/profile.py:52 users/models/user.py:489 -#: users/serializers/user.py:216 users/templates/users/user_detail.html:77 +#: users/serializers/user.py:220 users/templates/users/user_detail.html:77 #: users/templates/users/user_profile.html:87 msgid "MFA" msgstr "多因子认证" @@ -1224,7 +1224,7 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:387 users/serializers/user.py:213 +#: users/models/user.py:387 users/serializers/user.py:217 #: users/templates/users/user_profile.html:94 #: users/templates/users/user_profile.html:163 #: users/templates/users/user_profile.html:166 @@ -1233,7 +1233,7 @@ msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:388 users/serializers/user.py:214 +#: users/models/user.py:388 users/serializers/user.py:218 #: users/templates/users/user_profile.html:92 #: users/templates/users/user_profile.html:170 msgid "Enable" @@ -1675,31 +1675,43 @@ msgstr "资产和节点至少选一个" msgid "System users" msgstr "系统用户" -#: perms/models/asset_permission.py:31 settings/serializers/settings.py:56 +#: perms/models/asset_permission.py:35 settings/serializers/settings.py:56 msgid "All" msgstr "全部" -#: perms/models/asset_permission.py:32 +#: perms/models/asset_permission.py:36 msgid "Connect" msgstr "连接" -#: perms/models/asset_permission.py:33 +#: perms/models/asset_permission.py:37 msgid "Upload file" msgstr "上传文件" -#: perms/models/asset_permission.py:34 +#: perms/models/asset_permission.py:38 msgid "Download file" msgstr "下载文件" -#: perms/models/asset_permission.py:35 +#: perms/models/asset_permission.py:39 msgid "Upload download" msgstr "上传下载" -#: perms/models/asset_permission.py:83 +#: perms/models/asset_permission.py:40 +msgid "Clipboard copy" +msgstr "剪切板复制" + +#: perms/models/asset_permission.py:41 +msgid "Clipboard paste" +msgstr "剪切板粘贴" + +#: perms/models/asset_permission.py:42 +msgid "Clipboard copy paste" +msgstr "剪切板复制粘贴" + +#: perms/models/asset_permission.py:93 msgid "Actions" msgstr "动作" -#: perms/models/asset_permission.py:87 templates/_nav.html:78 +#: perms/models/asset_permission.py:97 templates/_nav.html:78 #: users/templates/users/_user_detail_nav_header.html:31 msgid "Asset permission" msgstr "资产授权" @@ -2704,8 +2716,8 @@ msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" #: users/forms/profile.py:137 users/forms/user.py:90 -#: users/serializers/user.py:177 users/serializers/user.py:258 -#: users/serializers/user.py:316 +#: users/serializers/user.py:181 users/serializers/user.py:262 +#: users/serializers/user.py:320 msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" @@ -2795,7 +2807,7 @@ msgstr "最后更新密码日期" msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" -#: users/serializers/user.py:69 users/serializers/user.py:229 +#: users/serializers/user.py:69 users/serializers/user.py:233 msgid "Is first login" msgstr "首次登录" @@ -2823,19 +2835,19 @@ msgstr "用户来源名" msgid "Role name" msgstr "角色名" -#: users/serializers/user.py:97 +#: users/serializers/user.py:101 msgid "Role limit to {}" msgstr "角色只能为 {}" -#: users/serializers/user.py:109 users/serializers/user.py:282 +#: users/serializers/user.py:113 users/serializers/user.py:286 msgid "Password does not match security rules" msgstr "密码不满足安全规则" -#: users/serializers/user.py:274 +#: users/serializers/user.py:278 msgid "The old password is incorrect" msgstr "旧密码错误" -#: users/serializers/user.py:288 +#: users/serializers/user.py:292 msgid "The newly set password is inconsistent" msgstr "两次密码不一致" @@ -2849,7 +2861,7 @@ msgstr "安全令牌验证" #: users/templates/users/_base_otp.html:14 users/templates/users/_user.html:13 #: users/templates/users/user_profile_update.html:55 -#: xpack/plugins/cloud/models.py:119 xpack/plugins/cloud/serializers.py:81 +#: xpack/plugins/cloud/models.py:119 xpack/plugins/cloud/serializers.py:82 msgid "Account" msgstr "账户" @@ -3736,7 +3748,7 @@ msgstr "" msgid "Cloud account" msgstr "云账号" -#: xpack/plugins/cloud/models.py:122 xpack/plugins/cloud/serializers.py:58 +#: xpack/plugins/cloud/models.py:122 xpack/plugins/cloud/serializers.py:59 msgid "Regions" msgstr "地域" @@ -3744,7 +3756,7 @@ msgstr "地域" msgid "Instances" msgstr "实例" -#: xpack/plugins/cloud/models.py:137 xpack/plugins/cloud/serializers.py:85 +#: xpack/plugins/cloud/models.py:137 xpack/plugins/cloud/serializers.py:86 msgid "Always update" msgstr "总是更新" @@ -3860,15 +3872,15 @@ msgstr "拉美-圣地亚哥" msgid "Tencent Cloud" msgstr "腾讯云" -#: xpack/plugins/cloud/serializers.py:56 +#: xpack/plugins/cloud/serializers.py:57 msgid "History count" msgstr "执行次数" -#: xpack/plugins/cloud/serializers.py:57 +#: xpack/plugins/cloud/serializers.py:58 msgid "Instance count" msgstr "实例个数" -#: xpack/plugins/cloud/serializers.py:84 +#: xpack/plugins/cloud/serializers.py:85 #: xpack/plugins/gathered_user/serializers.py:20 msgid "Periodic display" msgstr "定时执行" @@ -3957,6 +3969,12 @@ msgstr "企业版" msgid "Ultimate edition" msgstr "旗舰版" +#~ msgid "GUI copy" +#~ msgstr "GUI 复制" + +#~ msgid "GUI paste" +#~ msgstr "GUI 粘贴" + #~ msgid "Covered always" #~ msgstr "总是被覆盖" diff --git a/apps/perms/migrations/0011_auto_20200721_1739.py b/apps/perms/migrations/0011_auto_20200721_1739.py new file mode 100644 index 000000000..7e6b37188 --- /dev/null +++ b/apps/perms/migrations/0011_auto_20200721_1739.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.10 on 2020-07-21 09:39 + +from django.db import migrations, models + +from django.db.models import F +from ..models.asset_permission import Action + + +def migrate_asset_permission(apps, schema_editor): + # 已有的资产权限默认拥有剪切板复制粘贴动作 + AssetPermission = apps.get_model('perms', 'AssetPermission') + AssetPermission.objects.all().update(actions=F('actions').bitor(Action.CLIPBOARD_COPY_PASTE)) + + +class Migration(migrations.Migration): + + dependencies = [ + ('perms', '0010_auto_20191218_1705'), + ] + + operations = [ + migrations.AlterField( + model_name='assetpermission', + name='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'), + ), + migrations.RunPython(migrate_asset_permission) + ] diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index 8552edc74..f2755a568 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -21,11 +21,15 @@ logger = logging.getLogger(__name__) class Action: NONE = 0 - CONNECT = 0b00000001 - UPLOAD = 0b00000010 - DOWNLOAD = 0b00000100 + + CONNECT = 0b1 + UPLOAD = 0b1 << 1 + DOWNLOAD = 0b1 << 2 + CLIPBOARD_COPY = 0b1 << 3 + CLIPBOARD_PASTE = 0b1 << 4 + ALL = 0xff UPDOWNLOAD = UPLOAD | DOWNLOAD - ALL = 0b11111111 + CLIPBOARD_COPY_PASTE = CLIPBOARD_COPY | CLIPBOARD_PASTE DB_CHOICES = ( (ALL, _('All')), @@ -33,6 +37,9 @@ class Action: (UPLOAD, _('Upload file')), (DOWNLOAD, _('Download file')), (UPDOWNLOAD, _("Upload download")), + (CLIPBOARD_COPY, _('Clipboard copy')), + (CLIPBOARD_PASTE, _('Clipboard paste')), + (CLIPBOARD_COPY_PASTE, _('Clipboard copy paste')) ) NAME_MAP = { @@ -41,9 +48,12 @@ class Action: UPLOAD: "upload_file", DOWNLOAD: "download_file", UPDOWNLOAD: "updownload", + CLIPBOARD_COPY: 'clipboard_copy', + CLIPBOARD_PASTE: 'clipboard_paste', + CLIPBOARD_COPY_PASTE: 'clipboard_copy_paste' } - NAME_MAP_REVERSE = dict({v: k for k, v in NAME_MAP.items()}) + NAME_MAP_REVERSE = {v: k for k, v in NAME_MAP.items()} CHOICES = [] for i, j in DB_CHOICES: CHOICES.append((NAME_MAP[i], j)) From de3865fa1dbc4031bd1de56892ba9151120a9efa Mon Sep 17 00:00:00 2001 From: xinwen Date: Mon, 20 Jul 2020 10:42:22 +0800 Subject: [PATCH 11/40] =?UTF-8?q?refactor(orgs):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E7=BB=84=E7=BB=87=E8=A1=A8=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/audits/api.py | 4 +- apps/audits/filters.py | 2 +- apps/audits/models.py | 2 +- apps/common/const/choices.py | 8 + apps/common/db/aggregates.py | 7 +- apps/common/db/models.py | 48 ++++ apps/common/drf/fields.py | 43 +++ apps/jumpserver/api.py | 6 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 55392 -> 55579 bytes apps/locale/zh/LC_MESSAGES/django.po | 212 ++++++++------- apps/orgs/api.py | 24 +- .../migrations/0004_organizationmember.py | 33 +++ .../migrations/0005_auto_20200721_1937.py | 35 +++ .../migrations/0006_auto_20200721_1937.py | 32 +++ apps/orgs/models.py | 251 +++++++++++++----- apps/orgs/serializers.py | 42 ++- apps/orgs/signals_handler.py | 46 ++-- apps/orgs/tests.py | 17 +- apps/orgs/urls/api_urls.py | 6 - apps/users/api/mixins.py | 2 +- apps/users/api/user.py | 34 ++- apps/users/filters.py | 8 +- apps/users/forms/user.py | 4 +- apps/users/models/user.py | 101 ++++--- apps/users/serializers/user.py | 30 ++- apps/users/templates/users/user_detail.html | 2 +- apps/users/utils.py | 2 +- 27 files changed, 702 insertions(+), 299 deletions(-) create mode 100644 apps/common/const/choices.py create mode 100644 apps/common/db/models.py create mode 100644 apps/common/drf/fields.py create mode 100644 apps/orgs/migrations/0004_organizationmember.py create mode 100644 apps/orgs/migrations/0005_auto_20200721_1937.py create mode 100644 apps/orgs/migrations/0006_auto_20200721_1937.py diff --git a/apps/audits/api.py b/apps/audits/api.py index 7397b3596..0233bc1b2 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -43,7 +43,7 @@ class UserLoginLogViewSet(ListModelMixin, CommonGenericViewSet): @staticmethod def get_org_members(): - users = current_org.get_org_members().values_list('username', flat=True) + users = current_org.get_members().values_list('username', flat=True) return users def get_queryset(self): @@ -79,7 +79,7 @@ class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet): ordering = ['-datetime'] def get_queryset(self): - users = current_org.get_org_members() + users = current_org.get_members() queryset = super().get_queryset().filter( user__in=[user.__str__() for user in users] ) diff --git a/apps/audits/filters.py b/apps/audits/filters.py index 6db2d9b21..470c2c4b5 100644 --- a/apps/audits/filters.py +++ b/apps/audits/filters.py @@ -20,7 +20,7 @@ class CurrentOrgMembersFilter(filters.BaseFilterBackend): ] def _get_user_list(self): - users = current_org.get_org_members(exclude=('Auditor',)) + users = current_org.get_members(exclude=('Auditor',)) return users def filter_queryset(self, request, queryset, view): diff --git a/apps/audits/models.py b/apps/audits/models.py index 38a41554f..97aef40ce 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -124,7 +124,7 @@ class UserLoginLog(models.Model): Q(username__contains=keyword) ) if not current_org.is_root(): - username_list = current_org.get_org_members().values_list('username', flat=True) + username_list = current_org.get_members().values_list('username', flat=True) login_logs = login_logs.filter(username__in=username_list) return login_logs diff --git a/apps/common/const/choices.py b/apps/common/const/choices.py new file mode 100644 index 000000000..8de0c5fc8 --- /dev/null +++ b/apps/common/const/choices.py @@ -0,0 +1,8 @@ +from django.utils.translation import ugettext_lazy as _ + +from common.db.models import ChoiceSet + + +ADMIN = 'Admin' +USER = 'User' +AUDITOR = 'Auditor' diff --git a/apps/common/db/aggregates.py b/apps/common/db/aggregates.py index 081c1fea8..e04390299 100644 --- a/apps/common/db/aggregates.py +++ b/apps/common/db/aggregates.py @@ -3,10 +3,10 @@ from django.db.models import Aggregate class GroupConcat(Aggregate): function = 'GROUP_CONCAT' - template = '%(function)s(%(distinct)s %(expressions)s %(order_by)s %(separator))' + template = '%(function)s(%(expressions)s %(order_by)s %(separator)s)' allow_distinct = False - def __init__(self, expression, distinct=False, order_by=None, separator=',', **extra): + def __init__(self, expression, order_by=None, separator=',', **extra): order_by_clause = '' if order_by is not None: order = 'ASC' @@ -21,8 +21,7 @@ class GroupConcat(Aggregate): super().__init__( expression, - distinct='DISTINCT' if distinct else '', order_by=order_by_clause, - separator=f'SEPARATOR {separator}', + separator=f"SEPARATOR '{separator}'", **extra ) diff --git a/apps/common/db/models.py b/apps/common/db/models.py new file mode 100644 index 000000000..04d501b8f --- /dev/null +++ b/apps/common/db/models.py @@ -0,0 +1,48 @@ +from functools import partial + + +class Choice(str): + def __new__(cls, value, label): + self = super().__new__(cls, value) + self.label = label + return self + + +class ChoiceSetType(type): + def __new__(cls, name, bases, attrs): + _choices = [] + collected = set() + new_attrs = {} + for k, v in attrs.items(): + if isinstance(v, tuple): + v = Choice(*v) + assert v not in collected, 'Cannot be defined repeatedly' + _choices.append(v) + collected.add(v) + new_attrs[k] = v + for base in bases: + if hasattr(base, '_choices'): + for c in base._choices: + if c not in collected: + _choices.append(c) + collected.add(c) + new_attrs['_choices'] = _choices + new_attrs['_choices_dict'] = {c: c.label for c in _choices} + return type.__new__(cls, name, bases, new_attrs) + + def __contains__(self, item): + return self._choices_dict.__contains__(item) + + def __getitem__(self, item): + return self._choices_dict.__getitem__(item) + + def get(self, item, default=None): + return self._choices_dict.get(item, default=None) + + @property + def choices(self): + return [(c, c.label) for c in self._choices] + + +class ChoiceSet(metaclass=ChoiceSetType): + choices = None # 用于 Django Model 中的 choices 配置, 为了代码提示在此声明 diff --git a/apps/common/drf/fields.py b/apps/common/drf/fields.py new file mode 100644 index 000000000..e3b333d56 --- /dev/null +++ b/apps/common/drf/fields.py @@ -0,0 +1,43 @@ +from uuid import UUID + +from rest_framework.fields import get_attribute +from rest_framework.relations import ManyRelatedField, PrimaryKeyRelatedField, MANY_RELATION_KWARGS + + +class GroupConcatedManyRelatedField(ManyRelatedField): + def get_attribute(self, instance): + if hasattr(instance, 'pk') and instance.pk is None: + return [] + + attr = self.source_attrs[-1] + + # `gc` 是 `GroupConcat` 的缩写 + gc_attr = f'gc_{attr}' + if hasattr(instance, gc_attr): + gc_value = getattr(instance, gc_attr) + if isinstance(gc_value, str): + return [UUID(pk) for pk in set(gc_value.split(','))] + else: + return '' + + relationship = get_attribute(instance, self.source_attrs) + return relationship.all() if hasattr(relationship, 'all') else relationship + + +class GroupConcatedPrimaryKeyRelatedField(PrimaryKeyRelatedField): + @classmethod + def many_init(cls, *args, **kwargs): + list_kwargs = {'child_relation': cls(*args, **kwargs)} + for key in kwargs: + if key in MANY_RELATION_KWARGS: + list_kwargs[key] = kwargs[key] + return GroupConcatedManyRelatedField(**list_kwargs) + + def to_representation(self, value): + if self.pk_field is not None: + return self.pk_field.to_representation(value.pk) + + if hasattr(value, 'pk'): + return value.pk + else: + return value diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index 9558a92df..026a90b9a 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -128,7 +128,7 @@ class DatesLoginMetricMixin: @lazyproperty def dates_total_count_inactive_users(self): - total = current_org.get_org_members().count() + total = current_org.get_members().count() active = self.dates_total_count_active_users count = total - active if count < 0: @@ -137,7 +137,7 @@ class DatesLoginMetricMixin: @lazyproperty def dates_total_count_disabled_users(self): - return current_org.get_org_members().filter(is_active=False).count() + return current_org.get_members().filter(is_active=False).count() @lazyproperty def dates_total_count_active_assets(self): @@ -207,7 +207,7 @@ class DatesLoginMetricMixin: class TotalCountMixin: @staticmethod def get_total_count_users(): - return current_org.get_org_members().count() + return current_org.get_members().count() @staticmethod def get_total_count_assets(): diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 8fb1a75b49efdaf650b27b6d6e8ee016aab7d2dd..f72e19c129cdb9a828ece94b3631b84326a27fb3 100644 GIT binary patch delta 16951 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 delta 16766 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 PRXH-jC(h@1aLWG!Lh1O@ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index b819ee494..dd64657a7 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-07-21 16:29+0800\n" +"POT-Creation-Date: 2020-07-27 19:01+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -25,10 +25,10 @@ msgstr "自定义" #: assets/models/asset.py:145 assets/models/base.py:232 #: assets/models/cluster.py:18 assets/models/cmd_filter.py:21 #: assets/models/domain.py:20 assets/models/group.py:20 -#: assets/models/label.py:18 ops/mixin.py:24 orgs/models.py:12 +#: assets/models/label.py:18 ops/mixin.py:24 orgs/models.py:22 #: perms/models/base.py:48 settings/models.py:27 terminal/models.py:26 #: terminal/models.py:342 terminal/models.py:374 terminal/models.py:411 -#: users/forms/profile.py:20 users/models/group.py:15 users/models/user.py:467 +#: users/forms/profile.py:20 users/models/group.py:15 users/models/user.py:473 #: users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:154 @@ -75,9 +75,9 @@ msgstr "数据库" #: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:57 #: assets/models/domain.py:21 assets/models/domain.py:54 #: assets/models/group.py:23 assets/models/label.py:23 ops/models/adhoc.py:37 -#: orgs/models.py:18 perms/models/base.py:56 settings/models.py:32 +#: orgs/models.py:25 perms/models/base.py:56 settings/models.py:32 #: terminal/models.py:36 terminal/models.py:381 terminal/models.py:418 -#: users/models/group.py:16 users/models/user.py:500 +#: users/models/group.py:16 users/models/user.py:506 #: users/templates/users/user_detail.html:115 #: users/templates/users/user_granted_database_app.html:38 #: users/templates/users/user_granted_remote_app.html:37 @@ -131,8 +131,8 @@ msgstr "参数" #: applications/models/remote_app.py:39 assets/models/asset.py:224 #: assets/models/base.py:240 assets/models/cluster.py:28 #: assets/models/cmd_filter.py:26 assets/models/cmd_filter.py:60 -#: assets/models/group.py:21 common/mixins/models.py:49 orgs/models.py:16 -#: perms/models/base.py:54 users/models/user.py:508 +#: assets/models/group.py:21 common/mixins/models.py:49 orgs/models.py:23 +#: orgs/models.py:316 perms/models/base.py:54 users/models/user.py:514 #: users/serializers/group.py:35 users/templates/users/user_detail.html:97 #: xpack/plugins/change_auth_plan/models.py:81 xpack/plugins/cloud/models.py:56 #: xpack/plugins/cloud/models.py:146 xpack/plugins/gathered_user/models.py:30 @@ -146,8 +146,8 @@ msgstr "创建者" #: assets/models/domain.py:23 assets/models/gathered_user.py:19 #: assets/models/group.py:22 assets/models/label.py:25 #: common/mixins/models.py:50 ops/models/adhoc.py:38 ops/models/command.py:27 -#: orgs/models.py:17 perms/models/base.py:55 users/models/group.py:18 -#: users/templates/users/user_group_detail.html:58 +#: orgs/models.py:24 orgs/models.py:314 perms/models/base.py:55 +#: users/models/group.py:18 users/templates/users/user_group_detail.html:58 #: xpack/plugins/cloud/models.py:59 xpack/plugins/cloud/models.py:149 msgid "Date created" msgstr "创建日期" @@ -336,10 +336,10 @@ msgid "AuthBook" msgstr "" #: assets/models/base.py:233 assets/models/gathered_user.py:15 -#: audits/models.py:99 authentication/forms.py:10 +#: audits/models.py:99 authentication/forms.py:11 #: authentication/templates/authentication/login.html:21 -#: authentication/templates/authentication/xpack_login.html:93 -#: ops/models/adhoc.py:148 users/forms/profile.py:19 users/models/user.py:465 +#: authentication/templates/authentication/xpack_login.html:101 +#: ops/models/adhoc.py:148 users/forms/profile.py:19 users/models/user.py:471 #: users/templates/users/_select_user_modal.html:14 #: users/templates/users/user_detail.html:53 #: users/templates/users/user_list.html:15 @@ -350,9 +350,9 @@ msgid "Username" msgstr "用户名" #: assets/models/base.py:234 assets/serializers/asset_user.py:71 -#: authentication/forms.py:12 +#: authentication/forms.py:13 #: authentication/templates/authentication/login.html:29 -#: authentication/templates/authentication/xpack_login.html:101 +#: authentication/templates/authentication/xpack_login.html:109 #: users/forms/user.py:22 users/forms/user.py:193 #: users/templates/users/user_otp_check_password.html:13 #: users/templates/users/user_password_update.html:43 @@ -379,7 +379,7 @@ msgid "SSH public key" msgstr "SSH公钥" #: assets/models/base.py:239 assets/models/gathered_user.py:20 -#: common/mixins/models.py:51 ops/models/adhoc.py:39 +#: common/mixins/models.py:51 ops/models/adhoc.py:39 orgs/models.py:315 msgid "Date updated" msgstr "更新日期" @@ -391,7 +391,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:486 +#: assets/models/cluster.py:22 users/models/user.py:492 #: users/templates/users/user_detail.html:62 msgid "Phone" msgstr "手机" @@ -417,7 +417,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:627 +#: users/models/user.py:635 msgid "System" msgstr "系统" @@ -535,14 +535,15 @@ msgstr "默认资产组" #: assets/models/label.py:15 audits/models.py:36 audits/models.py:56 #: audits/models.py:69 audits/serializers.py:77 authentication/models.py:43 -#: perms/forms/asset_permission.py:83 perms/forms/database_app_permission.py:38 +#: orgs/models.py:16 orgs/models.py:312 perms/forms/asset_permission.py:83 +#: perms/forms/database_app_permission.py:38 #: perms/forms/remote_app_permission.py:40 perms/models/base.py:49 #: templates/index.html:78 terminal/backends/command/models.py:18 #: terminal/backends/command/serializers.py:12 terminal/models.py:185 #: tickets/models/ticket.py:35 tickets/models/ticket.py:130 #: tickets/serializers/request_asset_perm.py:55 #: tickets/serializers/ticket.py:27 users/forms/group.py:15 -#: users/models/user.py:160 users/models/user.py:176 users/models/user.py:615 +#: users/models/user.py:157 users/models/user.py:623 #: users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 @@ -707,14 +708,14 @@ msgid "Backend" msgstr "后端" #: assets/serializers/asset_user.py:75 users/forms/profile.py:148 -#: users/models/user.py:497 users/templates/users/user_password_update.html:48 +#: users/models/user.py:503 users/templates/users/user_password_update.html:48 #: users/templates/users/user_profile.html:69 #: users/templates/users/user_profile_update.html:46 #: users/templates/users/user_pubkey_update.html:46 msgid "Public key" msgstr "SSH公钥" -#: assets/serializers/asset_user.py:79 users/models/user.py:494 +#: assets/serializers/asset_user.py:79 users/models/user.py:500 msgid "Private key" msgstr "ssh私钥" @@ -999,8 +1000,8 @@ msgstr "Agent" #: audits/models.py:104 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: authentication/templates/authentication/login_otp.html:6 -#: users/forms/profile.py:52 users/models/user.py:489 -#: users/serializers/user.py:220 users/templates/users/user_detail.html:77 +#: users/forms/profile.py:52 users/models/user.py:495 +#: users/serializers/user.py:224 users/templates/users/user_detail.html:77 #: users/templates/users/user_profile.html:87 msgid "MFA" msgstr "多因子认证" @@ -1169,7 +1170,10 @@ msgstr "等待登录复核处理" msgid "Login confirm ticket was {}" msgstr "登录复核 {}" -#: authentication/forms.py:29 users/forms/user.py:199 +#: authentication/forms.py:26 authentication/forms.py:34 +#: authentication/templates/authentication/login.html:38 +#: authentication/templates/authentication/xpack_login.html:118 +#: users/forms/user.py:199 msgid "MFA code" msgstr "多因子认证验证码" @@ -1224,7 +1228,7 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:387 users/serializers/user.py:217 +#: users/models/user.py:393 users/serializers/user.py:221 #: users/templates/users/user_profile.html:94 #: users/templates/users/user_profile.html:163 #: users/templates/users/user_profile.html:166 @@ -1233,7 +1237,7 @@ msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:388 users/serializers/user.py:218 +#: users/models/user.py:394 users/serializers/user.py:222 #: users/templates/users/user_profile.html:92 #: users/templates/users/user_profile.html:170 msgid "Enable" @@ -1274,29 +1278,29 @@ msgid "Code error" msgstr "代码错误" #: authentication/templates/authentication/login.html:6 -#: authentication/templates/authentication/login.html:39 -#: authentication/templates/authentication/xpack_login.html:112 +#: authentication/templates/authentication/login.html:49 +#: authentication/templates/authentication/xpack_login.html:130 #: templates/_base_only_msg_content.html:51 templates/_header_bar.html:83 msgid "Login" msgstr "登录" #: authentication/templates/authentication/login.html:17 -#: authentication/templates/authentication/xpack_login.html:87 +#: authentication/templates/authentication/xpack_login.html:95 msgid "Captcha invalid" msgstr "验证码错误" -#: authentication/templates/authentication/login.html:50 -#: authentication/templates/authentication/xpack_login.html:116 +#: authentication/templates/authentication/login.html:60 +#: authentication/templates/authentication/xpack_login.html:134 #: users/templates/users/forgot_password.html:7 #: users/templates/users/forgot_password.html:8 msgid "Forgot password" msgstr "忘记密码" -#: authentication/templates/authentication/login.html:57 +#: authentication/templates/authentication/login.html:67 msgid "More login options" msgstr "更多登录方式" -#: authentication/templates/authentication/login.html:61 +#: authentication/templates/authentication/login.html:71 msgid "OpenID" msgstr "OpenID" @@ -1337,11 +1341,11 @@ msgstr "返回" msgid "Copy success" msgstr "复制成功" -#: authentication/templates/authentication/xpack_login.html:74 +#: authentication/templates/authentication/xpack_login.html:78 msgid "Welcome back, please enter username and password to login" msgstr "欢迎回来,请输入用户名和密码登录" -#: authentication/views/login.py:83 +#: authentication/views/login.py:82 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" @@ -1602,11 +1606,11 @@ msgstr "命令 `{}` 不允许被执行 ......." msgid "Task end" msgstr "任务结束" -#: ops/tasks.py:68 +#: ops/tasks.py:71 msgid "Clean task history period" msgstr "定期清除任务历史" -#: ops/tasks.py:81 +#: ops/tasks.py:84 msgid "Clean celery log period" msgstr "定期清除Celery日志" @@ -1622,18 +1626,35 @@ msgstr "更新任务内容: {}" msgid "Disk used more than 80%: {} => {}" msgstr "磁盘使用率超过 80%: {} => {}" -#: orgs/api.py:57 +#: orgs/api.py:54 msgid "Organization contains undeleted resources" msgstr "组织内包含未删除的资源" -#: orgs/api.py:61 +#: orgs/api.py:58 msgid "The current organization cannot be deleted" msgstr "当能删除当前所在组织" -#: orgs/mixins/models.py:56 orgs/mixins/serializers.py:26 orgs/models.py:31 +#: orgs/mixins/models.py:56 orgs/mixins/serializers.py:26 orgs/models.py:40 +#: orgs/models.py:311 msgid "Organization" msgstr "组织" +#: orgs/models.py:15 +msgid "Organization administrator" +msgstr "组织管理员" + +#: orgs/models.py:17 +msgid "Organization auditor" +msgstr "组织审计员" + +#: orgs/models.py:313 users/forms/user.py:27 users/models/user.py:483 +#: users/templates/users/_select_user_modal.html:15 +#: users/templates/users/user_detail.html:73 +#: users/templates/users/user_list.html:16 +#: users/templates/users/user_profile.html:55 +msgid "Role" +msgstr "角色" + #: perms/const.py:7 msgid "Ungrouped" msgstr "未分组" @@ -1651,7 +1672,8 @@ msgstr "提示:RDP 协议不支持单独控制上传或下载文件" #: perms/forms/asset_permission.py:86 perms/forms/database_app_permission.py:41 #: perms/forms/remote_app_permission.py:43 perms/models/base.py:50 #: templates/_nav.html:21 users/forms/user.py:168 users/models/group.py:31 -#: users/models/user.py:473 users/templates/users/_select_user_modal.html:16 +#: users/models/user.py:479 users/serializers/user.py:43 +#: users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_asset_permission.html:39 #: users/templates/users/user_asset_permission.html:67 #: users/templates/users/user_database_app_permission.html:38 @@ -1717,7 +1739,7 @@ msgid "Asset permission" msgstr "资产授权" #: perms/models/base.py:53 tickets/serializers/request_asset_perm.py:20 -#: users/models/user.py:505 users/templates/users/user_detail.html:93 +#: users/models/user.py:511 users/templates/users/user_detail.html:93 #: users/templates/users/user_profile.html:120 msgid "Date expired" msgstr "失效日期" @@ -2634,7 +2656,7 @@ msgstr "" "
\n" " " -#: users/api/user.py:119 +#: users/api/user.py:126 msgid "Could not reset self otp, use profile reset instead" msgstr "不能在该页面重置多因子认证, 请去个人信息页面重置" @@ -2680,7 +2702,7 @@ msgstr "确认密码" msgid "Password does not match" msgstr "密码不一致" -#: users/forms/profile.py:89 users/models/user.py:469 +#: users/forms/profile.py:89 users/models/user.py:475 #: users/templates/users/user_detail.html:57 #: users/templates/users/user_profile.html:59 msgid "Email" @@ -2716,20 +2738,12 @@ msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" #: users/forms/profile.py:137 users/forms/user.py:90 -#: users/serializers/user.py:181 users/serializers/user.py:262 -#: users/serializers/user.py:320 +#: users/serializers/user.py:185 users/serializers/user.py:266 +#: users/serializers/user.py:324 msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" -#: users/forms/user.py:27 users/models/user.py:477 -#: users/templates/users/_select_user_modal.html:15 -#: users/templates/users/user_detail.html:73 -#: users/templates/users/user_list.html:16 -#: users/templates/users/user_profile.html:55 -msgid "Role" -msgstr "角色" - -#: users/forms/user.py:31 users/models/user.py:512 +#: users/forms/user.py:31 users/models/user.py:518 #: users/templates/users/user_detail.html:89 #: users/templates/users/user_list.html:18 #: users/templates/users/user_profile.html:102 @@ -2749,105 +2763,105 @@ msgstr "添加到用户组" msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" -#: users/forms/user.py:124 users/serializers/user.py:30 +#: users/forms/user.py:124 users/serializers/user.py:31 msgid "Reset link will be generated and sent to the user" msgstr "生成重置密码链接,通过邮件发送给用户" -#: users/forms/user.py:125 users/serializers/user.py:31 +#: users/forms/user.py:125 users/serializers/user.py:32 msgid "Set password" msgstr "设置密码" -#: users/forms/user.py:132 users/serializers/user.py:38 +#: users/forms/user.py:132 users/serializers/user.py:39 #: xpack/plugins/change_auth_plan/models.py:61 #: xpack/plugins/change_auth_plan/serializers.py:30 msgid "Password strategy" msgstr "密码策略" -#: users/models/user.py:159 users/models/user.py:623 -msgid "Administrator" -msgstr "管理员" +#: users/models/user.py:156 +msgid "Super administrator" +msgstr "超级管理员" -#: users/models/user.py:161 +#: users/models/user.py:158 +msgid "Super auditor" +msgstr "超级审计员" + +#: users/models/user.py:159 msgid "Application" msgstr "应用程序" -#: users/models/user.py:162 -msgid "Auditor" -msgstr "审计员" - -#: users/models/user.py:172 -msgid "Org admin" -msgstr "组织管理员" - -#: users/models/user.py:174 -msgid "Org auditor" -msgstr "组织审计员" - -#: users/models/user.py:389 users/templates/users/user_profile.html:90 +#: users/models/user.py:395 users/templates/users/user_profile.html:90 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:456 +#: users/models/user.py:462 msgid "Local" msgstr "数据库" -#: users/models/user.py:480 +#: users/models/user.py:486 msgid "Avatar" msgstr "头像" -#: users/models/user.py:483 users/templates/users/user_detail.html:68 +#: users/models/user.py:489 users/templates/users/user_detail.html:68 msgid "Wechat" msgstr "微信" -#: users/models/user.py:516 +#: users/models/user.py:522 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:626 +#: users/models/user.py:631 +msgid "Administrator" +msgstr "管理员" + +#: users/models/user.py:634 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" -#: users/serializers/user.py:69 users/serializers/user.py:233 +#: users/serializers/user.py:72 users/serializers/user.py:237 msgid "Is first login" msgstr "首次登录" -#: users/serializers/user.py:70 +#: users/serializers/user.py:73 msgid "Is valid" msgstr "账户是否有效" -#: users/serializers/user.py:71 +#: users/serializers/user.py:74 msgid "Is expired" msgstr " 是否过期" -#: users/serializers/user.py:72 +#: users/serializers/user.py:75 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/user.py:76 +#: users/serializers/user.py:79 msgid "Groups name" msgstr "用户组名" -#: users/serializers/user.py:77 +#: users/serializers/user.py:80 msgid "Source name" msgstr "用户来源名" -#: users/serializers/user.py:78 -msgid "Role name" -msgstr "角色名" +#: users/serializers/user.py:81 +msgid "Organization role name" +msgstr "组织角色名称" -#: users/serializers/user.py:101 +#: users/serializers/user.py:82 +msgid "Super role name" +msgstr "超级角色名称" + +#: users/serializers/user.py:105 msgid "Role limit to {}" msgstr "角色只能为 {}" -#: users/serializers/user.py:113 users/serializers/user.py:286 +#: users/serializers/user.py:117 users/serializers/user.py:290 msgid "Password does not match security rules" msgstr "密码不满足安全规则" -#: users/serializers/user.py:278 +#: users/serializers/user.py:282 msgid "The old password is incorrect" msgstr "旧密码错误" -#: users/serializers/user.py:292 +#: users/serializers/user.py:296 msgid "The newly set password is inconsistent" msgstr "两次密码不一致" @@ -3969,6 +3983,15 @@ msgstr "企业版" msgid "Ultimate edition" msgstr "旗舰版" +#~ msgid "Auditor" +#~ msgstr "审计员" + +#~ msgid "Org admin" +#~ msgstr "组织管理员" + +#~ msgid "Role name" +#~ msgstr "角色名" + #~ msgid "GUI copy" #~ msgstr "GUI 复制" @@ -5508,9 +5531,6 @@ msgstr "旗舰版" #~ msgid "Admin" #~ msgstr "管理员" -#~ msgid "Organizations" -#~ msgstr "组织管理" - #~ msgid "Org detail" #~ msgstr "组织详情" diff --git a/apps/orgs/api.py b/apps/orgs/api.py index d8ba32635..e29a14e22 100644 --- a/apps/orgs/api.py +++ b/apps/orgs/api.py @@ -1,23 +1,20 @@ # -*- coding: utf-8 -*- # -from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext as _ from rest_framework import status, generics from rest_framework.views import Response from rest_framework_bulk import BulkModelViewSet from common.permissions import IsSuperUserOrAppUser -from .models import Organization +from .models import Organization, ROLE from .serializers import OrgSerializer, OrgReadSerializer, \ - OrgMembershipUserSerializer, OrgMembershipAdminSerializer, \ OrgAllUserSerializer, OrgRetrieveSerializer from users.models import User, UserGroup from assets.models import Asset, Domain, AdminUser, SystemUser, Label from perms.models import AssetPermission from orgs.utils import current_org from common.utils import get_logger -from .mixins.api import OrgMembershipModelViewSetMixin logger = get_logger(__file__) @@ -39,7 +36,7 @@ class OrgViewSet(BulkModelViewSet): def get_data_from_model(self, model): if model == User: - data = model.objects.filter(related_user_orgs__id=self.org.id) + data = model.objects.filter(orgs__id=self.org.id, m2m_org_members__role=ROLE.USER) else: data = model.objects.filter(org_id=self.org.id) return data @@ -64,18 +61,6 @@ class OrgViewSet(BulkModelViewSet): return Response({'msg': True}, status=status.HTTP_200_OK) -class OrgMembershipAdminsViewSet(OrgMembershipModelViewSetMixin, BulkModelViewSet): - serializer_class = OrgMembershipAdminSerializer - membership_class = Organization.admins.through - permission_classes = (IsSuperUserOrAppUser, ) - - -class OrgMembershipUsersViewSet(OrgMembershipModelViewSetMixin, BulkModelViewSet): - serializer_class = OrgMembershipUserSerializer - membership_class = Organization.users.through - permission_classes = (IsSuperUserOrAppUser, ) - - class OrgAllUserListApi(generics.ListAPIView): permission_classes = (IsSuperUserOrAppUser,) serializer_class = OrgAllUserSerializer @@ -84,6 +69,7 @@ class OrgAllUserListApi(generics.ListAPIView): def get_queryset(self): pk = self.kwargs.get("pk") - org = get_object_or_404(Organization, pk=pk) - users = org.get_org_users().only(*self.serializer_class.Meta.only_fields) + users = User.objects.filter( + orgs=pk, m2m_org_members__role=ROLE.USER + ).only(*self.serializer_class.Meta.only_fields) return users diff --git a/apps/orgs/migrations/0004_organizationmember.py b/apps/orgs/migrations/0004_organizationmember.py new file mode 100644 index 000000000..6b43a5850 --- /dev/null +++ b/apps/orgs/migrations/0004_organizationmember.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.10 on 2020-07-21 11:27 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('orgs', '0003_auto_20190916_1057'), + ] + + operations = [ + migrations.CreateModel( + name='OrganizationMember', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('role', models.CharField(choices=[('Admin', 'Administrator'), ('User', 'User'), ('Auditor', 'Auditor')], default='User', max_length=16, verbose_name='Role')), + ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')), + ('org', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='m2m_org_members', to='orgs.Organization', verbose_name='Organization')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='m2m_org_members', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'db_table': 'orgs_organization_members', + 'unique_together': {('org', 'user', 'role')}, + }, + ), + ] diff --git a/apps/orgs/migrations/0005_auto_20200721_1937.py b/apps/orgs/migrations/0005_auto_20200721_1937.py new file mode 100644 index 000000000..ac0ec8a08 --- /dev/null +++ b/apps/orgs/migrations/0005_auto_20200721_1937.py @@ -0,0 +1,35 @@ +# Generated by Django 2.2.10 on 2020-07-21 11:37 + +from django.db import migrations + + +def migrate_old_organization_members(apps, schema_editor): + org_model = apps.get_model("orgs", "Organization") + org_member_model = apps.get_model('orgs', 'OrganizationMember') + orgs = org_model.objects.all() + + roles = ['User', 'Auditor', 'Admin'] + + for org in orgs: + users = org.users.all().only('id') + auditors = org.auditors.all().only('id') + admins = org.admins.all().only('id') + total_members = zip([users, auditors, admins], roles) + + org_members = [] + for members, role in total_members: + for user in members: + org_user = org_member_model(user=user, org=org, role=role) + org_members.append(org_user) + org_member_model.objects.bulk_create(org_members) + + +class Migration(migrations.Migration): + + dependencies = [ + ('orgs', '0004_organizationmember'), + ] + + operations = [ + migrations.RunPython(migrate_old_organization_members) + ] diff --git a/apps/orgs/migrations/0006_auto_20200721_1937.py b/apps/orgs/migrations/0006_auto_20200721_1937.py new file mode 100644 index 000000000..fe0b1f477 --- /dev/null +++ b/apps/orgs/migrations/0006_auto_20200721_1937.py @@ -0,0 +1,32 @@ +# Generated by Django 2.2.10 on 2020-07-21 11:37 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('orgs', '0005_auto_20200721_1937'), + ] + + operations = [ + migrations.RemoveField( + model_name='organization', + name='admins', + ), + migrations.RemoveField( + model_name='organization', + name='auditors', + ), + migrations.RemoveField( + model_name='organization', + name='users', + ), + migrations.AddField( + model_name='organization', + name='members', + field=models.ManyToManyField(related_name='orgs', through='orgs.OrganizationMember', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/apps/orgs/models.py b/apps/orgs/models.py index 6e6e6dfd6..effabf50f 100644 --- a/apps/orgs/models.py +++ b/apps/orgs/models.py @@ -1,21 +1,30 @@ import uuid -from django.conf import settings +from functools import partial from django.db import models +from django.db.models import signals +from django.db.models import Q from django.utils.translation import ugettext_lazy as _ -from common.utils import is_uuid, lazyproperty +from common.utils import is_uuid +from common.const import choices +from common.db.models import ChoiceSet + + +class ROLE(ChoiceSet): + ADMIN = choices.ADMIN, _('Organization administrator') + USER = choices.USER, _('User') + AUDITOR = choices.AUDITOR, _("Organization auditor") class Organization(models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, unique=True, verbose_name=_("Name")) - users = models.ManyToManyField('users.User', related_name='related_user_orgs', blank=True) - admins = models.ManyToManyField('users.User', related_name='related_admin_orgs', blank=True) - auditors = models.ManyToManyField('users.User', related_name='related_audit_orgs', blank=True) created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by')) date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) + members = models.ManyToManyField('users.User', related_name='orgs', through='orgs.OrganizationMember', + through_fields=('org', 'user')) orgs = None CACHE_PREFIX = 'JMS_ORG_{}' @@ -72,29 +81,24 @@ class Organization(models.Model): org = cls.default() if default else None return org - # @lazyproperty - # lazyproperty 导致用户列表中角色显示出现不稳定的情况, 如果不加会导致数据库操作次数太多 - def org_users(self): + def get_org_members_by_role(self, role): from users.models import User if self.is_real(): - return self.users.all() - users = User.objects.filter(role=User.ROLE_USER) - if self.is_default() and not settings.DEFAULT_ORG_SHOW_ALL_USERS: - users = users.filter(related_user_orgs__isnull=True) + return self.members.filter(m2m_org_members__role=role) + users = User.objects.filter(role=role) return users - def get_org_users(self): - return self.org_users() + @property + def users(self): + return self.get_org_members_by_role(ROLE.USER) - # @lazyproperty - def org_admins(self): - from users.models import User - if self.is_real(): - return self.admins.all() - return User.objects.filter(role=User.ROLE_ADMIN) + @property + def admins(self): + return self.get_org_members_by_role(ROLE.ADMIN) - def get_org_admins(self): - return self.org_admins() + @property + def auditors(self): + return self.get_org_members_by_role(ROLE.AUDITOR) def org_id(self): if self.is_real(): @@ -104,87 +108,76 @@ class Organization(models.Model): else: return '' - # @lazyproperty - def org_auditors(self): + def get_members(self, exclude=()): from users.models import User if self.is_real(): - return self.auditors.all() - return User.objects.filter(role=User.ROLE_AUDITOR) + members = self.members.exclude(m2m_org_members__role__in=exclude) + else: + members = User.objects.exclude(role__in=exclude) - def get_org_auditors(self): - return self.org_auditors() - - def get_org_members(self, exclude=()): - from users.models import User - members = User.objects.none() - if 'Admin' not in exclude: - members |= self.get_org_admins() - if 'User' not in exclude: - members |= self.get_org_users() - if 'Auditor' not in exclude: - members |= self.get_org_auditors() - return members.exclude(role=User.ROLE_APP).distinct() + return members.exclude(role=User.ROLE.APP).distinct() def can_admin_by(self, user): if user.is_superuser: return True - if self.get_org_admins().filter(id=user.id): + if self.admins.filter(id=user.id).exists(): return True return False def can_audit_by(self, user): if user.is_super_auditor: return True - if self.get_org_auditors().filter(id=user.id): + if self.auditors.filter(id=user.id).exists(): return True return False def can_user_by(self, user): - if self.get_org_users().filter(id=user.id): + if self.users.filter(id=user.id).exists(): return True return False def is_real(self): return self.id not in (self.DEFAULT_NAME, self.ROOT_ID, self.SYSTEM_ID) + @classmethod + def get_user_orgs_by_role(cls, user, role): + if not isinstance(role, (tuple, list)): + role = (role, ) + + return cls.objects.filter( + m2m_org_members__role__in=role, + m2m_org_members__user_id=user.id + ).distinct() + @classmethod def get_user_admin_orgs(cls, user): - admin_orgs = [] if user.is_anonymous: - return admin_orgs - elif user.is_superuser: - admin_orgs = list(cls.objects.all()) - admin_orgs.append(cls.default()) - elif user.is_org_admin: - admin_orgs = user.related_admin_orgs.all() - return admin_orgs + return cls.objects.none() + if user.is_superuser: + return [*cls.objects.all(), cls.default()] + return cls.get_user_orgs_by_role(user, ROLE.ADMIN) @classmethod def get_user_user_orgs(cls, user): - user_orgs = [] if user.is_anonymous: - return user_orgs - user_orgs = user.related_user_orgs.all() - return user_orgs + return cls.objects.none() + return cls.get_user_orgs_by_role(user, ROLE.USER) @classmethod def get_user_audit_orgs(cls, user): - audit_orgs = [] if user.is_anonymous: - return audit_orgs - elif user.is_super_auditor: - audit_orgs = list(cls.objects.all()) - audit_orgs.append(cls.default()) - elif user.is_org_auditor: - audit_orgs = user.related_audit_orgs.all() - return audit_orgs + return cls.objects.none() + if user.is_super_auditor: + return [*cls.objects.all(), cls.default()] + return cls.get_user_orgs_by_role(user, ROLE.AUDITOR) @classmethod - def get_user_admin_or_audit_orgs(self, user): - admin_orgs = self.get_user_admin_orgs(user) - audit_orgs = self.get_user_audit_orgs(user) - orgs = set(admin_orgs) | set(audit_orgs) - return orgs + def get_user_admin_or_audit_orgs(cls, user): + if user.is_anonymous: + return cls.objects.none() + if user.is_superuser or user.is_super_auditor: + return [*cls.objects.all(), cls.default()] + return cls.get_user_orgs_by_role(user, (ROLE.AUDITOR, ROLE.ADMIN)) @classmethod def default(cls): @@ -211,8 +204,122 @@ class Organization(models.Model): from .utils import set_current_org set_current_org(self) - @classmethod - def all_orgs(cls): - orgs = list(cls.objects.all()) - orgs.append(cls.default()) - return orgs + +def _convert_to_uuid_set(users): + rst = set() + for user in users: + if isinstance(user, models.Model): + rst.add(user.id) + elif not isinstance(user, uuid.UUID): + rst.add(uuid.UUID(user)) + return rst + + +def _none2list(*args): + return ([] if v is None else v for v in args) + + +class OrgMemeberManager(models.Manager): + + def remove_users_by_role(self, org, users=None, admins=None, auditors=None): + if not any((users, admins, auditors)): + return + users, admins, auditors = _none2list(users, admins, auditors) + + send = partial(signals.m2m_changed.send, sender=self.model, instance=org, reverse=False, + model=Organization, pk_set=[*users, *admins, *auditors], using=self.db) + + send(action="pre_remove") + self.filter(org_id=org.id).filter( + Q(user__in=users, role=ROLE.USER) | + Q(user__in=admins, role=ROLE.ADMIN) | + Q(user__in=auditors, role=ROLE.AUDITOR) + ).delete() + send(action="post_remove") + + def add_users_by_role(self, org, users=None, admins=None, auditors=None): + if not any((users, admins, auditors)): + return + users, admins, auditors = _none2list(users, admins, auditors) + + add_mapper = ( + (users, ROLE.USER), + (admins, ROLE.ADMIN), + (auditors, ROLE.AUDITOR) + ) + + oms_add = [] + for users, role in add_mapper: + for user in users: + if isinstance(user, models.Model): + user = user.id + oms_add.append(self.model(org_id=org.id, user_id=user, role=role)) + + send = partial(signals.m2m_changed.send, sender=self.model, instance=org, reverse=False, + model=Organization, pk_set=[*users, *admins, *auditors], using=self.db) + + send(action='pre_add') + self.bulk_create(oms_add) + send(action='post_add') + + def _get_remove_add_set(self, new_users, old_users): + if new_users is None: + return None, None + new_users = _convert_to_uuid_set(new_users) + return (old_users - new_users), (new_users - old_users) + + def set_users_by_role(self, org, users=None, admins=None, auditors=None): + oms = self.filter(org_id=org.id).values_list('role', 'user_id') + + old_users, old_admins, old_auditors = set(), set(), set() + + mapper = { + ROLE.USER: old_users, + ROLE.ADMIN: old_admins, + ROLE.AUDITOR: old_auditors + } + + for role, user_id in oms: + if role in mapper: + mapper[role].add(user_id) + + users_remove, users_add = self._get_remove_add_set(users, old_users) + admins_remove, admins_add = self._get_remove_add_set(admins, old_admins) + auditors_remove, auditors_add = self._get_remove_add_set(auditors, old_auditors) + + self.remove_users_by_role( + org, + users_remove, + admins_remove, + auditors_remove + ) + + self.add_users_by_role( + org, + users_add, + admins_add, + auditors_add + ) + + +class OrganizationMember(models.Model): + """ + 注意:直接调用该 `Model.delete` `Model.objects.delete` 不会触发清理该用户的信号 + """ + + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + org = models.ForeignKey(Organization, related_name='m2m_org_members', on_delete=models.CASCADE, verbose_name=_('Organization')) + user = models.ForeignKey('users.User', related_name='m2m_org_members', on_delete=models.CASCADE, verbose_name=_('User')) + role = models.CharField(max_length=16, choices=ROLE.choices, default=ROLE.USER, verbose_name=_("Role")) + date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created")) + date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated")) + created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by')) + + objects = OrgMemeberManager() + + class Meta: + unique_together = [('org', 'user', 'role')] + db_table = 'orgs_organization_members' + + def __str__(self): + return '{} is {}: {}'.format(self.user.name, self.org.name, self.role) diff --git a/apps/orgs/serializers.py b/apps/orgs/serializers.py index ae98420e0..4cf54f92a 100644 --- a/apps/orgs/serializers.py +++ b/apps/orgs/serializers.py @@ -1,16 +1,18 @@ from rest_framework.serializers import ModelSerializer from rest_framework import serializers -from users.models import UserGroup -from assets.models import Asset, Domain, AdminUser, SystemUser, Label -from perms.models import AssetPermission + +from users.models.user import User from common.serializers import AdaptedBulkListSerializer -from .utils import set_current_org, get_current_org -from .models import Organization +from .models import Organization, OrganizationMember from .mixins.serializers import OrgMembershipSerializerMixin class OrgSerializer(ModelSerializer): + users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True) + admins = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True) + auditors = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True) + class Meta: model = Organization list_serializer_class = AdaptedBulkListSerializer @@ -21,11 +23,27 @@ class OrgSerializer(ModelSerializer): fields_m2m = ['users', 'admins', 'auditors'] fields = fields_small + fields_m2m read_only_fields = ['created_by', 'date_created'] - extra_kwargs = { - 'admins': {'write_only': True}, - 'users': {'write_only': True}, - 'auditors': {'write_only': True}, - } + + def create(self, validated_data): + members = self._pop_memebers(validated_data) + instance = Organization.objects.create(**validated_data) + OrganizationMember.objects.add_users_by_role(instance, *members) + return instance + + def _pop_memebers(self, validated_data): + return ( + validated_data.pop('users', None), + validated_data.pop('admins', None), + validated_data.pop('auditors', None) + ) + + def update(self, instance, validated_data): + members = self._pop_memebers(validated_data) + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.save() + OrganizationMember.objects.set_users_by_role(instance, *members) + return instance class OrgReadSerializer(OrgSerializer): @@ -34,14 +52,14 @@ class OrgReadSerializer(OrgSerializer): class OrgMembershipAdminSerializer(OrgMembershipSerializerMixin, ModelSerializer): class Meta: - model = Organization.admins.through + model = Organization.members.through list_serializer_class = AdaptedBulkListSerializer fields = '__all__' class OrgMembershipUserSerializer(OrgMembershipSerializerMixin, ModelSerializer): class Meta: - model = Organization.users.through + model = Organization.members.through list_serializer_class = AdaptedBulkListSerializer fields = '__all__' diff --git a/apps/orgs/signals_handler.py b/apps/orgs/signals_handler.py index 17e3d525e..eb7df9741 100644 --- a/apps/orgs/signals_handler.py +++ b/apps/orgs/signals_handler.py @@ -5,7 +5,7 @@ from django.db.models.signals import m2m_changed from django.db.models.signals import post_save from django.dispatch import receiver -from .models import Organization +from .models import Organization, OrganizationMember from .hands import set_current_org, current_org, Node, get_current_org from perms.models import AssetPermission from users.models import UserGroup @@ -26,23 +26,31 @@ def on_org_create_or_update(sender, instance=None, created=False, **kwargs): instance.expire_cache() -@receiver(m2m_changed, sender=Organization.users.through) -def on_org_user_changed(sender, instance=None, **kwargs): - if isinstance(instance, Organization): - old_org = current_org - set_current_org(instance) - if kwargs['action'] == 'pre_remove': - users = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) - for user in users: - perms = AssetPermission.objects.filter(users=user) - user_groups = UserGroup.objects.filter(users=user) - for perm in perms: - perm.users.remove(user) - for user_group in user_groups: - user_group.users.remove(user) - set_current_org(old_org) +def _remove_users(model, users, org, reverse=False): + if not isinstance(users, (tuple, list, set)): + users = (users, ) + + m2m_model = model.users.through + if reverse: + m2m_field_name = model.users.field.m2m_reverse_field_name() + else: + m2m_field_name = model.users.field.m2m_field_name() + m2m_model.objects.filter(**{'user__in': users, f'{m2m_field_name}__org_id': org.id}).delete() -@receiver(m2m_changed, sender=Organization.admins.through) -def on_org_admin_change(sender, **kwargs): - Organization._user_admin_orgs = None +def _clear_users_from_org(org, users): + if not users: + return + + old_org = current_org + set_current_org(org) + _remove_users(AssetPermission, users, org) + _remove_users(UserGroup, users, org, reverse=True) + set_current_org(old_org) + + +@receiver(m2m_changed, sender=OrganizationMember) +def on_org_user_changed(sender, instance=None, action=None, pk_set=None, **kwargs): + if action == 'post_remove': + leaved_users = set(pk_set) - set(instance.members.values_list('id', flat=True)) + _clear_users_from_org(instance, leaved_users) diff --git a/apps/orgs/tests.py b/apps/orgs/tests.py index 7ce503c2d..1b007fe3d 100644 --- a/apps/orgs/tests.py +++ b/apps/orgs/tests.py @@ -1,3 +1,16 @@ -from django.test import TestCase +from django.urls import reverse +from rest_framework.test import APITestCase +from rest_framework import status + +from users.models.user import User + + +class OrgTests(APITestCase): + def test_create(self): + print(User.objects.all()) + reverse('api-orgs:org-list') + + + +{"name":"a-07","admins":["138167d2-6843-4e25-b838-59657157c6c6"],"auditors":["8d4b3ec4-8339-4a2c-b33c-c2633da62c84"],"users":["ea60e8ce-876d-493b-a641-ff836258629c"]} -# Create your tests here. diff --git a/apps/orgs/urls/api_urls.py b/apps/orgs/urls/api_urls.py index 17f8c3c8a..e14435868 100644 --- a/apps/orgs/urls/api_urls.py +++ b/apps/orgs/urls/api_urls.py @@ -11,12 +11,6 @@ from .. import api app_name = 'orgs' router = DefaultRouter() -# 将会删除 -router.register(r'orgs/(?P[0-9a-zA-Z\-]{36})/membership/admins', - api.OrgMembershipAdminsViewSet, 'membership-admins') -router.register(r'orgs/(?P[0-9a-zA-Z\-]{36})/membership/users', - api.OrgMembershipUsersViewSet, 'membership-users'), - router.register(r'orgs', api.OrgViewSet, 'org') old_version_urlpatterns = [ diff --git a/apps/users/api/mixins.py b/apps/users/api/mixins.py index 117c2c28a..0e81bd8d2 100644 --- a/apps/users/api/mixins.py +++ b/apps/users/api/mixins.py @@ -9,7 +9,7 @@ from orgs.utils import current_org class UserQuerysetMixin: def get_queryset(self): if self.request.query_params.get('all') or not current_org.is_real(): - queryset = User.objects.exclude(role=User.ROLE_APP) + queryset = User.objects.exclude(role=User.ROLE.APP) else: queryset = utils.get_current_org_members() return queryset diff --git a/apps/users/api/user.py b/apps/users/api/user.py index 3ad748316..b1c039318 100644 --- a/apps/users/api/user.py +++ b/apps/users/api/user.py @@ -1,11 +1,13 @@ # ~*~ coding: utf-8 ~*~ from django.core.cache import cache +from django.db.models import CharField from django.utils.translation import ugettext as _ from rest_framework import generics from rest_framework.response import Response from rest_framework_bulk import BulkModelViewSet +from common.db.aggregates import GroupConcat from common.permissions import ( IsOrgAdmin, IsOrgAdminOrAppUser, CanUpdateDeleteUser, IsSuperUser @@ -13,6 +15,7 @@ from common.permissions import ( from common.mixins import CommonApiMixin from common.utils import get_logger from orgs.utils import current_org +from orgs.models import ROLE as ORG_ROLE, OrganizationMember from .. import serializers from ..serializers import UserSerializer, UserRetrieveSerializer from .mixins import UserQuerysetMixin @@ -39,7 +42,11 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet): extra_filter_backends = [OrgRoleUserFilterBackend] def get_queryset(self): - return super().get_queryset().prefetch_related('groups') + return super().get_queryset().annotate( + gc_m2m_org_members__role=GroupConcat('m2m_org_members__role'), + gc_groups__name=GroupConcat('groups__name'), + gc_groups=GroupConcat('groups__id', output_field=CharField()) + ) def send_created_signal(self, users): if not isinstance(users, list): @@ -48,11 +55,32 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet): post_user_create.send(self.__class__, user=user) def perform_create(self, serializer): + validated_data = serializer.validated_data + if isinstance(validated_data, list): + org_roles = [item.pop('org_role', None) for item in validated_data] + else: + org_roles = [validated_data.pop('org_role', None)] + users = serializer.save() if isinstance(users, User): users = [users] if current_org and current_org.is_real(): - current_org.users.add(*users) + mapper = { + ORG_ROLE.USER: [], + ORG_ROLE.ADMIN: [], + ORG_ROLE.AUDITOR: [] + } + + for user, role in zip(users, org_roles): + if role in mapper: + mapper[role].append(user) + else: + mapper[ORG_ROLE.USER].append(user) + OrganizationMember.objects.set_users_by_role( + current_org, users=mapper[ORG_ROLE.USER], + admins=mapper[ORG_ROLE.ADMIN], + auditors=mapper[ORG_ROLE.AUDITOR] + ) self.send_created_signal(users) def get_permissions(self): @@ -78,7 +106,7 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet): users_ids = [ d.get("id") or d.get("pk") for d in serializer.validated_data ] - users = current_org.get_org_members().filter(id__in=users_ids) + users = current_org.get_members().filter(id__in=users_ids) for user in users: self.check_object_permissions(self.request, user) return super().perform_bulk_update(serializer) diff --git a/apps/users/filters.py b/apps/users/filters.py index d12d3234e..0aa46609e 100644 --- a/apps/users/filters.py +++ b/apps/users/filters.py @@ -12,13 +12,13 @@ class OrgRoleUserFilterBackend(filters.BaseFilterBackend): return queryset if org_role == 'admins': - return queryset & (current_org.get_org_admins() | User.objects.filter(role=User.ROLE_ADMIN)) + return queryset & (current_org.admins | User.objects.filter(role=User.ROLE_ADMIN)) elif org_role == 'auditors': - return queryset & current_org.get_org_auditors() + return queryset & current_org.auditors elif org_role == 'users': - return queryset & current_org.get_org_users() + return queryset & current_org.users elif org_role == 'members': - return queryset & current_org.get_org_members() + return queryset & current_org.get_members() def get_schema_fields(self, view): return [ diff --git a/apps/users/forms/user.py b/apps/users/forms/user.py index a58e1fef1..e02aaf17e 100644 --- a/apps/users/forms/user.py +++ b/apps/users/forms/user.py @@ -17,14 +17,14 @@ __all__ = [ class UserCreateUpdateFormMixin(OrgModelForm): - role_choices = ((i, n) for i, n in User.ROLE_CHOICES if i != User.ROLE_APP) + role_choices = ((i, n) for i, n in User.ROLE.choices if i != User.ROLE.APP) password = forms.CharField( label=_('Password'), widget=forms.PasswordInput, max_length=128, strip=False, required=False, ) role = forms.ChoiceField( choices=role_choices, required=True, - initial=User.ROLE_USER, label=_("Role") + initial=User.ROLE.USER, label=_("Role") ) source = forms.ChoiceField( choices=get_source_choices, required=True, diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 89bfe4254..456cd1fbc 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -18,8 +18,10 @@ from django.shortcuts import reverse from common.local import LOCAL_DYNAMIC_SETTINGS from orgs.utils import current_org -from common.utils import signer, date_expired_default, get_logger, lazyproperty +from common.utils import date_expired_default, get_logger, lazyproperty from common import fields +from common.const import choices +from common.db.models import ChoiceSet from ..signals import post_user_change_password @@ -150,45 +152,58 @@ class AuthMixin: class RoleMixin: - ROLE_ADMIN = 'Admin' - ROLE_USER = 'User' - ROLE_APP = 'App' - ROLE_AUDITOR = 'Auditor' + class ROLE(ChoiceSet): + ADMIN = choices.ADMIN, _('Super administrator') + USER = choices.USER, _('User') + AUDITOR = choices.AUDITOR, _('Super auditor') + APP = 'App', _('Application') - ROLE_CHOICES = ( - (ROLE_ADMIN, _('Administrator')), - (ROLE_USER, _('User')), - (ROLE_APP, _('Application')), - (ROLE_AUDITOR, _("Auditor")) - ) - role = ROLE_USER + role = ROLE.USER @property - def role_display(self): + def super_role_display(self): + return self.get_role_display() + + @property + def org_role_display(self): + from orgs.models import ROLE as ORG_ROLE + if not current_org.is_real(): - return self.get_role_display() - roles = [] - if self in current_org.get_org_admins(): - roles.append(str(_('Org admin'))) - if self in current_org.get_org_auditors(): - roles.append(str(_('Org auditor'))) - if self in current_org.get_org_users(): - roles.append(str(_('User'))) - return " | ".join(roles) + if self.is_superuser: + return ORG_ROLE.ADMIN.label + else: + return ORG_ROLE.USER.label + + if hasattr(self, 'gc_m2m_org_members__role'): + names = self.gc_m2m_org_members__role + if isinstance(names, str): + roles = set(self.gc_m2m_org_members__role.split(',')) + else: + roles = set() + else: + roles = set(self.m2m_org_members.filter( + org_id=current_org.id + ).values_list('role', flat=True)) + + return ' | '.join([str(ORG_ROLE[role]) for role in roles if role in ORG_ROLE]) def current_org_roles(self): - roles = [] - if self.can_admin_current_org: - roles.append('Admin') - if self.can_audit_current_org: - roles.append('Auditor') - else: - roles.append('User') + from orgs.models import OrganizationMember, ROLE as ORG_ROLE + if not current_org.is_real(): + if self.is_superuser: + return [ORG_ROLE.ADMIN] + else: + return [ORG_ROLE.USER] + + roles = list(set(OrganizationMember.objects.filter( + org_id=current_org.id, user=self + ).values_list('role', flat=True))) + return roles @property def is_superuser(self): - if self.role == 'Admin': + if self.role == self.ROLE.ADMIN: return True else: return False @@ -196,13 +211,13 @@ class RoleMixin: @is_superuser.setter def is_superuser(self, value): if value is True: - self.role = 'Admin' + self.role = self.ROLE.ADMIN else: - self.role = 'User' + self.role = self.ROLE.USER @property def is_super_auditor(self): - return self.role == 'Auditor' + return self.role == self.ROLE.AUDITOR @property def is_common_user(self): @@ -216,7 +231,7 @@ class RoleMixin: @property def is_app(self): - return self.role == 'App' + return self.role == self.ROLE.APP @lazyproperty def user_orgs(self): @@ -240,14 +255,16 @@ class RoleMixin: @lazyproperty def is_org_admin(self): - if self.is_superuser or self.related_admin_orgs.exists(): + from orgs.models import ROLE as ORG_ROLE + if self.is_superuser or self.m2m_org_members.filter(role=ORG_ROLE.ADMIN).exists(): return True else: return False @lazyproperty def is_org_auditor(self): - if self.is_super_auditor or self.related_audit_orgs.exists(): + from orgs.models import ROLE as ORG_ROLE + if self.is_super_auditor or self.m2m_org_members.filter(role=ORG_ROLE.AUDITOR).exists(): return True else: return False @@ -283,7 +300,7 @@ class RoleMixin: def create_app_user(cls, name, comment): app = cls.objects.create( username=name, name=name, email='{}@local.domain'.format(name), - is_active=False, role='App', comment=comment, + is_active=False, role=cls.ROLE.APP, comment=comment, is_first_login=False, created_by='System' ) access_key = app.create_access_key() @@ -473,7 +490,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): blank=True, verbose_name=_('User group') ) role = models.CharField( - choices=RoleMixin.ROLE_CHOICES, default='User', max_length=10, + choices=RoleMixin.ROLE.choices, default='User', max_length=10, blank=True, verbose_name=_('Role') ) avatar = models.ImageField( @@ -526,6 +543,12 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): @property def groups_display(self): + if hasattr(self, 'gc_groups__name'): + names = self.gc_groups__name + if isinstance(names, str): + return ' '.join(set(self.gc_groups__name.split(','))) + else: + return '' return ' '.join([group.name for group in self.groups.all()]) @property @@ -646,7 +669,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): email=forgery_py.internet.email_address(), name=forgery_py.name.full_name(), password=make_password(forgery_py.lorem_ipsum.word()), - role=choice(list(dict(User.ROLE_CHOICES).keys())), + role=choice(list(dict(User.ROLE.choices).keys())), wechat=forgery_py.internet.user_name(True), comment=forgery_py.lorem_ipsum.sentence(), created_by=choice(cls.objects.all()).username) diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index bebc5042b..76485eb93 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -9,7 +9,8 @@ from common.utils import validate_ssh_public_key from common.mixins import CommonBulkSerializerMixin from common.serializers import AdaptedBulkListSerializer from common.permissions import CanUpdateDeleteUser -from ..models import User +from common.drf.fields import GroupConcatedPrimaryKeyRelatedField +from ..models import User, UserGroup __all__ = [ @@ -38,10 +39,16 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): label=_('Password strategy'), write_only=True ) mfa_level_display = serializers.ReadOnlyField(source='get_mfa_level_display') + groups = GroupConcatedPrimaryKeyRelatedField( + label=_('User group'), many=True, queryset=UserGroup.objects.all(), required=False + ) login_blocked = serializers.SerializerMethodField() can_update = serializers.SerializerMethodField() can_delete = serializers.SerializerMethodField() - + org_role = serializers.CharField( + label=_('Organization role name'), write_only=True, + allow_null=True, required=False, allow_blank=True + ) key_prefix_block = "_LOGIN_BLOCK_{}" class Meta: @@ -52,7 +59,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): # small 指的是 不需要计算的直接能从一张表中获取到的数据 fields_small = fields_mini + [ 'password', 'email', 'public_key', 'wechat', 'phone', 'mfa_level', 'mfa_enabled', - 'mfa_level_display', 'mfa_force_enabled', + 'mfa_level_display', 'mfa_force_enabled', 'super_role_display', 'comment', 'source', 'is_valid', 'is_expired', 'is_active', 'created_by', 'is_first_login', 'password_strategy', 'date_password_last_updated', 'date_expired', @@ -60,7 +67,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): ] fields = fields_small + [ 'groups', 'role', 'groups_display', 'role_display', - 'can_update', 'can_delete', 'login_blocked', + 'can_update', 'can_delete', 'login_blocked', 'org_role' ] extra_kwargs = { @@ -75,7 +82,8 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): 'can_delete': {'read_only': True}, 'groups_display': {'label': _('Groups name')}, 'source_display': {'label': _('Source name')}, - 'role_display': {'label': _('Role name')}, + 'role_display': {'label': _('Organization role name'), 'source': 'org_role_display'}, + 'super_role_display': {'label': _('Super role name')}, } def __init__(self, *args, **kwargs): @@ -87,17 +95,17 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): if not role: return choices = role._choices - choices.pop(User.ROLE_APP, None) + choices.pop(User.ROLE.APP, None) request = self.context.get('request') if request and hasattr(request, 'user') and not request.user.is_superuser: - choices.pop(User.ROLE_ADMIN, None) - choices.pop(User.ROLE_AUDITOR, None) + choices.pop(User.ROLE.ADMIN, None) + choices.pop(User.ROLE.AUDITOR, None) role._choices = choices def validate_role(self, value): request = self.context.get('request') - if not request.user.is_superuser and value != User.ROLE_USER: - role_display = dict(User.ROLE_CHOICES)[User.ROLE_USER] + if not request.user.is_superuser and value != User.ROLE.USER: + role_display = User.ROLE.USER.label msg = _("Role limit to {}".format(role_display)) raise serializers.ValidationError(msg) return value @@ -121,7 +129,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): role = self.initial_data.get('role') if self.instance: role = role or self.instance.role - if role == User.ROLE_AUDITOR: + if role == User.ROLE.AUDITOR: return [] return groups diff --git a/apps/users/templates/users/user_detail.html b/apps/users/templates/users/user_detail.html index e9bdf8b92..0b624b78a 100644 --- a/apps/users/templates/users/user_detail.html +++ b/apps/users/templates/users/user_detail.html @@ -71,7 +71,7 @@ {% endif %} {% trans 'Role' %}: - {{ object.role_display }} + {{ object.org_role_display }} {% trans 'MFA' %}: diff --git a/apps/users/utils.py b/apps/users/utils.py index af6e0197b..cf6be4c67 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -315,7 +315,7 @@ def construct_user_email(username, email): def get_current_org_members(exclude=()): from orgs.utils import current_org - return current_org.get_org_members(exclude=exclude) + return current_org.get_members(exclude=exclude) def get_source_choices(): From b33173042219526feffba7c153cd104d7c4004c5 Mon Sep 17 00:00:00 2001 From: xinwen Date: Tue, 28 Jul 2020 17:53:01 +0800 Subject: [PATCH 12/40] =?UTF-8?q?fix(users):=20=E6=9B=BF=E6=8D=A2=E6=97=A7?= =?UTF-8?q?=E6=9C=89=E8=A7=92=E8=89=B2=E5=B8=B8=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/ops/utils.py | 2 +- .../orgs/migrations/0007_auto_20200728_1805.py | 18 ++++++++++++++++++ apps/terminal/utils.py | 2 +- apps/tickets/api/request_asset_perm.py | 2 +- apps/tickets/serializers/request_asset_perm.py | 2 +- apps/users/filters.py | 2 +- apps/users/forms/user.py | 10 +++++----- .../migrations/0028_auto_20200728_1805.py | 18 ++++++++++++++++++ apps/users/serializers_v2/user.py | 2 +- apps/users/tasks.py | 4 ++-- 10 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 apps/orgs/migrations/0007_auto_20200728_1805.py create mode 100644 apps/users/migrations/0028_auto_20200728_1805.py diff --git a/apps/ops/utils.py b/apps/ops/utils.py index 81b5a1afd..83501a6cb 100644 --- a/apps/ops/utils.py +++ b/apps/ops/utils.py @@ -69,7 +69,7 @@ def send_server_performance_mail(path, usage, usages): from users.models import User subject = _("Disk used more than 80%: {} => {}").format(path, usage.percent) message = subject - admins = User.objects.filter(role=User.ROLE_ADMIN) + admins = User.objects.filter(role=User.ROLE.ADMIN) recipient_list = [u.email for u in admins if u.email] logger.info(subject) send_mail_async(subject, message, recipient_list, html_message=message) diff --git a/apps/orgs/migrations/0007_auto_20200728_1805.py b/apps/orgs/migrations/0007_auto_20200728_1805.py new file mode 100644 index 000000000..6c05e75b2 --- /dev/null +++ b/apps/orgs/migrations/0007_auto_20200728_1805.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2020-07-28 10:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orgs', '0006_auto_20200721_1937'), + ] + + operations = [ + migrations.AlterField( + model_name='organizationmember', + name='role', + field=models.CharField(choices=[('Admin', 'Organization administrator'), ('User', 'User'), ('Auditor', 'Organization auditor')], default='User', max_length=16, verbose_name='Role'), + ), + ] diff --git a/apps/terminal/utils.py b/apps/terminal/utils.py index cb8308fcd..2ea2768bc 100644 --- a/apps/terminal/utils.py +++ b/apps/terminal/utils.py @@ -20,7 +20,7 @@ def get_session_asset_list(): def get_session_user_list(): - return User.objects.exclude(role=User.ROLE_APP).values_list('username', flat=True) + return User.objects.exclude(role=User.ROLE.APP).values_list('username', flat=True) def get_session_system_user_list(): diff --git a/apps/tickets/api/request_asset_perm.py b/apps/tickets/api/request_asset_perm.py index 40add8659..c79d57d58 100644 --- a/apps/tickets/api/request_asset_perm.py +++ b/apps/tickets/api/request_asset_perm.py @@ -48,7 +48,7 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet): org_mapper = {} UserTuple = namedtuple('UserTuple', ('id', 'name', 'username')) user = request.user - superusers = User.objects.filter(role=User.ROLE_ADMIN) + superusers = User.objects.filter(role=User.ROLE.ADMIN) admins_with_org = User.objects.filter(related_admin_orgs__users=user).annotate( org_id=F('related_admin_orgs__id'), org_name=F('related_admin_orgs__name') diff --git a/apps/tickets/serializers/request_asset_perm.py b/apps/tickets/serializers/request_asset_perm.py index 54e5ed79c..9bdce49c1 100644 --- a/apps/tickets/serializers/request_asset_perm.py +++ b/apps/tickets/serializers/request_asset_perm.py @@ -58,7 +58,7 @@ class RequestAssetPermTicketSerializer(serializers.ModelSerializer): def validate_assignees(self, assignees): user = self.context['request'].user - count = User.objects.filter(Q(related_admin_orgs__users=user) | Q(role=User.ROLE_ADMIN)).filter( + count = User.objects.filter(Q(related_admin_orgs__users=user) | Q(role=User.ROLE.ADMIN)).filter( id__in=[assignee.id for assignee in assignees]).distinct().count() if count != len(assignees): diff --git a/apps/users/filters.py b/apps/users/filters.py index 0aa46609e..faf5959c8 100644 --- a/apps/users/filters.py +++ b/apps/users/filters.py @@ -12,7 +12,7 @@ class OrgRoleUserFilterBackend(filters.BaseFilterBackend): return queryset if org_role == 'admins': - return queryset & (current_org.admins | User.objects.filter(role=User.ROLE_ADMIN)) + return queryset & (current_org.admins | User.objects.filter(role=User.ROLE.ADMIN)) elif org_role == 'auditors': return queryset & current_org.auditors elif org_role == 'users': diff --git a/apps/users/forms/user.py b/apps/users/forms/user.py index e02aaf17e..f3852c0dd 100644 --- a/apps/users/forms/user.py +++ b/apps/users/forms/user.py @@ -60,9 +60,9 @@ class UserCreateUpdateFormMixin(OrgModelForm): roles = [] # Super admin user if self.request.user.is_superuser: - roles.append((User.ROLE_ADMIN, dict(User.ROLE_CHOICES).get(User.ROLE_ADMIN))) - roles.append((User.ROLE_USER, dict(User.ROLE_CHOICES).get(User.ROLE_USER))) - roles.append((User.ROLE_AUDITOR, dict(User.ROLE_CHOICES).get(User.ROLE_AUDITOR))) + roles.append((User.ROLE.ADMIN, User.ROLE.ADMIN.label)) + roles.append((User.ROLE.USER, User.ROLE.USER.label)) + roles.append((User.ROLE.AUDITOR, User.ROLE.AUDITOR.label)) # Org admin user else: @@ -70,10 +70,10 @@ class UserCreateUpdateFormMixin(OrgModelForm): # Update if user: role = kwargs.get('instance').role - roles.append((role, dict(User.ROLE_CHOICES).get(role))) + roles.append((role, User.ROLE[role])) # Create else: - roles.append((User.ROLE_USER, dict(User.ROLE_CHOICES).get(User.ROLE_USER))) + roles.append((User.ROLE.USER, User.ROLE.USER.label)) field = self.fields['role'] field.choices = set(roles) diff --git a/apps/users/migrations/0028_auto_20200728_1805.py b/apps/users/migrations/0028_auto_20200728_1805.py new file mode 100644 index 000000000..6e57d04a6 --- /dev/null +++ b/apps/users/migrations/0028_auto_20200728_1805.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2020-07-28 10:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0027_auto_20200616_1503'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='role', + field=models.CharField(blank=True, choices=[('Admin', 'Super administrator'), ('User', 'User'), ('Auditor', 'Super auditor'), ('App', 'Application')], default='User', max_length=10, verbose_name='Role'), + ), + ] diff --git a/apps/users/serializers_v2/user.py b/apps/users/serializers_v2/user.py index 884c789af..d2b9cfa1c 100644 --- a/apps/users/serializers_v2/user.py +++ b/apps/users/serializers_v2/user.py @@ -39,7 +39,7 @@ class ServiceAccountSerializer(serializers.ModelSerializer): def save(self, **kwargs): self.validated_data['email'] = self.get_email() self.validated_data['username'] = self.get_username() - self.validated_data['role'] = User.ROLE_APP + self.validated_data['role'] = User.ROLE.APP return super().save(**kwargs) def create(self, validated_data): diff --git a/apps/users/tasks.py b/apps/users/tasks.py index 5c7bc0e47..f575a3afc 100644 --- a/apps/users/tasks.py +++ b/apps/users/tasks.py @@ -22,7 +22,7 @@ logger = get_logger(__file__) @shared_task def check_password_expired(): - users = User.objects.filter(source=User.SOURCE_LOCAL).exclude(role=User.ROLE_APP) + users = User.objects.filter(source=User.SOURCE_LOCAL).exclude(role=User.ROLE.APP) for user in users: if not user.is_valid: continue @@ -49,7 +49,7 @@ def check_password_expired_periodic(): @shared_task def check_user_expired(): - users = User.objects.exclude(role=User.ROLE_APP) + users = User.objects.exclude(role=User.ROLE.APP) for user in users: if not user.is_valid: continue From f8e248f0af0c8b03161f82e8fd5f0f00da431563 Mon Sep 17 00:00:00 2001 From: xinwen Date: Thu, 9 Jul 2020 15:41:02 +0800 Subject: [PATCH 13/40] =?UTF-8?q?feat(ticket):=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E7=94=B3=E8=AF=B7=E8=B5=84=E4=BA=A7=E5=B7=A5=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/common/drf/api.py | 16 +- apps/common/mixins/api.py | 11 + apps/common/utils/common.py | 2 + apps/common/utils/timezone.py | 33 +++ apps/jumpserver/settings/custom.py | 2 + apps/locale/zh/LC_MESSAGES/django.mo | Bin 55579 -> 55837 bytes apps/locale/zh/LC_MESSAGES/django.po | 268 ++++++++++-------- apps/orgs/mixins/models.py | 1 - apps/tickets/api/request_asset_perm.py | 83 +++--- apps/tickets/api/ticket.py | 2 +- apps/tickets/exceptions.py | 6 +- .../migrations/0002_auto_20200723_1232.py | 18 -- .../migrations/0002_auto_20200728_1146.py | 30 ++ apps/tickets/mixins.py | 7 +- apps/tickets/models/ticket.py | 13 +- .../tickets/serializers/request_asset_perm.py | 142 ++++++++-- apps/tickets/tests.py | 90 +++++- apps/tickets/urls/api_urls.py | 2 +- apps/tickets/utils.py | 15 +- apps/users/models/user.py | 5 + apps/users/serializers/user.py | 8 +- 21 files changed, 522 insertions(+), 232 deletions(-) create mode 100644 apps/common/utils/timezone.py delete mode 100644 apps/tickets/migrations/0002_auto_20200723_1232.py create mode 100644 apps/tickets/migrations/0002_auto_20200728_1146.py diff --git a/apps/common/drf/api.py b/apps/common/drf/api.py index 523689e72..3d5b67b34 100644 --- a/apps/common/drf/api.py +++ b/apps/common/drf/api.py @@ -1,11 +1,21 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet -from ..mixins.api import SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin +from ..mixins.api import ( + SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, PaginatedResponseMixin +) -class JmsGenericViewSet(SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, GenericViewSet): +class JmsGenericViewSet(SerializerMixin2, + QuerySetMixin, + ExtraFilterFieldsMixin, + PaginatedResponseMixin, + GenericViewSet): pass -class JMSModelViewSet(SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, ModelViewSet): +class JMSModelViewSet(SerializerMixin2, + QuerySetMixin, + ExtraFilterFieldsMixin, + PaginatedResponseMixin, + ModelViewSet): pass diff --git a/apps/common/mixins/api.py b/apps/common/mixins/api.py index a58e8b079..5c17a5cca 100644 --- a/apps/common/mixins/api.py +++ b/apps/common/mixins/api.py @@ -67,6 +67,17 @@ class ExtraFilterFieldsMixin: return queryset +class PaginatedResponseMixin: + def get_paginated_response_with_query_set(self, queryset): + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + + class CommonApiMixin(SerializerMixin, ExtraFilterFieldsMixin): pass diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index 77a48dc60..fc9137814 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -11,6 +11,8 @@ import time import ipaddress import psutil +from .timezone import dt_formater + UUID_PATTERN = re.compile(r'\w{8}(-\w{4}){3}-\w{12}') ipip_db = None diff --git a/apps/common/utils/timezone.py b/apps/common/utils/timezone.py new file mode 100644 index 000000000..2b8779bae --- /dev/null +++ b/apps/common/utils/timezone.py @@ -0,0 +1,33 @@ +import datetime + +import pytz +from django.utils import timezone as dj_timezone +from rest_framework.fields import DateTimeField + +max = datetime.datetime.max.replace(tzinfo=datetime.timezone.utc) + + +def astimezone(dt: datetime.datetime, tzinfo: pytz.tzinfo.DstTzInfo): + assert dj_timezone.is_aware(dt) + return tzinfo.normalize(dt.astimezone(tzinfo)) + + +def as_china_cst(dt: datetime.datetime): + return astimezone(dt, pytz.timezone('Asia/Shanghai')) + + +def as_current_tz(dt: datetime.datetime): + return astimezone(dt, dj_timezone.get_current_timezone()) + + +def utcnow(): + return dj_timezone.now() + + +def now(): + return as_current_tz(utcnow()) + + +_rest_dt_field = DateTimeField() +dt_parser = _rest_dt_field.to_internal_value +dt_formater = _rest_dt_field.to_representation diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index e7d687dc3..b691d3ce5 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -96,3 +96,5 @@ XPACK_LICENSE_IS_VALID = DYNAMIC.XPACK_LICENSE_IS_VALID LOGO_URLS = DYNAMIC.LOGO_URLS CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED = CONFIG.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED + +DATETIME_DISPLAY_FORMAT = '%Y-%m-%d %H:%M:%S' diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index f72e19c129cdb9a828ece94b3631b84326a27fb3..6469256430141bfb82a02a2689a1564796fd76c6 100644 GIT binary patch delta 17288 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 Vk{;{|2u$)l($zO<{IO|i{s$Q<)$afR delta 17011 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 A+5i9m diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index dd64657a7..03a50defd 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-07-27 19:01+0800\n" +"POT-Creation-Date: 2020-07-28 11:25+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -28,7 +28,7 @@ msgstr "自定义" #: assets/models/label.py:18 ops/mixin.py:24 orgs/models.py:22 #: perms/models/base.py:48 settings/models.py:27 terminal/models.py:26 #: terminal/models.py:342 terminal/models.py:374 terminal/models.py:411 -#: users/forms/profile.py:20 users/models/group.py:15 users/models/user.py:473 +#: users/forms/profile.py:20 users/models/group.py:15 users/models/user.py:489 #: users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:154 @@ -47,7 +47,7 @@ msgid "Name" msgstr "名称" #: applications/models/database_app.py:22 assets/models/cmd_filter.py:52 -#: terminal/models.py:376 terminal/models.py:413 tickets/models/ticket.py:45 +#: terminal/models.py:376 terminal/models.py:413 tickets/models/ticket.py:46 #: users/templates/users/user_granted_database_app.html:35 msgid "Type" msgstr "类型" @@ -77,7 +77,7 @@ msgstr "数据库" #: assets/models/group.py:23 assets/models/label.py:23 ops/models/adhoc.py:37 #: orgs/models.py:25 perms/models/base.py:56 settings/models.py:32 #: terminal/models.py:36 terminal/models.py:381 terminal/models.py:418 -#: users/models/group.py:16 users/models/user.py:506 +#: users/models/group.py:16 users/models/user.py:522 #: users/templates/users/user_detail.html:115 #: users/templates/users/user_granted_database_app.html:38 #: users/templates/users/user_granted_remote_app.html:37 @@ -132,7 +132,7 @@ msgstr "参数" #: assets/models/base.py:240 assets/models/cluster.py:28 #: assets/models/cmd_filter.py:26 assets/models/cmd_filter.py:60 #: assets/models/group.py:21 common/mixins/models.py:49 orgs/models.py:23 -#: orgs/models.py:316 perms/models/base.py:54 users/models/user.py:514 +#: orgs/models.py:316 perms/models/base.py:54 users/models/user.py:530 #: users/serializers/group.py:35 users/templates/users/user_detail.html:97 #: xpack/plugins/change_auth_plan/models.py:81 xpack/plugins/cloud/models.py:56 #: xpack/plugins/cloud/models.py:146 xpack/plugins/gathered_user/models.py:30 @@ -189,7 +189,7 @@ msgstr "基础" msgid "Charset" msgstr "编码" -#: assets/models/asset.py:148 tickets/models/ticket.py:40 +#: assets/models/asset.py:148 tickets/models/ticket.py:41 msgid "Meta" msgstr "元数据" @@ -211,7 +211,7 @@ msgstr "IP" #: assets/models/asset.py:187 assets/serializers/asset_user.py:45 #: assets/serializers/gathered_user.py:20 settings/serializers/settings.py:51 -#: tickets/serializers/request_asset_perm.py:14 +#: tickets/serializers/request_asset_perm.py:21 #: users/templates/users/_granted_assets.html:25 #: users/templates/users/user_asset_permission.html:157 msgid "Hostname" @@ -339,7 +339,7 @@ msgstr "" #: audits/models.py:99 authentication/forms.py:11 #: authentication/templates/authentication/login.html:21 #: authentication/templates/authentication/xpack_login.html:101 -#: ops/models/adhoc.py:148 users/forms/profile.py:19 users/models/user.py:471 +#: ops/models/adhoc.py:148 users/forms/profile.py:19 users/models/user.py:487 #: users/templates/users/_select_user_modal.html:14 #: users/templates/users/user_detail.html:53 #: users/templates/users/user_list.html:15 @@ -391,7 +391,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:492 +#: assets/models/cluster.py:22 users/models/user.py:508 #: users/templates/users/user_detail.html:62 msgid "Phone" msgstr "手机" @@ -417,7 +417,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:635 +#: users/models/user.py:655 msgid "System" msgstr "系统" @@ -485,7 +485,7 @@ msgstr "每行一个命令" #: assets/models/cmd_filter.py:56 audits/models.py:57 #: authentication/templates/authentication/_access_key_modal.html:34 #: perms/forms/asset_permission.py:20 -#: tickets/serializers/request_asset_perm.py:54 +#: tickets/serializers/request_asset_perm.py:60 #: tickets/serializers/ticket.py:26 #: users/templates/users/_granted_assets.html:29 #: users/templates/users/user_asset_permission.html:44 @@ -540,10 +540,10 @@ msgstr "默认资产组" #: perms/forms/remote_app_permission.py:40 perms/models/base.py:49 #: templates/index.html:78 terminal/backends/command/models.py:18 #: terminal/backends/command/serializers.py:12 terminal/models.py:185 -#: tickets/models/ticket.py:35 tickets/models/ticket.py:130 -#: tickets/serializers/request_asset_perm.py:55 +#: tickets/models/ticket.py:36 tickets/models/ticket.py:135 +#: tickets/serializers/request_asset_perm.py:61 #: tickets/serializers/ticket.py:27 users/forms/group.py:15 -#: users/models/user.py:157 users/models/user.py:623 +#: users/models/user.py:157 users/models/user.py:643 #: users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 @@ -649,7 +649,7 @@ msgstr "SFTP根路径" #: perms/models/remote_app_permission.py:16 templates/_nav.html:45 #: terminal/backends/command/models.py:20 #: terminal/backends/command/serializers.py:14 terminal/models.py:189 -#: tickets/serializers/request_asset_perm.py:16 +#: tickets/serializers/request_asset_perm.py:23 #: users/templates/users/_granted_assets.html:27 #: users/templates/users/user_asset_permission.html:42 #: users/templates/users/user_asset_permission.html:76 @@ -708,14 +708,14 @@ msgid "Backend" msgstr "后端" #: assets/serializers/asset_user.py:75 users/forms/profile.py:148 -#: users/models/user.py:503 users/templates/users/user_password_update.html:48 +#: users/models/user.py:519 users/templates/users/user_password_update.html:48 #: users/templates/users/user_profile.html:69 #: users/templates/users/user_profile_update.html:46 #: users/templates/users/user_pubkey_update.html:46 msgid "Public key" msgstr "SSH公钥" -#: assets/serializers/asset_user.py:79 users/models/user.py:500 +#: assets/serializers/asset_user.py:79 users/models/user.py:516 msgid "Private key" msgstr "ssh私钥" @@ -875,7 +875,7 @@ msgstr "没有匹配到资产,结束任务" #: users/templates/users/user_list.html:98 #: users/templates/users/user_remote_app_permission.html:111 msgid "Delete" -msgstr "删除" +msgstr "删除文件" #: audits/models.py:27 msgid "Upload" @@ -920,7 +920,7 @@ msgid "Success" msgstr "成功" #: audits/models.py:43 ops/models/command.py:28 perms/models/base.py:52 -#: terminal/models.py:199 tickets/serializers/request_asset_perm.py:18 +#: terminal/models.py:199 tickets/serializers/request_asset_perm.py:25 #: xpack/plugins/change_auth_plan/models.py:177 #: xpack/plugins/change_auth_plan/models.py:308 #: xpack/plugins/gathered_user/models.py:76 @@ -1000,8 +1000,8 @@ msgstr "Agent" #: audits/models.py:104 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: authentication/templates/authentication/login_otp.html:6 -#: users/forms/profile.py:52 users/models/user.py:495 -#: users/serializers/user.py:224 users/templates/users/user_detail.html:77 +#: users/forms/profile.py:52 users/models/user.py:511 +#: users/serializers/user.py:234 users/templates/users/user_detail.html:77 #: users/templates/users/user_profile.html:87 msgid "MFA" msgstr "多因子认证" @@ -1011,7 +1011,7 @@ msgstr "多因子认证" msgid "Reason" msgstr "原因" -#: audits/models.py:106 tickets/serializers/request_asset_perm.py:53 +#: audits/models.py:106 tickets/serializers/request_asset_perm.py:59 #: tickets/serializers/ticket.py:25 xpack/plugins/cloud/models.py:211 #: xpack/plugins/cloud/models.py:269 msgid "Status" @@ -1193,7 +1193,7 @@ msgstr "SSH密钥" msgid "Reviewers" msgstr "审批人" -#: authentication/models.py:53 tickets/models/ticket.py:26 +#: authentication/models.py:53 tickets/models/ticket.py:27 #: users/templates/users/user_detail.html:250 msgid "Login confirm" msgstr "登录复核" @@ -1228,7 +1228,7 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:393 users/serializers/user.py:221 +#: users/models/user.py:409 users/serializers/user.py:231 #: users/templates/users/user_profile.html:94 #: users/templates/users/user_profile.html:163 #: users/templates/users/user_profile.html:166 @@ -1237,7 +1237,7 @@ msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:394 users/serializers/user.py:222 +#: users/models/user.py:410 users/serializers/user.py:232 #: users/templates/users/user_profile.html:92 #: users/templates/users/user_profile.html:170 msgid "Enable" @@ -1249,7 +1249,7 @@ msgstr "删除成功" #: authentication/templates/authentication/_access_key_modal.html:155 #: authentication/templates/authentication/_mfa_confirm_modal.html:53 -#: templates/_modal.html:22 tickets/models/ticket.py:70 +#: templates/_modal.html:22 tickets/models/ticket.py:73 msgid "Close" msgstr "关闭" @@ -1628,11 +1628,11 @@ msgstr "磁盘使用率超过 80%: {} => {}" #: orgs/api.py:54 msgid "Organization contains undeleted resources" -msgstr "组织内包含未删除的资源" +msgstr "" #: orgs/api.py:58 msgid "The current organization cannot be deleted" -msgstr "当能删除当前所在组织" +msgstr "" #: orgs/mixins/models.py:56 orgs/mixins/serializers.py:26 orgs/models.py:40 #: orgs/models.py:311 @@ -1647,7 +1647,7 @@ msgstr "组织管理员" msgid "Organization auditor" msgstr "组织审计员" -#: orgs/models.py:313 users/forms/user.py:27 users/models/user.py:483 +#: orgs/models.py:313 users/forms/user.py:27 users/models/user.py:499 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:73 #: users/templates/users/user_list.html:16 @@ -1672,7 +1672,7 @@ msgstr "提示:RDP 协议不支持单独控制上传或下载文件" #: perms/forms/asset_permission.py:86 perms/forms/database_app_permission.py:41 #: perms/forms/remote_app_permission.py:43 perms/models/base.py:50 #: templates/_nav.html:21 users/forms/user.py:168 users/models/group.py:31 -#: users/models/user.py:479 users/serializers/user.py:43 +#: users/models/user.py:495 users/serializers/user.py:48 #: users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_asset_permission.html:39 #: users/templates/users/user_asset_permission.html:67 @@ -1719,15 +1719,15 @@ msgstr "上传下载" #: perms/models/asset_permission.py:40 msgid "Clipboard copy" -msgstr "剪切板复制" +msgstr "" #: perms/models/asset_permission.py:41 msgid "Clipboard paste" -msgstr "剪切板粘贴" +msgstr "" #: perms/models/asset_permission.py:42 msgid "Clipboard copy paste" -msgstr "剪切板复制粘贴" +msgstr "" #: perms/models/asset_permission.py:93 msgid "Actions" @@ -1738,8 +1738,8 @@ msgstr "动作" msgid "Asset permission" msgstr "资产授权" -#: perms/models/base.py:53 tickets/serializers/request_asset_perm.py:20 -#: users/models/user.py:511 users/templates/users/user_detail.html:93 +#: perms/models/base.py:53 tickets/serializers/request_asset_perm.py:27 +#: users/models/user.py:527 users/templates/users/user_detail.html:93 #: users/templates/users/user_profile.html:120 msgid "Date expired" msgstr "失效日期" @@ -2479,124 +2479,151 @@ msgstr "结束日期" msgid "Args" msgstr "参数" -#: tickets/api/request_asset_perm.py:41 +#: tickets/api/request_asset_perm.py:43 msgid "Ticket closed" msgstr "工单已关闭" -#: tickets/api/request_asset_perm.py:44 +#: tickets/api/request_asset_perm.py:46 #, python-format msgid "Ticket has %s" msgstr "工单已%s" -#: tickets/api/request_asset_perm.py:69 -msgid "Superuser" -msgstr "超级管理员" - -#: tickets/api/request_asset_perm.py:99 +#: tickets/api/request_asset_perm.py:93 msgid "Confirm assets first" msgstr "请先确认资产" -#: tickets/api/request_asset_perm.py:102 +#: tickets/api/request_asset_perm.py:96 msgid "Confirmed assets changed" msgstr "确认的资产变更了" -#: tickets/api/request_asset_perm.py:106 +#: tickets/api/request_asset_perm.py:100 msgid "Confirm system-user first" msgstr "请先确认系统用户" -#: tickets/api/request_asset_perm.py:110 +#: tickets/api/request_asset_perm.py:104 msgid "Confirmed system-user changed" msgstr "确认的系统用户变更了" -#: tickets/api/request_asset_perm.py:113 xpack/plugins/cloud/models.py:202 +#: tickets/api/request_asset_perm.py:107 xpack/plugins/cloud/models.py:202 msgid "Succeed" msgstr "成功" -#: tickets/api/request_asset_perm.py:121 +#: tickets/api/request_asset_perm.py:114 +msgid "From request ticket: {} {}" +msgstr "来自工单申请: {} {}" + +#: tickets/api/request_asset_perm.py:116 msgid "{} request assets, approved by {}" msgstr "{} 申请资产,通过人 {}" -#: tickets/models/ticket.py:18 tickets/models/ticket.py:72 +#: tickets/models/ticket.py:19 tickets/models/ticket.py:75 msgid "Open" msgstr "开启" -#: tickets/models/ticket.py:19 +#: tickets/models/ticket.py:20 msgid "Closed" msgstr "关闭" -#: tickets/models/ticket.py:25 +#: tickets/models/ticket.py:26 msgid "General" msgstr "一般" -#: tickets/models/ticket.py:27 +#: tickets/models/ticket.py:28 msgid "Request asset permission" msgstr "申请资产权限" -#: tickets/models/ticket.py:32 +#: tickets/models/ticket.py:33 msgid "Approve" msgstr "同意" -#: tickets/models/ticket.py:33 +#: tickets/models/ticket.py:34 msgid "Reject" msgstr "拒绝" -#: tickets/models/ticket.py:36 tickets/models/ticket.py:131 +#: tickets/models/ticket.py:37 tickets/models/ticket.py:136 msgid "User display name" msgstr "用户显示名称" -#: tickets/models/ticket.py:38 +#: tickets/models/ticket.py:39 msgid "Title" msgstr "标题" -#: tickets/models/ticket.py:39 tickets/models/ticket.py:132 +#: tickets/models/ticket.py:40 tickets/models/ticket.py:137 msgid "Body" msgstr "内容" -#: tickets/models/ticket.py:41 +#: tickets/models/ticket.py:42 msgid "Assignee" msgstr "处理人" -#: tickets/models/ticket.py:42 +#: tickets/models/ticket.py:43 msgid "Assignee display name" msgstr "处理人名称" -#: tickets/models/ticket.py:43 +#: tickets/models/ticket.py:44 msgid "Assignees" msgstr "待处理人" -#: tickets/models/ticket.py:44 +#: tickets/models/ticket.py:45 msgid "Assignees display name" msgstr "待处理人名称" -#: tickets/models/ticket.py:73 +#: tickets/models/ticket.py:76 msgid "{} {} this ticket" msgstr "{} {} 这个工单" -#: tickets/models/ticket.py:84 +#: tickets/models/ticket.py:87 msgid "this ticket" msgstr "这个工单" -#: tickets/serializers/request_asset_perm.py:12 +#: tickets/serializers/request_asset_perm.py:19 msgid "IP group" msgstr "IP组" -#: tickets/serializers/request_asset_perm.py:24 +#: tickets/serializers/request_asset_perm.py:31 msgid "Confirmed assets" msgstr "确认的资产" -#: tickets/serializers/request_asset_perm.py:28 +#: tickets/serializers/request_asset_perm.py:34 msgid "Confirmed system user" msgstr "确认的系统用户" -#: tickets/serializers/request_asset_perm.py:65 -msgid "Must be organization admin or superuser" -msgstr "必须是组织管理员或者超级管理员" +#: tickets/serializers/request_asset_perm.py:83 +msgid "Invalid `org_id`" +msgstr "无效的 `org_id`" -#: tickets/utils.py:18 +#: tickets/serializers/request_asset_perm.py:93 +msgid "Field `assignees` must be organization admin or superuser" +msgstr "字段 assignees 必须是组织管理员或者超级管理员" + +#: tickets/serializers/request_asset_perm.py:143 +#, python-brace-format +msgid "" +"\n" +" Type: {type}
\n" +" User: {username}
\n" +" Ip group: {ips}
\n" +" Hostname: {hostname}
\n" +" System user: {system_user}
\n" +" Date start: {date_start}
\n" +" Date expired: {date_expired}
\n" +" " +msgstr "" +"\n" +" 类型: {type}
\n" +" 用户: {username}
\n" +" IP 组: {ips}
\n" +" 主机名: {hostname}
\n" +" 系统用户: {system_user}
\n" +" 开始时间: {date_start}
\n" +" 过期时间: {date_expired}
\n" +" " + +#: tickets/utils.py:20 msgid "New ticket" msgstr "新工单" -#: tickets/utils.py:21 +#: tickets/utils.py:28 #, python-brace-format msgid "" "\n" @@ -2621,11 +2648,11 @@ msgstr "" "
\n" " " -#: tickets/utils.py:40 +#: tickets/utils.py:47 msgid "Ticket has been reply" msgstr "工单已被回复" -#: tickets/utils.py:41 +#: tickets/utils.py:48 #, python-brace-format msgid "" "\n" @@ -2656,7 +2683,7 @@ msgstr "" "
\n" " " -#: users/api/user.py:126 +#: users/api/user.py:147 msgid "Could not reset self otp, use profile reset instead" msgstr "不能在该页面重置多因子认证, 请去个人信息页面重置" @@ -2702,7 +2729,7 @@ msgstr "确认密码" msgid "Password does not match" msgstr "密码不一致" -#: users/forms/profile.py:89 users/models/user.py:475 +#: users/forms/profile.py:89 users/models/user.py:491 #: users/templates/users/user_detail.html:57 #: users/templates/users/user_profile.html:59 msgid "Email" @@ -2738,12 +2765,12 @@ msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" #: users/forms/profile.py:137 users/forms/user.py:90 -#: users/serializers/user.py:185 users/serializers/user.py:266 -#: users/serializers/user.py:324 +#: users/serializers/user.py:194 users/serializers/user.py:276 +#: users/serializers/user.py:334 msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" -#: users/forms/user.py:31 users/models/user.py:518 +#: users/forms/user.py:31 users/models/user.py:534 #: users/templates/users/user_detail.html:89 #: users/templates/users/user_list.html:18 #: users/templates/users/user_profile.html:102 @@ -2763,15 +2790,15 @@ msgstr "添加到用户组" msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" -#: users/forms/user.py:124 users/serializers/user.py:31 +#: users/forms/user.py:124 users/serializers/user.py:36 msgid "Reset link will be generated and sent to the user" msgstr "生成重置密码链接,通过邮件发送给用户" -#: users/forms/user.py:125 users/serializers/user.py:32 +#: users/forms/user.py:125 users/serializers/user.py:37 msgid "Set password" msgstr "设置密码" -#: users/forms/user.py:132 users/serializers/user.py:39 +#: users/forms/user.py:132 users/serializers/user.py:44 #: xpack/plugins/change_auth_plan/models.py:61 #: xpack/plugins/change_auth_plan/serializers.py:30 msgid "Password strategy" @@ -2789,79 +2816,79 @@ msgstr "超级审计员" msgid "Application" msgstr "应用程序" -#: users/models/user.py:395 users/templates/users/user_profile.html:90 +#: users/models/user.py:411 users/templates/users/user_profile.html:90 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:462 +#: users/models/user.py:478 msgid "Local" msgstr "数据库" -#: users/models/user.py:486 +#: users/models/user.py:502 msgid "Avatar" msgstr "头像" -#: users/models/user.py:489 users/templates/users/user_detail.html:68 +#: users/models/user.py:505 users/templates/users/user_detail.html:68 msgid "Wechat" msgstr "微信" -#: users/models/user.py:522 +#: users/models/user.py:538 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:631 +#: users/models/user.py:651 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:634 +#: users/models/user.py:654 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" -#: users/serializers/user.py:72 users/serializers/user.py:237 -msgid "Is first login" -msgstr "首次登录" - -#: users/serializers/user.py:73 -msgid "Is valid" -msgstr "账户是否有效" - -#: users/serializers/user.py:74 -msgid "Is expired" -msgstr " 是否过期" - -#: users/serializers/user.py:75 -msgid "Avatar url" -msgstr "头像路径" - -#: users/serializers/user.py:79 -msgid "Groups name" -msgstr "用户组名" - -#: users/serializers/user.py:80 -msgid "Source name" -msgstr "用户来源名" - -#: users/serializers/user.py:81 +#: users/serializers/user.py:54 users/serializers/user.py:90 msgid "Organization role name" msgstr "组织角色名称" +#: users/serializers/user.py:81 users/serializers/user.py:247 +msgid "Is first login" +msgstr "首次登录" + #: users/serializers/user.py:82 +msgid "Is valid" +msgstr "账户是否有效" + +#: users/serializers/user.py:83 +msgid "Is expired" +msgstr " 是否过期" + +#: users/serializers/user.py:84 +msgid "Avatar url" +msgstr "头像路径" + +#: users/serializers/user.py:88 +msgid "Groups name" +msgstr "用户组名" + +#: users/serializers/user.py:89 +msgid "Source name" +msgstr "用户来源名" + +#: users/serializers/user.py:91 msgid "Super role name" msgstr "超级角色名称" -#: users/serializers/user.py:105 +#: users/serializers/user.py:114 msgid "Role limit to {}" msgstr "角色只能为 {}" -#: users/serializers/user.py:117 users/serializers/user.py:290 +#: users/serializers/user.py:126 users/serializers/user.py:300 msgid "Password does not match security rules" msgstr "密码不满足安全规则" -#: users/serializers/user.py:282 +#: users/serializers/user.py:292 msgid "The old password is incorrect" msgstr "旧密码错误" -#: users/serializers/user.py:296 +#: users/serializers/user.py:306 msgid "The newly set password is inconsistent" msgstr "两次密码不一致" @@ -3992,18 +4019,15 @@ msgstr "旗舰版" #~ msgid "Role name" #~ msgstr "角色名" -#~ msgid "GUI copy" -#~ msgstr "GUI 复制" - -#~ msgid "GUI paste" -#~ msgstr "GUI 粘贴" - #~ msgid "Covered always" #~ msgstr "总是被覆盖" #~ msgid "Account name" #~ msgstr "账户名称" +#~ msgid "Superuser" +#~ msgstr "超级管理员" + #~ msgid "Auditors cannot be join in the user group" #~ msgstr "审计员不能被加入到用户组" diff --git a/apps/orgs/mixins/models.py b/apps/orgs/mixins/models.py index 649c450cc..c6c18902d 100644 --- a/apps/orgs/mixins/models.py +++ b/apps/orgs/mixins/models.py @@ -62,7 +62,6 @@ class OrgModelMixin(models.Model): org = get_current_org() if org is None: return super().save(*args, **kwargs) - if org.is_real() or org.is_system(): self.org_id = org.id elif org.is_default(): diff --git a/apps/tickets/api/request_asset_perm.py b/apps/tickets/api/request_asset_perm.py index c79d57d58..40f908838 100644 --- a/apps/tickets/api/request_asset_perm.py +++ b/apps/tickets/api/request_asset_perm.py @@ -1,22 +1,23 @@ -from collections import namedtuple - from django.db.transaction import atomic -from django.db.models import F +from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from rest_framework.decorators import action from rest_framework.response import Response +from rest_framework.request import Request +from orgs.models import Organization, ROLE as ORG_ROLE from users.models.user import User from common.const.http import POST, GET from common.drf.api import JMSModelViewSet from common.permissions import IsValidUser from common.utils.django import get_object_or_none +from common.utils.timezone import dt_parser from common.drf.serializers import EmptySerializer from perms.models.asset_permission import AssetPermission, Asset from assets.models.user import SystemUser from ..exceptions import ( ConfirmedAssetsChanged, ConfirmedSystemUserChanged, - TicketClosed, TicketActionYet, NotHaveConfirmedAssets, + TicketClosed, TicketActionAlready, NotHaveConfirmedAssets, NotHaveConfirmedSystemUser ) from .. import serializers @@ -25,15 +26,15 @@ from ..permissions import IsAssignee class RequestAssetPermTicketViewSet(JMSModelViewSet): - queryset = Ticket.objects.filter(type=Ticket.TYPE_REQUEST_ASSET_PERM) + queryset = Ticket.origin_objects.filter(type=Ticket.TYPE_REQUEST_ASSET_PERM) serializer_classes = { 'default': serializers.RequestAssetPermTicketSerializer, 'approve': EmptySerializer, 'reject': EmptySerializer, - 'assignees': serializers.OrgAssigneeSerializer, + 'assignees': serializers.AssigneeSerializer, } permission_classes = (IsValidUser,) - filter_fields = ['status', 'title', 'action', 'user_display'] + filter_fields = ['status', 'title', 'action', 'user_display', 'org_id'] search_fields = ['user_display', 'title'] def _check_can_set_action(self, instance, action): @@ -41,49 +42,39 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet): raise TicketClosed(detail=_('Ticket closed')) if instance.action == action: action_display = dict(instance.ACTION_CHOICES).get(action) - raise TicketActionYet(detail=_('Ticket has %s') % action_display) + raise TicketActionAlready(detail=_('Ticket has %s') % action_display) @action(detail=False, methods=[GET], permission_classes=[IsValidUser]) - def assignees(self, request, *args, **kwargs): - org_mapper = {} - UserTuple = namedtuple('UserTuple', ('id', 'name', 'username')) + def assignees(self, request: Request, *args, **kwargs): user = request.user - superusers = User.objects.filter(role=User.ROLE.ADMIN) + org_id = request.query_params.get('org_id', Organization.DEFAULT_ID) - admins_with_org = User.objects.filter(related_admin_orgs__users=user).annotate( - org_id=F('related_admin_orgs__id'), org_name=F('related_admin_orgs__name') - ) + q = Q(role=User.ROLE.ADMIN) + if org_id != Organization.DEFAULT_ID: + q |= Q(m2m_org_members__role=ORG_ROLE.ADMIN, orgs__id=org_id, orgs__members=user) + org_admins = User.objects.filter(q).distinct() - for user in admins_with_org: - org_id = user.org_id + return self.get_paginated_response_with_query_set(org_admins) - if org_id not in org_mapper: - org_mapper[org_id] = { - 'org_name': user.org_name, - 'org_admins': set() # 去重 - } - org_mapper[org_id]['org_admins'].add(UserTuple(user.id, user.name, user.username)) + def _get_extra_comment(self, instance): + meta = instance.meta + ips = ', '.join(meta.get('ips', [])) + confirmed_assets = ', '.join(meta.get('confirmed_assets', [])) - result = [ - { - 'org_name': _('Superuser'), - 'org_admins': set(UserTuple(user.id, user.name, user.username) - for user in superusers) - } - ] - - for org in org_mapper.values(): - result.append(org) - serializer_class = self.get_serializer_class() - serilizer = serializer_class(instance=result, many=True) - return Response(data=serilizer.data) + return f''' + {_('IP group')}: {ips} + {_('Hostname')}: {meta.get('hostname', '')} + {_('System user')}: {meta.get('system_user', '')} + {_('Confirmed assets')}: {confirmed_assets} + {_('Confirmed system user')}: {meta.get('confirmed_system_user', '')} + ''' @action(detail=True, methods=[POST], permission_classes=[IsAssignee, IsValidUser]) def reject(self, request, *args, **kwargs): instance = self.get_object() action = instance.ACTION_REJECT self._check_can_set_action(instance, action) - instance.perform_action(action, request.user) + instance.perform_action(action, request.user, self._get_extra_comment(instance)) return Response() @action(detail=True, methods=[POST], permission_classes=[IsAssignee, IsValidUser]) @@ -109,29 +100,33 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet): if system_user is None: raise ConfirmedSystemUserChanged(detail=_('Confirmed system-user changed')) - self._create_asset_permission(instance, assets, system_user) + self._create_asset_permission(instance, assets, system_user, request.user) return Response({'detail': _('Succeed')}) - def _create_asset_permission(self, instance: Ticket, assets, system_user): + def _create_asset_permission(self, instance: Ticket, assets, system_user, user): meta = instance.meta request = self.request + ap_kwargs = { - 'name': meta.get('name', ''), + 'name': _('From request ticket: {} {}').format(instance.user_display, instance.id), 'created_by': self.request.user.username, 'comment': _('{} request assets, approved by {}').format(instance.user_display, - instance.assignee_display) + instance.assignees_display) } - date_start = meta.get('date_start') - date_expired = meta.get('date_expired') + date_start = dt_parser(meta.get('date_start')) + date_expired = dt_parser(meta.get('date_expired')) if date_start: ap_kwargs['date_start'] = date_start if date_expired: ap_kwargs['date_expired'] = date_expired with atomic(): - instance.perform_action(instance.ACTION_APPROVE, request.user) + instance.perform_action(instance.ACTION_APPROVE, + request.user, + self._get_extra_comment(instance)) ap = AssetPermission.objects.create(**ap_kwargs) ap.system_users.add(system_user) ap.assets.add(*assets) + ap.users.add(user) return ap diff --git a/apps/tickets/api/ticket.py b/apps/tickets/api/ticket.py index 5e3d90701..5a49c746d 100644 --- a/apps/tickets/api/ticket.py +++ b/apps/tickets/api/ticket.py @@ -11,7 +11,7 @@ from .. import serializers, models, mixins class TicketViewSet(mixins.TicketMixin, viewsets.ModelViewSet): serializer_class = serializers.TicketSerializer - queryset = models.Ticket.objects.all() + queryset = models.Ticket.origin_objects.all() permission_classes = (IsValidUser,) filter_fields = ['status', 'title', 'action', 'user_display'] search_fields = ['user_display', 'title'] diff --git a/apps/tickets/exceptions.py b/apps/tickets/exceptions.py index b8cb7ba5e..3332139b5 100644 --- a/apps/tickets/exceptions.py +++ b/apps/tickets/exceptions.py @@ -21,5 +21,9 @@ class TicketClosed(JMSException): pass -class TicketActionYet(JMSException): +class TicketActionAlready(JMSException): + pass + + +class OrgIdRequiredException(JMSException): pass diff --git a/apps/tickets/migrations/0002_auto_20200723_1232.py b/apps/tickets/migrations/0002_auto_20200723_1232.py deleted file mode 100644 index 26d980a4a..000000000 --- a/apps/tickets/migrations/0002_auto_20200723_1232.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.10 on 2020-07-23 04:32 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tickets', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='ticket', - name='type', - field=models.CharField(choices=[('general', 'General'), ('login_confirm', 'Login confirm'), ('request_asset', 'Request asset permission')], default='general', max_length=16, verbose_name='Type'), - ), - ] diff --git a/apps/tickets/migrations/0002_auto_20200728_1146.py b/apps/tickets/migrations/0002_auto_20200728_1146.py new file mode 100644 index 000000000..303395144 --- /dev/null +++ b/apps/tickets/migrations/0002_auto_20200728_1146.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.10 on 2020-07-28 03:46 + +from django.db import migrations, models +import django.db.models.manager + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0001_initial'), + ] + + operations = [ + migrations.AlterModelManagers( + name='ticket', + managers=[ + ('origin_objects', django.db.models.manager.Manager()), + ], + ), + migrations.AddField( + model_name='ticket', + name='org_id', + field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), + ), + migrations.AlterField( + model_name='ticket', + name='type', + field=models.CharField(choices=[('general', 'General'), ('login_confirm', 'Login confirm'), ('request_asset', 'Request asset permission')], default='general', max_length=16, verbose_name='Type'), + ), + ] diff --git a/apps/tickets/mixins.py b/apps/tickets/mixins.py index 6f052df66..c4a48d866 100644 --- a/apps/tickets/mixins.py +++ b/apps/tickets/mixins.py @@ -6,11 +6,12 @@ from .models import Ticket class TicketMixin: def get_queryset(self): + queryset = super().get_queryset() assign = self.request.GET.get('assign', None) if assign is None: - queryset = Ticket.get_related_tickets(self.request.user) + queryset = Ticket.get_related_tickets(self.request.user, queryset) elif assign in ['1']: - queryset = Ticket.get_assigned_tickets(self.request.user) + queryset = Ticket.get_assigned_tickets(self.request.user, queryset) else: - queryset = Ticket.get_my_tickets(self.request.user) + queryset = Ticket.get_my_tickets(self.request.user, queryset) return queryset diff --git a/apps/tickets/models/ticket.py b/apps/tickets/models/ticket.py index 631761069..4f68aa6e0 100644 --- a/apps/tickets/models/ticket.py +++ b/apps/tickets/models/ticket.py @@ -7,11 +7,12 @@ from django.utils.translation import ugettext_lazy as _ from common.mixins.models import CommonModelMixin from common.fields.model import JsonDictTextField +from orgs.mixins.models import OrgModelMixin __all__ = ['Ticket', 'Comment'] -class Ticket(CommonModelMixin): +class Ticket(OrgModelMixin, CommonModelMixin): STATUS_OPEN = 'open' STATUS_CLOSED = 'closed' STATUS_CHOICES = ( @@ -46,6 +47,8 @@ class Ticket(CommonModelMixin): status = models.CharField(choices=STATUS_CHOICES, max_length=16, default='open') action = models.CharField(choices=ACTION_CHOICES, max_length=16, default='', blank=True) + origin_objects = models.Manager() + def __str__(self): return '{}: {}'.format(self.user_display, self.title) @@ -79,13 +82,15 @@ class Ticket(CommonModelMixin): self.status = status self.save() - def create_action_comment(self, action, user): + def create_action_comment(self, action, user, extra_comment=None): action_display = dict(self.ACTION_CHOICES).get(action) body = '{} {} {}'.format(user, action_display, _("this ticket")) + if extra_comment is not None: + body += extra_comment self.comments.create(body=body, user=user, user_display=str(user)) - def perform_action(self, action, user): - self.create_action_comment(action, user) + def perform_action(self, action, user, extra_comment=None): + self.create_action_comment(action, user, extra_comment) self.action = action self.status = self.STATUS_CLOSED self.assignee = user diff --git a/apps/tickets/serializers/request_asset_perm.py b/apps/tickets/serializers/request_asset_perm.py index 9bdce49c1..3b2d72b7c 100644 --- a/apps/tickets/serializers/request_asset_perm.py +++ b/apps/tickets/serializers/request_asset_perm.py @@ -1,8 +1,15 @@ +from itertools import chain + from rest_framework import serializers +from django.conf import settings from django.utils.translation import ugettext_lazy as _ from django.urls import reverse from django.db.models import Q +from common.utils.timezone import dt_parser, dt_formater +from orgs.utils import tmp_to_root_org +from orgs.models import Organization, ROLE as ORG_ROLE +from assets.models.asset import Asset from users.models.user import User from ..models import Ticket @@ -22,9 +29,8 @@ class RequestAssetPermTicketSerializer(serializers.ModelSerializer): source='meta.confirmed_assets', default=list, required=False, label=_('Confirmed assets')) - confirmed_system_user = serializers.ListField(child=serializers.UUIDField(), - source='meta.confirmed_system_user', - default=list, required=False, + confirmed_system_user = serializers.UUIDField(source='meta.confirmed_system_user', + default='', required=False, label=_('Confirmed system user')) assets_waitlist_url = serializers.SerializerMethodField() system_user_waitlist_url = serializers.SerializerMethodField() @@ -36,7 +42,7 @@ class RequestAssetPermTicketSerializer(serializers.ModelSerializer): 'status', 'action', 'date_created', 'date_updated', 'system_user_waitlist_url', 'type', 'type_display', 'action_display', 'ips', 'confirmed_assets', 'date_start', 'date_expired', 'confirmed_system_user', 'hostname', - 'assets_waitlist_url', 'system_user' + 'assets_waitlist_url', 'system_user', 'org_id' ] m2m_fields = [ 'user', 'user_display', 'assignees', 'assignees_display', @@ -52,26 +58,44 @@ class RequestAssetPermTicketSerializer(serializers.ModelSerializer): extra_kwargs = { 'status': {'label': _('Status')}, 'action': {'label': _('Action')}, - 'user_display': {'label': _('User')} + 'user_display': {'label': _('User')}, + 'org_id': {'required': True} } - def validate_assignees(self, assignees): + def validate(self, attrs): + org_id = attrs.get('org_id') + assignees = attrs.get('assignees') + + instance = self.instance + if instance is not None: + if org_id and not assignees: + assignees = list(instance.assignees.all()) + elif assignees and not org_id: + org_id = instance.org_id + elif assignees and org_id: + pass + else: + return attrs + user = self.context['request'].user + org = Organization.get_instance(org_id) + if org is None: + raise serializers.ValidationError(_('Invalid `org_id`')) - count = User.objects.filter(Q(related_admin_orgs__users=user) | Q(role=User.ROLE.ADMIN)).filter( - id__in=[assignee.id for assignee in assignees]).distinct().count() + q = Q(role=User.ROLE.ADMIN) + if not org.is_default(): + q |= Q(m2m_org_members__role=ORG_ROLE.ADMIN, orgs__id=org_id, orgs__members=user) + q &= Q(id__in=[assignee.id for assignee in assignees]) + count = User.objects.filter(q).distinct().count() if count != len(assignees): - raise serializers.ValidationError(_('Must be organization admin or superuser')) - return assignees + raise serializers.ValidationError(_('Field `assignees` must be organization admin or superuser')) + return attrs def get_system_user_waitlist_url(self, instance: Ticket): if not self._is_assignee(instance): return None - meta = instance.meta - url = reverse('api-assets:system-user-list') - query = meta.get('system_user', '') - return '{}?search={}'.format(url, query) + return reverse('api-assets:system-user-list') def get_assets_waitlist_url(self, instance: Ticket): if not self._is_assignee(instance): @@ -81,37 +105,106 @@ class RequestAssetPermTicketSerializer(serializers.ModelSerializer): query = '' meta = instance.meta - ips = meta.get('ips', []) hostname = meta.get('hostname') - - if ips: - query = '?ips=%s' % ','.join(ips) - elif hostname: + if hostname: query = '?search=%s' % hostname return asset_api + query + def _recommend_assets(self, data, instance): + confirmed_assets = data.get('confirmed_assets') + if not confirmed_assets and self._is_assignee(instance): + ips = data.get('ips') + hostname = data.get('hostname') + limit = 5 + + q = Q(id=None) + if ips: + limit = len(ips) + 2 + q |= Q(ip__in=ips) + if hostname: + q |= Q(hostname__icontains=hostname) + + data['confirmed_assets'] = list( + map(lambda x: str(x), chain(*Asset.objects.filter(q)[0: limit].values_list('id')))) + + def to_representation(self, instance): + data = super().to_representation(instance) + self._recommend_assets(data, instance) + return data + + def _create_body(self, validated_data): + meta = validated_data['meta'] + type = dict(Ticket.TYPE_CHOICES).get(validated_data.get('type', '')) + date_start = dt_parser(meta.get('date_start')).strftime(settings.DATETIME_DISPLAY_FORMAT) + date_expired = dt_parser(meta.get('date_expired')).strftime(settings.DATETIME_DISPLAY_FORMAT) + + validated_data['body'] = _(''' + Type: {type}
+ User: {username}
+ Ip group: {ips}
+ Hostname: {hostname}
+ System user: {system_user}
+ Date start: {date_start}
+ Date expired: {date_expired}
+ ''').format( + type=type, + username=validated_data.get('user', ''), + ips=', '.join(meta.get('ips', [])), + hostname=meta.get('hostname', ''), + system_user=meta.get('system_user', ''), + date_start=date_start, + date_expired=date_expired + ) + def create(self, validated_data): + # `type` 与 `user` 用户不可提交, validated_data['type'] = self.Meta.model.TYPE_REQUEST_ASSET_PERM validated_data['user'] = self.context['request'].user + # `confirmed` 相关字段只能审批人修改,所以创建时直接清理掉 self._pop_confirmed_fields() + self._create_body(validated_data) return super().create(validated_data) def save(self, **kwargs): + """ + 做了一些数据转换 + """ meta = self.validated_data.get('meta', {}) + + org_id = self.validated_data.get('org_id') + if org_id is not None and org_id == Organization.DEFAULT_ID: + self.validated_data['org_id'] = '' + + # 时间的转换,好烦😭,可能有更好的办法吧 date_start = meta.get('date_start') if date_start: - meta['date_start'] = date_start.strftime('%Y-%m-%d %H:%M:%S%z') + meta['date_start'] = dt_formater(date_start) date_expired = meta.get('date_expired') if date_expired: - meta['date_expired'] = date_expired.strftime('%Y-%m-%d %H:%M:%S%z') - return super().save(**kwargs) + meta['date_expired'] = dt_formater(date_expired) + + # UUID 的转换 + confirmed_system_user = meta.get('confirmed_system_user') + if confirmed_system_user: + meta['confirmed_system_user'] = str(confirmed_system_user) + + confirmed_assets = meta.get('confirmed_assets') + if confirmed_assets: + new_confirmed_assets = [] + for asset in confirmed_assets: + new_confirmed_assets.append(str(asset)) + meta['confirmed_assets'] = new_confirmed_assets + with tmp_to_root_org(): + return super().save(**kwargs) def update(self, instance, validated_data): new_meta = validated_data['meta'] if not self._is_assignee(instance): self._pop_confirmed_fields() + + # Json 字段保存的坑😭 old_meta = instance.meta meta = {} meta.update(old_meta) @@ -134,8 +227,3 @@ class AssigneeSerializer(serializers.Serializer): id = serializers.UUIDField() name = serializers.CharField() username = serializers.CharField() - - -class OrgAssigneeSerializer(serializers.Serializer): - org_name = serializers.CharField() - org_admins = AssigneeSerializer(many=True) diff --git a/apps/tickets/tests.py b/apps/tickets/tests.py index 7ce503c2d..2b02a9016 100644 --- a/apps/tickets/tests.py +++ b/apps/tickets/tests.py @@ -1,3 +1,89 @@ -from django.test import TestCase +import datetime -# Create your tests here. +from common.utils.timezone import now +from django.urls import reverse +from rest_framework.test import APITestCase +from rest_framework import status + +from orgs.models import Organization, OrganizationMember, ROLE as ORG_ROLE +from orgs.utils import set_current_org +from users.models.user import User +from assets.models import Asset, AdminUser, SystemUser + + +class TicketTest(APITestCase): + def setUp(self): + Organization.objects.bulk_create([ + Organization(name='org-01'), + Organization(name='org-02'), + Organization(name='org-03'), + ]) + org_01, org_02, org_03 = Organization.objects.all() + self.org_01, self.org_02, self.org_03 = org_01, org_02, org_03 + + set_current_org(org_01) + + AdminUser.objects.bulk_create([ + AdminUser(name='au-01', username='au-01'), + AdminUser(name='au-02', username='au-02'), + AdminUser(name='au-03', username='au-03'), + ]) + + SystemUser.objects.bulk_create([ + SystemUser(name='su-01', username='su-01'), + SystemUser(name='su-02', username='su-02'), + SystemUser(name='su-03', username='su-03'), + ]) + + admin_users = AdminUser.objects.all() + Asset.objects.bulk_create([ + Asset(hostname='asset-01', ip='192.168.1.1', public_ip='192.168.1.1', admin_user=admin_users[0]), + Asset(hostname='asset-02', ip='192.168.1.2', public_ip='192.168.1.2', admin_user=admin_users[0]), + Asset(hostname='asset-03', ip='192.168.1.3', public_ip='192.168.1.3', admin_user=admin_users[0]), + ]) + + new_user = User.objects.create + new_org_memeber = OrganizationMember.objects.create + + u = new_user(name='user-01', username='user-01', email='user-01@jms.com') + new_org_memeber(org=org_01, user=u, role=ORG_ROLE.USER) + new_org_memeber(org=org_02, user=u, role=ORG_ROLE.USER) + self.user_01 = u + + u = new_user(name='org-admin-01', username='org-admin-01', email='org-admin-01@jms.com') + new_org_memeber(org=org_01, user=u, role=ORG_ROLE.ADMIN) + self.org_admin_01 = u + + u = new_user(name='org-admin-02', username='org-admin-02', email='org-admin-02@jms.com') + new_org_memeber(org=org_02, user=u, role=ORG_ROLE.ADMIN) + self.org_admin_02 = u + + def test_create_request_asset_perm(self): + url = reverse('api-tickets:ticket-request-asset-perm') + ticket_url = reverse('api-tickets:ticket') + + self.client.force_login(self.user_01) + + date_start = now() + date_expired = date_start + datetime.timedelta(days=7) + + data = { + "title": "request-01", + "ips": [ + "192.168.1.1" + ], + "date_start": date_start, + "date_expired": date_expired, + "hostname": "", + "system_user": "", + "org_id": self.org_01.id, + "assignees": [ + str(self.org_admin_01.id), + str(self.org_admin_02.id), + ] + } + + self.client.post(data) + + self.client.force_login(self.org_admin_01) + res = self.client.get(ticket_url, params={'assgin': 1}) diff --git a/apps/tickets/urls/api_urls.py b/apps/tickets/urls/api_urls.py index b086aa9d3..a7bd3f6e5 100644 --- a/apps/tickets/urls/api_urls.py +++ b/apps/tickets/urls/api_urls.py @@ -7,7 +7,7 @@ from .. import api app_name = 'tickets' router = BulkRouter() -# router.register('tickets/request-asset-perm', api.RequestAssetPermTicketViewSet, 'ticket-request-asset-perm') +router.register('tickets/request-asset-perm', api.RequestAssetPermTicketViewSet, 'ticket-request-asset-perm') router.register('tickets', api.TicketViewSet, 'ticket') router.register('tickets/(?P[0-9a-zA-Z\-]{36})/comments', api.TicketCommentViewSet, 'ticket-comment') diff --git a/apps/tickets/utils.py b/apps/tickets/utils.py index 13727a77d..97b5334e0 100644 --- a/apps/tickets/utils.py +++ b/apps/tickets/utils.py @@ -1,23 +1,30 @@ # -*- coding: utf-8 -*- # +from urllib.parse import urljoin from django.conf import settings from django.utils.translation import ugettext as _ -from common.utils import get_logger, reverse +from common.utils import get_logger from common.tasks import send_mail_async logger = get_logger(__name__) +from tickets.models import Ticket -def send_new_ticket_mail_to_assignees(ticket, assignees): +def send_new_ticket_mail_to_assignees(ticket: Ticket, assignees): recipient_list = [user.email for user in assignees] user = ticket.user if not recipient_list: logger.error("Ticket not has assignees: {}".format(ticket.id)) return subject = '{}: {}'.format(_("New ticket"), ticket.title) - detail_url = reverse('tickets:ticket-detail', - kwargs={'pk': ticket.id}, external=True) + + # 这里要设置前端地址,因为要直接跳转到页面 + if ticket.type == ticket.TYPE_REQUEST_ASSET_PERM: + detail_url = urljoin(settings.SITE_URL, f'/tickets/tickets/request-asset-perm/{ticket.id}') + else: + detail_url = urljoin(settings.SITE_URL, f'/tickets/tickets/{ticket.id}') + message = _("""

Your has a new ticket

diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 456cd1fbc..82791dbf5 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -233,6 +233,11 @@ class RoleMixin: def is_app(self): return self.role == self.ROLE.APP + @lazyproperty + def user_all_orgs(self): + from orgs.models import Organization + return Organization.get_user_all_orgs(self) + @lazyproperty def user_orgs(self): from orgs.models import Organization diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index 76485eb93..21c38d7bc 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -27,6 +27,11 @@ class UserOrgSerializer(serializers.Serializer): name = serializers.CharField() +class UserOrgLabelSerializer(serializers.Serializer): + value = serializers.CharField(source='id') + label = serializers.CharField(source='name') + + class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): EMAIL_SET_PASSWORD = _('Reset link will be generated and sent to the user') CUSTOM_PASSWORD = _('Set password') @@ -214,6 +219,7 @@ class UserRoleSerializer(serializers.Serializer): class UserProfileSerializer(UserSerializer): admin_or_audit_orgs = UserOrgSerializer(many=True, read_only=True) + user_all_orgs = UserOrgLabelSerializer(many=True, read_only=True) current_org_roles = serializers.ListField(read_only=True) public_key_comment = serializers.CharField( source='get_public_key_comment', required=False, read_only=True, max_length=128 @@ -231,7 +237,7 @@ class UserProfileSerializer(UserSerializer): class Meta(UserSerializer.Meta): fields = UserSerializer.Meta.fields + [ 'public_key_comment', 'public_key_hash_md5', 'admin_or_audit_orgs', 'current_org_roles', - 'guide_url' + 'guide_url', 'user_all_orgs' ] extra_kwargs = dict(UserSerializer.Meta.extra_kwargs) extra_kwargs.update({ From 3e6cd1c1d3a7cd57629498fe5ec695874123cb27 Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 28 Jul 2020 19:23:29 +0800 Subject: [PATCH 14/40] =?UTF-8?q?ci(dockerfile):=20=E4=BF=AE=E6=94=B9docke?= =?UTF-8?q?rfile=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 869955f17..78d506790 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,18 +9,23 @@ RUN cd utils && bash -ixeu build.sh FROM registry.fit2cloud.com/public/python:v3 +ARG PIP_MIRROR=https://pypi.douban.com/simple +ENV PIP_MIRROR=$PIP_MIRROR +ARG MYSQL_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/ +ENV MYSQL_MIRROR=$MYSQL_MIRROR + WORKDIR /opt/jumpserver -COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver +COPY ./requirements ./requirements RUN useradd jumpserver - RUN yum -y install epel-release && \ - echo -e "[mysql]\nname=mysql\nbaseurl=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo - -COPY . . + echo -e "[mysql]\nname=mysql\nbaseurl=${MYSQL_MIRROR}\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo RUN yum -y install $(cat requirements/rpm_requirements.txt) -RUN pip install --upgrade pip setuptools && pip install wheel && \ - pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements/requirements.txt || pip install -r requirements/requirements.txt +RUN pip install --upgrade pip setuptools wheel -i ${PIP_MIRROR} && \ + pip config set global.index-url ${PIP_MIRROR} +RUN pip install -r requirements/requirements.txt || pip install -r requirements/requirements.txt + +COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/ RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config RUN echo > config.yml From 34b188bbe7077314cd22e54f3491d4ce49ba7201 Mon Sep 17 00:00:00 2001 From: xinwen Date: Tue, 28 Jul 2020 20:28:38 +0800 Subject: [PATCH 15/40] =?UTF-8?q?fix(csv):=20=E4=BF=AE=E5=A4=8D`JMSCSVPars?= =?UTF-8?q?er`=E8=B0=83=E7=94=A8`serializer`=E5=AF=BC=E8=87=B4=E5=BE=AA?= =?UTF-8?q?=E7=8E=AF=E8=B0=83=E7=94=A8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/common/drf/parsers/csv.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/common/drf/parsers/csv.py b/apps/common/drf/parsers/csv.py index 58b3f7bc2..de0d14ea7 100644 --- a/apps/common/drf/parsers/csv.py +++ b/apps/common/drf/parsers/csv.py @@ -47,9 +47,9 @@ class JMSCSVParser(BaseParser): yield row @staticmethod - def _get_fields_map(serializer): + def _get_fields_map(serializer_cls): fields_map = {} - fields = serializer.fields + fields = serializer_cls().fields fields_map.update({v.label: k for k, v in fields.items()}) fields_map.update({k: k for k, _ in fields.items()}) return fields_map @@ -101,7 +101,7 @@ class JMSCSVParser(BaseParser): try: view = parser_context['view'] meta = view.request.META - serializer = view.get_serializer() + serializer_cls = view.get_serializer_class() except Exception as e: logger.debug(e, exc_info=True) raise ParseError('The resource does not support imports!') @@ -121,7 +121,7 @@ class JMSCSVParser(BaseParser): rows = self._gen_rows(binary, charset=encoding) header = next(rows) - fields_map = self._get_fields_map(serializer) + fields_map = self._get_fields_map(serializer_cls) header = [fields_map.get(name.strip('*'), '') for name in header] data = [] From 1b052a8729c68c213117eb529397640e652ecc6e Mon Sep 17 00:00:00 2001 From: xinwen Date: Wed, 29 Jul 2020 14:57:44 +0800 Subject: [PATCH 16/40] =?UTF-8?q?feat(terminal):=20=E7=BB=88=E7=AB=AF?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=B7=BB=E5=8A=A0=E6=89=B9=E9=87=8F=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/common/drf/api.py | 9 +++++++++ apps/common/drf/serializers.py | 20 ++++++++++++++++++ apps/common/exceptions.py | 3 ++- apps/common/serializers.py | 18 +++++------------ apps/locale/zh/LC_MESSAGES/django.mo | Bin 55837 -> 55907 bytes apps/locale/zh/LC_MESSAGES/django.po | 28 +++++++++++++++----------- apps/terminal/api/terminal.py | 11 ++++++---- apps/terminal/exceptions.py | 8 ++++++++ apps/terminal/serializers/terminal.py | 8 ++++---- apps/terminal/urls/api_urls.py | 2 +- 10 files changed, 72 insertions(+), 35 deletions(-) create mode 100644 apps/terminal/exceptions.py diff --git a/apps/common/drf/api.py b/apps/common/drf/api.py index 3d5b67b34..692d567f5 100644 --- a/apps/common/drf/api.py +++ b/apps/common/drf/api.py @@ -1,4 +1,5 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet +from rest_framework_bulk import BulkModelViewSet from ..mixins.api import ( SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, PaginatedResponseMixin @@ -19,3 +20,11 @@ class JMSModelViewSet(SerializerMixin2, PaginatedResponseMixin, ModelViewSet): pass + + +class JMSBulkModelViewSet(SerializerMixin2, + QuerySetMixin, + ExtraFilterFieldsMixin, + PaginatedResponseMixin, + BulkModelViewSet): + pass diff --git a/apps/common/drf/serializers.py b/apps/common/drf/serializers.py index bd92415a1..e767c32aa 100644 --- a/apps/common/drf/serializers.py +++ b/apps/common/drf/serializers.py @@ -1,5 +1,25 @@ from rest_framework.serializers import Serializer +from rest_framework.serializers import ModelSerializer +from rest_framework import serializers +from rest_framework_bulk.serializers import BulkListSerializer + +from common.mixins.serializers import BulkSerializerMixin +from common.mixins import BulkListSerializerMixin + +__all__ = ['EmptySerializer', 'BulkModelSerializer'] class EmptySerializer(Serializer): pass + + +class BulkModelSerializer(BulkSerializerMixin, ModelSerializer): + pass + + +class AdaptedBulkListSerializer(BulkListSerializerMixin, BulkListSerializer): + pass + + +class CeleryTaskSerializer(serializers.Serializer): + task = serializers.CharField(read_only=True) diff --git a/apps/common/exceptions.py b/apps/common/exceptions.py index e95cc2801..4b3718837 100644 --- a/apps/common/exceptions.py +++ b/apps/common/exceptions.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- # from rest_framework.exceptions import APIException +from rest_framework import status class JMSException(APIException): - pass + status_code = status.HTTP_400_BAD_REQUEST diff --git a/apps/common/serializers.py b/apps/common/serializers.py index eb657b4cc..971060641 100644 --- a/apps/common/serializers.py +++ b/apps/common/serializers.py @@ -1,14 +1,6 @@ -# -*- coding: utf-8 -*- -# +""" +老的代码统一到 `apps/common/drf/serializers.py` 中, +之后此文件废弃 +""" -from rest_framework_bulk.serializers import BulkListSerializer -from rest_framework import serializers -from .mixins import BulkListSerializerMixin - - -class AdaptedBulkListSerializer(BulkListSerializerMixin, BulkListSerializer): - pass - - -class CeleryTaskSerializer(serializers.Serializer): - task = serializers.CharField(read_only=True) +from common.drf.serializers import AdaptedBulkListSerializer, CeleryTaskSerializer diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 6469256430141bfb82a02a2689a1564796fd76c6..8599cd7c5784765fb2b455e74722b25e66e3775c 100644 GIT binary patch delta 16686 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 WZ4XCJx;JL-y&c;(u2~jY@c#fbz3KV@ delta 16670 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 JF6~z2e*i6C$-n>r diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 03a50defd..eb34c4400 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-07-28 11:25+0800\n" +"POT-Creation-Date: 2020-07-29 15:03+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -2419,6 +2419,10 @@ msgstr "会话" msgid "Risk level" msgstr "风险等级" +#: terminal/exceptions.py:8 +msgid "Bulk create not support" +msgstr "不支持批量创建" + #: terminal/models.py:27 msgid "Remote Address" msgstr "远端地址" @@ -2479,40 +2483,40 @@ msgstr "结束日期" msgid "Args" msgstr "参数" -#: tickets/api/request_asset_perm.py:43 +#: tickets/api/request_asset_perm.py:42 msgid "Ticket closed" msgstr "工单已关闭" -#: tickets/api/request_asset_perm.py:46 +#: tickets/api/request_asset_perm.py:45 #, python-format msgid "Ticket has %s" msgstr "工单已%s" -#: tickets/api/request_asset_perm.py:93 +#: tickets/api/request_asset_perm.py:90 msgid "Confirm assets first" msgstr "请先确认资产" -#: tickets/api/request_asset_perm.py:96 +#: tickets/api/request_asset_perm.py:93 msgid "Confirmed assets changed" msgstr "确认的资产变更了" -#: tickets/api/request_asset_perm.py:100 +#: tickets/api/request_asset_perm.py:97 msgid "Confirm system-user first" msgstr "请先确认系统用户" -#: tickets/api/request_asset_perm.py:104 +#: tickets/api/request_asset_perm.py:101 msgid "Confirmed system-user changed" msgstr "确认的系统用户变更了" -#: tickets/api/request_asset_perm.py:107 xpack/plugins/cloud/models.py:202 +#: tickets/api/request_asset_perm.py:104 xpack/plugins/cloud/models.py:202 msgid "Succeed" msgstr "成功" -#: tickets/api/request_asset_perm.py:114 +#: tickets/api/request_asset_perm.py:111 msgid "From request ticket: {} {}" msgstr "来自工单申请: {} {}" -#: tickets/api/request_asset_perm.py:116 +#: tickets/api/request_asset_perm.py:113 msgid "{} request assets, approved by {}" msgstr "{} 申请资产,通过人 {}" @@ -2592,11 +2596,11 @@ msgstr "确认的系统用户" msgid "Invalid `org_id`" msgstr "无效的 `org_id`" -#: tickets/serializers/request_asset_perm.py:93 +#: tickets/serializers/request_asset_perm.py:92 msgid "Field `assignees` must be organization admin or superuser" msgstr "字段 assignees 必须是组织管理员或者超级管理员" -#: tickets/serializers/request_asset_perm.py:143 +#: tickets/serializers/request_asset_perm.py:142 #, python-brace-format msgid "" "\n" diff --git a/apps/terminal/api/terminal.py b/apps/terminal/api/terminal.py index 705848fe0..2ee353e3e 100644 --- a/apps/terminal/api/terminal.py +++ b/apps/terminal/api/terminal.py @@ -5,17 +5,17 @@ import logging import uuid from django.core.cache import cache -from django.shortcuts import get_object_or_404, redirect -from django.utils import timezone +from django.shortcuts import get_object_or_404 from rest_framework import viewsets from rest_framework.views import APIView, Response from rest_framework.permissions import AllowAny - +from common.drf.api import JMSBulkModelViewSet from common.utils import get_object_or_none from common.permissions import IsAppUser, IsOrgAdminOrAppUser, IsSuperUser from ..models import Terminal, Status, Session from .. import serializers +from .. import exceptions __all__ = [ 'TerminalViewSet', 'TerminalTokenApi', 'StatusViewSet', 'TerminalConfig', @@ -23,13 +23,16 @@ __all__ = [ logger = logging.getLogger(__file__) -class TerminalViewSet(viewsets.ModelViewSet): +class TerminalViewSet(JMSBulkModelViewSet): queryset = Terminal.objects.filter(is_deleted=False) serializer_class = serializers.TerminalSerializer permission_classes = (IsSuperUser,) filter_fields = ['name', 'remote_addr'] def create(self, request, *args, **kwargs): + if isinstance(request.data, list): + raise exceptions.BulkCreateNotSupport() + name = request.data.get('name') remote_ip = request.META.get('REMOTE_ADDR') x_real_ip = request.META.get('X-Real-IP') diff --git a/apps/terminal/exceptions.py b/apps/terminal/exceptions.py new file mode 100644 index 000000000..a3b63a3e9 --- /dev/null +++ b/apps/terminal/exceptions.py @@ -0,0 +1,8 @@ +from django.utils.translation import ugettext_lazy as _ + +from common.exceptions import JMSException + + +class BulkCreateNotSupport(JMSException): + default_code = 'bulk_create_not_support' + default_detail = _('Bulk create not support') diff --git a/apps/terminal/serializers/terminal.py b/apps/terminal/serializers/terminal.py index c7a91d009..bfe992b19 100644 --- a/apps/terminal/serializers/terminal.py +++ b/apps/terminal/serializers/terminal.py @@ -1,18 +1,18 @@ from rest_framework import serializers -from common.mixins import BulkSerializerMixin -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import BulkModelSerializer, AdaptedBulkListSerializer from ..models import ( Terminal, Status, Session, Task ) -class TerminalSerializer(serializers.ModelSerializer): +class TerminalSerializer(BulkModelSerializer): session_online = serializers.SerializerMethodField() is_alive = serializers.BooleanField(read_only=True) class Meta: model = Terminal + list_serializer_class = AdaptedBulkListSerializer fields = [ 'id', 'name', 'remote_addr', 'http_port', 'ssh_port', 'comment', 'is_accepted', "is_active", 'session_online', @@ -30,7 +30,7 @@ class StatusSerializer(serializers.ModelSerializer): model = Status -class TaskSerializer(BulkSerializerMixin, serializers.ModelSerializer): +class TaskSerializer(BulkModelSerializer): class Meta: fields = '__all__' diff --git a/apps/terminal/urls/api_urls.py b/apps/terminal/urls/api_urls.py index d82a5ca5a..c60f97603 100644 --- a/apps/terminal/urls/api_urls.py +++ b/apps/terminal/urls/api_urls.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # -from django.urls import path, include, re_path +from django.urls import path, re_path from rest_framework_bulk.routes import BulkRouter from common import api as capi From e98235ca276527de184e7a815a6e26f76efbd0a3 Mon Sep 17 00:00:00 2001 From: xinwen Date: Wed, 29 Jul 2020 17:04:06 +0800 Subject: [PATCH 17/40] =?UTF-8?q?refactor(serializer):=20=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=20`BulkSerializerMixin`=20=E7=9A=84=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=20`ListSerializer`=20=E4=B8=BA=20`AdaptedBulkListSerializer`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/common/mixins/serializers.py | 10 +++++++++- apps/terminal/serializers/terminal.py | 1 - 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/common/mixins/serializers.py b/apps/common/mixins/serializers.py index 5c3a243cf..d9df17e1d 100644 --- a/apps/common/mixins/serializers.py +++ b/apps/common/mixins/serializers.py @@ -8,7 +8,6 @@ from rest_framework.utils import html from rest_framework.settings import api_settings from rest_framework.exceptions import ValidationError from rest_framework.fields import SkipField, empty - __all__ = ['BulkSerializerMixin', 'BulkListSerializerMixin', 'CommonSerializerMixin', 'CommonBulkSerializerMixin'] @@ -50,6 +49,15 @@ class BulkSerializerMixin(object): self.initial_data = data return super().run_validation(data) + @classmethod + def many_init(cls, *args, **kwargs): + meta = getattr(cls, 'Meta', None) + assert meta is not None, 'Must have `Meta`' + if not hasattr(meta, 'list_serializer_class'): + from common.drf.serializers import AdaptedBulkListSerializer + meta.list_serializer_class = AdaptedBulkListSerializer + return super(BulkSerializerMixin, cls).many_init(*args, **kwargs) + class BulkListSerializerMixin(object): """ diff --git a/apps/terminal/serializers/terminal.py b/apps/terminal/serializers/terminal.py index bfe992b19..b643dd16a 100644 --- a/apps/terminal/serializers/terminal.py +++ b/apps/terminal/serializers/terminal.py @@ -12,7 +12,6 @@ class TerminalSerializer(BulkModelSerializer): class Meta: model = Terminal - list_serializer_class = AdaptedBulkListSerializer fields = [ 'id', 'name', 'remote_addr', 'http_port', 'ssh_port', 'comment', 'is_accepted', "is_active", 'session_online', From 2ed0927b18358e53626e7da9d3166c4ebe9817a5 Mon Sep 17 00:00:00 2001 From: xinwen Date: Wed, 29 Jul 2020 17:46:43 +0800 Subject: [PATCH 18/40] =?UTF-8?q?fix(login):=20=E7=94=A8=E6=88=B7=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E5=A0=A1=E5=9E=92=E6=9C=BA=E7=9A=84=E6=97=B6=E5=80=99?= =?UTF-8?q?=E5=81=B6=E5=B0=94=E4=BC=9A=E5=87=BA=E7=8E=B0=E2=80=9C=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E8=A7=A3=E5=AF=86=E5=A4=B1=E8=B4=A5=E2=80=9D=EF=BC=8C?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E6=97=A0=E6=B3=95=E6=AD=A3=E5=B8=B8=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E3=80=82=20#4408?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/views/login.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index 141d0f6e7..a0b8804e1 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -109,12 +109,18 @@ class UserLoginView(mixins.AuthMixin, FormView): def get_context_data(self, **kwargs): # 生成加解密密钥对,public_key传递给前端,private_key存入session中供解密使用 - rsa_private_key, rsa_public_key = utils.gen_key_pair() - self.request.session['rsa_private_key'] = rsa_private_key + rsa_private_key = self.request.session.get('rsa_private_key') + rsa_public_key = self.request.session.get('rsa_public_key') + if not all((rsa_private_key, rsa_public_key)): + rsa_private_key, rsa_public_key = utils.gen_key_pair() + rsa_public_key = rsa_public_key.replace('\n', '\\n') + self.request.session['rsa_private_key'] = rsa_private_key + self.request.session['rsa_public_key'] = rsa_public_key + context = { 'demo_mode': os.environ.get("DEMO_MODE"), 'AUTH_OPENID': settings.AUTH_OPENID, - 'rsa_public_key': rsa_public_key.replace('\n', '\\n') + 'rsa_public_key': rsa_public_key } kwargs.update(context) return super().get_context_data(**kwargs) From 4e7a5d8d4f7443dd74c4b82e64a111d863661f4d Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 29 Jul 2020 19:46:45 +0800 Subject: [PATCH 19/40] =?UTF-8?q?ci:=20=E4=BF=AE=E6=94=B9docker=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 78d506790..7cf768d85 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,7 @@ RUN pip install --upgrade pip setuptools wheel -i ${PIP_MIRROR} && \ pip config set global.index-url ${PIP_MIRROR} RUN pip install -r requirements/requirements.txt || pip install -r requirements/requirements.txt -COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/ +COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config RUN echo > config.yml From 90f03dda62405531f8b529d4318659fc765b0362 Mon Sep 17 00:00:00 2001 From: xinwen Date: Fri, 31 Jul 2020 18:18:52 +0800 Subject: [PATCH 20/40] =?UTF-8?q?feat(authentication):=20=E7=B1=BB?= =?UTF-8?q?=E4=BC=BC=E8=85=BE=E8=AE=AF=E4=BC=81=E4=B8=9A=E9=82=AE=E5=8D=95?= =?UTF-8?q?=E7=82=B9=E7=99=BB=E5=BD=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/api/__init__.py | 1 + apps/authentication/api/sso.py | 77 +++++++++++ apps/authentication/backends/api.py | 10 +- apps/authentication/errors.py | 6 + apps/authentication/filters.py | 15 +++ .../migrations/0004_ssotoken.py | 32 +++++ apps/authentication/models.py | 14 +- apps/authentication/serializers.py | 9 +- apps/authentication/urls/api_urls.py | 1 + apps/common/db/models.py | 33 ++++- apps/common/drf/exc_handlers.py | 45 +++++++ apps/common/exceptions.py | 12 ++ apps/jumpserver/conf.py | 5 + apps/jumpserver/settings/auth.py | 3 + apps/jumpserver/settings/libs.py | 1 + apps/locale/zh/LC_MESSAGES/django.mo | Bin 55907 -> 56117 bytes apps/locale/zh/LC_MESSAGES/django.po | 122 ++++++++++-------- 17 files changed, 329 insertions(+), 57 deletions(-) create mode 100644 apps/authentication/api/sso.py create mode 100644 apps/authentication/filters.py create mode 100644 apps/authentication/migrations/0004_ssotoken.py create mode 100644 apps/common/drf/exc_handlers.py diff --git a/apps/authentication/api/__init__.py b/apps/authentication/api/__init__.py index 4f6475124..af5d8d1b4 100644 --- a/apps/authentication/api/__init__.py +++ b/apps/authentication/api/__init__.py @@ -6,3 +6,4 @@ from .token import * from .mfa import * from .access_key import * from .login_confirm import * +from .sso import * diff --git a/apps/authentication/api/sso.py b/apps/authentication/api/sso.py new file mode 100644 index 000000000..5740c80d8 --- /dev/null +++ b/apps/authentication/api/sso.py @@ -0,0 +1,77 @@ +from uuid import UUID +from urllib.parse import urlencode + +from django.contrib.auth import login +from django.conf import settings +from django.http.response import HttpResponseRedirect +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.request import Request + +from common.utils.timezone import utcnow +from common.const.http import POST, GET +from common.drf.api import JmsGenericViewSet +from common.drf.serializers import EmptySerializer +from common.permissions import IsSuperUser +from common.utils import reverse +from users.models import User +from ..serializers import SSOTokenSerializer +from ..models import SSOToken +from ..filters import AuthKeyQueryDeclaration +from ..mixins import AuthMixin +from ..errors import SSOAuthClosed + + +class SSOViewSet(AuthMixin, JmsGenericViewSet): + queryset = SSOToken.objects.all() + serializer_classes = { + 'get_login_url': SSOTokenSerializer, + 'login': EmptySerializer + } + + @action(methods=[POST], detail=False, permission_classes=[IsSuperUser]) + def get_login_url(self, request, *args, **kwargs): + if not settings.AUTH_SSO: + raise SSOAuthClosed() + + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + username = serializer.validated_data['username'] + user = User.objects.get(username=username) + + operator = request.user.username + # TODO `created_by` 和 `created_by` 可以通过 `ThreadLocal` 统一处理 + token = SSOToken.objects.create(user=user, created_by=operator, updated_by=operator) + query = { + 'authkey': token.authkey + } + login_url = '%s?%s' % (reverse('api-auth:sso-login', external=True), urlencode(query)) + return Response(data={'login_url': login_url}) + + @action(methods=[GET], detail=False, filter_backends=[AuthKeyQueryDeclaration], permission_classes=[]) + def login(self, request: Request, *args, **kwargs): + """ + 此接口违反了 `Restful` 的规范 + `GET` 应该是安全的方法,但此接口是不安全的 + """ + authkey = request.query_params.get('authkey') + try: + authkey = UUID(authkey) + token = SSOToken.objects.get(authkey=authkey, expired=False) + # 先过期,只能访问这一次 + token.expired = True + token.save() + except (ValueError, SSOToken.DoesNotExist): + self.send_auth_signal(success=False, reason=f'authkey invalid: {authkey}') + return HttpResponseRedirect(reverse('authentication:login')) + + # 判断是否过期 + if (utcnow().timestamp() - token.date_created.timestamp()) > settings.AUTH_SSO_AUTHKEY_TTL: + self.send_auth_signal(success=False, reason=f'authkey timeout: {authkey}') + return HttpResponseRedirect(reverse('authentication:login')) + + user = token.user + login(self.request, user, 'authentication.backends.api.SSOAuthentication') + self.send_auth_signal(success=True, user=user) + return HttpResponseRedirect(reverse('index')) diff --git a/apps/authentication/backends/api.py b/apps/authentication/backends/api.py index b61798695..ff62677ef 100644 --- a/apps/authentication/backends/api.py +++ b/apps/authentication/backends/api.py @@ -5,14 +5,13 @@ import uuid import time from django.core.cache import cache -from django.conf import settings from django.utils.translation import ugettext as _ from django.utils.six import text_type from django.contrib.auth import get_user_model +from django.contrib.auth.backends import ModelBackend from rest_framework import HTTP_HEADER_ENCODING from rest_framework import authentication, exceptions from common.auth import signature -from rest_framework.authentication import CSRFCheck from common.utils import get_object_or_none, make_signature, http_to_unixtime from ..models import AccessKey, PrivateToken @@ -197,3 +196,10 @@ class SignatureAuthentication(signature.SignatureAuthentication): return user, secret except AccessKey.DoesNotExist: return None, None + + +class SSOAuthentication(ModelBackend): + """ + 什么也不做呀😺 + """ + pass diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py index 20ec0aedf..241881ba0 100644 --- a/apps/authentication/errors.py +++ b/apps/authentication/errors.py @@ -4,6 +4,7 @@ from django.utils.translation import ugettext_lazy as _ from django.urls import reverse from django.conf import settings +from common.exceptions import JMSException from .signals import post_auth_failed from users.utils import ( increase_login_failed_count, get_login_failed_count @@ -205,3 +206,8 @@ class LoginConfirmOtherError(LoginConfirmBaseError): def __init__(self, ticket_id, status): msg = login_confirm_error_msg.format(status) super().__init__(ticket_id=ticket_id, msg=msg) + + +class SSOAuthClosed(JMSException): + default_code = 'sso_auth_closed' + default_detail = _('SSO auth closed') diff --git a/apps/authentication/filters.py b/apps/authentication/filters.py new file mode 100644 index 000000000..30ab8c157 --- /dev/null +++ b/apps/authentication/filters.py @@ -0,0 +1,15 @@ +from rest_framework import filters +from rest_framework.compat import coreapi, coreschema + + +class AuthKeyQueryDeclaration(filters.BaseFilterBackend): + def get_schema_fields(self, view): + return [ + coreapi.Field( + name='authkey', location='query', required=True, type='string', + schema=coreschema.String( + title='authkey', + description='authkey' + ) + ) + ] diff --git a/apps/authentication/migrations/0004_ssotoken.py b/apps/authentication/migrations/0004_ssotoken.py new file mode 100644 index 000000000..57d2f9805 --- /dev/null +++ b/apps/authentication/migrations/0004_ssotoken.py @@ -0,0 +1,32 @@ +# Generated by Django 2.2.10 on 2020-07-31 08:36 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('authentication', '0003_loginconfirmsetting'), + ] + + operations = [ + migrations.CreateModel( + name='SSOToken', + fields=[ + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('authkey', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='Token')), + ('expired', models.BooleanField(default=False, verbose_name='Expired')), + ('user', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/authentication/models.py b/apps/authentication/models.py index 6a60b3432..27a9d7857 100644 --- a/apps/authentication/models.py +++ b/apps/authentication/models.py @@ -1,10 +1,13 @@ import uuid -from django.db import models +from functools import partial + from django.utils import timezone from django.utils.translation import ugettext_lazy as _, ugettext as __ from rest_framework.authtoken.models import Token from django.conf import settings +from django.utils.crypto import get_random_string +from common.db import models from common.mixins.models import CommonModelMixin from common.utils import get_object_or_none, get_request_ip, get_ip_city @@ -76,3 +79,12 @@ class LoginConfirmSetting(CommonModelMixin): def __str__(self): return '{} confirm'.format(self.user.username) + +class SSOToken(models.JMSBaseModel): + """ + 类似腾讯企业邮的 [单点登录](https://exmail.qq.com/qy_mng_logic/doc#10036) + 出于安全考虑,这里的 `token` 使用一次随即过期。但我们保留每一个生成过的 `token`。 + """ + authkey = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name=_('Token')) + expired = models.BooleanField(default=False, verbose_name=_('Expired')) + user = models.ForeignKey('users.User', on_delete=models.PROTECT, verbose_name=_('User'), db_constraint=False) diff --git a/apps/authentication/serializers.py b/apps/authentication/serializers.py index f000c3438..f04b847b4 100644 --- a/apps/authentication/serializers.py +++ b/apps/authentication/serializers.py @@ -5,12 +5,12 @@ from rest_framework import serializers from common.utils import get_object_or_none from users.models import User from users.serializers import UserProfileSerializer -from .models import AccessKey, LoginConfirmSetting +from .models import AccessKey, LoginConfirmSetting, SSOToken __all__ = [ 'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer', - 'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', + 'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', 'SSOTokenSerializer', ] @@ -76,3 +76,8 @@ class LoginConfirmSettingSerializer(serializers.ModelSerializer): model = LoginConfirmSetting fields = ['id', 'user', 'reviewers', 'date_created', 'date_updated'] read_only_fields = ['date_created', 'date_updated'] + + +class SSOTokenSerializer(serializers.Serializer): + username = serializers.CharField(write_only=True) + login_url = serializers.CharField(read_only=True) diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index da59711c4..3027fdad0 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -8,6 +8,7 @@ from .. import api app_name = 'authentication' router = DefaultRouter() router.register('access-keys', api.AccessKeyViewSet, 'access-key') +router.register('sso', api.SSOViewSet, 'sso') urlpatterns = [ diff --git a/apps/common/db/models.py b/apps/common/db/models.py index 04d501b8f..5d9827c06 100644 --- a/apps/common/db/models.py +++ b/apps/common/db/models.py @@ -1,4 +1,18 @@ -from functools import partial +""" +此文件作为 `django.db.models` 的 shortcut + +这样做的优点与缺点为: +优点: + - 包命名都统一为 `models` + - 用户在使用的时候只导入本文件即可 +缺点: + - 此文件中添加代码的时候,注意不要跟 `django.db.models` 中的命名冲突 +""" + +import uuid + +from django.db.models import * +from django.utils.translation import ugettext_lazy as _ class Choice(str): @@ -46,3 +60,20 @@ class ChoiceSetType(type): class ChoiceSet(metaclass=ChoiceSetType): choices = None # 用于 Django Model 中的 choices 配置, 为了代码提示在此声明 + + +class JMSBaseModel(Model): + created_by = CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by')) + updated_by = CharField(max_length=32, null=True, blank=True, verbose_name=_('Updated by')) + date_created = DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) + date_updated = DateTimeField(auto_now=True, verbose_name=_('Date updated')) + + class Meta: + abstract = True + + +class JMSModel(JMSBaseModel): + id = UUIDField(default=uuid.uuid4, primary_key=True) + + class Meta: + abstract = True diff --git a/apps/common/drf/exc_handlers.py b/apps/common/drf/exc_handlers.py new file mode 100644 index 000000000..8515c95ec --- /dev/null +++ b/apps/common/drf/exc_handlers.py @@ -0,0 +1,45 @@ +from django.core.exceptions import PermissionDenied, ObjectDoesNotExist as DJObjectDoesNotExist +from django.http import Http404 +from django.utils.translation import gettext + +from rest_framework import exceptions +from rest_framework.views import set_rollback +from rest_framework.response import Response + +from common.exceptions import JMSObjectDoesNotExist + + +def extract_object_name(exc, index=0): + """ + `index` 是从 0 开始数的, 比如: + `No User matches the given query.` + 提取 `User`,`index=1` + """ + (msg, *_) = exc.args + return gettext(msg.split(sep=' ', maxsplit=index + 1)[index]) + + +def common_exception_handler(exc, context): + if isinstance(exc, Http404): + exc = JMSObjectDoesNotExist(object_name=extract_object_name(exc, 1)) + elif isinstance(exc, PermissionDenied): + exc = exceptions.PermissionDenied() + elif isinstance(exc, DJObjectDoesNotExist): + exc = JMSObjectDoesNotExist(object_name=extract_object_name(exc, 0)) + + if isinstance(exc, exceptions.APIException): + headers = {} + if getattr(exc, 'auth_header', None): + headers['WWW-Authenticate'] = exc.auth_header + if getattr(exc, 'wait', None): + headers['Retry-After'] = '%d' % exc.wait + + if isinstance(exc.detail, (list, dict)): + data = exc.detail + else: + data = {'detail': exc.detail} + + set_rollback() + return Response(data, status=exc.status_code, headers=headers) + + return None diff --git a/apps/common/exceptions.py b/apps/common/exceptions.py index 4b3718837..ded24374a 100644 --- a/apps/common/exceptions.py +++ b/apps/common/exceptions.py @@ -1,8 +1,20 @@ # -*- coding: utf-8 -*- # +from django.utils.translation import gettext_lazy as _ from rest_framework.exceptions import APIException from rest_framework import status class JMSException(APIException): status_code = status.HTTP_400_BAD_REQUEST + + +class JMSObjectDoesNotExist(APIException): + status_code = status.HTTP_404_NOT_FOUND + default_code = 'object_does_not_exist' + default_detail = _('%s object does not exist.') + + def __init__(self, detail=None, code=None, object_name=None): + if detail is None and object_name: + detail = self.default_detail % object_name + super(JMSObjectDoesNotExist, self).__init__(detail=detail, code=code) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 6f521c453..3d8b6098f 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -211,6 +211,9 @@ class Config(dict): 'CAS_LOGOUT_COMPLETELY': True, 'CAS_VERSION': 3, + 'AUTH_SSO': False, + 'AUTH_SSO_AUTHKEY_TTL': 60 * 15, + 'OTP_VALID_WINDOW': 2, 'OTP_ISSUER_NAME': 'JumpServer', 'EMAIL_SUFFIX': 'jumpserver.org', @@ -440,6 +443,8 @@ class DynamicConfig: backends.insert(0, 'jms_oidc_rp.backends.OIDCAuthCodeBackend') if self.static_config.get('AUTH_RADIUS'): backends.insert(0, 'authentication.backends.radius.RadiusBackend') + if self.static_config.get('AUTH_SSO'): + backends.insert(0, 'authentication.backends.api.SSOAuthentication') return backends def XPACK_LICENSE_IS_VALID(self): diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index ae31ba10d..92c0d82f1 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -94,6 +94,9 @@ CAS_VERSION = CONFIG.CAS_VERSION CAS_ROOT_PROXIED_AS = CONFIG.CAS_ROOT_PROXIED_AS CAS_CHECK_NEXT = lambda: lambda _next_page: True +# SSO Auth +AUTH_SSO = CONFIG.AUTH_SSO +AUTH_SSO_AUTHKEY_TTL = CONFIG.AUTH_SSO_AUTHKEY_TTL # Other setting TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION diff --git a/apps/jumpserver/settings/libs.py b/apps/jumpserver/settings/libs.py index eb07e299d..9e4b56e21 100644 --- a/apps/jumpserver/settings/libs.py +++ b/apps/jumpserver/settings/libs.py @@ -40,6 +40,7 @@ REST_FRAMEWORK = { 'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S %z', 'DATETIME_INPUT_FORMATS': ['iso-8601', '%Y-%m-%d %H:%M:%S %z'], 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', + 'EXCEPTION_HANDLER': 'common.drf.exc_handlers.common_exception_handler', # 'PAGE_SIZE': 100, # 'MAX_PAGE_SIZE': 5000 diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 8599cd7c5784765fb2b455e74722b25e66e3775c..e779e41643d2ea599b69b258531bf543208f7062 100644 GIT binary patch delta 16934 zcma*ucX(CBy2tTF2!TKfB#{zYXbAyA?;WIuszzK|qRIKzi?8np6obG^rvY zARtBQAiV@cQ9(cj@AtRYn{&PAue+XS_{{sxteIJ}*4le>INsitcKeRBo*zSe=Qtc0 z(>hKr?48|lf_)ulZ%Jhx=S5A&c?VzO6l_t;afaeeZ0+MX?Q1*EIpX9xjuV&0aVFGt zoL`A=)pMMQ_(Ofi`55CHIL;NkfhXy=x1r;N`8XbDVk5^nO~cYAj`IfgYw9?DI2HqO z3TDDZm=)JxKHP8Kz`Vph%^W8O=111=RK-$Q3u7=5b>4D}#zUBg`JKm9ijW9s?l{G< z0v5t1PhGrx11imv#M zb$Dd)@2DO46E$&$7LJn*v!ezmiQF}(8fL&+s0m^*Gro?zzI+nqKSQ*!1 zINnBG&}-EAX+3f7y$!^YByyt~nxZC(LtR-s>KW*X+OeUiexpzWPDbt64Agl`&DE&$ zk}wGOVQxHux^T}!DjFb|ms|seVK&Tz+N$!Xds*Ln$LwW}K;5!w7=kNNJGcWi-U;)( zdBc3@>T#Y@(bm7lN|?T_dj+*nSK0t|1%;A&q768mqQ6GhGAG1HBdv;7002jw1>r`F+1^W)RiZp?(HGe zI5$ymNeb#ho>|_fgWH}FJ=&@eD(a96^${73dU_k7cEp1@aV)C;3e>E&;D!RO!4l(*-=-N54DA5Py<#$Evymh3ga!` z4K>jqER18YC~m}xco}s8p&j|g!|bSW&!OhM=CR5{)RjF)P2_ZPR~Ux6f;^ZHqcIj6 z;S-#TnqXXKcVUxJJ24A2-csawbJn7E%D0RAH6Mbti9IDOF#v0ln1(v>H0rImgwc52 zV*dnpqF~gOWJUGQgL;UgP|r$bjKtQM7e`6j$*5bn2ld*XLfyg}sPiA8`n|*)%DWLqbxrTb>19Q|E<=(7d6g7jKGtq3;h{&LC?{Xj*4%0 z_ewIN8p6>Bqs$_hfj9=Wkn*T2tb=|Shj}m_*W*X1TN2&Fz0&$-Gt_)-umpDK!Tr|? zr;x~i3s6_M0{w9d>Y>_$YX8o>VBWxJ@+qjT4(;h~bwSiEDS`eNYx$O_`QlOY^y+7}U-*N9|Y})CXEu%!tEL zAKBwPRJ4*Mr~x)$dOU@CjV_>e;CIXW^>()~$P7ab7>=5#5Ne__sPkhn6L!N$9DwS- z0JSro$iH(Fw+bvTHc_yh*vb<6*Ry7$jfzY{X`aj&!h>OzWOKCFb=sdlLG<59P& zJ8D5Q(fj^?N<}N%ikk2e7Q&xUD-C(yeR^}F7E&Fx@^yFaUMyK0-Zo zqs-lGY_FpS`V}QGbMNkveLR~=v)W9uJ4_$lIL)Xt7kCDX7 zFf$%8FJc673hIh8Cc3``b6^(Y24>qt_Fq@tn}lAM;pTkQPpxgJEBy{N!FlsO>Q=ol zU!xZ4JJ5X%15sC)6GO1P#SO6_aRTa@o8Y0MiB@0??nRyO5OoV)p%&otfjdD)3?vRk zZGApedyKW0Lp_YuQ4d=y)WbT!+J{>_2DQVUX;ey6S%})2llV4X$HG{8ko*1J4!ut` zYD>qX2A+qyWy?_uS&Q14&6eL``F*JI4_p2MvSS|SM=BcdKI)48Kn?6O*gYWt!-(^s zCMtuvWtCAo6>IqpW&&!wUZ^b|irU#(sD&Lwjei8g^!}frQi{Yq)QQ=LxLX*7#fYn; zzRh}Begf)VFGJmmb=JNUwZL=cRn$bcQCs~Oz0VBl*1pEfdjHdY=$;sc+Txt36APQA zQ4>`~O&p8bfflF*cd>W?Y5}9n38*cfZtW{D2k}?6t$)Eu^g^L{ZP4%x?&%8P&*TX`rfaCnxF%Az%keZZ)0t2@-csN#_5<> z@4x>@KG#V^V1H75!2y5%O67R;04s%@dUGC zhOzFg%!|6k#Zco_MDP1w%^I4bCTxqk_nlGqxUc0uM7_@=Q3K6FKU`|AM&0`))CKH9 zJxkwV9!x>K?it3px2_C&zyIH~L@SJ-Ls!&C?pV~7EJ96~jGEv8`r)^zXW)#r-$X6= zG4{ges4ML<-aWrNYGDH{o;aTUS7I&+t#kuwf+JWI&!Rp+UZWOJa)SE-QWdomjZh2h zfO=*=K%GC!+CN2|w;8q2y{L!z1p4E(3EY28bf1KNlZ8%n8!DhW*1`<^>WzW&~CzAA_2pH|pt5#NwEQ`S2=k!Plq@-8{v;#hxFiXzNo@x59U-J5f01O2a#f zy0WU%+y&M~U0Gw)d96@8)enPlB8q%jJvTd-bOve)mOS5hoB}Jg;6-c++z7lsD<9ZJop&( z5C(ka<|E7)v$9$LGmkrPOAuox;vWHj4+FsWxaC$ zt5eYe8emOqX7O}y2mXeGx+TjkzXi1;yHE=}Y+f`Up>E}CEQlG_x#P!}Wl?WY70LWg zOKa$34n%+Q!%!2AvHVPn7hp#6t1Z6;BZ+re`*qZUe?l$rZ_5X-cgM?RMx#epUfvSb zP;o5k#J4P;VD?9?{3Fx^^DJJ2x@BLYo}u%oah{<+{%x_}=WZN`YR~pL_g`0-hlDJO zQCJb{V@K<_7PX*Fr~$u1^*@1H$R*3Cn7>>86{=t026w`6)cN_)`#Nr5|CNX(p@CYW zt}p?0MI%rvorM9o&|G6~GY^{Ip%!|@{MqtplH75FFo5zUoN1}FUDwfpyzuXdM-Gp-&1L=@;i#uRJ)Rh%Q zeV{b4{99%mYQlKbt?Z3@?FL~nT!c*I97X+I@FhlK$*uY}<^H#!q7&ao4K&0YW6nff z;Zk!os(+HjJ28ZKKUT*xsBtp$ovnVk%_!73rOb*_?|%(T)I*)v1oalQMP11#%dauF zVNUV~Exv|1h##UR@a5Nx9^y=>g%w1NTfwY@>h~6U^n)XgioQGsp|)rS>V%`15zkqC z1A~blqXu|odEf2sgjrDeB4#PnI2AE3)fOv$+#%(Y}8N`>z!`JKc_fs5s2xd=?i* zt-PG&Ynb)02>B+c1r5OrI0?gWHfn+EP!IKH)cE_&;~uMAL=A8k^|ZgReE2SRfGAXZ zanxH-3q!Fk>dN222u#GRI2$$5=cw~`q52)S{9Vg?ezQcnFWisB5Y&~HLTzCc)BtZ; zKEZq+b!9_P6M4+ZsQwF3_jrTl_hMnygUyks&yz{0Z@KpbkK?iPQ7de~;8 zUgIRxd8bhMs}?^)o$up0=ynJ}4V2fcf;yoMYC-)_0}VDmF=tr&rxvd=x1t{20~Vh{ zO?V6Q;cuvRPuN%P2T2jsmrixmMBPvmCz?Z1TRhU7WX?hLUyAC#0rjjTqju_R)OiWNSZ<>VF$Gu+P`-!~td~>Po{cE@GBK zEuf;sbPlvzu5h)rCz-oZ3prx#XDojab>3ant$ktbVTavu zbE5a(|4LEOCsQrd3WuOh9D{nV=cAs5L#TUq6}6DpsQ&4`aR&@RQ~cj zV(o3w`~G*bhJoftb1LdvYY}Qo*P#ad!aQW2M12`uKtFtB{)U?PrRjIXy|7SJzsMu( zzpgBXgsg`;A>KOlwtOOXCqLQZXJ*h*cR^980ZXCIuZZ4nM=VJEj^#fxCz-Q0`%h&^QSHPk}ypfCQ3n)n|x_*-{t^P=*lQ4`j-d^6Mzceea6^e6U=r=k_j zw1x$ki+Hughphdy#TU#w=C9^!Gt+T*;_Rq#3!x?|X8H11nD|ZPwe~o@-HI~}wZg5a z1sun4e1Q5uNqfRQF%0t(S3_N42g~cn8)azK)eJ@+8j%cEmQg0KNbI|0@-} ze!<_lTU-ydh3zo{dt(evLEY0msD)m`nfM1L;)GM~EqG;SKJES#D_~Z}2K0Li>*7N6 z=lOGPP|<`BF%$lYdjI{-xL241wUv>mor%F}7;E|QsQ%MXKRQ=n7Tjqb!vNwd<^$At z&(Qn-|NGk#erMf=K-7R?sCyS>`BIpfxT577nk}ro1L`47KwZ#Kb0TWt3sJXdCu%{L z&vO4&xo?T5sI5)&z1txivl2&HTphKbMi#d+JDYt`S2i45;Ao34m_K3&`MVas_@4b& z;vW(kIOGTSeb0q@Sn`|gPz#-ny0Yb{^R{3per@qZ)Hsh&{hnEz?wlJ3n|V>=mGDr} z!&MdaaP_o?$>svofU8jxZbt3UA*_m5F*oKo?+#c2-z2Vs+KKU~&zEVaab}?=USjQ@ z^;X$p?!w-5IDnd{*adgux~PHTPy@9`4b%-AVIpe616UHj!9w^Pbz!+L@~MY4P~V2P zkqhGYza>I0xf2yf4O9!Wrs0)AO&ovOow$qH2el)E&57u}GSt?uM=f-><-fN03~Ib9 z7|Q(4bt*dHchnDyv{&4L^CR;+J$?8`M!ZQ`LZT(55xFFaj=`8i9XYeT(L{|*n8_vv$uq>h~wUzS&gxSlt|2L}F1T$Zxi`l)Hhzv_zhvD%7U zF&n%jz9!y>^>C#1*FWp%ctz~_jLHUY$jV7Pwfg^uBLC`imvgsKc2deS!4hj5W6mKr zfwt5miprO1*#Bvk%x9}g?Xa54*R;)}==P*%SFX^9yW||SPI+kC_Fv~cCGJTXYU36& zKP0z;dRy$w`Tt-o{rgWnDko_8n9|$^7In$%7)JSkwssU9q4*^#{EJeP5<^g&{#z;E zQg4Q*)q$e`MW6RND!MrPXuC@3*>f3Q}2`C9p9zlHtMKAy(1+p z9pAtTIFbBX>eZ?LOg%mRtQ#R|r{|TZ)Cjx!3 zRNQ*<#y6m97%o@<&S?2)SNy+C}I>+(*PYC^}}7D`NQ}#QK-dtT>1AHuW-;RTTYa zT?zP-^U_e)F_Ab9Kc#qnC3ryNmz2Yl8kDzbSdD`yzO<(vTd3qC_lYG&Slc{|qFkr# z8P26#qfa)hL~eZQ0Jw%+7OX}-i|&6Vf@n%dN=+KOp^m0>cuvgshclb{w>W{4fwGsf zj5ZwyY1>7)K%5u*;x%jYrJeuC%z1~BlXBMbv+xmdf4%=Y?z=c+QU7(CjxBTurQRI> zK>p6*bg(vkWv?V(m=aFWH=vI4F3w;x6aBYP8q;SzXW+MKaw&NH}{1-m<_ICfsOsr!vZTf0X zqtAa|l7}cyD1O#KoqnKxfwISP{6=*GIq^B_L7qxo$48VX`smlt0L%H)p&a$IcCPw) zzsuow%0}zw-H$eEwBtMt`KV{4bf-Sw@^4^8;$`@Z@{obwr@Tj*OyREpP76%J(v)13 z4aD^+*{Hv!bfbQZF?0;4zL!#5_dly8&rt6~c|lo6$xq%xIZu7Id!Eybdg{@iN(k*; zu>qx&wdK@!)KB3M%1Oe@#3iV|OBq3Z68ck8p9g0g>{zcbm)L5DYK)R7B+aI4NU>ZPpS zi$0$aPb02recz*AN#|Oh;+UVdqWA^*s+8ZTm&PwJ9nZv9b}pgw4*CCF|DWDK37%T& z9Vrv%-TOBqcZPx+lvnzEVPILu;`L{oo4 zt|Udr9`Y&VbnMptk0x=S@(sz2ZvOxLnMnQ!r7L}QQyNkp(qo_Xn*6Wcm1(j4D-c32H-=LtQ7RIT;l!UPttmfHQpnH8 zzO?DMN|{OAnbN_VasRjjB`ATEJGA*>I_7tN;>3&Cn-W4j0>@Dn&}k@^A(xD)#{()i z$+f1Gwd5fBj-!NAhErydZ-A-CBkHvYe#YwfEzU*HQ3la5ox~AJ>T#6He#&>`reaaiBi`{a8%|!6-^q z;++`6*{f1Vz^};Zm|?1ZiM~F1CCNRoe4-lubqt`sihh@9U-++lWpX+^SEwY?c*_#! zY#_J!fB)P}ekJ84saX0pp?;e3gz^r#FUUnxPdy@t!w62=xa+6~SmP}$K;H!Y{?iWV zXhAt-1N>&*P=g(T^!t(gA}mJkJA8$=@dIpvb19!w5{TPVPEbB1*3req`~R*~wF2ch z^Z)1gliFE}e#OO1)}NDW+W_xcTRMwd6Z=?wrfFTA&p7vE%7-LoP_9$g@uQ3TXU2$# zh~!l@3kBsUlo-*sbI-0_21X?G?V1?ze&2x+U5E5Y99Vqiv8GvldkpBBkbF3yY* zW)Yn}7}!0cOYgpkNy|E5!b V|Gh=q?#v{9bd*0``04L!)9E9U4cwUp(o)@>G=ba;NQQ7mF#qzw!DxUY0czjg~epAi! zhGG2bo_7Vu;xYQQtKoTJu|1zxu$JeYq@h|J&r6P}>arNjhl#NWCd5jZ0zbnD>|lH@WjP;r8pC5A)*T)Pv7;`bdH=9a65(h9VzQBu^rk>|r$LAP=r|WxO z0=$ih@Ui*UjNia5Gz>#&563VpjPbDs>b&~a9)$tS@AbA01C<~ij=J)(sEMazN?eK> z;8)~sc}Fo2PoO5ah)M7!YT~D;1qU>ACPj^#4zpn{^!c}hN)Hl)u^2u?bKu$jKX+08r5$iY6oXyQv40Iuq)=Ds9X3LbpdZs3l3<^{_EZaH+EZ=4>e##)V-{S zny9hmTc8H&j9IWB>I!FLUR;7fcm_4mHB`R`sGSLE;&wP3mCxRU{ntuslSqz@Q3G_v z6xa`W$i0cETk}0?Vbf6)%)!j~GZw+$u@J^?>UkxxD5k+)sPiVG#*aqb+BrTd1*ojF zhO4NF?xL>jF{<9(4uRQMc%C)WTk&wluK0=cUHXsENy5(|ZZEBZ*siURn%8olpQZa7ok^S4HhiGgSXCQCHjrwUdKT=Z!U| zU^wxQsGZo^iv8Ds2T5qlPGbtZi@In3LoG0Nlsh2=6{kW?kQH@)A=Jd>P&@Y-YGF~B z20L3k3bml`QRB>tviE-}30=`z)E4eR4fq>sVV6)>_}KC6 zff_F_^0;_KQ9CsS)8Txqh&wIzd0%>7c@pVSCq|(@ayw#9>~8T4)DFx=UCClp|5d2> ze-r9iIgHuxPt1r3+qtjf+^8LDg}Tsj$T&W4E){L%64Wi&h??jiYJy{^3C~;pI;JPS zgC#Idd$*-!Q2lG7&Toi1uQ_UG+FJfARKI@yywCH7QYlPgJZh_Vp&pJ~sIB`OwXpZ7 zXCkc(R z&b-067}(L}6Qk~BT2%kysP>AeajIcBHbh-%U(^K+!FV_xbs>|{|NhUX5}U+oa~`psw%;2HZMf2qU3^ z%A>Zj7HWce7ze*V4b&cW<-ITyPBvpuJ98ejW7kmk_#r01*j?R6bz;;)GNaBf=%b=7 zYm9n*TA_Aepyel`ws4v`8#Um3)I=*$6KzGEe-aboBg}@cQT;P^b32m@1BnY;>?=b> z9jc)wZh(ofz2$qO?)?zd*Jm{9N|&RqWF7k78q`kRLQQZ7bK>8q1*Pil#>s+OSaD=N zpVyj7Rua8Y3z>=GxCj&Behk7>s0pv27VyA)ikjdx>ej{m%I#Q?8G^cH$x)vhg;C$0 z4g7NdyHe5D?kLpP=`7SvtVO*hdr=EGjhf&pYRhk-uJko(p|N_n3kXK_OO1L>Ghk*c zg(27))!qZ+>HY6ZMOQQk^+7Wn_3%waJ=KfNU09R&GU|2B+S5HdIZ!)S0JV@(s0*l$ z!Po+Ifn8AJ`!EEf(f|H0prRG6!9o~=CGj3=VVQcliE^U0Iudo|Wh`G6HDO)Le_?h( zEvOIbLWZFhG6%JT>w0njHNhbgx`N}VfiIyRy4$FS?q4&ww|jW9VG{Cn%qR>e?t!}E z$(Rr4VNyJ9-b7vbGt}D>yAS)XN`^k}v$-Vdb!>#1przRz^%@Q}N1+xv9uwnK)DFegnfq985`l#rHo~T0c%sC%{>wNoc8|A+Yi zHQ-a!7JFa2txb(uST)r6bx`9r#YpUi!OZXdL?s1@)tCqOqdq8}T0UfeTX{Crt;mmR zFN0cO3$rb1qRyzT?t}hk26bykp`NL6sPkrHBEA0$sOZEs<`&dMdr@0<614-DP*-{% zHNb2018V022D*N!P(L(spcYaQb739S!Uv#k?R4~MWs9h!!k@7+?m@j30fXG@mkG6? zlBj!L9kq~pmZ5))P#>Meumna z52%3xhqyQys()J4mF7V0Tp`o~OQ6oHhS{+mhT{Np7V3hxVLJ4kp^~1;6V#Rl4|V^N zkrDL^r7`M?2cULlEb9AyJ!*n8*b3iaXZ)NUt%&<@00s+SNjwj;;d^9WpO<;K z`xlT3$cL8K8*}45Oo^c*cr!3JuE5Kf8owRsws0P5A=^*`-^OAXG|K(`paSYv4My$I zNHZFf>;0c+9X4THI_$(y+>2WAdGv4bw=Q1-wS{$13+#aU{1}S5H9w&4@j}#iYfw8I zWBL841)W6y`+t#&?(rRK_y_eqzd{WZG}=8x$<1`Ad!HGTV?NZwR2eg1Ys`g1QMYb6 zYP_u$AH{IuE9lcl;(IE(lB8qYggG&QxCm;1(w47{T4*!uifvI>c--30pcZt);+Gc3 z8tWFA61DJxs9RiSEc>qyj?N@>rE^dp9LrEE-i%tr7M3Hfk2){f^0QGp zHXpT+Wf&iSL0#A`48o(>70+Q7tl*pAJ~%p{wsIiqAsT8n!ZC4KUY&1gMnjWZmzQuI0xc}p+#3QlnJGa8ss4M#gb>eQ+R$W0| z*(223@g9RQ$@lIGGor3AH)^~hs9RVH)vpdlU`xvn#VoP7|C6cIp<&ZxeH!qWh-iK! z;7AOQ?RkIUN#dYse0Q_r%lL}8;BzTL_)O3hzoeoMkmd8;C)959IBM&!TKpIj5x+A77q~b%>e($2zj+EZ!FALQ{e?L&bRqu? zgQZaq?+jGGQw!ODO>~7scD!vSSmYK^5Ou{RFauUaJ$&sgKhPX)PBrJF#$Ao-ztKF5 ziHWaT`-4U7zdAl85rVH#D+^rgp4Mcj1r$bINd=3$m|vq7Hp=2f<~qyoM2&Y9HU3>| z{}=UpA&zf}`)hLw)I@1e3(9VBKGefh3Ugx<%!DH_A1*;XBWF?P-9e4>$b5rZV8Bw> zp4H5Q>gOwDmGah5$82P_u=chV_rN%u*x&Ml%rU4>&}pa%S787iMNM=X{of6!D}RZ6 zF7W$rnfri9X=XEvpzc*=vw`K?ptiJ!InW$wPBLd;0R0wVdHmVp`_}#hL-hW?wT1*g zyOkzItuTWbX;wx}+!V9m7pMtFo0Cv)(KK_l%Xc#SnIll=e{b=0ix;8JTVeT~=zsr@P|?aSpeA@?v3_sq zo~1_J)BLD`>tYZ#wz##$9W3sNTJYEAD9lbAjn!}~YMfXr*?+AlaHShCE$YOqsD%`? zd_}Xi<(r`TbwCZ&$Jz&>|8=x@5o(;(s0-YQx}fu@^B%2a|25Ea64G1cOl+nxv!fPT z*sNyx=BR-?T0G30WX?uSw9Mi)7H>yA+Q_=DbOkZm%nQ7>T`fQ$zd}De?P$#xu>+FV!i2Insur~1| zi!Y;g=pGipw-)DK=i;)Mn0yb^xI>T&^LeAF=mTY`HLNh#qbA&fde{!2Ub_>R2VYr2DJH|7sD2~O z$*6vFF+DCueP`^&qIeC}Ka8J22`~Z`=Rscxl@e4mKn-iChnldJ<@=jMtbG(_q&*t7 zBkNJ)Y)6fA6qDd-YySgd6F)YeU_9dI8@d0%RNj!#l?85c1EfXmKqllTfma%n;|Cmn2UH8YC-!k5HDaD-bDWb zP*3xF)PxCsai%o0pw5p(y}p$#-vM=gA8Q}vqoSvO5{6dKd3IPSs}coQ|zE7ZVo zx43>OQTa$zzO2RdQJ;iSs4E?U+QBiXeseAFTW6JRsFm$U4S3YNh&tg8>K?zgeBf61 zizqp&e_7N-wNN|O*z!?kSJXHIES})>c{8c#VOe5swho6-C!9syl6$BHB-`d5&P-U8 zxD@KjdShA~i~3wxZ1FiPMEn%hFY9*KUJCtx|L;acCyqc}$xo=ASZQvs{8rQz9<=zR zwO>Om^iT7ZnP7*@r$;TkfLY%1_0a$Rf2j%`dZH#Cg1X{Ss0r7aTg`puG1Ldj1=PKM zk6K9Doo<|bW)ZVAs=X3w$D5!}iH=lK;~=bw(@+b#VLme7m;t}KfkIJNmd)a#7S}+X z|GC)#{ZBpSBtHlXX*afBB+IwHfy6U ztfjU0F#DTBQ0I@@<#QdQNoat@sEIaOd=gU;-$cE~Z%`+u-R<%@P;mv+`E@OhLXFea z9D^EX5$b|>pw8Rpv&u>H8mhwsi=UeRp`PA^d)$c;s0s671eQg$w?Tc5^hbT^j7Lqh z0X6O}^B`)6eaEbF!Mue!;UVgT*Qkf(18Pf?>~$w*M)k{!df!W0z7l32u4C=puo>|{ z%!8*f6MjI>_j#H2xfAlER#X=?aWk_u>PkCU+}|96TEHlaqfzJ2wERMIt+@lWL%*Re zZ1mXvV322FzO0Np!!WTXIcAF)I_T-zuP=!UPgUs-9s(( zCHgdAyn}Aw5HmGuL7B~5s0ky@vZ#q_S$h=firSk)QT=CF`(n$l#7^Y*Tb%b0`>zwL z9&!t4jRC}+Q3LcuJbZ>&n3@^|+alqf7tJQJJZDNK(!kGa>b8tPdXirU#}7>dKOxc4jp5pq_ye7B@icU~7x}nVi(7#<_8Z{ST$` z%n|`--9V{P9Wz^8$l@|)9n^rWP!ClX)I&AX^4rWqsD+(HO?Vx(Gf%NJCh(nePj5BU zfbFpo_D1c*X4D7EPSikqQ2mZt`$hAHc^A9U{unh;^Yd=vKB#dv;_j$#zoZx4zK}pn;Sq`2>UzlhUeeesoHCyBfkbgE zh`*^(@BbVU8>prxF#&txOmd$dIW68n+Z+FhuC>`G{pQ&Tr)l3#=|bs4P9IX*k(tED z>A#u!m-vMExsN|S9x+zGf*(k>z;`ymDeAqbzrfd&3e>-+PjSjV>KDlQ|BemuDLS@O zGE)lB-cs!x-SH-|js(KlW6{T>A#Hradd09A`N`PW+KZ6i zLj6zcoS%9C?K*Bz=W%d{?xCjGFQoyc2)S4E$%ZM2A5gD>XNd!` zvpdJqQKl2Gqx9LCk9depz(W(zFb3a87{_Sl(`f>as2;i1Kwmx3C__`X6pLL zP+wZ2o|L7OD`dxWMq%pluneUDB`10Q2IzGrucIXD)xS*Jbo>k3;WaEq;cLs=k0a=J zhcbwghPXEVk1~q79X>AuiA0v*BgUIYJdJq0_1J1glDk3M$78P5x03Hivf3wY>T`g& z5G5AR+dJYPZ4Q2(_`l=SXGknQ20GI4qau!K)PJC~ru3rdXlKTA53WD=2LrdKgcH@p z9{#NRV~O>DLq0F{uRdw}iJXpg6n$T9{`fIa(oTAS;nryZ^`Gd}mXeCxdg59559M3y z-wEpx>v)Y*h~rbPP|uFfa2svIs4wyNVl3+KDKBW_`guyMLH{XO6&g|#H$WXZaSm4X zN9>MS3jeb{;~3~qN(bT}u{>?_DLRT$*7$2Yg|yG09$@ED^?9WT7E!7Z9m33%Iuspi zDd(*|g8DHAA3!{k`hTip9jrl4-&s0}5bGN@(9X+>b%_5We~R*mc%8NV5}WHUOHh=C zEfgK2Z~&zQ`8JdYa(#(QQ}=%iyPv5u5DnA{hX(vh{RQzIz0nED?vk+bAt4mnb^44{fM-qI}KypJ7>YgVe~;fl`RP zehsw6)>xH3d+}>bfjYjW{u@O{CVb?>zNA_^XdUE)!g$ z1XKTv4yo|~@yBBgaa_s_ijFCi!}QZJ3v*Et(pH%`E%hUmr2ba-Uy;Toen?3{nMHm$ zeaBgQ#C?LMl-rbOk{c=esgIy@1itlu^Y^CKmzG4hnwB)EBd)^>CH~HGUy^G=c}YCg zaubOsQtx8@-_ zxx|=&I1Lt{ETTjbH)WjBxPa1#+%LpmtxQ&rPPiaL9Mw{wzfO;)D|4jWF zS=saM3m)RAKubsf*h-*#31e(@{e0ob2S!P+}+x$;HQy$HGtQW0MJQx%2Ra2P`;rYA|9gzM?30!DYGddl#fSj&K*Wjfzpq> zeqyh)_GGkuMJcTJU&kp*4-)CELtebj9I zAaxzT6DOwr-i6*BEJ*H#-v9apS#&1HAUeIG^roz`++p(Xh(A5r5agp&qCBKbq&^%31nE>nZ(s#EHXwCGJmIM#uCtY{1yW8*T6>Vi={!7=j55at?LW$8N-Za4nXz{88Gv zP|r$f9P_k%WN=LB&psrNsnoteSj@h H8UOzP<>ldS diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index eb34c4400..92d93b15f 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-07-29 15:03+0800\n" +"POT-Creation-Date: 2020-07-31 19:20+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -131,9 +131,10 @@ msgstr "参数" #: applications/models/remote_app.py:39 assets/models/asset.py:224 #: assets/models/base.py:240 assets/models/cluster.py:28 #: assets/models/cmd_filter.py:26 assets/models/cmd_filter.py:60 -#: assets/models/group.py:21 common/mixins/models.py:49 orgs/models.py:23 -#: orgs/models.py:316 perms/models/base.py:54 users/models/user.py:530 -#: users/serializers/group.py:35 users/templates/users/user_detail.html:97 +#: assets/models/group.py:21 common/db/models.py:66 common/mixins/models.py:49 +#: orgs/models.py:23 orgs/models.py:316 perms/models/base.py:54 +#: users/models/user.py:530 users/serializers/group.py:35 +#: users/templates/users/user_detail.html:97 #: xpack/plugins/change_auth_plan/models.py:81 xpack/plugins/cloud/models.py:56 #: xpack/plugins/cloud/models.py:146 xpack/plugins/gathered_user/models.py:30 msgid "Created by" @@ -144,7 +145,7 @@ msgstr "创建者" #: applications/models/remote_app.py:42 assets/models/asset.py:225 #: assets/models/base.py:238 assets/models/cluster.py:26 #: assets/models/domain.py:23 assets/models/gathered_user.py:19 -#: assets/models/group.py:22 assets/models/label.py:25 +#: assets/models/group.py:22 assets/models/label.py:25 common/db/models.py:68 #: common/mixins/models.py:50 ops/models/adhoc.py:38 ops/models/command.py:27 #: orgs/models.py:24 orgs/models.py:314 perms/models/base.py:55 #: users/models/group.py:18 users/templates/users/user_group_detail.html:58 @@ -241,7 +242,7 @@ msgstr "节点" #: assets/models/asset.py:196 assets/models/cmd_filter.py:22 #: assets/models/domain.py:55 assets/models/label.py:22 -#: authentication/models.py:45 +#: authentication/models.py:48 msgid "Is active" msgstr "激活" @@ -379,7 +380,8 @@ msgid "SSH public key" msgstr "SSH公钥" #: assets/models/base.py:239 assets/models/gathered_user.py:20 -#: common/mixins/models.py:51 ops/models/adhoc.py:39 orgs/models.py:315 +#: common/db/models.py:69 common/mixins/models.py:51 ops/models/adhoc.py:39 +#: orgs/models.py:315 msgid "Date updated" msgstr "更新日期" @@ -534,9 +536,9 @@ msgid "Default asset group" msgstr "默认资产组" #: assets/models/label.py:15 audits/models.py:36 audits/models.py:56 -#: audits/models.py:69 audits/serializers.py:77 authentication/models.py:43 -#: orgs/models.py:16 orgs/models.py:312 perms/forms/asset_permission.py:83 -#: perms/forms/database_app_permission.py:38 +#: audits/models.py:69 audits/serializers.py:77 authentication/models.py:46 +#: authentication/models.py:90 orgs/models.py:16 orgs/models.py:312 +#: perms/forms/asset_permission.py:83 perms/forms/database_app_permission.py:38 #: perms/forms/remote_app_permission.py:40 perms/models/base.py:49 #: templates/index.html:78 terminal/backends/command/models.py:18 #: terminal/backends/command/serializers.py:12 terminal/models.py:185 @@ -1042,94 +1044,94 @@ msgstr "运行用户" msgid "Code is invalid" msgstr "Code无效" -#: authentication/backends/api.py:53 +#: authentication/backends/api.py:52 msgid "Invalid signature header. No credentials provided." msgstr "" -#: authentication/backends/api.py:56 +#: authentication/backends/api.py:55 msgid "Invalid signature header. Signature string should not contain spaces." msgstr "" -#: authentication/backends/api.py:63 +#: authentication/backends/api.py:62 msgid "Invalid signature header. Format like AccessKeyId:Signature" msgstr "" -#: authentication/backends/api.py:67 +#: authentication/backends/api.py:66 msgid "" "Invalid signature header. Signature string should not contain invalid " "characters." msgstr "" -#: authentication/backends/api.py:87 authentication/backends/api.py:103 +#: authentication/backends/api.py:86 authentication/backends/api.py:102 msgid "Invalid signature." msgstr "" -#: authentication/backends/api.py:94 +#: authentication/backends/api.py:93 msgid "HTTP header: Date not provide or not %a, %d %b %Y %H:%M:%S GMT" msgstr "" -#: authentication/backends/api.py:99 +#: authentication/backends/api.py:98 msgid "Expired, more than 15 minutes" msgstr "" -#: authentication/backends/api.py:106 +#: authentication/backends/api.py:105 msgid "User disabled." msgstr "用户已禁用" -#: authentication/backends/api.py:124 +#: authentication/backends/api.py:123 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication/backends/api.py:127 +#: authentication/backends/api.py:126 msgid "Invalid token header. Sign string should not contain spaces." msgstr "" -#: authentication/backends/api.py:134 +#: authentication/backends/api.py:133 msgid "" "Invalid token header. Sign string should not contain invalid characters." msgstr "" -#: authentication/backends/api.py:145 +#: authentication/backends/api.py:144 msgid "Invalid token or cache refreshed." msgstr "" -#: authentication/errors.py:22 +#: authentication/errors.py:23 msgid "Username/password check failed" msgstr "用户名/密码 校验失败" -#: authentication/errors.py:23 +#: authentication/errors.py:24 msgid "Password decrypt failed" msgstr "密码解密失败" -#: authentication/errors.py:24 +#: authentication/errors.py:25 msgid "MFA failed" msgstr "多因子认证失败" -#: authentication/errors.py:25 +#: authentication/errors.py:26 msgid "MFA unset" msgstr "多因子认证没有设定" -#: authentication/errors.py:26 +#: authentication/errors.py:27 msgid "Username does not exist" msgstr "用户名不存在" -#: authentication/errors.py:27 +#: authentication/errors.py:28 msgid "Password expired" msgstr "密码已过期" -#: authentication/errors.py:28 +#: authentication/errors.py:29 msgid "Disabled or expired" msgstr "禁用或失效" -#: authentication/errors.py:29 +#: authentication/errors.py:30 msgid "This account is inactive." msgstr "此账户已禁用" -#: authentication/errors.py:39 +#: authentication/errors.py:40 msgid "No session found, check your cookie" msgstr "会话已变更,刷新页面" -#: authentication/errors.py:41 +#: authentication/errors.py:42 #, python-brace-format msgid "" "The username or password you entered is incorrect, please enter it again. " @@ -1139,37 +1141,41 @@ msgstr "" "您输入的用户名或密码不正确,请重新输入。 您还可以尝试 {times_try} 次(账号将" "被临时 锁定 {block_time} 分钟)" -#: authentication/errors.py:47 +#: authentication/errors.py:48 msgid "" "The account has been locked (please contact admin to unlock it or try again " "after {} minutes)" msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)" -#: authentication/errors.py:50 users/views/profile/otp.py:107 +#: authentication/errors.py:51 users/views/profile/otp.py:107 #: users/views/profile/otp.py:146 users/views/profile/otp.py:166 msgid "MFA code invalid, or ntp sync server time" msgstr "MFA验证码不正确,或者服务器端时间不对" -#: authentication/errors.py:52 +#: authentication/errors.py:53 msgid "MFA required" msgstr "需要多因子认证" -#: authentication/errors.py:53 +#: authentication/errors.py:54 msgid "MFA not set, please set it first" msgstr "多因子认证没有设置,请先完成设置" -#: authentication/errors.py:54 +#: authentication/errors.py:55 msgid "Login confirm required" msgstr "需要登录复核" -#: authentication/errors.py:55 +#: authentication/errors.py:56 msgid "Wait login confirm ticket for accept" msgstr "等待登录复核处理" -#: authentication/errors.py:56 +#: authentication/errors.py:57 msgid "Login confirm ticket was {}" msgstr "登录复核 {}" +#: authentication/errors.py:213 +msgid "SSO auth closed" +msgstr "SSO 认证关闭了" + #: authentication/forms.py:26 authentication/forms.py:34 #: authentication/templates/authentication/login.html:38 #: authentication/templates/authentication/xpack_login.html:118 @@ -1177,7 +1183,7 @@ msgstr "登录复核 {}" msgid "MFA code" msgstr "多因子认证验证码" -#: authentication/models.py:19 +#: authentication/models.py:22 #: authentication/templates/authentication/_access_key_modal.html:32 #: perms/models/base.py:51 users/templates/users/_select_user_modal.html:18 #: users/templates/users/user_detail.html:132 @@ -1185,23 +1191,31 @@ msgstr "多因子认证验证码" msgid "Active" msgstr "激活中" -#: authentication/models.py:39 +#: authentication/models.py:42 msgid "Private Token" msgstr "SSH密钥" -#: authentication/models.py:44 users/templates/users/user_detail.html:258 +#: authentication/models.py:47 users/templates/users/user_detail.html:258 msgid "Reviewers" msgstr "审批人" -#: authentication/models.py:53 tickets/models/ticket.py:27 +#: authentication/models.py:56 tickets/models/ticket.py:27 #: users/templates/users/user_detail.html:250 msgid "Login confirm" msgstr "登录复核" -#: authentication/models.py:63 +#: authentication/models.py:66 msgid "City" msgstr "城市" +#: authentication/models.py:88 +msgid "Token" +msgstr "" + +#: authentication/models.py:89 +msgid "Expired" +msgstr "过期时间" + #: authentication/templates/authentication/_access_key_modal.html:6 msgid "API key list" msgstr "API Key列表" @@ -1349,7 +1363,7 @@ msgstr "欢迎回来,请输入用户名和密码登录" msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:172 +#: authentication/views/login.py:178 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -1357,15 +1371,15 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
\n" " 不要关闭本页面" -#: authentication/views/login.py:177 +#: authentication/views/login.py:183 msgid "No ticket found" msgstr "没有发现工单" -#: authentication/views/login.py:209 +#: authentication/views/login.py:215 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:210 +#: authentication/views/login.py:216 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" @@ -1379,11 +1393,20 @@ msgstr "%(name)s 创建成功" msgid "%(name)s was updated successfully" msgstr "%(name)s 更新成功" +#: common/db/models.py:67 +msgid "Updated by" +msgstr "更新人" + #: common/drf/parsers/csv.py:22 #, python-format msgid "The max size of CSV is %d bytes" msgstr "CSV 文件最大为 %d 字节" +#: common/exceptions.py:15 +#, python-format +msgid "%s object does not exist." +msgstr "%s对象不存在" + #: common/fields/form.py:33 msgid "Not a valid json" msgstr "不是合法json" @@ -5541,9 +5564,6 @@ msgstr "旗舰版" #~ msgid "Corporation" #~ msgstr "公司" -#~ msgid "Expired" -#~ msgstr "过期时间" - #~ msgid "Edition" #~ msgstr "版本" From 8ee7230eadd3f651fe45b55debd5a436b6f26ac5 Mon Sep 17 00:00:00 2001 From: ibuler Date: Fri, 31 Jul 2020 19:40:27 +0800 Subject: [PATCH 21/40] =?UTF-8?q?fix(auth):=20=E4=BF=AE=E5=A4=8Dradius=20d?= =?UTF-8?q?ecode=20error=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/backends/radius.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/authentication/backends/radius.py b/apps/authentication/backends/radius.py index e45fd8033..8edd2124c 100644 --- a/apps/authentication/backends/radius.py +++ b/apps/authentication/backends/radius.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- # +import traceback from django.contrib.auth import get_user_model from radiusauth.backends import RADIUSBackend, RADIUSRealmBackend from django.conf import settings -from pyrad.packet import AccessRequest User = get_user_model() @@ -27,6 +27,17 @@ class CreateUserMixin: user.save() return user + def _perform_radius_auth(self, client, packet): + # TODO: 等待官方库修复这个BUG + try: + return super()._perform_radius_auth(client, packet) + except UnicodeError as e: + import sys + tb = ''.join(traceback.format_exception(*sys.exc_info(), limit=2, chain=False)) + if tb.find("cl.decode") != -1: + return [], False, False + return None + def authenticate(self, *args, **kwargs): # 校验用户时,会传入public_key参数,父类authentication中不接受public_key参数,所以要pop掉 # TODO:需要优化各backend的authenticate方法,django进行调用前会检测各authenticate的参数 From f0d564180c2802a8da7d431e15136a5cb61e9ed1 Mon Sep 17 00:00:00 2001 From: Orange Date: Tue, 4 Aug 2020 10:33:07 +0800 Subject: [PATCH 22/40] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96Issues=20Templa?= =?UTF-8?q?te?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 5c40fded3..879177926 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -4,6 +4,9 @@ ##### 使用版本 [请提供你使用的JumpServer版本 如 2.0.1 注: 1.4及以下版本不再提供支持] +##### 使用浏览器版本 +[请提供你使用的浏览器版本 如 Chrome 84.0.4147.105 ] + ##### 问题复现步骤 1. [步骤1] 2. [步骤2] From c3c5801d2e23af2541e4effa238c5bf212cbba96 Mon Sep 17 00:00:00 2001 From: xinwen Date: Mon, 3 Aug 2020 16:17:46 +0800 Subject: [PATCH 23/40] =?UTF-8?q?refactor(orgs):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E7=BB=84=E7=BB=87=E4=B8=8E=E7=94=A8=E6=88=B7=E5=85=B3=E7=B3=BB?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/urls/api_urls.py | 1 - apps/common/db/models.py | 7 ++++++- apps/common/drf/api.py | 14 ++++++++++++- apps/common/mixins/api.py | 22 +++++++++++++++++--- apps/orgs/api.py | 26 ++++++++++++----------- apps/orgs/filters.py | 16 ++++++++++++++ apps/orgs/mixins/serializers.py | 9 +------- apps/orgs/models.py | 12 ++++++++++- apps/orgs/serializers.py | 37 +++++++++++++-------------------- apps/orgs/urls/api_urls.py | 11 +++++----- apps/users/serializers/user.py | 2 +- 11 files changed, 100 insertions(+), 57 deletions(-) create mode 100644 apps/orgs/filters.py diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index 279bd26be..d70accc23 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -1,7 +1,6 @@ # coding:utf-8 from django.urls import path, re_path from rest_framework_nested import routers -# from rest_framework.routers import DefaultRouter from rest_framework_bulk.routes import BulkRouter from common import api as capi diff --git a/apps/common/db/models.py b/apps/common/db/models.py index 5d9827c06..b807b9dd5 100644 --- a/apps/common/db/models.py +++ b/apps/common/db/models.py @@ -12,11 +12,12 @@ import uuid from django.db.models import * +from django.db.models.functions import Concat from django.utils.translation import ugettext_lazy as _ class Choice(str): - def __new__(cls, value, label): + def __new__(cls, value, label=''): # `deepcopy` 的时候不会传 `label` self = super().__new__(cls, value) self.label = label return self @@ -77,3 +78,7 @@ class JMSModel(JMSBaseModel): class Meta: abstract = True + + +def concated_display(name1, name2): + return Concat(F(name1), Value('('), F(name2), Value(')')) diff --git a/apps/common/drf/api.py b/apps/common/drf/api.py index 692d567f5..febd4467e 100644 --- a/apps/common/drf/api.py +++ b/apps/common/drf/api.py @@ -2,7 +2,8 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework_bulk import BulkModelViewSet from ..mixins.api import ( - SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, PaginatedResponseMixin + SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, PaginatedResponseMixin, + RelationMixin, AllowBulkDestoryMixin ) @@ -26,5 +27,16 @@ class JMSBulkModelViewSet(SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, PaginatedResponseMixin, + AllowBulkDestoryMixin, BulkModelViewSet): pass + + +class JMSBulkRelationModelViewSet(SerializerMixin2, + QuerySetMixin, + ExtraFilterFieldsMixin, + PaginatedResponseMixin, + RelationMixin, + AllowBulkDestoryMixin, + BulkModelViewSet): + pass diff --git a/apps/common/mixins/api.py b/apps/common/mixins/api.py index 5c17a5cca..3e9aea665 100644 --- a/apps/common/mixins/api.py +++ b/apps/common/mixins/api.py @@ -11,6 +11,8 @@ from django.core.cache import cache from django.http import JsonResponse from rest_framework.response import Response from rest_framework.settings import api_settings +from rest_framework import status +from rest_framework_bulk.drf3.mixins import BulkDestroyModelMixin from common.drf.filters import IDSpmFilter, CustomFilter, IDInFilter from ..utils import lazyproperty @@ -223,10 +225,11 @@ class RelationMixin: self.through = getattr(self.m2m_field.model, self.m2m_field.attname).through def get_queryset(self): + # 注意,此处拦截了 `get_queryset` 没有 `super` queryset = self.through.objects.all() return queryset - def send_post_add_signal(self, instances): + def send_m2m_changed_signal(self, instances, action): if not isinstance(instances, list): instances = [instances] @@ -239,13 +242,17 @@ class RelationMixin: for from_obj, to_ids in from_to_mapper.items(): m2m_changed.send( - sender=self.through, instance=from_obj, action='post_add', + sender=self.through, instance=from_obj, action=action, reverse=False, model=self.to_model, pk_set=to_ids ) def perform_create(self, serializer): instance = serializer.save() - self.send_post_add_signal(instance) + self.send_m2m_changed_signal(instance, 'post_add') + + def perform_destroy(self, instance): + instance.delete() + self.send_m2m_changed_signal(instance, 'post_remove') class SerializerMixin2: @@ -275,3 +282,12 @@ class QuerySetMixin: queryset = serializer_class.setup_eager_loading(queryset) return queryset + + +class AllowBulkDestoryMixin: + def allow_bulk_destroy(self, qs, filtered): + """ + 我们规定,批量删除的情况必须用 `id` 指定要删除的数据。 + """ + query = str(filtered.query) + return '`id` IN (' in query or '`id` =' in query diff --git a/apps/orgs/api.py b/apps/orgs/api.py index e29a14e22..d283019d3 100644 --- a/apps/orgs/api.py +++ b/apps/orgs/api.py @@ -7,14 +7,18 @@ from rest_framework.views import Response from rest_framework_bulk import BulkModelViewSet from common.permissions import IsSuperUserOrAppUser +from common.drf.api import JMSBulkRelationModelViewSet from .models import Organization, ROLE -from .serializers import OrgSerializer, OrgReadSerializer, \ - OrgAllUserSerializer, OrgRetrieveSerializer +from .serializers import ( + OrgSerializer, OrgReadSerializer, + OrgRetrieveSerializer, OrgMemberSerializer +) from users.models import User, UserGroup from assets.models import Asset, Domain, AdminUser, SystemUser, Label from perms.models import AssetPermission from orgs.utils import current_org from common.utils import get_logger +from .filters import OrgMemberRelationFilterSet logger = get_logger(__file__) @@ -61,15 +65,13 @@ class OrgViewSet(BulkModelViewSet): return Response({'msg': True}, status=status.HTTP_200_OK) -class OrgAllUserListApi(generics.ListAPIView): +class OrgMemberRelationBulkViewSet(JMSBulkRelationModelViewSet): permission_classes = (IsSuperUserOrAppUser,) - serializer_class = OrgAllUserSerializer - filter_fields = ("username", "name") - search_fields = filter_fields + m2m_field = Organization.members.field + serializer_class = OrgMemberSerializer + filterset_class = OrgMemberRelationFilterSet - def get_queryset(self): - pk = self.kwargs.get("pk") - users = User.objects.filter( - orgs=pk, m2m_org_members__role=ROLE.USER - ).only(*self.serializer_class.Meta.only_fields) - return users + def perform_bulk_destroy(self, queryset): + objs = list(queryset.all().prefetch_related('user', 'org')) + queryset.delete() + self.send_m2m_changed_signal(objs, action='post_remove') diff --git a/apps/orgs/filters.py b/apps/orgs/filters.py new file mode 100644 index 000000000..df68e468f --- /dev/null +++ b/apps/orgs/filters.py @@ -0,0 +1,16 @@ +from django_filters.rest_framework import filterset +from django_filters.rest_framework import filters + +from .models import OrganizationMember + + +class UUIDInFilter(filters.BaseInFilter, filters.UUIDFilter): + pass + + +class OrgMemberRelationFilterSet(filterset.FilterSet): + id = UUIDInFilter(field_name='id', lookup_expr='in') + + class Meta: + model = OrganizationMember + fields = ('org_id', 'user_id', 'role', 'id') diff --git a/apps/orgs/mixins/serializers.py b/apps/orgs/mixins/serializers.py index 2b415e31b..fed9d1713 100644 --- a/apps/orgs/mixins/serializers.py +++ b/apps/orgs/mixins/serializers.py @@ -11,8 +11,7 @@ from ..utils import get_current_org_id_for_serializer __all__ = [ "OrgResourceSerializerMixin", "BulkOrgResourceSerializerMixin", - "BulkOrgResourceModelSerializer", "OrgMembershipSerializerMixin", - "OrgResourceModelSerializerMixin", + "BulkOrgResourceModelSerializer", "OrgResourceModelSerializerMixin", ] @@ -53,9 +52,3 @@ class BulkOrgResourceSerializerMixin(BulkSerializerMixin, OrgResourceSerializerM class BulkOrgResourceModelSerializer(BulkOrgResourceSerializerMixin, serializers.ModelSerializer): pass - - -class OrgMembershipSerializerMixin: - def run_validation(self, initial_data=None): - initial_data['organization'] = str(self.context['org'].id) - return super().run_validation(initial_data) diff --git a/apps/orgs/models.py b/apps/orgs/models.py index effabf50f..c72d1ae82 100644 --- a/apps/orgs/models.py +++ b/apps/orgs/models.py @@ -149,6 +149,13 @@ class Organization(models.Model): m2m_org_members__user_id=user.id ).distinct() + @classmethod + def get_user_all_orgs(cls, user): + return [ + *cls.objects.filter(members=user).distinct(), + cls.default() + ] + @classmethod def get_user_admin_orgs(cls, user): if user.is_anonymous: @@ -161,7 +168,10 @@ class Organization(models.Model): def get_user_user_orgs(cls, user): if user.is_anonymous: return cls.objects.none() - return cls.get_user_orgs_by_role(user, ROLE.USER) + return [ + *cls.get_user_orgs_by_role(user, ROLE.USER), + cls.default() + ] @classmethod def get_user_audit_orgs(cls, user): diff --git a/apps/orgs/serializers.py b/apps/orgs/serializers.py index 4cf54f92a..5b20ce47e 100644 --- a/apps/orgs/serializers.py +++ b/apps/orgs/serializers.py @@ -1,11 +1,12 @@ - +from django.db.models import F from rest_framework.serializers import ModelSerializer from rest_framework import serializers from users.models.user import User from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import BulkModelSerializer +from common.db.models import concated_display as display from .models import Organization, OrganizationMember -from .mixins.serializers import OrgMembershipSerializerMixin class OrgSerializer(ModelSerializer): @@ -50,30 +51,20 @@ class OrgReadSerializer(OrgSerializer): pass -class OrgMembershipAdminSerializer(OrgMembershipSerializerMixin, ModelSerializer): +class OrgMemberSerializer(BulkModelSerializer): + org_display = serializers.CharField() + user_display = serializers.CharField() + class Meta: model = Organization.members.through - list_serializer_class = AdaptedBulkListSerializer - fields = '__all__' + fields = ('id', 'org', 'user', 'role', 'org_display', 'user_display') - -class OrgMembershipUserSerializer(OrgMembershipSerializerMixin, ModelSerializer): - class Meta: - model = Organization.members.through - list_serializer_class = AdaptedBulkListSerializer - fields = '__all__' - - -class OrgAllUserSerializer(serializers.Serializer): - user = serializers.UUIDField(read_only=True, source='id') - user_display = serializers.SerializerMethodField() - - class Meta: - only_fields = ['id', 'username', 'name'] - - @staticmethod - def get_user_display(obj): - return str(obj) + @classmethod + def setup_eager_loading(cls, queryset): + return queryset.annotate( + org_display=F('org__name'), + user_display=display('user__name', 'user__username') + ).distinct() class OrgRetrieveSerializer(OrgReadSerializer): diff --git a/apps/orgs/urls/api_urls.py b/apps/orgs/urls/api_urls.py index e14435868..56a135fcd 100644 --- a/apps/orgs/urls/api_urls.py +++ b/apps/orgs/urls/api_urls.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- # -from django.urls import re_path, path +from django.urls import re_path from rest_framework.routers import DefaultRouter +from rest_framework_bulk.routes import BulkRouter from common import api as capi from .. import api @@ -10,15 +11,13 @@ from .. import api app_name = 'orgs' router = DefaultRouter() +bulk_router = BulkRouter() router.register(r'orgs', api.OrgViewSet, 'org') +bulk_router.register(r'org-memeber-relation', api.OrgMemberRelationBulkViewSet, 'org-memeber-relation') old_version_urlpatterns = [ re_path('(?Porg)/.*', capi.redirect_plural_name_api) ] -urlpatterns = [ - path('/users/all/', api.OrgAllUserListApi.as_view(), name='org-all-users'), -] - -urlpatterns += router.urls + old_version_urlpatterns +urlpatterns = router.urls + bulk_router.urls + old_version_urlpatterns diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index 21c38d7bc..4924aef17 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -18,7 +18,7 @@ __all__ = [ 'ChangeUserPasswordSerializer', 'ResetOTPSerializer', 'UserProfileSerializer', 'UserOrgSerializer', 'UserUpdatePasswordSerializer', 'UserUpdatePublicKeySerializer', - 'UserRetrieveSerializer' + 'UserRetrieveSerializer', 'MiniUserSerializer', ] From f6a4253936e92345eb7af9598c8a91d12da7cf0d Mon Sep 17 00:00:00 2001 From: xinwen Date: Tue, 4 Aug 2020 16:13:16 +0800 Subject: [PATCH 24/40] =?UTF-8?q?feat(ticket):=20=E5=B7=A5=E5=8D=95?= =?UTF-8?q?=E5=85=B3=E9=97=AD=E7=94=9F=E6=88=90=20Comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/mixins.py | 8 +- apps/authentication/models.py | 2 +- apps/common/db/models.py | 2 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 56117 -> 56305 bytes apps/locale/zh/LC_MESSAGES/django.po | 108 ++++++++++-------- apps/tickets/api/request_asset_perm.py | 32 +++--- apps/tickets/exceptions.py | 15 ++- .../migrations/0003_auto_20200804_1551.py | 18 +++ apps/tickets/models/ticket.py | 66 +++++------ .../tickets/serializers/request_asset_perm.py | 6 +- apps/tickets/serializers/ticket.py | 46 +++++--- apps/tickets/utils.py | 2 +- 12 files changed, 180 insertions(+), 125 deletions(-) create mode 100644 apps/tickets/migrations/0003_auto_20200804_1551.py diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index cdc7856bd..a450f610a 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -139,7 +139,7 @@ class AuthMixin: def get_ticket_or_create(self, confirm_setting): ticket = self.get_ticket() - if not ticket or ticket.status == ticket.STATUS_CLOSED: + if not ticket or ticket.status == ticket.STATUS.CLOSED: ticket = confirm_setting.create_confirm_ticket(self.request) self.request.session['auth_ticket_id'] = str(ticket.id) return ticket @@ -148,12 +148,12 @@ class AuthMixin: ticket = self.get_ticket() if not ticket: raise errors.LoginConfirmOtherError('', "Not found") - if ticket.status == ticket.STATUS_OPEN: + if ticket.status == ticket.STATUS.OPEN: raise errors.LoginConfirmWaitError(ticket.id) - elif ticket.action == ticket.ACTION_APPROVE: + elif ticket.action == ticket.ACTION.APPROVE: self.request.session["auth_confirm"] = "1" return - elif ticket.action == ticket.ACTION_REJECT: + elif ticket.action == ticket.ACTION.REJECT: raise errors.LoginConfirmOtherError( ticket.id, ticket.get_action_display() ) diff --git a/apps/authentication/models.py b/apps/authentication/models.py index 27a9d7857..208d9c9fb 100644 --- a/apps/authentication/models.py +++ b/apps/authentication/models.py @@ -71,7 +71,7 @@ class LoginConfirmSetting(CommonModelMixin): reviewer = self.reviewers.all() ticket = Ticket.objects.create( user=self.user, title=title, body=body, - type=Ticket.TYPE_LOGIN_CONFIRM, + type=Ticket.TYPE.LOGIN_CONFIRM, ) ticket.assignees.set(reviewer) return ticket diff --git a/apps/common/db/models.py b/apps/common/db/models.py index b807b9dd5..502df31e9 100644 --- a/apps/common/db/models.py +++ b/apps/common/db/models.py @@ -52,7 +52,7 @@ class ChoiceSetType(type): return self._choices_dict.__getitem__(item) def get(self, item, default=None): - return self._choices_dict.get(item, default=None) + return self._choices_dict.get(item, default) @property def choices(self): diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index e779e41643d2ea599b69b258531bf543208f7062..cb2695eeceba7079ef663998880c7098de03d2f7 100644 GIT binary patch delta 16926 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�}uY4P8v1tmV`&QE1#H4C5?QU)7irE~1R64R|= zE`|{=ws@1pJ5d9lM7{SHQP0eEGyJ@pup#P#+M>qkg{g74#WPUj#G?AG_gP}UC61X_ zP&@Dt^^m%F4Gpvf<1$W`TYE<+=QwKFr z160SB)}fo(%N&3`X&-``=pJg~1Q*>nX;9;&M~#ym>tGSoP7lGNI1+Q|{ohDMS9S@P z;A_-3o`sj(z&la#NsAw#26~I>67Z>a*-f13ikmpX%!gW7akB>cFATNy-O&H<|AVYy zxIe)!9;h9ejkrV z@fBQV^_H~Hqp#-oAJqwdrs)6ah(ChZ25p%RKAZf<3sZ#AKmKUXc@qgHU}a1Gj#Vi) z>34)u+4>)$&)1Y237G$DOC~4rht&(?MB@J*kF8C)VYIc!!jwSjx;?KcXDBVLUtjXy zP~U+`E!TMKBjfFCLct68RrsL zqn;L%QGTN6$Uym(cKzE@X4+5TNYvrK?eUcvBzczn{|nA6>IvyMilWcx_=C?GFO=JH zo%WryeL;K(=V4BAGp%DLa^;94C<~~M#inZ8}{Ps(8b z`$we^olf9DN(|*JokDC-La&7lsIOSRlg~rk7dxwwV-Imh>yw9kb8-R1J22~i#`%=^ zp~XMZ-bwGYexTf@tfc(+C_xZKr}cKiC)j{ej2!>#l(&=ivy|(UW0XPU@><_T)caF1 zQglplcpI&KGVvVhzG3|F1En-2F-1pdhyTyo{F#{htYfs}-#@$B;7WWzDPr+#>J=Dk zff_g-Q7=H-EgSzf>Z3cK?tdXVJf{?=gZ{^`WH!iC>XDTClp`92V*_;^`uvZjS?Ku zpgxk4hZ01b3y0A5IVJu`W%bDT1lA>YiT(p9*(u2=3(2LVEje}n2f-=)iQoqtpoF;| zhf;b``q8HkMaOt+FG9VW2H{A8A7d>{Nqaf!r|>)LTN*#4EFzznqT?%y?m-P9T@(Z~e*at^ax>8?EDMcwr(Q)75-M4dB;`j8;NhwMFw6zbzatZi{ z@H8~&m`39pEQ256LGo*<@3D6E(ZA<^GktwjYTx$?ISx9!E%XebUI%r&rL6MTc!%&J zXTGF-X1SI)n6i}8g#08sPtEHoLsa3YLz|ANSQ1xbf&gyY`<$?ulAc66N#&#>>*bCW+nxkSlAz8AS&cn0rN%2RYi zQ?Bb{YZo2=dsHHxMSdUUB=w#&=CwYrsBb4d^dvT zIP^bxUm*(y&^Us66y>?)5?~}JG{#hx3uY&l#E*)fS-u*+w#hGB`xwRKKZkqP&zk(d z*d5llnuBzDNy$NnJ+wBWG@(?XTp;%ar7i8}De*@&t6U*BH6hP&I$wIKBPickw|<;< znEHNgt^d}=4p!BENcp@hZv#XsrOQ2z_`ZaP#YQI_%xr5N!tERJg^4=AH3?>OOoN)d{V zRrJv@z+C7;uN3XA$Zf?Glz*syfX8t%WfT3X;3I#3?!iPF$C5}uxk^3$$VS}M;@Z@= zQgj@}#*{e9L&{ci6)1Nodx>?NP3NXbFn=h}S!zX|-90S{C9 zk&K|je9AcLFENagozjZ(kvqxjfo+KE;|tEy@s~LU_meM5$w2vva-Li_)G@*OxoG-= zN+G-liG0I}@6x3>%?ofmuE5`L4P`MUD}4fZj$ctaBAGs2g_#>M7K7vKq8$04O$}Q^YDY>b4 zp}bH1E$81u9ffU7)dOfhPMJ?>9sjF9FddH4m{C1AuF$9>JNenv%TOOn$wGY{xk+lY zV=?hpR^y`ikehj?Zu^wy5cOL-8kU9pHKGYkA%o%A0F@UScZGn^?#59#Fns z*YhUf5A{6n15B##d6)1S9;e-|2A&rY;Q72s4L$D^6^j~sURLb)n&$=KI1It5m=PCX zHe8AMagTWo^HC0H;(0l-0J46sDwf8Y7>5H<{gz-X9>BcJ@7*U;lt6G(&ntlyu`qVS z^Ed^6#IDUa8LZu$)54Y*j7es1^F7pp$6+|m!U$Y}X>lj2-vQM#zjum^c6`$s+_UnZ zs1taOnmAnx&&!TEPy>`g?wVH((_u~21PPcKo1-S~j9Tyzb2Mt)Y3Pe4^D!C!kzilS z_pu^&Xz3cxK~1y_i{MI(!Bdz9pP~kQi8{gHR-TsytDqLv*ldkDnIzN(dbQ&Gwc;TJ zbPqqka9oTU@C($v+>e^*u*Hv~2D*fW@HT3P!L8lB&W`CR*FjCx1l6uR>SRWuPJC8t z&R-St323EzFe@HL4R8gs;cet$_gfzm5dXjrQ4@^s zM?{{eU{5%l^2J_mYen{%#ddu^i>VZtln{p$2S+ zI>PR#1-^@VHYT7Ru6d{lHluFgPSk6A5_Jo&q59uLwR?s+ncqwGmV4TBVQ$K0P%CVL zdP>`&I(9`pYy&Y6M_GJ4s^1(``%PBA3pLI@jKbrnjsAw(&=d5fArsi$?IZ)LA`$~I z#w?2ID951|QUSHY+8Bh1m=}|94Ze@MC9yr+PV1RXQ1i9HlGvdK_g^cVN+2E1N9}Ma zrpJw_hiWIP{)BnXyoRyF@1Tx4yr(nqVvz z!)d7a4%AT}LQU{3rp8OCfv%!Zp}B#7x)gi{t+)u5#W<{tZ=x3VF>0b^sH6T2wexKj{|YtX5sP0k zZ=x1-AN6`Z@6Y*bC0Pf!BP@!VpeAYu^-%-2Ks|KrQ4d`|a{@+FUW}RXuz4P%DBnTt zIKx2qyI@YtLb<-#b|B}ko%bf7*JZdl5B00o7pR?{KuvJgyp6h5PtBL8g$53CuVDyk zhq*8eD_FS!7NXn*^~_E5kZB4Z z-ofmG8m|}Xh=-z1HW{_BeW>vdV}#!S(_~5$_!-qP#}IdfF<6{(b=0?6FN;q^-Rs4u zTd~^ex1knz#=MN0=mzSj@1y^jLEYMym|5?C>i1m72-FehLUk-+mO)Ka6*X}J>I7P# z7Tnp&15gVXWllsL`3$RHia9B7LM`YR`ihb{M@Bmg80r=fg<4@8=D;#o8ylkDk{OsC z*P<475OwdrLoMhcM&cdR$p#K{3$B2wuZ!A9t6`kKI=(?bJMV@n53~luP)9ZfwV+9; zXJk5RrwgpS6t&Rx=>HtRSjy+I0tO6sJFbN4R~t26!{MC20!^)=4f+>^8nBy{eOQF@ zcvQy?7>-*|C-4pGmYhTNzisi~Q4i@0)QMys;cjg))V$?=mZ^%`*{i6AO;9J&6E)#r zD-TB<-9*$gGRw*zTm5p>z1@U5x!tG*9zgXwjWKu;qtF-dzRMIu?YK7P#txVVhoX*j z9+t<|s9#jBqIMj>3F>6RP~ZDiP!n{(4mcKj;0>&WjX&T|&Nu_}>HSYXlFxMlu^5fx zP!q4iQg{^k5cK}RV%UF_d-xXOddgdH4R#srp6<{w?gV2|3#pA7w>MVAWUPcoF|FSJ zG-KTng_yZ8D-B{%_p$~CVm;J7Z-iQKM@)meEq(xXg6B|g#~+vt(~WbtG9T&|7e|d3 zkN)?+npM1pny@YE-giRXK1_qxum=8& z>K8rP#Y>`2tUPKVRWU6#@{!3%rX^}c9kCbo#6ox$^#SrX>L@c!anDA$8HLq}$DthSyOmec&p*@EM!{3n+|@i6_id zYd(mu3+9-`!y3S!_wf|vPiFJSEZX;-!-Gfp@Q0qq_nNnNF5ie)X&$dC?!vva8#13qy(&V;Pk%Z@s!ShEJ^quk2s-$9+wcr1!Dung|R zJop<%WAGwQP&+D1CJJ9eHRy|Va4H_b?{PDJyx4u9G+p98u--D?LEVDUR-TQ)l$V;H zo4ZiY$TwC#g+4XBVikAI2dI1X6jdLv)ESEUgv)K^@~8o8S-F9gTbmusE>_>m9D*8u z>{9Nij%Yf8BDe$F;tkYOTz#2qI0QA(D2&00=0=NOKrQqd=EeJ{hcNh47mqUI%*tlH zPknCSmITzHo!J*dD33->Jky+qp_CV)cJMjo$1hO}xP;osFILX7+{N>v7FNv431%Cg zRdhiOI2tvBK+ZKOh{%r=VbnVh(5bd*}7M#c8`OOljPtq#Ld_J!Q8Fd(7 z4c4|OYFVj)bo+KnG)mP5ToRV4F!Ev=%DISA7eABLJ} zti@+pc|K+!zQW=gF`DvrtG|j`@UN%^{$ugbHEz7zW-R)&^9mNIhAJnZIySR-7qdTV zKQtV8s{;l$A7FG^qDJ%pz5=K#{Jh0^AeE7Fb3nX9(J^bt56I2 z95vv6RQqG7g){?~CW=dVBl0S(j=wZkr`9gRS(G#P{O zBXgzsg}Ki>fm-M#^EZp9TIa^igu&FunB{zysfn8CH7mEWawiNS-UGG3!KeYoo3qU& zs1Ks`<|&M${17#6)O!A#AI6}{`%vTfj#%c5Ra{3+bjRY)%%IQhHzd?Yb4C0J`=bU7 z-rx*J4IF94U?a-qtUMBRLesF6-v1>Q_|65qTNpxvY#ZHxg-|;yhWbEhZ1HAhB5J}U z)UE7|dhOoD;cEgb5g#G znjnzhX7mteL@lfkYTSxuZB)Bv=+iG8iDdNU@h<9!W}-SA!3=oD%GWTI@_p0*FDxFo z#Z8z66)$R*MvW7X`LGV^M7pBJ>AQvVR|g+~%s9>(%*6o8OUzF(4ds=X3D==^whPt& zBw3R?ctbBB+&@w|EV+E*2%; z7`31wm<}gn1kOe+a5d_o-hdi^k9pK*ne(UtZlRv`rxuUg?gofK)t5lM1vN1o>!5c2 zI!56@%!ad36Mcs2w;k2)sKswt-1pD|X})wHiD9UnmPQ?671RLDEZ)U@8@00`sEK^$ z6jb~9sC&HD;=8a2)h0c?D|39<99`iJ6hokW>f3Jv`m~ahWHiuq^N#rks{RS;$TRG5Fzqh8~6sD39=@yk|z zjOriY+vggDp$5ulRzY=WgIZ8O)IfvHG3HFG|HR76%}uC>cdwPtpeDSI`SBsD-WRdo zeUKDIed$z3P1FrF@j!DZ>WD|0lg&A(_KQ&M*P@=4&8U+)i0XF^)$S+M>;9XI`@AP) z@)AgOz%_`%M9Ohk9LHe++-&t{QSEP_1`asrCJr{kQ9F&aa#6E1Y60<9u7m0I{x`CU z)@En3AL@ukpmvgs+TjYTUuW(>E#$D(pSJjURKHuOTl>`NBM!N7bD{t5|4Nh5CsR$- z3WuONjzzuK^H9&i0o1*_j9SP`RQohvy8**c@ginP)WRyD+Pz{nw)(c{fB)aKib3W` za~kSfYXRy=SEB~}(mY@uM|~Nc!yvq8K15CY%nUm0HWrR*7k!xX*UsVy$hxQwN!FmZ z#Rp<{;!~{r*vxdqEhq*xU};qUc=UfeVj;?}Tl{@Z-%`6^bz=;J&W*b&>{eDweQ{{u35{X$Q;Bd&`&!uA-2 zy)h1_qVDNV)IzV|Ec^=x;>45g7Q8SspK`y76*MbjecCm{I`|Q$=lS!lk#8d|xP*~#pS+SzbyfupT_&ioO>h~KjE)9*Qd1^y+Vfx~`q?|W|4 z!&1O(hg#@t)XtWm`fbE;JZR%nlr8(YUV?YSJFpD4_8&x!`0I&rkL|l z1Fk?#xB+!S2e2w$#yptwtQ)W*zDl_^>LezhK3}Gz#z{s^ywK`>Yb>+T+>X6zuopE^ z@pEqCI;epXQ3JI{4b%-A;y~1dd$AOLjfL?EYGZlM^Qnh5P~V0(kPY$szXifBxQR-j z2C9kKQt`^5CQiEOChlzZL7m88a}xS@hC2E+sD#c$qvRzuvL|n-k&q2~vk~&@IDWA0bzxcJj zA#~}R;U$%m2=*oaIf+-p`-k!;q;%AMPFgA{*Ty^;hihoZZ=e3hyF0!?{08c(NWLQ} zHSMxuMVv%@75VDqeZNsiOXeT7<{IJf|13v*FAXP8lHx4Ilpe_^XP4fEo zs6wAA_zBL%ukl^{fRsf3I_Y=XFQdH3fBs~akaT@YMHP~+wxns6S2-7{HtBr^NVyh~ zS!r!X5-&_zV&zRZlKN4kzbvMHO=*(}+hZTy|662Es>ZJEl$+C`CaEW}h1NOQe2#pf zc)hW{#b#hyQf1E%bj>DKRG$#KhLH64o7r#io%z9JnW)gUz|z5?GR1yY}KZ6uSQ*cc1=Mp)%sj3Hg6 z@-co$xZA zMOvIn=g+l|%I&0cG|q>8@d`;-Ac_B#%zK@bi}anvlkpzq{#L&2Z_S@5QU42?u8q`( zlW&TDA%C#&I#`{)mzVkfeG8S5Bz@26s^IVjn;Ge_k<^GbYe-|rXCe(F=_*gG6KNPJ zJ!y~&d%@)U(dGrIAo+T@mvoo(F=bt=b^mLSLJ1T?U0WUh_j4EVS(Fm-FHHH&=t`fb z)~Xe@u|X9|MShRQBsC!YYVD#K<0Hzta-gpzKaSeOZ(&BtpHX=f6Da?U_x;V?kIa;H zO`%R-z^O2h*a6ZXq#&zTn;*!ZBki;pzleGvw10wnj;E#E|Mw}x&`7`Y44{Fo^t32X z{yXdJ4_e)y6xLf_@izWS_v0+_{A4qbx|5%0by+c<@?v~Uy34q4>;At*W(tX4^t~3i z4$F{olh#tMN6JqAC8-GN?TrkJw6V zVQnvwPq}<&Xqc6XQrMQ1jr5STgjfd*#i`b@ALRn{EM&bG;J>7&)W1rduH4A4#s2Ft z`O=o}MVm2{r&E5#+P@v5YUASL-DyuX-^MfAh8 z7JJ9qUo_9t{u@#wt6xC;ZL718SB!FJ(tORoj?54`lqWxt{29D~D@bFB=b(Iue9Bdr zShfF@M^iDG*eFs>@<*t94d+l^MSdzavCgSk{AS9!?va}1;0tPS<79EXtio9sCjZ<0h0Og^+Gi7ldg@ztZnK_9lgqkHYb!`Lr3TPw27) zHxo#?ej)P{vDTz=7JQe+<4KXE;iP2Z^)cnTN4^$?->^D7+(h_Y)o?WhP~(-5TsoYDm&` zh*X301*sSHTS&gOR!PjOLm=hqL3}%fZg|_`6Ue9gPaCy;X8l`XJJPEv^XT?~oBwH> zhIme5eXub;q3&mWtDK_n4e34=|BxP8raz&@SZ~O#ZT!2Vq{ze};qT z6U1Vo$d4jbrMwNp=)BxNfZl4Z{RDN*G?l+V;{d&c#D1~(Kvn$b8bE$I?JiLN(SPEV ziRrq81F5@iq0VRZEr=bk{twM-)*gIb2n~OvVgVK>asprA4SWY1k@$KYeTY`#0r_(kj!~fUPM9SbmmiO}tO(`~m4b r0y9Zh$?N*j!OzXrYnCmwc~<{l!#B?#8(w+yiM{)?Y+iA?VTS(!Gk54a diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 92d93b15f..fb3ab714f 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-07-31 19:20+0800\n" +"POT-Creation-Date: 2020-08-04 15:33+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -47,7 +47,7 @@ msgid "Name" msgstr "名称" #: applications/models/database_app.py:22 assets/models/cmd_filter.py:52 -#: terminal/models.py:376 terminal/models.py:413 tickets/models/ticket.py:46 +#: terminal/models.py:376 terminal/models.py:413 tickets/models/ticket.py:40 #: users/templates/users/user_granted_database_app.html:35 msgid "Type" msgstr "类型" @@ -131,8 +131,8 @@ msgstr "参数" #: applications/models/remote_app.py:39 assets/models/asset.py:224 #: assets/models/base.py:240 assets/models/cluster.py:28 #: assets/models/cmd_filter.py:26 assets/models/cmd_filter.py:60 -#: assets/models/group.py:21 common/db/models.py:66 common/mixins/models.py:49 -#: orgs/models.py:23 orgs/models.py:316 perms/models/base.py:54 +#: assets/models/group.py:21 common/db/models.py:67 common/mixins/models.py:49 +#: orgs/models.py:23 orgs/models.py:326 perms/models/base.py:54 #: users/models/user.py:530 users/serializers/group.py:35 #: users/templates/users/user_detail.html:97 #: xpack/plugins/change_auth_plan/models.py:81 xpack/plugins/cloud/models.py:56 @@ -145,9 +145,9 @@ msgstr "创建者" #: applications/models/remote_app.py:42 assets/models/asset.py:225 #: assets/models/base.py:238 assets/models/cluster.py:26 #: assets/models/domain.py:23 assets/models/gathered_user.py:19 -#: assets/models/group.py:22 assets/models/label.py:25 common/db/models.py:68 +#: assets/models/group.py:22 assets/models/label.py:25 common/db/models.py:69 #: common/mixins/models.py:50 ops/models/adhoc.py:38 ops/models/command.py:27 -#: orgs/models.py:24 orgs/models.py:314 perms/models/base.py:55 +#: orgs/models.py:24 orgs/models.py:324 perms/models/base.py:55 #: users/models/group.py:18 users/templates/users/user_group_detail.html:58 #: xpack/plugins/cloud/models.py:59 xpack/plugins/cloud/models.py:149 msgid "Date created" @@ -190,7 +190,7 @@ msgstr "基础" msgid "Charset" msgstr "编码" -#: assets/models/asset.py:148 tickets/models/ticket.py:41 +#: assets/models/asset.py:148 tickets/models/ticket.py:35 msgid "Meta" msgstr "元数据" @@ -380,8 +380,8 @@ msgid "SSH public key" msgstr "SSH公钥" #: assets/models/base.py:239 assets/models/gathered_user.py:20 -#: common/db/models.py:69 common/mixins/models.py:51 ops/models/adhoc.py:39 -#: orgs/models.py:315 +#: common/db/models.py:70 common/mixins/models.py:51 ops/models/adhoc.py:39 +#: orgs/models.py:325 msgid "Date updated" msgstr "更新日期" @@ -488,7 +488,7 @@ msgstr "每行一个命令" #: authentication/templates/authentication/_access_key_modal.html:34 #: perms/forms/asset_permission.py:20 #: tickets/serializers/request_asset_perm.py:60 -#: tickets/serializers/ticket.py:26 +#: tickets/serializers/ticket.py:30 #: users/templates/users/_granted_assets.html:29 #: users/templates/users/user_asset_permission.html:44 #: users/templates/users/user_asset_permission.html:79 @@ -537,14 +537,14 @@ msgstr "默认资产组" #: assets/models/label.py:15 audits/models.py:36 audits/models.py:56 #: audits/models.py:69 audits/serializers.py:77 authentication/models.py:46 -#: authentication/models.py:90 orgs/models.py:16 orgs/models.py:312 +#: authentication/models.py:90 orgs/models.py:16 orgs/models.py:322 #: perms/forms/asset_permission.py:83 perms/forms/database_app_permission.py:38 #: perms/forms/remote_app_permission.py:40 perms/models/base.py:49 #: templates/index.html:78 terminal/backends/command/models.py:18 #: terminal/backends/command/serializers.py:12 terminal/models.py:185 -#: tickets/models/ticket.py:36 tickets/models/ticket.py:135 +#: tickets/models/ticket.py:30 tickets/models/ticket.py:137 #: tickets/serializers/request_asset_perm.py:61 -#: tickets/serializers/ticket.py:27 users/forms/group.py:15 +#: tickets/serializers/ticket.py:31 users/forms/group.py:15 #: users/models/user.py:157 users/models/user.py:643 #: users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 @@ -692,7 +692,7 @@ msgstr "协议重复: {}" msgid "Hardware info" msgstr "硬件信息" -#: assets/serializers/asset.py:112 orgs/mixins/serializers.py:27 +#: assets/serializers/asset.py:112 orgs/mixins/serializers.py:26 msgid "Org name" msgstr "组织名称" @@ -1014,7 +1014,7 @@ msgid "Reason" msgstr "原因" #: audits/models.py:106 tickets/serializers/request_asset_perm.py:59 -#: tickets/serializers/ticket.py:25 xpack/plugins/cloud/models.py:211 +#: tickets/serializers/ticket.py:29 xpack/plugins/cloud/models.py:211 #: xpack/plugins/cloud/models.py:269 msgid "Status" msgstr "状态" @@ -1199,7 +1199,7 @@ msgstr "SSH密钥" msgid "Reviewers" msgstr "审批人" -#: authentication/models.py:56 tickets/models/ticket.py:27 +#: authentication/models.py:56 tickets/models/ticket.py:23 #: users/templates/users/user_detail.html:250 msgid "Login confirm" msgstr "登录复核" @@ -1263,7 +1263,7 @@ msgstr "删除成功" #: authentication/templates/authentication/_access_key_modal.html:155 #: authentication/templates/authentication/_mfa_confirm_modal.html:53 -#: templates/_modal.html:22 tickets/models/ticket.py:73 +#: templates/_modal.html:22 tickets/models/ticket.py:67 msgid "Close" msgstr "关闭" @@ -1393,7 +1393,7 @@ msgstr "%(name)s 创建成功" msgid "%(name)s was updated successfully" msgstr "%(name)s 更新成功" -#: common/db/models.py:67 +#: common/db/models.py:68 msgid "Updated by" msgstr "更新人" @@ -1649,16 +1649,16 @@ msgstr "更新任务内容: {}" msgid "Disk used more than 80%: {} => {}" msgstr "磁盘使用率超过 80%: {} => {}" -#: orgs/api.py:54 +#: orgs/api.py:58 msgid "Organization contains undeleted resources" msgstr "" -#: orgs/api.py:58 +#: orgs/api.py:62 msgid "The current organization cannot be deleted" msgstr "" -#: orgs/mixins/models.py:56 orgs/mixins/serializers.py:26 orgs/models.py:40 -#: orgs/models.py:311 +#: orgs/mixins/models.py:56 orgs/mixins/serializers.py:25 orgs/models.py:40 +#: orgs/models.py:321 msgid "Organization" msgstr "组织" @@ -1670,7 +1670,7 @@ msgstr "组织管理员" msgid "Organization auditor" msgstr "组织审计员" -#: orgs/models.py:313 users/forms/user.py:27 users/models/user.py:499 +#: orgs/models.py:323 users/forms/user.py:27 users/models/user.py:499 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:73 #: users/templates/users/user_list.html:16 @@ -2506,100 +2506,108 @@ msgstr "结束日期" msgid "Args" msgstr "参数" -#: tickets/api/request_asset_perm.py:42 -msgid "Ticket closed" -msgstr "工单已关闭" - -#: tickets/api/request_asset_perm.py:45 +#: tickets/api/request_asset_perm.py:44 #, python-format msgid "Ticket has %s" msgstr "工单已%s" -#: tickets/api/request_asset_perm.py:90 +#: tickets/api/request_asset_perm.py:89 msgid "Confirm assets first" msgstr "请先确认资产" -#: tickets/api/request_asset_perm.py:93 +#: tickets/api/request_asset_perm.py:92 msgid "Confirmed assets changed" msgstr "确认的资产变更了" -#: tickets/api/request_asset_perm.py:97 +#: tickets/api/request_asset_perm.py:96 msgid "Confirm system-user first" msgstr "请先确认系统用户" -#: tickets/api/request_asset_perm.py:101 +#: tickets/api/request_asset_perm.py:100 msgid "Confirmed system-user changed" msgstr "确认的系统用户变更了" -#: tickets/api/request_asset_perm.py:104 xpack/plugins/cloud/models.py:202 +#: tickets/api/request_asset_perm.py:103 xpack/plugins/cloud/models.py:202 msgid "Succeed" msgstr "成功" -#: tickets/api/request_asset_perm.py:111 +#: tickets/api/request_asset_perm.py:110 msgid "From request ticket: {} {}" msgstr "来自工单申请: {} {}" -#: tickets/api/request_asset_perm.py:113 +#: tickets/api/request_asset_perm.py:112 msgid "{} request assets, approved by {}" msgstr "{} 申请资产,通过人 {}" -#: tickets/models/ticket.py:19 tickets/models/ticket.py:75 +#: tickets/exceptions.py:23 +msgid "Ticket closed" +msgstr "工单已关闭" + +#: tickets/exceptions.py:32 +msgid "Only assignee can operate ticket" +msgstr "只有审批人可以操作工单" + +#: tickets/exceptions.py:37 +msgid "Ticket can not be operated" +msgstr "不能操作该工单" + +#: tickets/models/ticket.py:18 tickets/models/ticket.py:69 msgid "Open" msgstr "开启" -#: tickets/models/ticket.py:20 +#: tickets/models/ticket.py:19 msgid "Closed" msgstr "关闭" -#: tickets/models/ticket.py:26 +#: tickets/models/ticket.py:22 msgid "General" msgstr "一般" -#: tickets/models/ticket.py:28 +#: tickets/models/ticket.py:24 msgid "Request asset permission" msgstr "申请资产权限" -#: tickets/models/ticket.py:33 +#: tickets/models/ticket.py:27 msgid "Approve" msgstr "同意" -#: tickets/models/ticket.py:34 +#: tickets/models/ticket.py:28 msgid "Reject" msgstr "拒绝" -#: tickets/models/ticket.py:37 tickets/models/ticket.py:136 +#: tickets/models/ticket.py:31 tickets/models/ticket.py:138 msgid "User display name" msgstr "用户显示名称" -#: tickets/models/ticket.py:39 +#: tickets/models/ticket.py:33 msgid "Title" msgstr "标题" -#: tickets/models/ticket.py:40 tickets/models/ticket.py:137 +#: tickets/models/ticket.py:34 tickets/models/ticket.py:139 msgid "Body" msgstr "内容" -#: tickets/models/ticket.py:42 +#: tickets/models/ticket.py:36 msgid "Assignee" msgstr "处理人" -#: tickets/models/ticket.py:43 +#: tickets/models/ticket.py:37 msgid "Assignee display name" msgstr "处理人名称" -#: tickets/models/ticket.py:44 +#: tickets/models/ticket.py:38 msgid "Assignees" msgstr "待处理人" -#: tickets/models/ticket.py:45 +#: tickets/models/ticket.py:39 msgid "Assignees display name" msgstr "待处理人名称" -#: tickets/models/ticket.py:76 +#: tickets/models/ticket.py:70 msgid "{} {} this ticket" msgstr "{} {} 这个工单" -#: tickets/models/ticket.py:87 +#: tickets/models/ticket.py:85 msgid "this ticket" msgstr "这个工单" diff --git a/apps/tickets/api/request_asset_perm.py b/apps/tickets/api/request_asset_perm.py index 40f908838..c12ea3070 100644 --- a/apps/tickets/api/request_asset_perm.py +++ b/apps/tickets/api/request_asset_perm.py @@ -1,4 +1,3 @@ -from django.db.transaction import atomic from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from rest_framework.decorators import action @@ -26,7 +25,7 @@ from ..permissions import IsAssignee class RequestAssetPermTicketViewSet(JMSModelViewSet): - queryset = Ticket.origin_objects.filter(type=Ticket.TYPE_REQUEST_ASSET_PERM) + queryset = Ticket.origin_objects.filter(type=Ticket.TYPE.REQUEST_ASSET_PERM) serializer_classes = { 'default': serializers.RequestAssetPermTicketSerializer, 'approve': EmptySerializer, @@ -38,10 +37,10 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet): search_fields = ['user_display', 'title'] def _check_can_set_action(self, instance, action): - if instance.status == instance.STATUS_CLOSED: - raise TicketClosed(detail=_('Ticket closed')) + if instance.status == instance.STATUS.CLOSED: + raise TicketClosed if instance.action == action: - action_display = dict(instance.ACTION_CHOICES).get(action) + action_display = instance.ACTION.get(action) raise TicketActionAlready(detail=_('Ticket has %s') % action_display) @action(detail=False, methods=[GET], permission_classes=[IsValidUser]) @@ -72,7 +71,7 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet): @action(detail=True, methods=[POST], permission_classes=[IsAssignee, IsValidUser]) def reject(self, request, *args, **kwargs): instance = self.get_object() - action = instance.ACTION_REJECT + action = instance.ACTION.REJECT self._check_can_set_action(instance, action) instance.perform_action(action, request.user, self._get_extra_comment(instance)) return Response() @@ -80,7 +79,7 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet): @action(detail=True, methods=[POST], permission_classes=[IsAssignee, IsValidUser]) def approve(self, request, *args, **kwargs): instance = self.get_object() - action = instance.ACTION_APPROVE + action = instance.ACTION.APPROVE self._check_can_set_action(instance, action) meta = instance.meta @@ -100,10 +99,10 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet): if system_user is None: raise ConfirmedSystemUserChanged(detail=_('Confirmed system-user changed')) - self._create_asset_permission(instance, assets, system_user, request.user) + self._create_asset_permission(instance, assets, system_user) return Response({'detail': _('Succeed')}) - def _create_asset_permission(self, instance: Ticket, assets, system_user, user): + def _create_asset_permission(self, instance: Ticket, assets, system_user): meta = instance.meta request = self.request @@ -120,13 +119,12 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet): if date_expired: ap_kwargs['date_expired'] = date_expired - with atomic(): - instance.perform_action(instance.ACTION_APPROVE, - request.user, - self._get_extra_comment(instance)) - ap = AssetPermission.objects.create(**ap_kwargs) - ap.system_users.add(system_user) - ap.assets.add(*assets) - ap.users.add(user) + instance.perform_action(instance.ACTION.APPROVE, + request.user, + self._get_extra_comment(instance)) + ap = AssetPermission.objects.create(**ap_kwargs) + ap.system_users.add(system_user) + ap.assets.add(*assets) + ap.users.add(instance.user) return ap diff --git a/apps/tickets/exceptions.py b/apps/tickets/exceptions.py index 3332139b5..5e5dedd21 100644 --- a/apps/tickets/exceptions.py +++ b/apps/tickets/exceptions.py @@ -1,3 +1,5 @@ +from django.utils.translation import gettext_lazy as _ + from common.exceptions import JMSException @@ -18,12 +20,19 @@ class ConfirmedSystemUserChanged(JMSException): class TicketClosed(JMSException): - pass + default_detail = _('Ticket closed') + default_code = 'ticket_closed' class TicketActionAlready(JMSException): pass -class OrgIdRequiredException(JMSException): - pass +class OnlyTicketAssigneeCanOperate(JMSException): + default_detail = _('Only assignee can operate ticket') + default_code = 'can_not_operate' + + +class TicketCanNotOperate(JMSException): + default_detail = _('Ticket can not be operated') + default_code = 'ticket_can_not_be_operated' diff --git a/apps/tickets/migrations/0003_auto_20200804_1551.py b/apps/tickets/migrations/0003_auto_20200804_1551.py new file mode 100644 index 000000000..936dbc5bb --- /dev/null +++ b/apps/tickets/migrations/0003_auto_20200804_1551.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2020-08-04 07:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0002_auto_20200728_1146'), + ] + + operations = [ + migrations.AlterField( + model_name='ticket', + name='assignee_display', + field=models.CharField(blank=True, default='', max_length=128, null=True, verbose_name='Assignee display name'), + ), + ] diff --git a/apps/tickets/models/ticket.py b/apps/tickets/models/ticket.py index 4f68aa6e0..3e979f244 100644 --- a/apps/tickets/models/ticket.py +++ b/apps/tickets/models/ticket.py @@ -5,6 +5,7 @@ from django.db import models from django.db.models import Q from django.utils.translation import ugettext_lazy as _ +from common.db.models import ChoiceSet from common.mixins.models import CommonModelMixin from common.fields.model import JsonDictTextField from orgs.mixins.models import OrgModelMixin @@ -13,26 +14,19 @@ __all__ = ['Ticket', 'Comment'] class Ticket(OrgModelMixin, CommonModelMixin): - STATUS_OPEN = 'open' - STATUS_CLOSED = 'closed' - STATUS_CHOICES = ( - (STATUS_OPEN, _("Open")), - (STATUS_CLOSED, _("Closed")) - ) - TYPE_GENERAL = 'general' - TYPE_LOGIN_CONFIRM = 'login_confirm' - TYPE_REQUEST_ASSET_PERM = 'request_asset' - TYPE_CHOICES = ( - (TYPE_GENERAL, _("General")), - (TYPE_LOGIN_CONFIRM, _("Login confirm")), - (TYPE_REQUEST_ASSET_PERM, _('Request asset permission')) - ) - ACTION_APPROVE = 'approve' - ACTION_REJECT = 'reject' - ACTION_CHOICES = ( - (ACTION_APPROVE, _('Approve')), - (ACTION_REJECT, _('Reject')), - ) + class STATUS(ChoiceSet): + OPEN = 'open', _("Open") + CLOSED = 'closed', _("Closed") + + class TYPE(ChoiceSet): + GENERAL = 'general', _("General") + LOGIN_CONFIRM = 'login_confirm', _("Login confirm") + REQUEST_ASSET_PERM = 'request_asset', _('Request asset permission') + + class ACTION(ChoiceSet): + APPROVE = 'approve', _('Approve') + REJECT = 'reject', _('Reject') + user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='%(class)s_requested', verbose_name=_("User")) user_display = models.CharField(max_length=128, verbose_name=_("User display name")) @@ -40,12 +34,12 @@ class Ticket(OrgModelMixin, CommonModelMixin): body = models.TextField(verbose_name=_("Body")) meta = JsonDictTextField(verbose_name=_("Meta"), default='{}') assignee = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='%(class)s_handled', verbose_name=_("Assignee")) - assignee_display = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Assignee display name")) + assignee_display = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Assignee display name"), default='') assignees = models.ManyToManyField('users.User', related_name='%(class)s_assigned', verbose_name=_("Assignees")) assignees_display = models.CharField(max_length=128, verbose_name=_("Assignees display name"), blank=True) - type = models.CharField(max_length=16, choices=TYPE_CHOICES, default=TYPE_GENERAL, verbose_name=_("Type")) - status = models.CharField(choices=STATUS_CHOICES, max_length=16, default='open') - action = models.CharField(choices=ACTION_CHOICES, max_length=16, default='', blank=True) + type = models.CharField(max_length=16, choices=TYPE.choices, default=TYPE.GENERAL, verbose_name=_("Type")) + status = models.CharField(choices=STATUS.choices, max_length=16, default='open') + action = models.CharField(choices=ACTION.choices, max_length=16, default='', blank=True) origin_objects = models.Manager() @@ -69,30 +63,38 @@ class Ticket(OrgModelMixin, CommonModelMixin): return self.get_action_display() def create_status_comment(self, status, user): - if status == self.STATUS_CLOSED: + if status == self.STATUS.CLOSED: action = _("Close") else: action = _("Open") body = _('{} {} this ticket').format(self.user, action) self.comments.create(user=user, body=body) - def perform_status(self, status, user): - if self.status == status: - return + def perform_status(self, status, user, extra_comment=None): + self.create_comment( + self.STATUS.get(status), + user, + extra_comment + ) self.status = status + self.assignee = user + self.assignees_display = str(user) self.save() - def create_action_comment(self, action, user, extra_comment=None): - action_display = dict(self.ACTION_CHOICES).get(action) + def create_comment(self, action_display, user, extra_comment=None): body = '{} {} {}'.format(user, action_display, _("this ticket")) if extra_comment is not None: body += extra_comment self.comments.create(body=body, user=user, user_display=str(user)) def perform_action(self, action, user, extra_comment=None): - self.create_action_comment(action, user, extra_comment) + self.create_comment( + self.ACTION.get(action), + user, + extra_comment + ) self.action = action - self.status = self.STATUS_CLOSED + self.status = self.STATUS.CLOSED self.assignee = user self.assignees_display = str(user) self.save() diff --git a/apps/tickets/serializers/request_asset_perm.py b/apps/tickets/serializers/request_asset_perm.py index 3b2d72b7c..521d2582e 100644 --- a/apps/tickets/serializers/request_asset_perm.py +++ b/apps/tickets/serializers/request_asset_perm.py @@ -17,7 +17,7 @@ from ..models import Ticket class RequestAssetPermTicketSerializer(serializers.ModelSerializer): ips = serializers.ListField(child=serializers.IPAddressField(), source='meta.ips', default=list, label=_('IP group')) - hostname = serializers.CharField(max_length=256, source='meta.hostname', default=None, + hostname = serializers.CharField(max_length=256, source='meta.hostname', default='', allow_blank=True, label=_('Hostname')) system_user = serializers.CharField(max_length=256, source='meta.system_user', default='', allow_blank=True, label=_('System user')) @@ -135,7 +135,7 @@ class RequestAssetPermTicketSerializer(serializers.ModelSerializer): def _create_body(self, validated_data): meta = validated_data['meta'] - type = dict(Ticket.TYPE_CHOICES).get(validated_data.get('type', '')) + type = Ticket.TYPE.get(validated_data.get('type', '')) date_start = dt_parser(meta.get('date_start')).strftime(settings.DATETIME_DISPLAY_FORMAT) date_expired = dt_parser(meta.get('date_expired')).strftime(settings.DATETIME_DISPLAY_FORMAT) @@ -159,7 +159,7 @@ class RequestAssetPermTicketSerializer(serializers.ModelSerializer): def create(self, validated_data): # `type` 与 `user` 用户不可提交, - validated_data['type'] = self.Meta.model.TYPE_REQUEST_ASSET_PERM + validated_data['type'] = self.Meta.model.TYPE.REQUEST_ASSET_PERM validated_data['user'] = self.context['request'].user # `confirmed` 相关字段只能审批人修改,所以创建时直接清理掉 self._pop_confirmed_fields() diff --git a/apps/tickets/serializers/ticket.py b/apps/tickets/serializers/ticket.py index f6c995ae0..34724be3a 100644 --- a/apps/tickets/serializers/ticket.py +++ b/apps/tickets/serializers/ticket.py @@ -3,14 +3,18 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from .. import models +from ..exceptions import ( + TicketClosed, OnlyTicketAssigneeCanOperate, + TicketCanNotOperate +) +from ..models import Ticket, Comment __all__ = ['TicketSerializer', 'CommentSerializer'] class TicketSerializer(serializers.ModelSerializer): class Meta: - model = models.Ticket + model = Ticket fields = [ 'id', 'user', 'user_display', 'title', 'body', 'assignees', 'assignees_display', 'assignee', 'assignee_display', @@ -32,17 +36,33 @@ class TicketSerializer(serializers.ModelSerializer): return super().create(validated_data) def update(self, instance, validated_data): - action = validated_data.get("action") - user = self.context["request"].user + action = validated_data.get('action') + user = self.context['request'].user + + if instance.type not in (Ticket.TYPE.GENERAL, + Ticket.TYPE.LOGIN_CONFIRM): + # 暂时的兼容操作吧,后期重构工单 + raise TicketCanNotOperate + + if instance.status == instance.STATUS.CLOSED: + raise TicketClosed + + if action: + if user not in instance.assignees.all(): + raise OnlyTicketAssigneeCanOperate + + # 有 `action` 时忽略 `status` + validated_data.pop('status', None) + + instance = super().update(instance, validated_data) + if not instance.status == instance.STATUS.CLOSED and action: + instance.perform_action(action, user) + else: + status = validated_data.get('status') + instance = super().update(instance, validated_data) + if status: + instance.perform_status(status, user) - if action and user not in instance.assignees.all(): - error = {"action": "Only assignees can update"} - raise serializers.ValidationError(error) - if instance.status == instance.STATUS_CLOSED: - validated_data.pop('action') - instance = super().update(instance, validated_data) - if not instance.status == instance.STATUS_CLOSED and action: - instance.perform_action(action, user) return instance @@ -65,7 +85,7 @@ class CommentSerializer(serializers.ModelSerializer): ) class Meta: - model = models.Comment + model = Comment fields = [ 'id', 'ticket', 'body', 'user', 'user_display', 'date_created', 'date_updated' diff --git a/apps/tickets/utils.py b/apps/tickets/utils.py index 97b5334e0..152b5182b 100644 --- a/apps/tickets/utils.py +++ b/apps/tickets/utils.py @@ -20,7 +20,7 @@ def send_new_ticket_mail_to_assignees(ticket: Ticket, assignees): subject = '{}: {}'.format(_("New ticket"), ticket.title) # 这里要设置前端地址,因为要直接跳转到页面 - if ticket.type == ticket.TYPE_REQUEST_ASSET_PERM: + if ticket.type == ticket.TYPE.REQUEST_ASSET_PERM: detail_url = urljoin(settings.SITE_URL, f'/tickets/tickets/request-asset-perm/{ticket.id}') else: detail_url = urljoin(settings.SITE_URL, f'/tickets/tickets/{ticket.id}') From 15fe7f810bbc3c1cce660b525056216407ba8a13 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Wed, 5 Aug 2020 14:09:23 +0800 Subject: [PATCH 25/40] =?UTF-8?q?perf(url):=20=E4=BC=98=E5=8C=96=20/api/do?= =?UTF-8?q?cs/=3F=20=E9=83=BD=E5=8F=AF=E4=BB=A5=E8=AE=BF=E9=97=AE=E6=96=87?= =?UTF-8?q?=E6=A1=A3=20(#4446)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ibuler --- apps/jumpserver/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index 60c04676f..d39eb56c5 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -75,8 +75,8 @@ if settings.DEBUG: urlpatterns += [ re_path('^api/swagger(?P\.json|\.yaml)$', views.get_swagger_view().without_ui(cache_timeout=1), name='schema-json'), - path('api/docs/', views.get_swagger_view().with_ui('swagger', cache_timeout=1), name="docs"), - path('api/redoc/', views.get_swagger_view().with_ui('redoc', cache_timeout=1), name='redoc'), + re_path('api/docs/?', views.get_swagger_view().with_ui('swagger', cache_timeout=1), name="docs"), + re_path('api/redoc/?', views.get_swagger_view().with_ui('redoc', cache_timeout=1), name='redoc'), re_path('^api/v2/swagger(?P\.json|\.yaml)$', views.get_swagger_view().without_ui(cache_timeout=1), name='schema-json'), From a25da8d479ccad18889cac9afed3021ba725cdde Mon Sep 17 00:00:00 2001 From: xinwen Date: Wed, 5 Aug 2020 16:04:05 +0800 Subject: [PATCH 26/40] =?UTF-8?q?feat(authentication):=20=E8=B6=85?= =?UTF-8?q?=E7=BA=A7=E7=AE=A1=E7=90=86=E5=91=98=E5=AF=86=E7=A0=81=E4=B8=8D?= =?UTF-8?q?=E8=83=BD=E6=98=AF`admin`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/api/token.py | 4 +++- apps/authentication/errors.py | 9 +++++++++ apps/authentication/mixins.py | 21 +++++++++++++++++++++ apps/authentication/urls/view_urls.py | 1 + apps/authentication/views/login.py | 17 +++++++++++++++++ apps/common/utils/django.py | 1 - apps/locale/zh/LC_MESSAGES/django.mo | Bin 56305 -> 56492 bytes apps/locale/zh/LC_MESSAGES/django.po | 23 ++++++++++++++--------- 8 files changed, 65 insertions(+), 11 deletions(-) diff --git a/apps/authentication/api/token.py b/apps/authentication/api/token.py index 6c6a34aa2..f7516496c 100644 --- a/apps/authentication/api/token.py +++ b/apps/authentication/api/token.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # - +from django.shortcuts import redirect from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.generics import CreateAPIView @@ -40,3 +40,5 @@ class TokenCreateApi(AuthMixin, CreateAPIView): return Response(e.as_data(), status=400) except errors.NeedMoreInfoError as e: return Response(e.as_data(), status=200) + except errors.PasswdTooSimple as e: + return redirect(e.url) diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py index 241881ba0..26363363e 100644 --- a/apps/authentication/errors.py +++ b/apps/authentication/errors.py @@ -211,3 +211,12 @@ class LoginConfirmOtherError(LoginConfirmBaseError): class SSOAuthClosed(JMSException): default_code = 'sso_auth_closed' default_detail = _('SSO auth closed') + + +class PasswdTooSimple(JMSException): + default_code = 'passwd_too_simple' + default_detail = _('Your password is too simple, please change it for security') + + def __init__(self, url, *args, **kwargs): + super(PasswdTooSimple, self).__init__(*args, **kwargs) + self.url = url diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index a450f610a..23ddac3cf 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +from urllib.parse import urlencode from functools import partial import time + from django.conf import settings from django.contrib.auth import authenticate +from django.shortcuts import reverse from common.utils import get_object_or_none, get_request_ip, get_logger from users.models import User @@ -91,6 +94,8 @@ class AuthMixin: elif user.password_has_expired: raise CredentialError(error=errors.reason_password_expired) + self._check_passwd_is_too_simple(user, password) + clean_failed_count(username, ip) request.session['auth_password'] = 1 request.session['user_id'] = str(user.id) @@ -98,6 +103,22 @@ class AuthMixin: request.session['auth_backend'] = auth_backend return user + @classmethod + def _check_passwd_is_too_simple(cls, user, password): + if user.is_superuser and password == 'admin': + reset_passwd_url = reverse('authentication:reset-password') + query_str = urlencode({ + 'token': user.generate_reset_token() + }) + reset_passwd_url = f'{reset_passwd_url}?{query_str}' + + flash_page_url = reverse('authentication:passwd-too-simple-flash-msg') + query_str = urlencode({ + 'redirect_url': reset_passwd_url + }) + + raise errors.PasswdTooSimple(f'{flash_page_url}?{query_str}') + def check_user_auth_if_need(self): request = self.request if request.session.get('auth_password') and \ diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py index 12e1fea84..467e32d0d 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -21,6 +21,7 @@ urlpatterns = [ path('password/forgot/sendmail-success/', users_view.UserForgotPasswordSendmailSuccessView.as_view(), name='forgot-password-sendmail-success'), path('password/reset/', users_view.UserResetPasswordView.as_view(), name='reset-password'), + path('password/too-simple-flash-msg/', views.FlashPasswdTooSimpleMsgView.as_view(), name='passwd-too-simple-flash-msg'), path('password/reset/success/', users_view.UserResetPasswordSuccessView.as_view(), name='reset-password-success'), path('password/verify/', users_view.UserVerifyPasswordView.as_view(), name='user-verify-password'), diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index a0b8804e1..d6331aa82 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -29,6 +29,7 @@ from ..forms import get_user_login_form_cls __all__ = [ 'UserLoginView', 'UserLogoutView', 'UserLoginGuardView', 'UserLoginWaitConfirmView', + 'FlashPasswdTooSimpleMsgView', ] @@ -91,6 +92,8 @@ class UserLoginView(mixins.AuthMixin, FormView): new_form._errors = form.errors context = self.get_context_data(form=new_form) return self.render_to_response(context) + except errors.PasswdTooSimple as e: + return redirect(e.url) return self.redirect_to_guard_view() def redirect_to_guard_view(self): @@ -151,6 +154,8 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView): return self.format_redirect_url(self.login_confirm_url) except errors.MFAUnsetError as e: return e.url + except errors.PasswdTooSimple as e: + return e.url else: auth_login(self.request, user) self.send_auth_signal(success=True, user=user) @@ -222,4 +227,16 @@ class UserLogoutView(TemplateView): return super().get_context_data(**kwargs) +@method_decorator(never_cache, name='dispatch') +class FlashPasswdTooSimpleMsgView(TemplateView): + template_name = 'flash_message_standalone.html' + def get(self, request, *args, **kwargs): + context = { + 'title': _('Please change your password'), + 'messages': _('Your password is too simple, please change it for security'), + 'interval': 5, + 'redirect_url': request.GET.get('redirect_url'), + 'auto_redirect': True, + } + return self.render_to_response(context) diff --git a/apps/common/utils/django.py b/apps/common/utils/django.py index 50c3f0ea1..b2a919401 100644 --- a/apps/common/utils/django.py +++ b/apps/common/utils/django.py @@ -2,7 +2,6 @@ # import re from django.shortcuts import reverse as dj_reverse -from django.db.models import Subquery, QuerySet from django.conf import settings from django.utils import timezone diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index cb2695eeceba7079ef663998880c7098de03d2f7..10950173da98413cd8ba4da1ad59f6963a7ed21a 100644 GIT binary patch delta 15538 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\n" "Language-Team: JumpServer team\n" @@ -1359,11 +1359,11 @@ msgstr "复制成功" msgid "Welcome back, please enter username and password to login" msgstr "欢迎回来,请输入用户名和密码登录" -#: authentication/views/login.py:82 +#: authentication/views/login.py:83 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:178 +#: authentication/views/login.py:183 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -1371,18 +1371,26 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
\n" " 不要关闭本页面" -#: authentication/views/login.py:183 +#: authentication/views/login.py:188 msgid "No ticket found" msgstr "没有发现工单" -#: authentication/views/login.py:215 +#: authentication/views/login.py:220 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:216 +#: authentication/views/login.py:221 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" +#: authentication/views/login.py:236 +msgid "Please change your password" +msgstr "请修改密码" + +#: authentication/views/login.py:237 +msgid "Your password is too simple, please change it for security" +msgstr "你的密码过于简单,为了安全,请修改" + #: common/const/__init__.py:6 #, python-format msgid "%(name)s was created successfully" @@ -4222,9 +4230,6 @@ msgstr "旗舰版" #~ msgid "Update asset user auth" #~ msgstr "更新资产用户认证信息" -#~ msgid "Please input password" -#~ msgstr "请输入密码" - #~ msgid "Asset user auth" #~ msgstr "资产用户信息" From a14f121fadf283ed930df4e8e29810d7e890bfcb Mon Sep 17 00:00:00 2001 From: xinwen Date: Wed, 5 Aug 2020 18:26:07 +0800 Subject: [PATCH 27/40] =?UTF-8?q?fix(orgs):=20=E7=BB=84=E7=BB=87=E6=88=90?= =?UTF-8?q?=E5=91=98=E5=85=B3=E7=B3=BB=E6=8E=A5=E5=8F=A3=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?`role=5Fdisplay`=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/orgs/serializers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/orgs/serializers.py b/apps/orgs/serializers.py index 5b20ce47e..d7e8ae2d1 100644 --- a/apps/orgs/serializers.py +++ b/apps/orgs/serializers.py @@ -6,7 +6,7 @@ from users.models.user import User from common.serializers import AdaptedBulkListSerializer from common.drf.serializers import BulkModelSerializer from common.db.models import concated_display as display -from .models import Organization, OrganizationMember +from .models import Organization, OrganizationMember, ROLE as ORG_ROLE class OrgSerializer(ModelSerializer): @@ -54,10 +54,11 @@ class OrgReadSerializer(OrgSerializer): class OrgMemberSerializer(BulkModelSerializer): org_display = serializers.CharField() user_display = serializers.CharField() + role_display = serializers.CharField(source='get_role_display') class Meta: - model = Organization.members.through - fields = ('id', 'org', 'user', 'role', 'org_display', 'user_display') + model = OrganizationMember + fields = ('id', 'org', 'user', 'role', 'org_display', 'user_display', 'role_display') @classmethod def setup_eager_loading(cls, queryset): From 1a9d9e4145815b28c71382c113c8bf7518e1fe8e Mon Sep 17 00:00:00 2001 From: xinwen Date: Wed, 5 Aug 2020 19:42:48 +0800 Subject: [PATCH 28/40] =?UTF-8?q?feat(ticket):=20=E7=94=B3=E8=AF=B7?= =?UTF-8?q?=E8=B5=84=E4=BA=A7=E5=B7=A5=E5=8D=95=E6=B7=BB=E5=8A=A0`actions`?= =?UTF-8?q?=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/tickets/api/request_asset_perm.py | 6 ++++-- apps/tickets/serializers/request_asset_perm.py | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/tickets/api/request_asset_perm.py b/apps/tickets/api/request_asset_perm.py index c12ea3070..5b62b8dcf 100644 --- a/apps/tickets/api/request_asset_perm.py +++ b/apps/tickets/api/request_asset_perm.py @@ -13,6 +13,7 @@ from common.utils.django import get_object_or_none from common.utils.timezone import dt_parser from common.drf.serializers import EmptySerializer from perms.models.asset_permission import AssetPermission, Asset +from perms.models import Action from assets.models.user import SystemUser from ..exceptions import ( ConfirmedAssetsChanged, ConfirmedSystemUserChanged, @@ -105,12 +106,14 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet): def _create_asset_permission(self, instance: Ticket, assets, system_user): meta = instance.meta request = self.request + actions = meta.get('actions', Action.CONNECT) ap_kwargs = { 'name': _('From request ticket: {} {}').format(instance.user_display, instance.id), 'created_by': self.request.user.username, 'comment': _('{} request assets, approved by {}').format(instance.user_display, - instance.assignees_display) + instance.assignees_display), + 'actions': actions, } date_start = dt_parser(meta.get('date_start')) date_expired = dt_parser(meta.get('date_expired')) @@ -118,7 +121,6 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet): ap_kwargs['date_start'] = date_start if date_expired: ap_kwargs['date_expired'] = date_expired - instance.perform_action(instance.ACTION.APPROVE, request.user, self._get_extra_comment(instance)) diff --git a/apps/tickets/serializers/request_asset_perm.py b/apps/tickets/serializers/request_asset_perm.py index 521d2582e..8827f482c 100644 --- a/apps/tickets/serializers/request_asset_perm.py +++ b/apps/tickets/serializers/request_asset_perm.py @@ -11,10 +11,14 @@ from orgs.utils import tmp_to_root_org from orgs.models import Organization, ROLE as ORG_ROLE from assets.models.asset import Asset from users.models.user import User +from perms.serializers import ActionsField +from perms.models import Action from ..models import Ticket class RequestAssetPermTicketSerializer(serializers.ModelSerializer): + actions = ActionsField(source='meta.actions', choices=Action.DB_CHOICES, + default=Action.CONNECT) ips = serializers.ListField(child=serializers.IPAddressField(), source='meta.ips', default=list, label=_('IP group')) hostname = serializers.CharField(max_length=256, source='meta.hostname', default='', @@ -42,7 +46,7 @@ class RequestAssetPermTicketSerializer(serializers.ModelSerializer): 'status', 'action', 'date_created', 'date_updated', 'system_user_waitlist_url', 'type', 'type_display', 'action_display', 'ips', 'confirmed_assets', 'date_start', 'date_expired', 'confirmed_system_user', 'hostname', - 'assets_waitlist_url', 'system_user', 'org_id' + 'assets_waitlist_url', 'system_user', 'org_id', 'actions' ] m2m_fields = [ 'user', 'user_display', 'assignees', 'assignees_display', From ec2b3b4cda72806b4a22550dfb82227eaeeff1d1 Mon Sep 17 00:00:00 2001 From: xinwen Date: Fri, 7 Aug 2020 13:53:17 +0800 Subject: [PATCH 29/40] =?UTF-8?q?fix(user):=20=E8=B0=83=E6=95=B4`User`?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/users/models/user.py | 2 +- apps/users/serializers/user.py | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 82791dbf5..6bda75b0a 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -161,7 +161,7 @@ class RoleMixin: role = ROLE.USER @property - def super_role_display(self): + def role_display(self): return self.get_role_display() @property diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index 4924aef17..65a8ee4ea 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -10,6 +10,7 @@ from common.mixins import CommonBulkSerializerMixin from common.serializers import AdaptedBulkListSerializer from common.permissions import CanUpdateDeleteUser from common.drf.fields import GroupConcatedPrimaryKeyRelatedField +from orgs.models import ROLE as ORG_ROLE from ..models import User, UserGroup @@ -50,9 +51,10 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): login_blocked = serializers.SerializerMethodField() can_update = serializers.SerializerMethodField() can_delete = serializers.SerializerMethodField() - org_role = serializers.CharField( + org_role = serializers.ChoiceField( label=_('Organization role name'), write_only=True, - allow_null=True, required=False, allow_blank=True + allow_null=True, required=False, allow_blank=True, + choices=ORG_ROLE.choices ) key_prefix_block = "_LOGIN_BLOCK_{}" @@ -64,7 +66,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): # small 指的是 不需要计算的直接能从一张表中获取到的数据 fields_small = fields_mini + [ 'password', 'email', 'public_key', 'wechat', 'phone', 'mfa_level', 'mfa_enabled', - 'mfa_level_display', 'mfa_force_enabled', 'super_role_display', + 'mfa_level_display', 'mfa_force_enabled', 'role_display', 'org_role_display', 'comment', 'source', 'is_valid', 'is_expired', 'is_active', 'created_by', 'is_first_login', 'password_strategy', 'date_password_last_updated', 'date_expired', @@ -87,8 +89,8 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): 'can_delete': {'read_only': True}, 'groups_display': {'label': _('Groups name')}, 'source_display': {'label': _('Source name')}, - 'role_display': {'label': _('Organization role name'), 'source': 'org_role_display'}, - 'super_role_display': {'label': _('Super role name')}, + 'org_role_display': {'label': _('Organization role name')}, + 'role_display': {'label': _('Super role name')}, } def __init__(self, *args, **kwargs): From f1e29a91f71342d19268b4b09f1926b25445baf6 Mon Sep 17 00:00:00 2001 From: xinwen Date: Fri, 7 Aug 2020 17:40:10 +0800 Subject: [PATCH 30/40] =?UTF-8?q?fix(users):=20=E7=94=A8=E6=88=B7=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E6=B7=BB=E5=8A=A0=E6=B1=87=E6=80=BB=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/locale/zh/LC_MESSAGES/django.mo | Bin 56492 -> 56543 bytes apps/locale/zh/LC_MESSAGES/django.po | 98 ++++++++++++++------------- apps/users/serializers/user.py | 6 +- 3 files changed, 56 insertions(+), 48 deletions(-) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 10950173da98413cd8ba4da1ad59f6963a7ed21a..70a1cb7e6a2a57ee6f9fef158505be694af3221c 100644 GIT binary patch delta 13517 zcmYk>2Y8P6{>SnAi6oLpB(Wk^qCx~gM34kg1XXI!(o$6I)l#D#r8cGR@^5>LqOI1f zQL6M1YR@*+P~#Xiilatp=~4gp$NfFmb#h(t`h9=%`}^JFd9ZqGzJG1a=Ra2{aK6J& zWIo3!h3^-4oY+9e2~Sb2;|zV)ak}6zoQ;QY40dVgI2{8V=SMt2TshluI{O^Q*Tiw| z5O;0rI5V(CGsl^Ld3YK-Kj%0{=vTA3;}i*S{7yg%$2m?z%nOcF6t7`0{*4hB)XH%R zU|}qVaaa~>nVm6?cnrqkM@at8Ce$9bVlv)9jf-vVI7yg^WmwTA zi2Gn|Jb~&M(ZO4&BvvGj!$fR`K{yoEZzSpj-@$0yh)Qgac@%Xrd8iFs>cIIcOUFcQ6^}U?_fzT4*1t-;b!1d5Ajkuuh!6 z8lpRSnbyLh#962bI$$w;8M)b=k*I;AQHf1JEif6&;XF*k&6tWeF$3c}J5C9F5jAck zYW}f)3c9wFu{zGThP|kTj-z&#hw66`bz*l>6Ffpq7|1iKOAv}07h{$|jVq4@F%3&& zW7Nj|uTangQ&AJn!WdkDI;yp(Yq`Vx-n?YqM_sb~T^*+omPDOkRn&Zq%@@qh<}03l zXCMXLBqK2${iq#mMeTGaY6tsK{Z671yMQI|F2-WSi{8SCsD}vi`vnLs3Tm3ns6g3vE8U0=2`tY)IztgB0k2- zSfPjG)WNo>4a~&tI2Sc<%bwo49sE{!1(m=M)Ix8fGMt6l!2&FcOR+ib#@kr%6>otj zsKf$$c_&c-HD3&JpE~iVOEC^h;S6ku{uNen9UD-QzqdEA8S1HMgGt!Q;Il!H61;`FH=MrS%@u`Opfc(brlKC(rl?EU88yBa=Eq?e%lgh}3cA}})U{cO%5Wd* z9>_%vJc~M^8yJX>to{jVT*PZ$|4OL#ny7iwF&?uq1YbpMXbAd)D2$_^olLZbc^E*v z#9W3U#Gj%PS&Q1?b_~YjSO)WOB|bo1lBNB;o$fIAq1HQw)$l|=uD>!2>hCQOjoM)e z48IN0Q0rXk&-sT@xJyL{KE}!z zJix23fjY{1s0A8gK5T~tuoG(Mz3>^FXnu-1nf<5}JBGT%XHo0@j{1~*;-{cYiVpN9 zNWlEWO;L|gE7S=Lu=??+Bb;K+LQOaiwa_BeLaR{YcVPiMhZXQTs(2*}1_fQ*fhI;)9pdQmGEQiS$ zspr3qb?Ax8^i|Z32BKaxBQOdlqweYj=6Y;Sd>Hk(#tifBof4>ppFt&(jM_j37Q~jQ z4R%9+G=*UlB5@LGfQ#|?F=~f7n2LKb10SIhOdIYkSQmBVjZr7i+UmPtA>y7^|GN1W zYMm*=Ie+cQr9v64MIGgC)B-=Fc5(qV@lDjtcOP~0g^utnixr5|F$#N_!!VwBB5LQ$ zu_|uBXuL3j^H<>>6*_^S*S*K4sF{NL%GDC}SoTFNFvOgIx@7N}|A$I+5k}x=s10ty zLU_#Li>Sx*v7bT$h1fT|o3K7=!A_Wr!%zd4pf1G*R3ckY3++HHv>$Z|ezNw9)_xsz zqW4fYU)V_R=1oM6_orAz2I{CAV-0MDI+=;s7T?E;copL?YLs^ktD}y#9_p!Rfx2`Z zQHgd(olqaEA7J&vk@@}37;Bh?%E(1cxDd6YRj7%#pc2Z#BAAO>=o;$M{f0WJCstqR zO|Lx`HD5gHF|UR?*=87|=YJ#xEieu>@f1wKf1m~)!D4s@tKdDBsf?}YwXD*94TMsH&!oQ2xK7E}T!Pzhed;&>GsqchgKi?cC? zuqP_9(Wq-b6?KAhummnfo$OXrf;Yx;{_5}-71~MIIB#HK)WRh#u83-{j=GuBQ3++? z(-#kFr>!mSgi5p*>O{w363)h2xCOQ0TjMx?4SYz2CUoBN;vh2|wUffA2}@dB6Dtze zMUCr&g|R>C1jeIwI14p?q1Atax=A;nPGpatg0Ag(R3g{SJE)!giRu_Q-aC;v)Phwk zPC=baebhWnEpB7&T~Ryji#oaCs080cjq^{TkVs(;#^V0iQdAm zV0GdN$V<~%kCm~)B=6>Hi))Gd<4TNu$9t20hdRM?s6-y3<}Eu}|Hwsl%_yW(F#&b0 zzCjJxg*vLU<_#=L{1DYIa*B5;Vo=w<6e{uR7=*K}-bJ0{3e;1w6ZJkhiQ%m8{6;}X z_aD?X51;ByTpV@OrBU@&F+XOYo{om7Yu?K0yP%%$o~ZsKFc|&j6x6*i8?})S(661Y zrBDVBU?seUN~rKOZ^Ck@I1S@58}+7q6}6+Ws0Cfr0!vT}uSAVoZ|ym#gpc3=%$vsf z2U2J>-J75pDzo+$_qTW?D$!Y}1y*2X+<1kUj;Q5<=6Z>C zNA0jLYTQta!kHL}3z4VB@2sX!kcyqCogPE&^fYS1%cyI01J&;_D&g?=y!xt`KmrZ0 zCG~ygX;wZ>u{Wl=+_VAQ?RcDc>wJ==|BMCv!kGF6mZdvNRU&UF_|j%2)Elq1#jR2Eb+@>;#lx0!{%UyBD*V=A zis_;j_!xCUE3qP;z^?c=>Td4xiPz6X&9?{>@e}i~)&Gu4^e@z78S*LT&t2#we(E)3 zn$64(<}0X)hoJh8G-qH0@nUOVZEi%}<=>z-a1hJlkEjG5pf-}v|Cv`&#Y{(K*2LmI z=5VW5e@(a;wZP}r{x#}7um|(uNvl7D!Nk8>d>eHyJ;uuDFTTS2{BDd@spyNkNfw|6 zu0UnH*4&0laKE)*HE&t_eKX*5Z+xU#%q)$XFTsobP6`EOT*n&fn$MwLsU1-Z4n`$3 z+uGkpJzh&uJKutOKO8hqnb*ueP?swB3(q20P|yD}6m+C1W~P~Kwlce*66l2uu)oEt ztbGIOl5Ds7!>FC+q7poB-ZO(&vM%d8u@n-p9BP4PW-HWV)X^Mb_0!Bb7)tvB)IuLy z{ThomVHou}R(}{P5a*)$KSIAU&cDjbFb367$*gTQMkUt9;?5TLLG5gy)%(p^sKh@+ z&A-9oU8qZT8g)tUtg`1nYPGkM7}P>#Ev{&B4GgC~-E4x1#O<&tjzx{zjY{Ys>SWGh zI9^31^1IasuJPInuHpREP@D>NtcaQ@%{tV@r;nq>eNg>}pmyj-?dSv4xV5NxzA|^2 z$ILV4uc$;HXaW^N*Lnj>p(akUxS`p~?1oyXpT$EhehVY0e+QM|JZoQOt~0lzUPK2> z{|ySsR79-vCeB1Y_?(6opF!>Dl6l+eA7do-fnR#;g8@wH-peD{j?W_svee#;s4>U)hHZlfv zZ%oAmJ^$}fs6xeN)Iyg~e@QIDXK4j|9`${FIBMW@)I_fNvAG77$TwyVs{ekAf51Y- zr?5WW#HZi?EAy>F9qX74Q4_T=+nHU>o~Q&~Lp=p=pxz>bUhy3`t?G!L78QtCT?rKj7nr6>I=sR)Q87=s1sU^>VFBt@V3Q&q3(^4&EELpsD5#% z^{QBX_GWwjTUdv77)OU5s1uohnrH?pk&iG6msgZM7(cwOhOn-bsgfu@d#K zp%QX21V8grC_-T!D#Pzk&+#GD0;kL?<~`H|&Q|YkFO2G!h8o`x)&3mnDd>iUu?NQD z>llykVlniuqo4`)q6X$-EMBpC=Nqp+0@W`b^+rra?X(5z1UsU}543u}IUSXliyHrt zxdQ3$cQ#SbHQr|pd03J77mFjdc?*?59a#cuqGU4zwQy65yPADb_sZ+$WUF6*>c1Q- z>G|JGK?z*I;&=yZVc2$WXU}3u;!ddd!w8E%!Bpa%s1x|z+QazEgW^ooxHhPS2cb@4 zjQO_eS>KsPK}R`X6}ZGYtVU(J(cEjEGH;?1e_}@d)9W9Hnx}@Dg<7};>LzT5T5mi) z{r~@IRx!_9hy1e`0BhoWRQ*Bod(`-os1vxn)9-co zjS5|>2dDvsa=ebQsJH?up=7fjD$$nK-oxx`4n{5X2CCmg)cE2H1pvG-7bIhaGe%fOHMJxPt9m=2Rr^hJ*aU%pq}%~sCy!6pLgk!FqrsxRR6Z<*Mu)y!yD!})D9=1 zI?gsfvi23Ih1Obqj(OBPjrtI~hD!7SYQEt8o>69r{hYsUq68|GSsgPAwQv)&9csZI z);c86Bw_5#9e2w~37B@WLje8N5(CY{6`5#Y(CYX#$XdWiuXI6j6JZWA= zo!niELk@ZyD2ke=6b50E)mOtX;##QoXHh5ff}etR@)CyPDAZ&2HfG^+)PPH<6S{5w zf%%C4wm9IB*B**`SsZoPYcGwOFTqSP>wEg0=dGf%*#|Z8aMVJht$q?#B%X_UuD6S({fc-)G~_%rH~h8*`2t%UQ48=?OH+4+frE{454F>As1u!zI-$8(57(ey4L2wh!n>$1qd~dezfhJi ztDq*TkLvfl#V?`84?ra}+~RTOBvk(y*1pK1qx+g|iJQsDMi!9!RS~urM&R-K8vWAo9Wz^2@Vmo|jamy24 zeS6f-UbJ|y#c!Y{o`!n<=b`SM56tgT>xKQ~Z7jx5K@%lnVa%}j1=K`6Q716K;;|M_ zHs_-z{1kPwZ9v^@=dC^<&ufoF%~uMwUIo+%`O_)XrO+02SIi{N8}-5o zI_XUmit1Mw)gEsqnpJTi^=Vifm!THkhMMOnl91o|fr2JFgU{oys0q_fd0)5dVtL|0 zsGYgE9KS++I(GTloA?b(C7y<=-){9MQS;rvVm>Y@DuK{5N`UJh@wC7{Hli{tYt}?1 z))aLK+M|xJx781}cr0qd$*4;>9o2sk>I=sj)Vzn0^_{EyF&%pa@P`|b663q?mQU&A z+vdJS`G7k=B`fF)+Ctq^)E;tEY7FpgcNf)I;fr*$Q{#dPvesr4t2;uxnesh1sb;y*07h=2 z=9b%`W+UHq_rsdKd^_F8HB)^D-ITN;zW=yO(`x&|+*4_}u{-G3gpn0+?|XMPe}3y8sht^m!&=t3(RDI?>)n=hYWu!+C)XJe8puq| zXx-vItkWv!B&DC+mg$v!>)g@laiJl!UZL)1cWHWJ(07zhyGPO!OaGJ7|Nb-}UO;|3 z`91DNXH@f@bDzzK@!fH|WHj}KyGt`l`u=vm%}5Q}L(fO<{fu%&^ZsX!#x~hmH#xIf z^nO}$nCZViC)`1qG0_jG-DOp`@lSVtW~0zAZI+Afh0NN%hi+`$)X>e;eMXPp-41oj z`7XPo>(&nbhPr(D+?=c!_ekAFzDsU&y>dbM*%D%Pk^m>V*K5E;r z{84vny{z(EZGpqsp5Gt%{Wrf`SfDPySNP3wOV=+M@dvg2822yS=VsTB3%W+>E4N?$ zxQM%y;`r6iFYf&MGefUh^rM@UHOjZ!U7D3y{#WW!nPCY|$JO|6T+8oLes!A%yS@f7 zWpC25gUE-E`PI*1e%CxL@jowadV{#wmDW;EV<{(Fe9RrzAS=Mh=Wb}w(YIk^=(E3O vhYlYyuuuHpo`d>qyfLLgi|8xsr(T(`?b?cY*QRf{Ja_ih6&sUsCx!k$_gWv( delta 13453 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 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 2ab609743..d7e1584ed 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-08-05 16:43+0800\n" +"POT-Creation-Date: 2020-08-07 18:48+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -212,7 +212,7 @@ msgstr "IP" #: assets/models/asset.py:187 assets/serializers/asset_user.py:45 #: assets/serializers/gathered_user.py:20 settings/serializers/settings.py:51 -#: tickets/serializers/request_asset_perm.py:21 +#: tickets/serializers/request_asset_perm.py:25 #: users/templates/users/_granted_assets.html:25 #: users/templates/users/user_asset_permission.html:157 msgid "Hostname" @@ -487,7 +487,7 @@ msgstr "每行一个命令" #: assets/models/cmd_filter.py:56 audits/models.py:57 #: authentication/templates/authentication/_access_key_modal.html:34 #: perms/forms/asset_permission.py:20 -#: tickets/serializers/request_asset_perm.py:60 +#: tickets/serializers/request_asset_perm.py:64 #: tickets/serializers/ticket.py:30 #: users/templates/users/_granted_assets.html:29 #: users/templates/users/user_asset_permission.html:44 @@ -543,7 +543,7 @@ msgstr "默认资产组" #: templates/index.html:78 terminal/backends/command/models.py:18 #: terminal/backends/command/serializers.py:12 terminal/models.py:185 #: tickets/models/ticket.py:30 tickets/models/ticket.py:137 -#: tickets/serializers/request_asset_perm.py:61 +#: tickets/serializers/request_asset_perm.py:65 #: tickets/serializers/ticket.py:31 users/forms/group.py:15 #: users/models/user.py:157 users/models/user.py:643 #: users/serializers/group.py:20 @@ -651,7 +651,7 @@ msgstr "SFTP根路径" #: perms/models/remote_app_permission.py:16 templates/_nav.html:45 #: terminal/backends/command/models.py:20 #: terminal/backends/command/serializers.py:14 terminal/models.py:189 -#: tickets/serializers/request_asset_perm.py:23 +#: tickets/serializers/request_asset_perm.py:27 #: users/templates/users/_granted_assets.html:27 #: users/templates/users/user_asset_permission.html:42 #: users/templates/users/user_asset_permission.html:76 @@ -922,7 +922,7 @@ msgid "Success" msgstr "成功" #: audits/models.py:43 ops/models/command.py:28 perms/models/base.py:52 -#: terminal/models.py:199 tickets/serializers/request_asset_perm.py:25 +#: terminal/models.py:199 tickets/serializers/request_asset_perm.py:29 #: xpack/plugins/change_auth_plan/models.py:177 #: xpack/plugins/change_auth_plan/models.py:308 #: xpack/plugins/gathered_user/models.py:76 @@ -1003,7 +1003,7 @@ msgstr "Agent" #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: authentication/templates/authentication/login_otp.html:6 #: users/forms/profile.py:52 users/models/user.py:511 -#: users/serializers/user.py:234 users/templates/users/user_detail.html:77 +#: users/serializers/user.py:240 users/templates/users/user_detail.html:77 #: users/templates/users/user_profile.html:87 msgid "MFA" msgstr "多因子认证" @@ -1013,7 +1013,7 @@ msgstr "多因子认证" msgid "Reason" msgstr "原因" -#: audits/models.py:106 tickets/serializers/request_asset_perm.py:59 +#: audits/models.py:106 tickets/serializers/request_asset_perm.py:63 #: tickets/serializers/ticket.py:29 xpack/plugins/cloud/models.py:211 #: xpack/plugins/cloud/models.py:269 msgid "Status" @@ -1176,6 +1176,10 @@ msgstr "登录复核 {}" msgid "SSO auth closed" msgstr "SSO 认证关闭了" +#: authentication/errors.py:218 authentication/views/login.py:237 +msgid "Your password is too simple, please change it for security" +msgstr "你的密码过于简单,为了安全,请修改" + #: authentication/forms.py:26 authentication/forms.py:34 #: authentication/templates/authentication/login.html:38 #: authentication/templates/authentication/xpack_login.html:118 @@ -1242,7 +1246,7 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:409 users/serializers/user.py:231 +#: users/models/user.py:409 users/serializers/user.py:237 #: users/templates/users/user_profile.html:94 #: users/templates/users/user_profile.html:163 #: users/templates/users/user_profile.html:166 @@ -1251,7 +1255,7 @@ msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:410 users/serializers/user.py:232 +#: users/models/user.py:410 users/serializers/user.py:238 #: users/templates/users/user_profile.html:92 #: users/templates/users/user_profile.html:170 msgid "Enable" @@ -1387,10 +1391,6 @@ msgstr "退出登录成功,返回到登录页面" msgid "Please change your password" msgstr "请修改密码" -#: authentication/views/login.py:237 -msgid "Your password is too simple, please change it for security" -msgstr "你的密码过于简单,为了安全,请修改" - #: common/const/__init__.py:6 #, python-format msgid "%(name)s was created successfully" @@ -1703,7 +1703,7 @@ msgstr "提示:RDP 协议不支持单独控制上传或下载文件" #: perms/forms/asset_permission.py:86 perms/forms/database_app_permission.py:41 #: perms/forms/remote_app_permission.py:43 perms/models/base.py:50 #: templates/_nav.html:21 users/forms/user.py:168 users/models/group.py:31 -#: users/models/user.py:495 users/serializers/user.py:48 +#: users/models/user.py:495 users/serializers/user.py:49 #: users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_asset_permission.html:39 #: users/templates/users/user_asset_permission.html:67 @@ -1769,7 +1769,7 @@ msgstr "动作" msgid "Asset permission" msgstr "资产授权" -#: perms/models/base.py:53 tickets/serializers/request_asset_perm.py:27 +#: perms/models/base.py:53 tickets/serializers/request_asset_perm.py:31 #: users/models/user.py:527 users/templates/users/user_detail.html:93 #: users/templates/users/user_profile.html:120 msgid "Date expired" @@ -2514,36 +2514,36 @@ msgstr "结束日期" msgid "Args" msgstr "参数" -#: tickets/api/request_asset_perm.py:44 +#: tickets/api/request_asset_perm.py:45 #, python-format msgid "Ticket has %s" msgstr "工单已%s" -#: tickets/api/request_asset_perm.py:89 +#: tickets/api/request_asset_perm.py:90 msgid "Confirm assets first" msgstr "请先确认资产" -#: tickets/api/request_asset_perm.py:92 +#: tickets/api/request_asset_perm.py:93 msgid "Confirmed assets changed" msgstr "确认的资产变更了" -#: tickets/api/request_asset_perm.py:96 +#: tickets/api/request_asset_perm.py:97 msgid "Confirm system-user first" msgstr "请先确认系统用户" -#: tickets/api/request_asset_perm.py:100 +#: tickets/api/request_asset_perm.py:101 msgid "Confirmed system-user changed" msgstr "确认的系统用户变更了" -#: tickets/api/request_asset_perm.py:103 xpack/plugins/cloud/models.py:202 +#: tickets/api/request_asset_perm.py:104 xpack/plugins/cloud/models.py:202 msgid "Succeed" msgstr "成功" -#: tickets/api/request_asset_perm.py:110 +#: tickets/api/request_asset_perm.py:112 msgid "From request ticket: {} {}" msgstr "来自工单申请: {} {}" -#: tickets/api/request_asset_perm.py:112 +#: tickets/api/request_asset_perm.py:114 msgid "{} request assets, approved by {}" msgstr "{} 申请资产,通过人 {}" @@ -2619,27 +2619,27 @@ msgstr "{} {} 这个工单" msgid "this ticket" msgstr "这个工单" -#: tickets/serializers/request_asset_perm.py:19 +#: tickets/serializers/request_asset_perm.py:23 msgid "IP group" msgstr "IP组" -#: tickets/serializers/request_asset_perm.py:31 +#: tickets/serializers/request_asset_perm.py:35 msgid "Confirmed assets" msgstr "确认的资产" -#: tickets/serializers/request_asset_perm.py:34 +#: tickets/serializers/request_asset_perm.py:38 msgid "Confirmed system user" msgstr "确认的系统用户" -#: tickets/serializers/request_asset_perm.py:83 +#: tickets/serializers/request_asset_perm.py:87 msgid "Invalid `org_id`" msgstr "无效的 `org_id`" -#: tickets/serializers/request_asset_perm.py:92 +#: tickets/serializers/request_asset_perm.py:96 msgid "Field `assignees` must be organization admin or superuser" msgstr "字段 assignees 必须是组织管理员或者超级管理员" -#: tickets/serializers/request_asset_perm.py:142 +#: tickets/serializers/request_asset_perm.py:146 #, python-brace-format msgid "" "\n" @@ -2808,8 +2808,8 @@ msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" #: users/forms/profile.py:137 users/forms/user.py:90 -#: users/serializers/user.py:194 users/serializers/user.py:276 -#: users/serializers/user.py:334 +#: users/serializers/user.py:200 users/serializers/user.py:282 +#: users/serializers/user.py:340 msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" @@ -2833,15 +2833,15 @@ msgstr "添加到用户组" msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" -#: users/forms/user.py:124 users/serializers/user.py:36 +#: users/forms/user.py:124 users/serializers/user.py:37 msgid "Reset link will be generated and sent to the user" msgstr "生成重置密码链接,通过邮件发送给用户" -#: users/forms/user.py:125 users/serializers/user.py:37 +#: users/forms/user.py:125 users/serializers/user.py:38 msgid "Set password" msgstr "设置密码" -#: users/forms/user.py:132 users/serializers/user.py:44 +#: users/forms/user.py:132 users/serializers/user.py:45 #: xpack/plugins/change_auth_plan/models.py:61 #: xpack/plugins/change_auth_plan/serializers.py:30 msgid "Password strategy" @@ -2887,51 +2887,55 @@ msgstr "管理员" msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" -#: users/serializers/user.py:54 users/serializers/user.py:90 +#: users/serializers/user.py:55 users/serializers/user.py:93 msgid "Organization role name" msgstr "组织角色名称" -#: users/serializers/user.py:81 users/serializers/user.py:247 +#: users/serializers/user.py:59 +msgid "Total role name" +msgstr "汇总角色名称" + +#: users/serializers/user.py:84 users/serializers/user.py:253 msgid "Is first login" msgstr "首次登录" -#: users/serializers/user.py:82 +#: users/serializers/user.py:85 msgid "Is valid" msgstr "账户是否有效" -#: users/serializers/user.py:83 +#: users/serializers/user.py:86 msgid "Is expired" msgstr " 是否过期" -#: users/serializers/user.py:84 +#: users/serializers/user.py:87 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/user.py:88 +#: users/serializers/user.py:91 msgid "Groups name" msgstr "用户组名" -#: users/serializers/user.py:89 +#: users/serializers/user.py:92 msgid "Source name" msgstr "用户来源名" -#: users/serializers/user.py:91 +#: users/serializers/user.py:94 msgid "Super role name" msgstr "超级角色名称" -#: users/serializers/user.py:114 +#: users/serializers/user.py:120 msgid "Role limit to {}" msgstr "角色只能为 {}" -#: users/serializers/user.py:126 users/serializers/user.py:300 +#: users/serializers/user.py:132 users/serializers/user.py:306 msgid "Password does not match security rules" msgstr "密码不满足安全规则" -#: users/serializers/user.py:292 +#: users/serializers/user.py:298 msgid "The old password is incorrect" msgstr "旧密码错误" -#: users/serializers/user.py:306 +#: users/serializers/user.py:312 msgid "The newly set password is inconsistent" msgstr "两次密码不一致" diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index 65a8ee4ea..908978d75 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -56,6 +56,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): allow_null=True, required=False, allow_blank=True, choices=ORG_ROLE.choices ) + total_role_display = serializers.SerializerMethodField(label=_('Total role name')) key_prefix_block = "_LOGIN_BLOCK_{}" class Meta: @@ -67,7 +68,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): fields_small = fields_mini + [ 'password', 'email', 'public_key', 'wechat', 'phone', 'mfa_level', 'mfa_enabled', 'mfa_level_display', 'mfa_force_enabled', 'role_display', 'org_role_display', - 'comment', 'source', 'is_valid', 'is_expired', + 'total_role_display', 'comment', 'source', 'is_valid', 'is_expired', 'is_active', 'created_by', 'is_first_login', 'password_strategy', 'date_password_last_updated', 'date_expired', 'avatar_url', 'source_display', 'date_joined', 'last_login' @@ -109,6 +110,9 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): choices.pop(User.ROLE.AUDITOR, None) role._choices = choices + def get_total_role_display(self, instance): + return ' | '.join({str(instance.role_display), str(instance.org_role_display)}) + def validate_role(self, value): request = self.context.get('request') if not request.user.is_superuser and value != User.ROLE.USER: From ffde306a04d3e7a632db06a5b4199abef29d6b66 Mon Sep 17 00:00:00 2001 From: xinwen Date: Mon, 10 Aug 2020 14:45:03 +0800 Subject: [PATCH 31/40] =?UTF-8?q?fix(assets):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E5=88=A0=E9=99=A4=E8=B5=84=E4=BA=A7=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/orgs/mixins/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/orgs/mixins/api.py b/apps/orgs/mixins/api.py index 635e415bf..f40f9f7fa 100644 --- a/apps/orgs/mixins/api.py +++ b/apps/orgs/mixins/api.py @@ -55,8 +55,8 @@ class OrgBulkModelViewSet(CommonApiMixin, OrgQuerySetMixin, BulkModelViewSet): filtered_count = filtered.count() if filtered_count == 1: return True - if qs_count <= filtered_count: - return False + if qs_count > filtered_count: + return True if self.request.query_params.get('spm', ''): return True return False From 25d1b3334f96a0c12b4705f38b67d92cbc1f13f8 Mon Sep 17 00:00:00 2001 From: huamaolin Date: Fri, 7 Aug 2020 10:44:27 +0800 Subject: [PATCH 32/40] =?UTF-8?q?fix(OperateLog):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E6=97=A5=E5=BF=97=E6=8C=89=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E6=85=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增加表OperateLog datetime字段索引 --- apps/audits/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/audits/models.py b/apps/audits/models.py index 97aef40ce..c959bc35c 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -58,7 +58,7 @@ class OperateLog(OrgModelMixin): resource_type = models.CharField(max_length=64, verbose_name=_("Resource Type")) resource = models.CharField(max_length=128, verbose_name=_("Resource")) remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) - datetime = models.DateTimeField(auto_now=True, verbose_name=_('Datetime')) + datetime = models.DateTimeField(auto_now=True, verbose_name=_('Datetime'), db_index=True) def __str__(self): return "<{}> {} <{}>".format(self.user, self.action, self.resource) From 0a242c3e81e100021226682217c8dbe50b13f95c Mon Sep 17 00:00:00 2001 From: xinwen Date: Tue, 11 Aug 2020 11:25:29 +0800 Subject: [PATCH 33/40] =?UTF-8?q?fix(audis):=20=E7=94=9F=E6=88=90=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E6=97=A5=E5=BF=97=E6=97=B6=E9=97=B4=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E7=B4=A2=E5=BC=95=E8=BF=81=E7=A7=BB=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0010_auto_20200811_1122.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 apps/audits/migrations/0010_auto_20200811_1122.py diff --git a/apps/audits/migrations/0010_auto_20200811_1122.py b/apps/audits/migrations/0010_auto_20200811_1122.py new file mode 100644 index 000000000..f274bf815 --- /dev/null +++ b/apps/audits/migrations/0010_auto_20200811_1122.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.13 on 2020-08-11 03:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('audits', '0009_auto_20200624_1654'), + ] + + operations = [ + migrations.AlterField( + model_name='operatelog', + name='datetime', + field=models.DateTimeField(auto_now=True, db_index=True, verbose_name='Datetime'), + ), + ] From 91649a39083a45c3bab26656671b4c6fe6e9b8ee Mon Sep 17 00:00:00 2001 From: xinwen Date: Fri, 7 Aug 2020 11:52:34 +0800 Subject: [PATCH 34/40] =?UTF-8?q?feat(applications):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=20k8s=20=E5=BA=94=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/applications/api/__init__.py | 1 + apps/applications/api/k8s_app.py | 20 +++ apps/applications/migrations/0005_k8sapp.py | 34 +++++ apps/applications/models/__init__.py | 1 + apps/applications/models/k8s_app.py | 27 ++++ apps/applications/serializers/__init__.py | 1 + apps/applications/serializers/k8s_app.py | 22 ++++ apps/applications/urls/api_urls.py | 1 + .../migrations/0054_auto_20200807_1032.py | 23 ++++ apps/assets/models/user.py | 3 + apps/assets/serializers/system_user.py | 5 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 56543 -> 56654 bytes apps/locale/zh/LC_MESSAGES/django.po | 107 +++++++++------- apps/perms/api/__init__.py | 3 + apps/perms/api/k8s_app_permission.py | 21 ++++ apps/perms/api/k8s_app_permission_relation.py | 111 ++++++++++++++++ apps/perms/api/user_k8s_app_permission.py | 119 ++++++++++++++++++ .../perms/migrations/0012_k8sapppermission.py | 44 +++++++ apps/perms/models/__init__.py | 1 + apps/perms/models/k8s_app_permission.py | 39 ++++++ apps/perms/serializers/__init__.py | 2 + apps/perms/serializers/k8s_app_permission.py | 50 ++++++++ .../k8s_app_permission_relation.py | 73 +++++++++++ apps/perms/serializers/user_permission.py | 11 ++ apps/perms/urls/api_urls.py | 2 + apps/perms/urls/k8s_app_permission.py | 45 +++++++ apps/perms/utils/__init__.py | 1 + apps/perms/utils/k8s_app_permission.py | 93 ++++++++++++++ .../migrations/0025_auto_20200810_1735.py | 18 +++ apps/terminal/models.py | 1 + 30 files changed, 831 insertions(+), 48 deletions(-) create mode 100644 apps/applications/api/k8s_app.py create mode 100644 apps/applications/migrations/0005_k8sapp.py create mode 100644 apps/applications/models/k8s_app.py create mode 100644 apps/applications/serializers/k8s_app.py create mode 100644 apps/assets/migrations/0054_auto_20200807_1032.py create mode 100644 apps/perms/api/k8s_app_permission.py create mode 100644 apps/perms/api/k8s_app_permission_relation.py create mode 100644 apps/perms/api/user_k8s_app_permission.py create mode 100644 apps/perms/migrations/0012_k8sapppermission.py create mode 100644 apps/perms/models/k8s_app_permission.py create mode 100644 apps/perms/serializers/k8s_app_permission.py create mode 100644 apps/perms/serializers/k8s_app_permission_relation.py create mode 100644 apps/perms/urls/k8s_app_permission.py create mode 100644 apps/perms/utils/k8s_app_permission.py create mode 100644 apps/terminal/migrations/0025_auto_20200810_1735.py diff --git a/apps/applications/api/__init__.py b/apps/applications/api/__init__.py index a707cfde6..0e6e940ee 100644 --- a/apps/applications/api/__init__.py +++ b/apps/applications/api/__init__.py @@ -1,2 +1,3 @@ from .remote_app import * from .database_app import * +from .k8s_app import * diff --git a/apps/applications/api/k8s_app.py b/apps/applications/api/k8s_app.py new file mode 100644 index 000000000..5cc63b546 --- /dev/null +++ b/apps/applications/api/k8s_app.py @@ -0,0 +1,20 @@ +# coding: utf-8 +# + +from orgs.mixins.api import OrgBulkModelViewSet + +from .. import models +from .. import serializers +from ..hands import IsOrgAdminOrAppUser + +__all__ = [ + 'K8sAppViewSet', +] + + +class K8sAppViewSet(OrgBulkModelViewSet): + model = models.K8sApp + filter_fields = ('name',) + search_fields = filter_fields + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = serializers.K8sAppSerializer diff --git a/apps/applications/migrations/0005_k8sapp.py b/apps/applications/migrations/0005_k8sapp.py new file mode 100644 index 000000000..3f6964a88 --- /dev/null +++ b/apps/applications/migrations/0005_k8sapp.py @@ -0,0 +1,34 @@ +# Generated by Django 2.2.13 on 2020-08-07 07:13 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('applications', '0004_auto_20191218_1705'), + ] + + operations = [ + migrations.CreateModel( + name='K8sApp', + fields=[ + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('type', models.CharField(choices=[('k8s', 'Kubernetes')], default='k8s', max_length=128, verbose_name='Type')), + ('cluster', models.CharField(max_length=1024, verbose_name='Cluster')), + ('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')), + ], + options={ + 'verbose_name': 'KubernetesApp', + 'ordering': ('name',), + 'unique_together': {('org_id', 'name')}, + }, + ), + ] diff --git a/apps/applications/models/__init__.py b/apps/applications/models/__init__.py index a707cfde6..0e6e940ee 100644 --- a/apps/applications/models/__init__.py +++ b/apps/applications/models/__init__.py @@ -1,2 +1,3 @@ from .remote_app import * from .database_app import * +from .k8s_app import * diff --git a/apps/applications/models/k8s_app.py b/apps/applications/models/k8s_app.py new file mode 100644 index 000000000..c4f0591ca --- /dev/null +++ b/apps/applications/models/k8s_app.py @@ -0,0 +1,27 @@ +from django.utils.translation import gettext_lazy as _ + +from common.db import models +from orgs.mixins.models import OrgModelMixin + + +class K8sApp(OrgModelMixin, models.JMSModel): + class TYPE(models.ChoiceSet): + K8S = 'k8s', _('Kubernetes') + + name = models.CharField(max_length=128, verbose_name=_('Name')) + type = models.CharField( + default=TYPE.K8S, choices=TYPE.choices, + max_length=128, verbose_name=_('Type') + ) + cluster = models.CharField(max_length=1024, verbose_name=_('Cluster')) + comment = models.TextField( + max_length=128, default='', blank=True, verbose_name=_('Comment') + ) + + def __str__(self): + return self.name + + class Meta: + unique_together = [('org_id', 'name'), ] + verbose_name = _('KubernetesApp') + ordering = ('name', ) diff --git a/apps/applications/serializers/__init__.py b/apps/applications/serializers/__init__.py index a707cfde6..0e6e940ee 100644 --- a/apps/applications/serializers/__init__.py +++ b/apps/applications/serializers/__init__.py @@ -1,2 +1,3 @@ from .remote_app import * from .database_app import * +from .k8s_app import * diff --git a/apps/applications/serializers/k8s_app.py b/apps/applications/serializers/k8s_app.py new file mode 100644 index 000000000..68fafbc86 --- /dev/null +++ b/apps/applications/serializers/k8s_app.py @@ -0,0 +1,22 @@ +from rest_framework import serializers + +from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from .. import models + +__all__ = [ + 'K8sAppSerializer', +] + + +class K8sAppSerializer(BulkOrgResourceModelSerializer): + type_display = serializers.CharField(source='get_type_display', read_only=True) + + class Meta: + model = models.K8sApp + fields = [ + 'id', 'name', 'type', 'type_display', 'comment', 'created_by', + 'date_created', 'date_updated', 'cluster' + ] + read_only_fields = [ + 'id', 'created_by', 'date_created', 'date_updated', + ] diff --git a/apps/applications/urls/api_urls.py b/apps/applications/urls/api_urls.py index 1186bf1a2..42d0fe524 100644 --- a/apps/applications/urls/api_urls.py +++ b/apps/applications/urls/api_urls.py @@ -12,6 +12,7 @@ app_name = 'applications' router = BulkRouter() router.register(r'remote-apps', api.RemoteAppViewSet, 'remote-app') router.register(r'database-apps', api.DatabaseAppViewSet, 'database-app') +router.register(r'k8s-apps', api.K8sAppViewSet, 'k8s-app') urlpatterns = [ path('remote-apps//connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'), diff --git a/apps/assets/migrations/0054_auto_20200807_1032.py b/apps/assets/migrations/0054_auto_20200807_1032.py new file mode 100644 index 000000000..288b78e25 --- /dev/null +++ b/apps/assets/migrations/0054_auto_20200807_1032.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.13 on 2020-08-07 02:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0053_auto_20200723_1232'), + ] + + operations = [ + migrations.AddField( + model_name='systemuser', + name='token', + field=models.TextField(default='', verbose_name='Token'), + ), + migrations.AlterField( + model_name='systemuser', + name='protocol', + field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet'), ('vnc', 'vnc'), ('mysql', 'mysql'), ('k8s', 'k8s')], default='ssh', max_length=16, verbose_name='Protocol'), + ), + ] diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 7085a3b2b..d787cf7e7 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -91,12 +91,14 @@ class SystemUser(BaseUser): PROTOCOL_TELNET = 'telnet' PROTOCOL_VNC = 'vnc' PROTOCOL_MYSQL = 'mysql' + PROTOCOL_K8S = 'k8s' PROTOCOL_CHOICES = ( (PROTOCOL_SSH, 'ssh'), (PROTOCOL_RDP, 'rdp'), (PROTOCOL_TELNET, 'telnet'), (PROTOCOL_VNC, 'vnc'), (PROTOCOL_MYSQL, 'mysql'), + (PROTOCOL_K8S, 'k8s'), ) LOGIN_AUTO = 'auto' @@ -118,6 +120,7 @@ class SystemUser(BaseUser): login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode')) cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True) sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root")) + token = models.TextField(default='', verbose_name=_('Token')) _prefer = 'system_user' def __str__(self): diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index 7f3ad5372..1f2a05867 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -33,13 +33,14 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): 'login_mode', 'login_mode_display', 'priority', 'username_same_with_user', 'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment', - 'auto_generate_key', 'sftp_root', + 'auto_generate_key', 'sftp_root', 'token', 'assets_amount', 'date_created', 'created_by' ] extra_kwargs = { 'password': {"write_only": True}, 'public_key': {"write_only": True}, 'private_key': {"write_only": True}, + 'token': {"write_only": True}, 'nodes_amount': {'label': _('Node')}, 'assets_amount': {'label': _('Asset')}, 'login_mode_display': {'label': _('Login mode display')}, @@ -169,7 +170,7 @@ class SystemUserWithAuthInfoSerializer(SystemUserSerializer): 'login_mode', 'login_mode_display', 'priority', 'username_same_with_user', 'auto_push', 'sudo', 'shell', 'comment', - 'auto_generate_key', 'sftp_root', + 'auto_generate_key', 'sftp_root', 'token' ] extra_kwargs = { 'nodes_amount': {'label': _('Node')}, diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 70a1cb7e6a2a57ee6f9fef158505be694af3221c..27d124dbad066e7c581997b82e21ae13c5acb293 100644 GIT binary patch delta 16271 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 delta 16179 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/k8s-apps/', api.UserGrantedK8sAppsApi.as_view(), name='user-k8s-apps'), + path('k8s-apps/', api.UserGrantedK8sAppsApi.as_view(), name='my-k8s-apps'), + + # k8sApps as tree + path('/k8s-apps/tree/', api.UserGrantedK8sAppsAsTreeApi.as_view(), name='user-k8ss-apps-tree'), + path('k8s-apps/tree/', api.UserGrantedK8sAppsAsTreeApi.as_view(), name='my-k8ss-apps-tree'), + + path('/k8s-apps//system-users/', api.UserGrantedK8sAppSystemUsersApi.as_view(), name='user-k8s-app-system-users'), + path('k8s-apps//system-users/', api.UserGrantedK8sAppSystemUsersApi.as_view(), name='user-k8s-app-system-users'), +] + +user_group_permission_urlpatterns = [ + path('/k8s-apps/', api.UserGroupGrantedK8sAppsApi.as_view(), name='user-group-k8s-apps'), +] + +permission_urlpatterns = [ + path('/users/all/', api.K8sAppPermissionAllUserListApi.as_view(), name='k8s-app-permission-all-users'), + path('/k8s-apps/all/', api.K8sAppPermissionAllK8sAppListApi.as_view(), name='k8s-app-permission-all-k8s-apps'), + + path('user/validate/', api.ValidateUserK8sAppPermissionApi.as_view(), name='validate-user-k8s-app-permission'), +] + +k8s_app_permission_urlpatterns = [ + path('users/', include(user_permission_urlpatterns)), + path('user-groups/', include(user_group_permission_urlpatterns)), + path('k8s-app-permissions/', include(permission_urlpatterns)) +] + +k8s_app_permission_urlpatterns += router.urls diff --git a/apps/perms/utils/__init__.py b/apps/perms/utils/__init__.py index c6581b858..35e29adb6 100644 --- a/apps/perms/utils/__init__.py +++ b/apps/perms/utils/__init__.py @@ -4,3 +4,4 @@ from .asset_permission import * from .remote_app_permission import * from .database_app_permission import * +from .k8s_app_permission import * \ No newline at end of file diff --git a/apps/perms/utils/k8s_app_permission.py b/apps/perms/utils/k8s_app_permission.py new file mode 100644 index 000000000..578fa6380 --- /dev/null +++ b/apps/perms/utils/k8s_app_permission.py @@ -0,0 +1,93 @@ +# coding: utf-8 +# + +from django.utils.translation import ugettext as _ +from django.db.models import Q + +from orgs.utils import set_to_root_org +from ..models import K8sAppPermission +from common.tree import TreeNode +from applications.models import K8sApp +from assets.models import SystemUser + + +def get_user_k8s_app_permissions(user, include_group=True): + if include_group: + groups = user.groups.all() + arg = Q(users=user) | Q(user_groups__in=groups) + else: + arg = Q(users=user) + return K8sAppPermission.objects.all().valid().filter(arg) + + +def get_user_group_k8s_app_permission(user_group): + return K8sAppPermission.objects.all().valid().filter( + user_groups=user_group + ) + + +class K8sAppPermissionUtil: + get_permissions_map = { + 'User': get_user_k8s_app_permissions, + 'UserGroup': get_user_group_k8s_app_permission + } + + def __init__(self, obj): + self.object = obj + self.change_org_if_need() + + @staticmethod + def change_org_if_need(): + set_to_root_org() + + @property + def permissions(self): + obj_class = self.object.__class__.__name__ + func = self.get_permissions_map[obj_class] + _permissions = func(self.object) + return _permissions + + def get_k8s_apps(self): + k8s_apps = K8sApp.objects.filter( + granted_by_permissions__in=self.permissions + ).distinct() + return k8s_apps + + def get_k8s_app_system_users(self, k8s_app): + queryset = self.permissions + kwargs = {'k8s_apps': k8s_app} + queryset = queryset.filter(**kwargs) + system_users_ids = queryset.values_list('system_users', flat=True) + system_users_ids = system_users_ids.distinct() + system_users = SystemUser.objects.filter(id__in=system_users_ids) + system_users = system_users.order_by('-priority') + return system_users + + +def construct_k8s_apps_tree_root(): + tree_root = { + 'id': 'ID_K8S_APP_ROOT', + 'name': _('KubernetesApp'), + 'title': 'K8sApp', + 'pId': '', + 'open': False, + 'isParent': True, + 'iconSkin': '', + 'meta': {'type': 'k8s_app'} + } + return TreeNode(**tree_root) + + +def parse_k8s_app_to_tree_node(parent, k8s_app): + pid = parent.id if parent else '' + tree_node = { + 'id': k8s_app.id, + 'name': k8s_app.name, + 'title': k8s_app.name, + 'pId': pid, + 'open': False, + 'isParent': False, + 'iconSkin': 'file', + 'meta': {'type': 'k8s_app'} + } + return TreeNode(**tree_node) diff --git a/apps/terminal/migrations/0025_auto_20200810_1735.py b/apps/terminal/migrations/0025_auto_20200810_1735.py new file mode 100644 index 000000000..96a06915c --- /dev/null +++ b/apps/terminal/migrations/0025_auto_20200810_1735.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.13 on 2020-08-10 09:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0024_auto_20200715_1713'), + ] + + operations = [ + migrations.AlterField( + model_name='session', + name='protocol', + field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('vnc', 'vnc'), ('telnet', 'telnet'), ('mysql', 'mysql'), ('k8s', 'kubernetes')], db_index=True, default='ssh', max_length=8), + ), + ] diff --git a/apps/terminal/models.py b/apps/terminal/models.py index f3914cc1c..ba43d131a 100644 --- a/apps/terminal/models.py +++ b/apps/terminal/models.py @@ -179,6 +179,7 @@ class Session(OrgModelMixin): ('vnc', 'vnc'), ('telnet', 'telnet'), ('mysql', 'mysql'), + ('k8s', 'kubernetes') ) id = models.UUIDField(default=uuid.uuid4, primary_key=True) From 54fe4835f60df64e4401aa73d0fe211e0227dfbe Mon Sep 17 00:00:00 2001 From: xinwen Date: Tue, 11 Aug 2020 19:12:59 +0800 Subject: [PATCH 35/40] =?UTF-8?q?fix(authentication):=20`SSO`=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E6=B7=BB=E5=8A=A0=20`next=5Furl`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/api/sso.py | 25 +++++++++++++++++-------- apps/authentication/serializers.py | 1 + 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/authentication/api/sso.py b/apps/authentication/api/sso.py index 5740c80d8..b953b8d37 100644 --- a/apps/authentication/api/sso.py +++ b/apps/authentication/api/sso.py @@ -21,16 +21,19 @@ from ..filters import AuthKeyQueryDeclaration from ..mixins import AuthMixin from ..errors import SSOAuthClosed +NEXT_URL = 'next' +AUTH_KEY = 'authkey' + class SSOViewSet(AuthMixin, JmsGenericViewSet): queryset = SSOToken.objects.all() serializer_classes = { - 'get_login_url': SSOTokenSerializer, + 'login_url': SSOTokenSerializer, 'login': EmptySerializer } - @action(methods=[POST], detail=False, permission_classes=[IsSuperUser]) - def get_login_url(self, request, *args, **kwargs): + @action(methods=[POST], detail=False, permission_classes=[IsSuperUser], url_path='login-url') + def login_url(self, request, *args, **kwargs): if not settings.AUTH_SSO: raise SSOAuthClosed() @@ -39,12 +42,14 @@ class SSOViewSet(AuthMixin, JmsGenericViewSet): username = serializer.validated_data['username'] user = User.objects.get(username=username) + next_url = serializer.validated_data.get(NEXT_URL) operator = request.user.username # TODO `created_by` 和 `created_by` 可以通过 `ThreadLocal` 统一处理 token = SSOToken.objects.create(user=user, created_by=operator, updated_by=operator) query = { - 'authkey': token.authkey + AUTH_KEY: token.authkey, + NEXT_URL: next_url or '' } login_url = '%s?%s' % (reverse('api-auth:sso-login', external=True), urlencode(query)) return Response(data={'login_url': login_url}) @@ -55,7 +60,11 @@ class SSOViewSet(AuthMixin, JmsGenericViewSet): 此接口违反了 `Restful` 的规范 `GET` 应该是安全的方法,但此接口是不安全的 """ - authkey = request.query_params.get('authkey') + authkey = request.query_params.get(AUTH_KEY) + next_url = request.query_params.get(NEXT_URL) + if not next_url or not next_url.startswith('/'): + next_url = reverse('index') + try: authkey = UUID(authkey) token = SSOToken.objects.get(authkey=authkey, expired=False) @@ -63,15 +72,15 @@ class SSOViewSet(AuthMixin, JmsGenericViewSet): token.expired = True token.save() except (ValueError, SSOToken.DoesNotExist): - self.send_auth_signal(success=False, reason=f'authkey invalid: {authkey}') + self.send_auth_signal(success=False, reason='authkey_invalid') return HttpResponseRedirect(reverse('authentication:login')) # 判断是否过期 if (utcnow().timestamp() - token.date_created.timestamp()) > settings.AUTH_SSO_AUTHKEY_TTL: - self.send_auth_signal(success=False, reason=f'authkey timeout: {authkey}') + self.send_auth_signal(success=False, reason='authkey_timeout') return HttpResponseRedirect(reverse('authentication:login')) user = token.user login(self.request, user, 'authentication.backends.api.SSOAuthentication') self.send_auth_signal(success=True, user=user) - return HttpResponseRedirect(reverse('index')) + return HttpResponseRedirect(next_url) diff --git a/apps/authentication/serializers.py b/apps/authentication/serializers.py index f04b847b4..7d666db4c 100644 --- a/apps/authentication/serializers.py +++ b/apps/authentication/serializers.py @@ -81,3 +81,4 @@ class LoginConfirmSettingSerializer(serializers.ModelSerializer): class SSOTokenSerializer(serializers.Serializer): username = serializers.CharField(write_only=True) login_url = serializers.CharField(read_only=True) + next = serializers.CharField(write_only=True, allow_blank=True, required=False, allow_null=True) From 31720c9dccbf7dd5167e1c3fb5833748f026a58a Mon Sep 17 00:00:00 2001 From: xinwen Date: Tue, 11 Aug 2020 15:38:19 +0800 Subject: [PATCH 36/40] =?UTF-8?q?feat(assets):=20=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=B7=BB=E5=8A=A0=20`home`=20`system=5Fgroup?= =?UTF-8?q?s`=20=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0055_auto_20200811_1845.py | 23 +++++++++++ apps/assets/models/user.py | 3 ++ apps/assets/serializers/system_user.py | 6 ++- apps/assets/tasks/push_system_user.py | 40 ++++++++++++++++--- 4 files changed, 64 insertions(+), 8 deletions(-) create mode 100644 apps/assets/migrations/0055_auto_20200811_1845.py diff --git a/apps/assets/migrations/0055_auto_20200811_1845.py b/apps/assets/migrations/0055_auto_20200811_1845.py new file mode 100644 index 000000000..739378c78 --- /dev/null +++ b/apps/assets/migrations/0055_auto_20200811_1845.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.13 on 2020-08-11 10:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0054_auto_20200807_1032'), + ] + + operations = [ + migrations.AddField( + model_name='systemuser', + name='home', + field=models.CharField(blank=True, default='', max_length=4096, verbose_name='Home'), + ), + migrations.AddField( + model_name='systemuser', + name='system_groups', + field=models.CharField(blank=True, default='', max_length=4096, verbose_name='System groups'), + ), + ] diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index d787cf7e7..3fb862760 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -9,6 +9,7 @@ from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinValueValidator, MaxValueValidator from common.utils import signer +from common.fields.model import JsonListCharField from .base import BaseUser from .asset import Asset @@ -121,6 +122,8 @@ class SystemUser(BaseUser): cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True) sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root")) token = models.TextField(default='', verbose_name=_('Token')) + home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True) + system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True) _prefer = 'system_user' def __str__(self): diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index 1f2a05867..0fcff5b1d 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -34,7 +34,8 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): 'priority', 'username_same_with_user', 'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment', 'auto_generate_key', 'sftp_root', 'token', - 'assets_amount', 'date_created', 'created_by' + 'assets_amount', 'date_created', 'created_by', + 'home', 'system_groups' ] extra_kwargs = { 'password': {"write_only": True}, @@ -144,13 +145,14 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): class SystemUserListSerializer(SystemUserSerializer): + class Meta(SystemUserSerializer.Meta): fields = [ 'id', 'name', 'username', 'protocol', 'login_mode', 'login_mode_display', 'priority', "username_same_with_user", 'auto_push', 'sudo', 'shell', 'comment', - "assets_amount", + "assets_amount", 'home', 'system_groups', 'auto_generate_key', 'sftp_root', ] diff --git a/apps/assets/tasks/push_system_user.py b/apps/assets/tasks/push_system_user.py index eb3978178..d947be77d 100644 --- a/apps/assets/tasks/push_system_user.py +++ b/apps/assets/tasks/push_system_user.py @@ -3,9 +3,10 @@ from itertools import groupby from celery import shared_task from django.utils.translation import ugettext as _ +from django.db.models import Empty from common.utils import encrypt_password, get_logger -from orgs.utils import tmp_to_org, org_aware_func +from orgs.utils import org_aware_func from . import const from .utils import clean_ansible_task_hosts, group_asset_by_platform @@ -17,20 +18,42 @@ __all__ = [ ] +def _split_by_comma(raw: str): + try: + return [i.strip() for i in raw.split(',')] + except AttributeError: + return [] + + +def _dump_args(args: dict): + return ' '.join([f'{k}={v}' for k, v in args.items() if v is not Empty]) + + def get_push_unixlike_system_user_tasks(system_user, username=None): if username is None: username = system_user.username password = system_user.password public_key = system_user.public_key + groups = _split_by_comma(system_user.system_groups) + + if groups: + groups = '"%s"' % ','.join(groups) + + add_user_args = { + 'name': username, + 'shell': system_user.shell or Empty, + 'state': 'present', + 'home': system_user.home or Empty, + 'groups': groups or Empty + } + tasks = [ { 'name': 'Add user {}'.format(username), 'action': { 'module': 'user', - 'args': 'name={} shell={} state=present'.format( - username, system_user.shell or '/bin/bash', - ), + 'args': _dump_args(add_user_args), } }, { @@ -102,6 +125,11 @@ def get_push_windows_system_user_tasks(system_user, username=None): if username is None: username = system_user.username password = system_user.password + groups = {'Users', 'Remote Desktop Users'} + if system_user.system_groups: + groups.update(_split_by_comma(system_user.system_groups)) + groups = ','.join(groups) + tasks = [] if not password: return tasks @@ -116,9 +144,9 @@ def get_push_windows_system_user_tasks(system_user, username=None): 'update_password=always ' 'password_expired=no ' 'password_never_expires=yes ' - 'groups="Users,Remote Desktop Users" ' + 'groups="{}" ' 'groups_action=add ' - ''.format(username, username, password), + ''.format(username, username, password, groups), } } tasks.append(task) From 962ea67b8410c7c09d97f266f929ed94df9c705d Mon Sep 17 00:00:00 2001 From: xinwen Date: Wed, 12 Aug 2020 15:54:06 +0800 Subject: [PATCH 37/40] =?UTF-8?q?refactor(authentication):=20=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E8=A7=A3=E5=AF=86=E6=8A=BD=E5=8F=96=E6=88=90=E6=96=B9?= =?UTF-8?q?=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/const.py | 2 ++ apps/authentication/mixins.py | 30 +++++++++++++++++++----------- apps/authentication/views/login.py | 16 +++++++++++----- 3 files changed, 32 insertions(+), 16 deletions(-) create mode 100644 apps/authentication/const.py diff --git a/apps/authentication/const.py b/apps/authentication/const.py new file mode 100644 index 000000000..f5cf56471 --- /dev/null +++ b/apps/authentication/const.py @@ -0,0 +1,2 @@ +RSA_PRIVATE_KEY = 'rsa_private_key' +RSA_PUBLIC_KEY = 'rsa_public_key' diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 23ddac3cf..38a8a852c 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -16,6 +16,7 @@ from users.utils import ( from . import errors from .utils import rsa_decrypt from .signals import post_auth_success, post_auth_failed +from .const import RSA_PRIVATE_KEY logger = get_logger(__name__) @@ -55,7 +56,19 @@ class AuthMixin: logger.warn('Ip was blocked' + ': ' + username + ':' + ip) raise errors.BlockLoginError(username=username, ip=ip) - def check_user_auth(self): + def decrypt_passwd(self, raw_passwd): + # 获取解密密钥,对密码进行解密 + rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY) + if rsa_private_key is not None: + try: + return rsa_decrypt(raw_passwd, rsa_private_key) + except Exception as e: + logger.error(e, exc_info=True) + logger.error(f'Decrypt password faild: password[{raw_passwd}] rsa_private_key[{rsa_private_key}]') + return None + return raw_passwd + + def check_user_auth(self, decrypt_passwd=False): self.check_is_block() request = self.request if hasattr(request, 'data'): @@ -70,14 +83,9 @@ class AuthMixin: CredentialError = partial(errors.CredentialError, username=username, ip=ip, request=request) - # 获取解密密钥,对密码进行解密 - rsa_private_key = request.session.get('rsa_private_key') - if rsa_private_key is not None: - try: - password = rsa_decrypt(password, rsa_private_key) - except Exception as e: - logger.error(e, exc_info=True) - logger.error('Need decrypt password => {}'.format(password)) + if decrypt_passwd: + password = self.decrypt_passwd(password) + if not password: raise CredentialError(error=errors.reason_password_decrypt_failed) user = authenticate(request, @@ -119,14 +127,14 @@ class AuthMixin: raise errors.PasswdTooSimple(f'{flash_page_url}?{query_str}') - def check_user_auth_if_need(self): + def check_user_auth_if_need(self, decrypt_passwd=False): request = self.request if request.session.get('auth_password') and \ request.session.get('user_id'): user = self.get_user_from_session() if user: return user - return self.check_user_auth() + return self.check_user_auth(decrypt_passwd=decrypt_passwd) def check_user_mfa_if_need(self, user): if self.request.session.get('auth_mfa'): diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index d6331aa82..5493ac3c7 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -22,6 +22,7 @@ from common.utils import get_request_ip, get_object_or_none from users.utils import ( redirect_user_first_login_or_index ) +from ..const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY from .. import mixins, errors, utils from ..forms import get_user_login_form_cls @@ -82,7 +83,7 @@ class UserLoginView(mixins.AuthMixin, FormView): if not self.request.session.test_cookie_worked(): return HttpResponse(_("Please enable cookies and try again.")) try: - self.check_user_auth() + self.check_user_auth(decrypt_passwd=True) except errors.AuthFailedError as e: form.add_error(None, e.msg) ip = self.get_request_ip() @@ -94,6 +95,7 @@ class UserLoginView(mixins.AuthMixin, FormView): return self.render_to_response(context) except errors.PasswdTooSimple as e: return redirect(e.url) + self.clear_rsa_key() return self.redirect_to_guard_view() def redirect_to_guard_view(self): @@ -110,15 +112,19 @@ class UserLoginView(mixins.AuthMixin, FormView): else: return get_user_login_form_cls() + def clear_rsa_key(self): + self.request.session[RSA_PRIVATE_KEY] = None + self.request.session[RSA_PUBLIC_KEY] = None + def get_context_data(self, **kwargs): # 生成加解密密钥对,public_key传递给前端,private_key存入session中供解密使用 - rsa_private_key = self.request.session.get('rsa_private_key') - rsa_public_key = self.request.session.get('rsa_public_key') + rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY) + rsa_public_key = self.request.session.get(RSA_PUBLIC_KEY) if not all((rsa_private_key, rsa_public_key)): rsa_private_key, rsa_public_key = utils.gen_key_pair() rsa_public_key = rsa_public_key.replace('\n', '\\n') - self.request.session['rsa_private_key'] = rsa_private_key - self.request.session['rsa_public_key'] = rsa_public_key + self.request.session[RSA_PRIVATE_KEY] = rsa_private_key + self.request.session[RSA_PUBLIC_KEY] = rsa_public_key context = { 'demo_mode': os.environ.get("DEMO_MODE"), From 4cf5573c368bb0f987c08ac4b1a7ecd3d03d2570 Mon Sep 17 00:00:00 2001 From: xinwen Date: Wed, 12 Aug 2020 17:21:04 +0800 Subject: [PATCH 38/40] =?UTF-8?q?fix(users):=20=E4=BF=AE=E5=A4=8D=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=B8=8E=E7=94=A8=E6=88=B7=E7=BB=84=E5=85=B3=E7=B3=BB?= =?UTF-8?q?=E5=8F=98=E5=8C=96=E6=97=B6=E6=B2=A1=E8=A7=A6=E5=8F=91=E4=BF=A1?= =?UTF-8?q?=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/users/api/relation.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/users/api/relation.py b/apps/users/api/relation.py index fbab92ee9..218d52142 100644 --- a/apps/users/api/relation.py +++ b/apps/users/api/relation.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- # -from rest_framework_bulk import BulkModelViewSet from django.db.models import F +from common.drf.api import JMSBulkRelationModelViewSet from common.permissions import IsOrgAdmin from .. import serializers from ..models import User @@ -11,17 +11,17 @@ from ..models import User __all__ = ['UserUserGroupRelationViewSet'] -class UserUserGroupRelationViewSet(BulkModelViewSet): +class UserUserGroupRelationViewSet(JMSBulkRelationModelViewSet): filter_fields = ('user', 'usergroup') search_fields = filter_fields serializer_class = serializers.UserUserGroupRelationSerializer permission_classes = (IsOrgAdmin,) + m2m_field = User.groups.field def get_queryset(self): - queryset = User.groups.through.objects.all()\ - .annotate(user_display=F('user__name'))\ - .annotate(usergroup_display=F('usergroup__name')) - return queryset + return super().get_queryset().annotate( + user_display=F('user__name'), usergroup_display=F('usergroup__name') + ) def allow_bulk_destroy(self, qs, filtered): if filtered.count() != 1: From 21b4a8600c5eb8597a87dcb59d60c0064715f880 Mon Sep 17 00:00:00 2001 From: xinwen Date: Thu, 13 Aug 2020 14:09:40 +0800 Subject: [PATCH 39/40] =?UTF-8?q?fix(terminal):=20`Session`=20`can=5Fjoin`?= =?UTF-8?q?=20=E6=B7=BB=E5=8A=A0=20`k8s`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/terminal/models.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/apps/terminal/models.py b/apps/terminal/models.py index ba43d131a..6fdf8e349 100644 --- a/apps/terminal/models.py +++ b/apps/terminal/models.py @@ -16,6 +16,7 @@ from users.models import User from orgs.mixins.models import OrgModelMixin from common.mixins import CommonModelMixin from common.fields.model import EncryptJsonDictTextField +from common.db.models import ChoiceSet from .backends import get_multi_command_storage from .backends.command.models import AbstractSessionCommand from . import const @@ -169,18 +170,17 @@ class Status(models.Model): class Session(OrgModelMixin): - LOGIN_FROM_CHOICES = ( - ('ST', 'SSH Terminal'), - ('WT', 'Web Terminal'), - ) - PROTOCOL_CHOICES = ( - ('ssh', 'ssh'), - ('rdp', 'rdp'), - ('vnc', 'vnc'), - ('telnet', 'telnet'), - ('mysql', 'mysql'), - ('k8s', 'kubernetes') - ) + class LOGIN_FROM(ChoiceSet): + ST = 'ST', 'SSH Terminal' + WT = 'WT', 'Web Terminal' + + class PROTOCOL(ChoiceSet): + SSH = 'ssh', 'ssh' + RDP = 'rdp', 'rdp' + VNC = 'vnc', 'vnc' + TELNET = 'telnet', 'telnet' + MYSQL = 'mysql', 'mysql' + K8S = 'k8s', 'kubernetes' id = models.UUIDField(default=uuid.uuid4, primary_key=True) user = models.CharField(max_length=128, verbose_name=_("User"), db_index=True) @@ -189,14 +189,14 @@ class Session(OrgModelMixin): asset_id = models.CharField(blank=True, default='', max_length=36, db_index=True) system_user = models.CharField(max_length=128, verbose_name=_("System user"), db_index=True) system_user_id = models.CharField(blank=True, default='', max_length=36, db_index=True) - login_from = models.CharField(max_length=2, choices=LOGIN_FROM_CHOICES, default="ST", verbose_name=_("Login from")) + login_from = models.CharField(max_length=2, choices=LOGIN_FROM.choices, default="ST", verbose_name=_("Login from")) remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) is_success = models.BooleanField(default=True, db_index=True) is_finished = models.BooleanField(default=False, db_index=True) has_replay = models.BooleanField(default=False, verbose_name=_("Replay")) has_command = models.BooleanField(default=False, verbose_name=_("Command")) terminal = models.ForeignKey(Terminal, null=True, on_delete=models.SET_NULL) - protocol = models.CharField(choices=PROTOCOL_CHOICES, default='ssh', max_length=8, db_index=True) + protocol = models.CharField(choices=PROTOCOL.choices, default='ssh', max_length=8, db_index=True) date_start = models.DateTimeField(verbose_name=_("Date start"), db_index=True, default=timezone.now) date_end = models.DateTimeField(verbose_name=_("Date end"), null=True) @@ -246,9 +246,10 @@ class Session(OrgModelMixin): @property def can_join(self): + _PROTOCOL = self.PROTOCOL if self.is_finished: return False - if self.protocol not in ['ssh', 'telnet', 'mysql']: + if self.protocol not in [_PROTOCOL.SSH, _PROTOCOL.TELNET, _PROTOCOL.MYSQL, _PROTOCOL.K8S]: return False return True From 0e0c9275bd995d70ec1bb0c76d73c92200704dec Mon Sep 17 00:00:00 2001 From: xinwen Date: Thu, 13 Aug 2020 16:47:58 +0800 Subject: [PATCH 40/40] =?UTF-8?q?fix(application):=20=E8=BF=9C=E7=A8=8B?= =?UTF-8?q?=E5=BA=94=E7=94=A8`MySQL=20Workbench`=E6=B7=BB=E5=8A=A0`port`?= =?UTF-8?q?=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/applications/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/applications/const.py b/apps/applications/const.py index af3531c36..773f30b9f 100644 --- a/apps/applications/const.py +++ b/apps/applications/const.py @@ -23,6 +23,7 @@ REMOTE_APP_TYPE_CHROME_FIELDS = [ REMOTE_APP_TYPE_MYSQL_WORKBENCH_FIELDS = [ {'name': 'mysql_workbench_ip'}, {'name': 'mysql_workbench_name'}, + {'name': 'mysql_workbench_port'}, {'name': 'mysql_workbench_username'}, {'name': 'mysql_workbench_password', 'write_only': True} ]