From 17ddb3bbbd095c678d1f06106605e21a44ab4523 Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 13 Dec 2017 17:21:08 +0800 Subject: [PATCH] =?UTF-8?q?[Feture]=20=E4=BD=BF=E7=94=A8=E4=BF=A1=E5=8F=B7?= =?UTF-8?q?=E8=A7=A3=E8=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/__init__.py | 1 - apps/assets/api.py | 5 +- apps/assets/apps.py | 1 + apps/assets/const.py | 6 +- apps/assets/forms.py | 169 +++++++------- apps/assets/hands.py | 2 +- apps/assets/models/cluster.py | 2 +- apps/assets/models/user.py | 140 +++++------ apps/assets/serializers.py | 7 +- apps/assets/signals.py | 29 +-- apps/assets/tasks.py | 221 ++++++++++++++---- .../assets/templates/assets/_system_user.html | 54 ++--- .../templates/assets/admin_user_detail.html | 41 +++- .../assets/templates/assets/asset_detail.html | 2 +- .../templates/assets/system_user_asset.html | 16 +- .../templates/assets/system_user_auth.html | 73 ++++++ .../templates/assets/system_user_detail.html | 58 ++++- apps/assets/urls/views_urls.py | 1 + apps/assets/views/admin_user.py | 33 +-- apps/assets/views/asset.py | 6 - apps/assets/views/system_user.py | 68 ++++-- apps/common/celery.py | 18 -- apps/common/utils.py | 9 +- apps/fixtures/init.json | 2 +- apps/static/css/jumpserver.css | 4 +- apps/templates/_base_list.html | 3 - apps/templates/_message.html | 22 ++ apps/templates/base.html | 24 +- apps/terminal/views/session.py | 2 +- 29 files changed, 603 insertions(+), 416 deletions(-) create mode 100644 apps/assets/templates/assets/system_user_auth.html diff --git a/apps/assets/__init__.py b/apps/assets/__init__.py index a3128cc9a..e69de29bb 100644 --- a/apps/assets/__init__.py +++ b/apps/assets/__init__.py @@ -1 +0,0 @@ -from . import signals diff --git a/apps/assets/api.py b/apps/assets/api.py index 6df0c184e..4d99ea836 100644 --- a/apps/assets/api.py +++ b/apps/assets/api.py @@ -22,7 +22,7 @@ from django.shortcuts import get_object_or_404 from common.mixins import IDInFilterMixin from common.utils import get_object_or_none -from .hands import IsSuperUser, IsAppUser, IsValidUser, \ +from .hands import IsSuperUser, IsAppUser, IsValidUser, IsSuperUserOrAppUser, \ get_user_granted_assets, push_users from .models import AssetGroup, Asset, Cluster, SystemUser, AdminUser from . import serializers @@ -156,7 +156,7 @@ class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView): class SystemUserAuthInfoApi(generics.RetrieveAPIView): """Get system user auth info""" queryset = SystemUser.objects.all() - permission_classes = (IsAppUser,) + permission_classes = (IsSuperUserOrAppUser,) def retrieve(self, request, *args, **kwargs): system_user = self.get_object() @@ -166,7 +166,6 @@ class SystemUserAuthInfoApi(generics.RetrieveAPIView): 'username': system_user.username, 'password': system_user.password, 'private_key': system_user.private_key, - 'auth_method': system_user.auth_method, } return Response(data) diff --git a/apps/assets/apps.py b/apps/assets/apps.py index 41eead508..15733d634 100644 --- a/apps/assets/apps.py +++ b/apps/assets/apps.py @@ -8,5 +8,6 @@ class AssetsConfig(AppConfig): def ready(self): from .signals import on_app_ready + from . import tasks on_app_ready.send(self.__class__) super().ready() diff --git a/apps/assets/const.py b/apps/assets/const.py index 36a539c20..820d07b65 100644 --- a/apps/assets/const.py +++ b/apps/assets/const.py @@ -2,4 +2,8 @@ # ADMIN_USER_CONN_CACHE_KEY_PREFIX = "ADMIN_USER_CONN_" -SYSTEM_USER_CONN_CACHE_KEY_PREFIX = 'SYSTEM_USER_CONN_' +SYSTEM_USER_CONN_CACHE_KEY_PREFIX = "SYSTEM_USER_CONN_" +UPDATE_ASSETS_HARDWARE_PERIOD_LOCK_KEY = "UPDATE_ASSETS_HARDWARE_PERIOD_LOCK_KEY" +TEST_ADMIN_USER_CONNECTABILITY_PEROID_KEY = "TEST_ADMIN_USER_CONNECTABILITY_KEY" +TEST_SYSTEM_USER_CONNECTABILITY_PEROID_KEY = "TEST_SYSTEM_USER_CONNECTABILITY_PEROID_KEY" +PUSH_SYSTEM_USER_PERIOD_KEY = "PUSH_SYSTEM_USER_PERIOD_KEY" diff --git a/apps/assets/forms.py b/apps/assets/forms.py index a067a4a3c..bfaa3bf50 100644 --- a/apps/assets/forms.py +++ b/apps/assets/forms.py @@ -1,4 +1,5 @@ # coding:utf-8 +import uuid from django import forms from django.utils.translation import gettext_lazy as _ @@ -142,7 +143,7 @@ class ClusterForm(forms.ModelForm): class AdminUserForm(forms.ModelForm): # Form field name can not start with `_`, so redefine it, password = forms.CharField( - widget=forms.PasswordInput, max_length=100, + widget=forms.PasswordInput, max_length=128, strip=True, required=False, help_text=_('If also set private key, use that first'), ) @@ -199,114 +200,46 @@ class SystemUserForm(forms.ModelForm): # Admin user assets define, let user select, save it in form not in view auto_generate_key = forms.BooleanField(initial=True, required=False) # Form field name can not start with `_`, so redefine it, - password = forms.CharField(widget=forms.PasswordInput, required=False, max_length=100, strip=True) + password = forms.CharField(widget=forms.PasswordInput, required=False, max_length=128, strip=True) # Need use upload private key file except paste private key content private_key_file = forms.FileField(required=False) - def __init__(self, *args, **kwargs): - super(SystemUserForm, self).__init__(*args, **kwargs) - def save(self, commit=True): # Because we define custom field, so we need rewrite :method: `save` - system_user = super(SystemUserForm, self).save(commit=commit) - password = self.cleaned_data['password'] + system_user = super().save() + password = self.cleaned_data.get('password', None) private_key_file = self.cleaned_data.get('private_key_file') + auto_generate_key = self.cleaned_data.get('auto_generate_key') - if system_user.auth_method == 'P': - if password: - system_user.password = password - elif system_user.auth_method == 'K': - if self.cleaned_data['auto_generate_key']: - private_key, public_key = ssh_key_gen(username=system_user.name) - logger.info('Generate private key and public key') - else: - private_key = private_key_file.read().strip() - public_key = ssh_pubkey_gen(private_key=private_key) - system_user.private_key = private_key - system_user.public_key = public_key - system_user.save() - return self.instance - - def clean_private_key_file(self): - if self.data['auth_method'] == 'K' and \ - not self.cleaned_data['auto_generate_key']: - if not self.cleaned_data['private_key_file']: - raise forms.ValidationError(_('Private key required')) - else: - key_string = self.cleaned_data['private_key_file'].read() - self.cleaned_data['private_key_file'].seek(0) - if not validate_ssh_private_key(key_string): - raise forms.ValidationError(_('Invalid private key')) - return self.cleaned_data['private_key_file'] - - def clean_password(self): - if self.data['auth_method'] == 'P': - if not self.cleaned_data.get('password'): - raise forms.ValidationError(_('Password required')) - return self.cleaned_data['password'] - - class Meta: - model = SystemUser - fields = [ - 'name', 'username', 'protocol', 'auto_generate_key', 'password', - 'private_key_file', 'auth_method', 'auto_push', 'sudo', - 'comment', 'shell', 'cluster' - ] - widgets = { - 'name': forms.TextInput(attrs={'placeholder': _('Name')}), - 'username': forms.TextInput(attrs={'placeholder': _('Username')}), - 'cluster': forms.SelectMultiple( - attrs={'class': 'select2', - 'data-placeholder': _(' Select clusters')}), - } - help_texts = { - 'name': '* required', - 'username': '* required', - 'cluster': 'If auto push checked, then push system user to that cluster assets', - 'auto_push': 'Auto push system user to asset', - } - - -class SystemUserUpdateForm(forms.ModelForm): - # Admin user assets define, let user select, save it in form not in view - auto_generate_key = forms.BooleanField(initial=False, required=False) - # Form field name can not start with `_`, so redefine it, - password = forms.CharField(widget=forms.PasswordInput, required=False, max_length=100, strip=True) - # Need use upload private key file except paste private key content - private_key_file = forms.FileField(required=False) - - def __init__(self, *args, **kwargs): - super(SystemUserUpdateForm, self).__init__(*args, **kwargs) - - def save(self, commit=True): - # Because we define custom field, so we need rewrite :method: `save` - system_user = super(SystemUserUpdateForm, self).save(commit=commit) - password = self.cleaned_data['password'] - private_key_file = self.cleaned_data.get('private_key_file') - - if system_user.auth_method == 'P' and password: - system_user.password = password - elif system_user.auth_method == 'K' and private_key_file: - private_key = private_key_file.read().strip() + if auto_generate_key: + logger.info('Auto set system user auth') + system_user.auto_gen_auth() + else: + private_key = private_key_file.read().strip().decode('utf-8') public_key = ssh_pubkey_gen(private_key=private_key) - system_user.private_key = private_key - system_user.public_key = public_key - system_user.save() - return self.instance + system_user.set_auth(password=password, private_key=private_key, public_key=public_key) + return system_user def clean_private_key_file(self): - if self.data['auth_method'] == 'K' and self.cleaned_data['private_key_file']: + if self.cleaned_data.get('private_key_file'): key_string = self.cleaned_data['private_key_file'].read() self.cleaned_data['private_key_file'].seek(0) if not validate_ssh_private_key(key_string): raise forms.ValidationError(_('Invalid private key')) return self.cleaned_data['private_key_file'] + def clean_password(self): + if not self.cleaned_data.get('password') and \ + not self.cleaned_data.get('private_key_file') and \ + not self.cleaned_data.get('auto_generate_key'): + raise forms.ValidationError(_('Auth info required, private_key or password')) + return self.cleaned_data['password'] + class Meta: model = SystemUser fields = [ - 'name', 'username', 'protocol', - 'auth_method', 'auto_push', 'sudo', + 'name', 'username', 'protocol', 'auto_generate_key', + 'password', 'private_key_file', 'auto_push', 'sudo', 'comment', 'shell', 'cluster' ] widgets = { @@ -319,10 +252,64 @@ class SystemUserUpdateForm(forms.ModelForm): help_texts = { 'name': '* required', 'username': '* required', - 'cluster': 'If auto push checked, then push system user to that cluster assets', + 'cluster': 'If auto push checked, system user will be create at cluster assets', 'auto_push': 'Auto push system user to asset', } +class SystemUserUpdateForm(forms.ModelForm): + class Meta: + model = SystemUser + fields = [ + 'name', 'username', 'protocol', + 'sudo', 'comment', 'shell', 'cluster' + ] + widgets = { + 'name': forms.TextInput(attrs={'placeholder': _('Name')}), + 'username': forms.TextInput(attrs={'placeholder': _('Username')}), + 'cluster': forms.SelectMultiple( + attrs={'class': 'select2', + 'data-placeholder': _(' Select clusters')}), + } + help_texts = { + 'name': '* required', + 'username': '* required', + 'cluster': 'If auto push checked, then push system user to that cluster assets', + } + + +class SystemUserAuthForm(forms.Form): + password = forms.CharField(widget=forms.PasswordInput, required=False, max_length=128, strip=True) + private_key_file = forms.FileField(required=False) + + def clean_private_key_file(self): + if self.cleaned_data.get('private_key_file'): + key_string = self.cleaned_data['private_key_file'].read() + self.cleaned_data['private_key_file'].seek(0) + if not validate_ssh_private_key(key_string): + raise forms.ValidationError(_('Invalid private key')) + return self.cleaned_data['private_key_file'] + + def clean_password(self): + if not self.cleaned_data.get('password') and \ + not self.cleaned_data.get('private_key_file'): + msg = _('Auth info required, private_key or password') + raise forms.ValidationError(msg) + return self.cleaned_data['password'] + + def update(self, system_user): + password = self.cleaned_data.get('password') + private_key_file = self.cleaned_data.get('private_key_file') + + if private_key_file: + private_key = private_key_file.read().strip() + public_key = ssh_pubkey_gen(private_key=private_key) + else: + private_key = None + public_key = None + system_user.set_auth(password=password, private_key=private_key, public_key=public_key) + return system_user + + class FileForm(forms.Form): file = forms.FileField() diff --git a/apps/assets/hands.py b/apps/assets/hands.py index 7d9986156..1545bce62 100644 --- a/apps/assets/hands.py +++ b/apps/assets/hands.py @@ -12,7 +12,7 @@ from users.utils import AdminUserRequiredMixin -from users.permissions import IsAppUser, IsSuperUser, IsValidUser +from users.permissions import IsAppUser, IsSuperUser, IsValidUser, IsSuperUserOrAppUser from users.models import User, UserGroup from perms.utils import get_user_granted_assets from perms.tasks import push_users \ No newline at end of file diff --git a/apps/assets/models/cluster.py b/apps/assets/models/cluster.py index be90a1be6..3b66555c3 100644 --- a/apps/assets/models/cluster.py +++ b/apps/assets/models/cluster.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) class Cluster(models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=32, verbose_name=_('Name')) - admin_user = models.ForeignKey('assets.AdminUser', null=True, on_delete=models.SET_NULL, verbose_name=_("Admin user")) + admin_user = models.ForeignKey('assets.AdminUser', null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_("Admin user")) bandwidth = models.CharField(max_length=32, blank=True, verbose_name=_('Bandwidth')) contact = models.CharField(max_length=128, blank=True, verbose_name=_('Contact')) phone = models.CharField(max_length=32, blank=True, verbose_name=_('Phone')) diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index e7df0f151..073b7809d 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- # -from __future__ import unicode_literals import os import logging import uuid @@ -12,7 +11,7 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from django.conf import settings -from common.utils import signer, ssh_key_string_to_obj +from common.utils import signer, ssh_key_string_to_obj, ssh_key_gen from .utils import private_key_validator @@ -20,53 +19,41 @@ __all__ = ['AdminUser', 'SystemUser',] logger = logging.getLogger(__name__) -class AdminUser(models.Model): - """ - A privileged user that ansible can use it to push system user and so on - """ - BECOME_METHOD_CHOICES = ( - ('sudo', 'sudo'), - ('su', 'su'), - ) +class AssetUser(models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, unique=True, verbose_name=_('Name')) username = models.CharField(max_length=16, verbose_name=_('Username')) _password = models.CharField(max_length=256, blank=True, null=True, verbose_name=_('Password')) - _private_key = models.TextField(max_length=4096, blank=True, null=True, verbose_name=_('SSH private key'), validators=[private_key_validator,]) - become = models.BooleanField(default=True) - become_method = models.CharField(choices=BECOME_METHOD_CHOICES, default='sudo', max_length=4) - become_user = models.CharField(default='root', max_length=64) - _become_pass = models.CharField(default='', max_length=128) + _private_key = models.TextField(max_length=4096, blank=True, null=True, verbose_name=_('SSH private key'), validators=[private_key_validator, ]) _public_key = models.TextField(max_length=4096, blank=True, verbose_name=_('SSH public key')) comment = models.TextField(blank=True, verbose_name=_('Comment')) date_created = models.DateTimeField(auto_now_add=True, null=True) created_by = models.CharField(max_length=32, null=True, verbose_name=_('Created by')) - def __str__(self): - return self.name - @property def password(self): if self._password: return signer.unsign(self._password) else: - return '' + return None @password.setter def password(self, password_raw): - self._password = signer.sign(password_raw) + raise AttributeError("Using set_auth do that") + # self._password = signer.sign(password_raw) @property def private_key(self): if self._private_key: key_str = signer.unsign(self._private_key) - return ssh_key_string_to_obj(key_str) + return ssh_key_string_to_obj(key_str, password=self.password) else: return None @private_key.setter def private_key(self, private_key_raw): - self._private_key = signer.sign(private_key_raw) + raise AttributeError("Using set_auth do that") + # self._private_key = signer.sign(private_key_raw) @property def private_key_file(self): @@ -74,19 +61,62 @@ class AdminUser(models.Model): return None project_dir = settings.PROJECT_DIR tmp_dir = os.path.join(project_dir, 'tmp') - key_name = md5(self._private_key.encode()).hexdigest() + key_str = signer.unsign(self._private_key) + key_name = md5(key_str.encode('utf-8')).hexdigest() key_path = os.path.join(tmp_dir, key_name) if not os.path.exists(key_path): - self.private_key.write_private_key_file(key_path) + with open(key_path, 'w') as f: + f.write(key_str) + os.chmod(key_path, 0o400) return key_path @property def public_key(self): return signer.unsign(self._public_key) - @public_key.setter - def public_key(self, public_key_raw): - self._public_key = signer.sign(public_key_raw) + def set_auth(self, password=None, private_key=None, public_key=None): + update_fields = [] + if password: + self._password = signer.sign(password) + update_fields.append('_password') + if private_key: + self._private_key = signer.sign(private_key) + update_fields.append('_private_key') + if public_key: + self._public_key = signer.sign(public_key) + update_fields.append('_public_key') + + if update_fields: + self.save(update_fields=update_fields) + + def auto_gen_auth(self): + password = str(uuid.uuid4()) + private_key, public_key = ssh_key_gen( + username=self.name, password=password + ) + self.set_auth(password=password, + private_key=private_key, + public_key=public_key) + + class Meta: + abstract = True + + +class AdminUser(AssetUser): + """ + A privileged user that ansible can use it to push system user and so on + """ + BECOME_METHOD_CHOICES = ( + ('sudo', 'sudo'), + ('su', 'su'), + ) + become = models.BooleanField(default=True) + become_method = models.CharField(choices=BECOME_METHOD_CHOICES, default='sudo', max_length=4) + become_user = models.CharField(default='root', max_length=64) + _become_pass = models.CharField(default='', max_length=128) + + def __str__(self): + return self.name @property def become_pass(self): @@ -131,7 +161,7 @@ class AdminUser(models.Model): continue -class SystemUser(models.Model): +class SystemUser(AssetUser): PROTOCOL_CHOICES = ( ('ssh', 'ssh'), ) @@ -139,68 +169,15 @@ class SystemUser(models.Model): ('P', 'Password'), ('K', 'Public key'), ) - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - name = models.CharField(max_length=128, unique=True, verbose_name=_('Name')) - username = models.CharField(max_length=16, verbose_name=_('Username')) cluster = models.ManyToManyField('assets.Cluster', verbose_name=_("Cluster")) - _password = models.CharField(max_length=256, blank=True, verbose_name=_('Password')) protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol')) - _private_key = models.TextField(max_length=8192, blank=True, verbose_name=_('SSH private key')) - _public_key = models.TextField(max_length=8192, blank=True, verbose_name=_('SSH public key')) - auth_method = models.CharField(choices=AUTH_METHOD_CHOICES, default='K', max_length=1, verbose_name=_('Auth method')) auto_push = models.BooleanField(default=True, verbose_name=_('Auto push')) sudo = models.TextField(default='/sbin/ifconfig', verbose_name=_('Sudo')) shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell')) - date_created = models.DateTimeField(auto_now_add=True) - created_by = models.CharField(max_length=32, blank=True, verbose_name=_('Created by')) - comment = models.TextField(max_length=128, blank=True, verbose_name=_('Comment')) def __str__(self): return self.name - @property - def password(self): - if self._password: - return signer.unsign(self._password) - return None - - @password.setter - def password(self, password_raw): - self._password = signer.sign(password_raw) - - @property - def private_key(self): - if self._private_key: - return signer.unsign(self._private_key) - return None - - @private_key.setter - def private_key(self, private_key_raw): - self._private_key = signer.sign(private_key_raw) - - @property - def private_key_file(self): - if not self.private_key: - return None - project_dir = settings.PROJECT_DIR - tmp_dir = os.path.join(project_dir, 'tmp') - key_name = md5(self._private_key.encode()).hexdigest() - key_path = os.path.join(tmp_dir, key_name) - if not os.path.exists(key_path): - self.private_key.write_private_key_file(key_path) - return key_path - - @property - def public_key(self): - if self._public_key: - return signer.unsign(self._public_key) - else: - return None - - @public_key.setter - def public_key(self, public_key_raw): - self._public_key = signer.sign(public_key_raw) - def _to_secret_json(self): """Push system user use it""" return { @@ -228,7 +205,6 @@ class SystemUser(models.Model): 'name': self.name, 'username': self.username, 'protocol': self.protocol, - 'auth_method': self.auth_method, 'auto_push': self.auto_push, } diff --git a/apps/assets/serializers.py b/apps/assets/serializers.py index a22de9ef9..c46942ad0 100644 --- a/apps/assets/serializers.py +++ b/apps/assets/serializers.py @@ -115,7 +115,7 @@ class SystemUserSerializer(serializers.ModelSerializer): class AssetSystemUserSerializer(serializers.ModelSerializer): class Meta: model = SystemUser - fields = ('id', 'name', 'username', 'protocol', 'auth_method', 'comment') + fields = ('id', 'name', 'username', 'protocol', 'comment') class SystemUserUpdateAssetsSerializer(serializers.ModelSerializer): @@ -202,7 +202,10 @@ class ClusterSerializer(BulkSerializerMixin, serializers.ModelSerializer): @staticmethod def get_admin_user_name(obj): - return obj.admin_user.name + try: + return obj.admin_user.name + except AttributeError: + return '' class AssetGroupGrantedSerializer(BulkSerializerMixin, serializers.ModelSerializer): diff --git a/apps/assets/signals.py b/apps/assets/signals.py index d8223eaba..a02a16d20 100644 --- a/apps/assets/signals.py +++ b/apps/assets/signals.py @@ -1,31 +1,6 @@ # -*- coding: utf-8 -*- # +from django.dispatch import Signal -from django.dispatch import Signal, receiver - -from common.utils import get_logger - -logger = get_logger(__file__) -on_asset_created = Signal(providing_args=['asset']) on_app_ready = Signal() - - -@receiver(on_asset_created) -def update_asset_info(sender, asset=None, **kwargs): - from .tasks import update_assets_hardware_info - logger.debug("Receive asset create signal, update asset hardware info") - update_assets_hardware_info.delay([asset]) - - -@receiver(on_asset_created) -def test_admin_user_connective(sender, asset=None, **kwargs): - from .tasks import test_admin_user_connectability_manual - logger.debug("Receive asset create signal, test admin user connectability") - test_admin_user_connectability_manual.delay(asset) - - -@receiver(on_app_ready) -def test_admin_user_on_app_ready(sender, **kwargs): - from .tasks import test_admin_user_connectability_period - logger.debug("Receive app ready signal, test admin connectability") - test_admin_user_connectability_period.delay() +on_system_user_auth_changed = Signal(providing_args=['system_user']) diff --git a/apps/assets/tasks.py b/apps/assets/tasks.py index 46d85db5d..e459039c6 100644 --- a/apps/assets/tasks.py +++ b/apps/assets/tasks.py @@ -3,15 +3,23 @@ import json from celery import shared_task from django.core.cache import cache +from django.dispatch import receiver +from django.db.models.signals import post_save -from common.utils import get_object_or_none, capacity_convert, sum_capacity, encrypt_password, get_logger +from common.utils import get_object_or_none, capacity_convert, \ + sum_capacity, encrypt_password, get_logger +from common.celery import app as celery_app from .models import SystemUser, AdminUser, Asset -from .const import ADMIN_USER_CONN_CACHE_KEY_PREFIX, SYSTEM_USER_CONN_CACHE_KEY_PREFIX +from .const import ADMIN_USER_CONN_CACHE_KEY_PREFIX, SYSTEM_USER_CONN_CACHE_KEY_PREFIX, \ + UPDATE_ASSETS_HARDWARE_PERIOD_LOCK_KEY, TEST_ADMIN_USER_CONNECTABILITY_PEROID_KEY, \ + TEST_SYSTEM_USER_CONNECTABILITY_PEROID_KEY, PUSH_SYSTEM_USER_PERIOD_KEY +from .signals import on_app_ready FORKS = 10 TIMEOUT = 60 logger = get_logger(__file__) +CACHE_MAX_TIME = 60*60*60 @shared_task @@ -75,7 +83,7 @@ def update_assets_hardware_info(assets): asset.save() for hostname, task in summary['dark'].items(): - logger.warn("Update {} hardware info error: {}".format( + logger.error("Update {} hardware info error: {}".format( hostname, task[name], )) @@ -88,8 +96,15 @@ def update_assets_hardware_period(): Update asset hardware period task :return: """ - assets = Asset.objects.filter(type__in=['Server', 'VM']) - update_assets_hardware_info(assets) + if cache.get(UPDATE_ASSETS_HARDWARE_PERIOD_LOCK_KEY) == 1: + logger.debug("Update asset hardware period task is running, passed") + return {} + try: + cache.set(UPDATE_ASSETS_HARDWARE_PERIOD_LOCK_KEY, 1, CACHE_MAX_TIME) + assets = Asset.objects.filter(type__in=['Server', 'VM']) + return update_assets_hardware_info(assets) + finally: + cache.set(UPDATE_ASSETS_HARDWARE_PERIOD_LOCK_KEY, 0) @shared_task @@ -100,6 +115,7 @@ def test_admin_user_connectability(admin_user): :return: """ from ops.utils import run_adhoc + assets = admin_user.get_related_assets() hosts = [asset.hostname for asset in assets] tasks = [ @@ -117,16 +133,26 @@ def test_admin_user_connectability(admin_user): @shared_task def test_admin_user_connectability_period(): # assets = Asset.objects.filter(type__in=['Server', 'VM']) - admin_users = AdminUser.objects.all() - for admin_user in admin_users: - summary = test_admin_user_connectability(admin_user) + if cache.get(TEST_ADMIN_USER_CONNECTABILITY_PEROID_KEY) == 1: + logger.debug("Test admin user connectablity period task is running, passed") + return - cache.set(ADMIN_USER_CONN_CACHE_KEY_PREFIX + admin_user.name, summary, 60*60*60) - for i in summary['contacted']: - cache.set(ADMIN_USER_CONN_CACHE_KEY_PREFIX + i, 1, 60*60*60) + logger.debug("Test admin user connectablity period task start") + try: + cache.set(TEST_ADMIN_USER_CONNECTABILITY_PEROID_KEY, 1, CACHE_MAX_TIME) + admin_users = AdminUser.objects.all() + for admin_user in admin_users: + summary = test_admin_user_connectability(admin_user) - for i in summary['dark']: - cache.set(ADMIN_USER_CONN_CACHE_KEY_PREFIX + i, 0, 60*60*60) + cache.set(ADMIN_USER_CONN_CACHE_KEY_PREFIX + admin_user.name, summary, 60*60*60) + for i in summary['contacted']: + cache.set(ADMIN_USER_CONN_CACHE_KEY_PREFIX + i, 1, 60*60*60) + + for i, error in summary['dark'].items(): + cache.set(ADMIN_USER_CONN_CACHE_KEY_PREFIX + i, 0, 60*60*60) + logger.error(error) + finally: + cache.set(TEST_ADMIN_USER_CONNECTABILITY_PEROID_KEY, 0) @shared_task @@ -175,9 +201,18 @@ def test_system_user_connectability(system_user): @shared_task def test_system_user_connectability_period(): - for system_user in SystemUser.objects.all(): - summary = test_system_user_connectability(system_user) - cache.set(SYSTEM_USER_CONN_CACHE_KEY_PREFIX + system_user.name, summary, 60*60*60) + if cache.get(TEST_SYSTEM_USER_CONNECTABILITY_PEROID_KEY) == 1: + logger.debug("Test admin user connectablity period task is running, passed") + return + + logger.debug("Test system user connectablity period task start") + try: + cache.set(TEST_SYSTEM_USER_CONNECTABILITY_PEROID_KEY, 1, CACHE_MAX_TIME) + for system_user in SystemUser.objects.all(): + summary = test_system_user_connectability(system_user) + cache.set(SYSTEM_USER_CONN_CACHE_KEY_PREFIX + system_user.name, summary, 60*60*60) + finally: + cache.set(TEST_SYSTEM_USER_CONNECTABILITY_PEROID_KEY, 0) def get_push_system_user_tasks(system_user): @@ -217,18 +252,18 @@ def get_push_system_user_tasks(system_user): return tasks -PUSH_SYSTEM_USER_PERIOD_TASK_NAME = 'PUSH SYSTEM USER [{}] PERIOD...' -PUSH_SYSTEM_USER_TASK_NAME = 'PUSH SYSTEM USER [{}] ASSETS' - - -def push_system_user(system_user, assets, name): +def push_system_user(system_user, assets, task_name=None): from ops.utils import get_task_by_name, run_adhoc_object, \ create_task, create_adhoc if system_user.auto_push and assets: - task = get_task_by_name(name) + if task_name is None: + task_name = 'PUSH-SYSTEM-USER-{}'.format(system_user.name) + + task = get_task_by_name(task_name) if not task: - task = create_task(name, created_by="System") + logger.debug("Doesn't get task {}, create it".format(task_name)) + task = create_task(task_name, created_by="System") task.save() tasks = get_push_system_user_tasks(system_user) hosts = [asset.hostname for asset in assets] @@ -236,36 +271,124 @@ def push_system_user(system_user, assets, name): adhoc = task.get_latest_adhoc() if not adhoc or adhoc.task != tasks or adhoc.hosts != hosts: + logger.debug("Task {} not exit or changed, create new version".format(task_name)) adhoc = create_adhoc(task=task, tasks=tasks, pattern='all', options=options, hosts=hosts, run_as_admin=True) - return run_adhoc_object(adhoc) + logger.debug("Task {} start execute".format(task_name)) + result = run_adhoc_object(adhoc) + return result.results_summary + else: + msg = "Task {} does'nt execute, because not auto_push " \ + "is not True, or not assets".format(task_name) + logger.debug(msg) + return {} + + +@shared_task +def push_system_user_to_cluster_assets(system_user, task_name=None): + logger.debug("{} task start".format(task_name)) + assets = system_user.get_clusters_assets() + summary = push_system_user(system_user, assets, task_name) + + for h in summary.get("contacted", []): + logger.debug("Push system user {} to {} success".format(system_user.name, h)) + for h, msg in summary.get('dark', {}).items(): + logger.error('Push system user {} to {} failed: {}'.format( + system_user.name, h, msg + )) + return summary @shared_task def push_system_user_period(): - logger.debug("Push system user period") - for s in SystemUser.objects.filter(auto_push=True): - assets = s.get_clusters_assets() - - name = PUSH_SYSTEM_USER_PERIOD_TASK_NAME.format(s.name) - push_system_user(s, assets, name) - - -def push_system_user_to_assets_if_need(system_user, assets=None, asset_groups=None): - assets_to_push = [] - system_user_assets = system_user.assets.all() - if assets: - assets_to_push.extend(assets) - if asset_groups: - for group in asset_groups: - assets_to_push.extend(group.assets.all()) - - assets_need_push = set(assets_to_push) - set(system_user_assets) - if not assets_need_push: + if cache.get(PUSH_SYSTEM_USER_PERIOD_KEY) == 1: + logger.debug("push system user period task is running, passed") return - logger.debug("Push system user {} to {} assets".format( - system_user.name, ', '.join([asset.hostname for asset in assets_need_push]) - )) - result = push_system_user(system_user, assets_need_push, PUSH_SYSTEM_USER_TASK_NAME) - system_user.assets.add(*tuple(assets_need_push)) - return result + + logger.debug("Push system user period task start") + try: + cache.set(PUSH_SYSTEM_USER_PERIOD_KEY, 1, timeout=CACHE_MAX_TIME) + for system_user in SystemUser.objects.filter(auto_push=True): + task_name = 'PUSH-SYSTEM-USER-PERIOD' + push_system_user_to_cluster_assets(system_user, task_name) + finally: + cache.set(PUSH_SYSTEM_USER_PERIOD_KEY, 0) + + +# def push_system_user_to_assets_if_need(system_user, assets=None, asset_groups=None): +# assets_to_push = [] +# system_user_assets = system_user.assets.all() +# if assets: +# assets_to_push.extend(assets) +# if asset_groups: +# for group in asset_groups: +# assets_to_push.extend(group.assets.all()) +# +# assets_need_push = set(assets_to_push) - set(system_user_assets) +# if not assets_need_push: +# return +# logger.debug("Push system user {} to {} assets".format( +# system_user.name, ', '.join([asset.hostname for asset in assets_need_push]) +# )) +# result = push_system_user(system_user, assets_need_push, PUSH_SYSTEM_USER_TASK_NAME) +# system_user.assets.add(*tuple(assets_need_push)) +# return result + + +@receiver(post_save, sender=Asset, dispatch_uid="my_unique_identifier") +def update_asset_info(sender, instance=None, created=False, **kwargs): + if instance and created: + logger.debug("Receive asset create signal, update asset hardware info") + update_assets_hardware_info.delay([instance]) + + +@receiver(post_save, sender=Asset, dispatch_uid="my_unique_identifier") +def test_admin_user_connective(sender, instance=None, created=False, **kwargs): + if instance and created: + logger.debug("Receive asset create signal, test admin user connectability") + test_admin_user_connectability_manual.delay(instance) + + +@receiver(post_save, sender=SystemUser) +def push_system_user_on_change(sender, instance=None, created=False, **kwargs): + if instance and instance.auto_push: + logger.debug("System user `{}` auth changed, push it".format(instance.name)) + task_name = "PUSH-SYSTEM-USER-ON-CREATED-{}".format(instance.name) + push_system_user_to_cluster_assets.delay(instance, task_name) + + +@receiver(post_save, sender=SystemUser) +def push_system_user_on_change(sender, instance=None, update_fields=None, **kwargs): + fields_check = {'_password', '_private_key', '_public_key'} + auth_changed = update_fields & fields_check if update_fields else None + if instance and instance.auto_push and auth_changed: + logger.debug("System user `{}` auth changed, push it".format(instance.name)) + task_name = "PUSH-SYSTEM-USER-ON-CREATED-{}".format(instance.name) + push_system_user_to_cluster_assets.delay(instance, task_name) + + +@receiver(on_app_ready, dispatch_uid="my_unique_identifier") +def test_admin_user_on_app_ready(sender, **kwargs): + logger.debug("Receive app ready signal, test admin connectability") + test_admin_user_connectability_period.delay() + + +celery_app.conf['CELERYBEAT_SCHEDULE'].update( + { + 'update_assets_hardware_period': { + 'task': 'assets.tasks.update_assets_hardware_period', + 'schedule': 60*60*24, + 'args': (), + }, + 'test-admin-user-connectability_period': { + 'task': 'assets.tasks.test_admin_user_connectability_period', + 'schedule': 60*60, + 'args': (), + }, + 'push_system_user_period': { + 'task': 'assets.tasks.push_system_user_period', + 'schedule': 60*60, + 'args': (), + } + } +) diff --git a/apps/assets/templates/assets/_system_user.html b/apps/assets/templates/assets/_system_user.html index 41c6dda51..fe1836e8d 100644 --- a/apps/assets/templates/assets/_system_user.html +++ b/apps/assets/templates/assets/_system_user.html @@ -39,30 +39,29 @@ {% bootstrap_field form.username layout="horizontal" %} {% bootstrap_field form.protocol layout="horizontal" %} {% bootstrap_field form.cluster layout="horizontal" %} -

{% trans 'Auth' %}

- {% bootstrap_field form.auth_method layout="horizontal" %} + {% block auth %} - -
+

{% trans 'Auth' %}

+
{{ form.auto_generate_key}}
-
- {% bootstrap_field form.private_key_file layout="horizontal" %} -
+ +
+
+ {% bootstrap_field form.private_key_file layout="horizontal" %} + {% bootstrap_field form.password layout="horizontal" %}
- {% endblock %}
{{ form.auto_push}}
+ {% endblock %}

{% trans 'Other' %}

{% bootstrap_field form.sudo layout="horizontal" %} {% bootstrap_field form.shell layout="horizontal" %} @@ -82,41 +81,20 @@ {% endblock %} {% block custom_foot_js %} {% endblock %} \ No newline at end of file diff --git a/apps/assets/templates/assets/admin_user_detail.html b/apps/assets/templates/assets/admin_user_detail.html index d4aaae23d..1f80b8fe9 100644 --- a/apps/assets/templates/assets/admin_user_detail.html +++ b/apps/assets/templates/assets/admin_user_detail.html @@ -86,10 +86,10 @@ - + @@ -97,6 +97,42 @@
{% trans 'Reset auth' %}:{% trans 'Test auth all assets manual' %}: - +
+
+
+ {% trans 'Using this as cluster admin user' %} +
+
+ + + + + + + + + + + + {% for cluster in admin_user.cluster_set.all %} + + + + + {% endfor %} + +
+ +
+ +
{{ cluster.name }} + +
+
+
@@ -192,6 +228,7 @@ function adminUserDelete(name, url) { jumpserver.assets_selected = {}; jumpserver.asset_groups_selected = {}; $(document).ready(function () { + $('.select2').select2(); }) .on('click', '.btn-delete-admin-user', function () { var $this = $(this); diff --git a/apps/assets/templates/assets/asset_detail.html b/apps/assets/templates/assets/asset_detail.html index 7e5df44b6..ba4986b4b 100644 --- a/apps/assets/templates/assets/asset_detail.html +++ b/apps/assets/templates/assets/asset_detail.html @@ -236,7 +236,7 @@ - {% endif %} + {% endif %} diff --git a/apps/assets/templates/assets/system_user_asset.html b/apps/assets/templates/assets/system_user_asset.html index 6a9261a22..edda1c222 100644 --- a/apps/assets/templates/assets/system_user_asset.html +++ b/apps/assets/templates/assets/system_user_asset.html @@ -16,8 +16,18 @@
  • {% trans 'Detail' %}
  • -
  • - {% trans 'Attached assets' %} +
  • + + {% trans 'Auth' %} + +
  • +
  • + + {% trans 'Assets' %} + +
  • +
  • + Update
  • @@ -131,7 +141,7 @@ function initAssetsTable() { }} ], ajax_url: '{% url "api-assets:asset-list" %}?system_user_id={{ system_user.id }}', - columns: [{data: "hostname" }, {data: "ip" }, {data: "port" }, {data: "is_online" }], + columns: [{data: "hostname" }, {data: "ip" }, {data: "port" }, {data: "is_connective" }], op_html: $('#actions').html() }; jumpserver.initDataTable(options); diff --git a/apps/assets/templates/assets/system_user_auth.html b/apps/assets/templates/assets/system_user_auth.html new file mode 100644 index 000000000..2c68666eb --- /dev/null +++ b/apps/assets/templates/assets/system_user_auth.html @@ -0,0 +1,73 @@ +{% extends 'base.html' %} +{% load static %} +{% load i18n %} +{% load bootstrap3 %} + +{% block content %} +
    +
    +
    +
    + +
    +
    +
    +
    + {{ system_user.name }} +
    + + + + + + + + + + +
    +
    +
    +
    + {% csrf_token %} + {% bootstrap_field form.password layout="horizontal" %} +
    + {% bootstrap_field form.private_key_file layout="horizontal" %} +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +{% endblock %} + diff --git a/apps/assets/templates/assets/system_user_detail.html b/apps/assets/templates/assets/system_user_detail.html index 465e0c23d..c4a026a5d 100644 --- a/apps/assets/templates/assets/system_user_detail.html +++ b/apps/assets/templates/assets/system_user_detail.html @@ -17,6 +17,11 @@
  • {% trans 'Detail' %}
  • +
  • + + {% trans 'Auth' %} + +
  • {% trans 'Attached assets' %} @@ -61,10 +66,6 @@ {% trans 'Protocol' %}: {{ system_user.protocol }} - - {% trans 'Auto push' %}: - {{ system_user.auto_push|yesno:"Yes,No,Unknown" }} - {% trans 'Sudo' %}: {{ system_user.sudo }} @@ -129,14 +130,50 @@ +{# #} +{# {% trans 'Change auth period' %}:#} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} + + + + +
    +
    + {% trans 'Clusters' %} +
    +
    + + + + + + + + + + + + {% for cluster in system_user.cluster.all %} - - + + + {% endfor %}
    + +
    + +
    {% trans 'Reset auth' %}: - - - - {{ cluster.name }} + +
    @@ -150,6 +187,7 @@ {% endblock %} {% block custom_foot_js %} @@ -48,7 +47,6 @@ {% endblock %} {% endblock %} -{# {% block tags_list %}{% endblock %}#}
    {% block table_container %} @@ -64,7 +62,6 @@ {% endblock %}
    - {# Update batch #} {% block content_bottom_left %} {% endblock %}
    {% block table_pagination %} diff --git a/apps/templates/_message.html b/apps/templates/_message.html index 6f0811ae2..4d654aa74 100644 --- a/apps/templates/_message.html +++ b/apps/templates/_message.html @@ -1,3 +1,25 @@ +{% load i18n %} +{% block first_login_message %} + {% if user.is_authenticated and user.is_first_login %} +
    + {% url 'users:user-first-login' as first_login_url %} + {% blocktrans %} + Your information was incomplete. Please click this link to complete your information. + {% endblocktrans %} +
    + {% endif %} +{% endblock %} +{% block update_public_key_message %} + {% if user.is_authenticated and not user.is_public_key_valid %} +
    + {% url 'users:user-pubkey-update' as user_pubkey_update %} + {% blocktrans %} + Your ssh public key not set or expired. Please click this link to update your + {% endblocktrans %} +
    + {% endif %} +{% endblock %} + {% if messages %} {% for message in messages %}
    diff --git a/apps/templates/base.html b/apps/templates/base.html index 58bd303a1..43f2ab2af 100644 --- a/apps/templates/base.html +++ b/apps/templates/base.html @@ -5,9 +5,7 @@ - Jumpserver - {% include '_head_css_js.html' %} @@ -20,27 +18,7 @@
    {% include '_header_bar.html' %} {% include '_message.html' %} - {% block first_login_message %} - {% if user.is_authenticated and user.is_first_login %} -
    - {% url 'users:user-first-login' as first_login_url %} - {% blocktrans %} - Your information was incomplete. Please click this link to complete your information. - {% endblocktrans %} -
    - {% endif %} - {% endblock %} - {% block update_public_key_message %} - {% if user.is_authenticated and not user.is_public_key_valid %} -
    - {% url 'users:user-pubkey-update' as user_pubkey_update %} - {% blocktrans %} - Your ssh public key not set or expired. Please click this link to update your - {% endblocktrans %} -
    - {% endif %} - {% endblock %} - {% block help_message %}{% endblock %} + {% block help_message %} {% endblock %} {% block content %}{% endblock %} {% include '_footer.html' %}
    diff --git a/apps/terminal/views/session.py b/apps/terminal/views/session.py index 658742ad2..572d95504 100644 --- a/apps/terminal/views/session.py +++ b/apps/terminal/views/session.py @@ -124,7 +124,7 @@ class SessionDetailView(SingleObjectMixin, ListView): model = Session def get_queryset(self): - self.object = self.get_object(self.model.objects.all()) + self.object = self.get_object() return command_store.filter(session=self.object.id) def get_context_data(self, **kwargs):