Unverified Commit 8fede58c authored by 老广's avatar 老广 Committed by GitHub

Merge pull request #2456 from jumpserver/dev

Dev
parents 0e00451e 37090421
...@@ -6,6 +6,7 @@ RUN useradd jumpserver ...@@ -6,6 +6,7 @@ RUN useradd jumpserver
COPY ./requirements /tmp/requirements COPY ./requirements /tmp/requirements
RUN rpm -ivh https://repo.mysql.com/mysql57-community-release-el6.rpm
RUN yum -y install epel-release openldap-clients telnet && cd /tmp/requirements && \ RUN yum -y install epel-release openldap-clients telnet && cd /tmp/requirements && \
yum -y install $(cat rpm_requirements.txt) yum -y install $(cat rpm_requirements.txt)
......
...@@ -46,7 +46,7 @@ Jumpserver采纳分布式架构,支持多机房跨区域部署,中心节点 ...@@ -46,7 +46,7 @@ Jumpserver采纳分布式架构,支持多机房跨区域部署,中心节点
### License & Copyright ### License & Copyright
Copyright (c) 2014-2018 Beijing Duizhan Tech, Inc., All rights reserved. Copyright (c) 2014-2019 Beijing Duizhan Tech, Inc., All rights reserved.
Licensed under The GNU General Public License version 2 (GPLv2) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at Licensed under The GNU General Public License version 2 (GPLv2) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
......
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
__version__ = "1.4.9"
__version__ = "1.4.8"
...@@ -5,3 +5,4 @@ from .system_user import * ...@@ -5,3 +5,4 @@ from .system_user import *
from .node import * from .node import *
from .domain import * from .domain import *
from .cmd_filter import * from .cmd_filter import *
from .asset_user import *
# -*- coding: utf-8 -*-
#
from rest_framework.response import Response
from rest_framework import viewsets, status, generics
from rest_framework.pagination import LimitOffsetPagination
from common.permissions import IsOrgAdminOrAppUser
from common.utils import get_object_or_none, get_logger
from ..backends.multi import AssetUserManager
from ..models import Asset
from .. import serializers
from ..tasks import test_asset_users_connectivity_manual
__all__ = [
'AssetUserViewSet', 'AssetUserAuthInfoApi', 'AssetUserTestConnectiveApi',
]
logger = get_logger(__name__)
class AssetUserViewSet(viewsets.GenericViewSet):
pagination_class = LimitOffsetPagination
serializer_class = serializers.AssetUserSerializer
permission_classes = (IsOrgAdminOrAppUser, )
http_method_names = ['get', 'post']
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
def get_queryset(self):
username = self.request.GET.get('username')
asset_id = self.request.GET.get('asset_id')
asset = get_object_or_none(Asset, pk=asset_id)
queryset = AssetUserManager.filter(username=username, asset=asset)
return queryset
def filter_queryset(self, queryset):
queryset = sorted(
queryset,
key=lambda q: (q.asset.hostname, q.connectivity, q.username)
)
return queryset
class AssetUserAuthInfoApi(generics.RetrieveAPIView):
serializer_class = serializers.AssetUserAuthInfoSerializer
permission_classes = (IsOrgAdminOrAppUser,)
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
status_code = status.HTTP_200_OK
if not instance:
status_code = status.HTTP_400_BAD_REQUEST
return Response(serializer.data, status=status_code)
def get_object(self):
username = self.request.GET.get('username')
asset_id = self.request.GET.get('asset_id')
asset = get_object_or_none(Asset, pk=asset_id)
try:
instance = AssetUserManager.get(username, asset)
except Exception as e:
logger.error(e, exc_info=True)
return None
else:
return instance
class AssetUserTestConnectiveApi(generics.RetrieveAPIView):
"""
Test asset users connective
"""
def get_asset_users(self):
username = self.request.GET.get('username')
asset_id = self.request.GET.get('asset_id')
asset = get_object_or_none(Asset, pk=asset_id)
asset_users = AssetUserManager.filter(username=username, asset=asset)
return asset_users
def retrieve(self, request, *args, **kwargs):
asset_users = self.get_asset_users()
task = test_asset_users_connectivity_manual.delay(asset_users)
return Response({"task": task.id})
...@@ -51,9 +51,10 @@ class GatewayTestConnectionApi(SingleObjectMixin, APIView): ...@@ -51,9 +51,10 @@ class GatewayTestConnectionApi(SingleObjectMixin, APIView):
model = Gateway model = Gateway
object = None object = None
def get(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.object = self.get_object(Gateway.objects.all()) self.object = self.get_object(Gateway.objects.all())
ok, e = self.object.test_connective() local_port = self.request.data.get('port') or self.object.port
ok, e = self.object.test_connective(local_port=local_port)
if ok: if ok:
return Response("ok") return Response("ok")
else: else:
......
...@@ -30,7 +30,7 @@ from ..tasks import push_system_user_to_assets_manual, \ ...@@ -30,7 +30,7 @@ from ..tasks import push_system_user_to_assets_manual, \
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [ __all__ = [
'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi',
'SystemUserPushApi', 'SystemUserTestConnectiveApi', 'SystemUserPushApi', 'SystemUserTestConnectiveApi',
'SystemUserAssetsListView', 'SystemUserPushToAssetApi', 'SystemUserAssetsListView', 'SystemUserPushToAssetApi',
'SystemUserTestAssetConnectivityApi', 'SystemUserCommandFilterRuleListApi', 'SystemUserTestAssetConnectivityApi', 'SystemUserCommandFilterRuleListApi',
...@@ -68,6 +68,22 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): ...@@ -68,6 +68,22 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
return Response(status=204) return Response(status=204)
class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
"""
Get system user with asset auth info
"""
queryset = SystemUser.objects.all()
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.SystemUserAuthSerializer
def get_object(self):
instance = super().get_object()
aid = self.kwargs.get('aid')
asset = get_object_or_404(Asset, pk=aid)
instance.load_specific_asset_auth(asset)
return instance
class SystemUserPushApi(generics.RetrieveAPIView): class SystemUserPushApi(generics.RetrieveAPIView):
""" """
Push system user to cluster assets api Push system user to cluster assets api
......
# -*- coding: utf-8 -*-
#
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from abc import abstractmethod
class NotSupportError(Exception):
pass
class BaseBackend:
ObjectDoesNotExist = ObjectDoesNotExist
MultipleObjectsReturned = MultipleObjectsReturned
NotSupportError = NotSupportError
MSG_NOT_EXIST = '{} Object matching query does not exist'
MSG_MULTIPLE = '{} get() returned more than one object ' \
'-- it returned {}!'
@classmethod
def get(cls, username, asset):
instances = cls.filter(username, asset)
if len(instances) == 1:
return instances[0]
elif len(instances) == 0:
cls.raise_does_not_exist(cls.__name__)
else:
cls.raise_multiple_return(cls.__name__, len(instances))
@classmethod
@abstractmethod
def filter(cls, username=None, asset=None, latest=True):
"""
:param username: 用户名
:param asset: <Asset>对象
:param latest: 是否是最新记录
:return: 元素为<AuthBook>的可迭代对象(<list> or <QuerySet>)
"""
pass
@classmethod
@abstractmethod
def create(cls, **kwargs):
"""
:param kwargs:
{
name, username, asset, comment, password, public_key, private_key,
(org_id)
}
:return: <AuthBook>对象
"""
pass
@classmethod
def raise_does_not_exist(cls, name):
raise cls.ObjectDoesNotExist(cls.MSG_NOT_EXIST.format(name))
@classmethod
def raise_multiple_return(cls, name, length):
raise cls.MultipleObjectsReturned(cls.MSG_MULTIPLE.format(name, length))
# -*- coding: utf-8 -*-
#
from assets.models import AuthBook
from ..base import BaseBackend
class AuthBookBackend(BaseBackend):
@classmethod
def filter(cls, username=None, asset=None, latest=True):
queryset = AuthBook.objects.all()
if username:
queryset = queryset.filter(username=username)
if asset:
queryset = queryset.filter(asset=asset)
if latest:
queryset = queryset.latest_version()
return queryset
@classmethod
def create(cls, **kwargs):
auth_info = {
'password': kwargs.pop('password', ''),
'public_key': kwargs.pop('public_key', ''),
'private_key': kwargs.pop('private_key', '')
}
obj = AuthBook.objects.create(**kwargs)
obj.set_auth(**auth_info)
return obj
# -*- coding: utf-8 -*-
#
# from django.conf import settings
from .db import AuthBookBackend
# from .vault import VaultBackend
def get_backend():
default_backend = AuthBookBackend
# if settings.BACKEND_ASSET_USER_AUTH_VAULT:
# return VaultBackend
return default_backend
# -*- coding: utf-8 -*-
#
from ..base import BaseBackend
class VaultBackend(BaseBackend):
@classmethod
def get(cls, username, asset):
pass
@classmethod
def filter(cls, username=None, asset=None, latest=True):
pass
@classmethod
def create(cls, **kwargs):
pass
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.dispatch import Signal
on_app_ready = Signal()
# -*- coding: utf-8 -*-
#
from assets.models import Asset
from ..base import BaseBackend
from .utils import construct_authbook_object
class AdminUserBackend(BaseBackend):
@classmethod
def filter(cls, username=None, asset=None, **kwargs):
instances = cls.construct_authbook_objects(username, asset)
return instances
@classmethod
def _get_assets(cls, asset):
if not asset:
assets = Asset.objects.all().prefetch_related('admin_user')
else:
assets = [asset]
return assets
@classmethod
def construct_authbook_objects(cls, username, asset):
instances = []
assets = cls._get_assets(asset)
for asset in assets:
if username and asset.admin_user.username != username:
continue
instance = construct_authbook_object(asset.admin_user, asset)
instances.append(instance)
return instances
@classmethod
def create(cls, **kwargs):
raise cls.NotSupportError("Not support create")
# -*- coding: utf-8 -*-
#
from ..base import BaseBackend
from .admin_user import AdminUserBackend
from .system_user import SystemUserBackend
class AssetUserBackend(BaseBackend):
@classmethod
def filter(cls, username=None, asset=None, **kwargs):
admin_user_instances = AdminUserBackend.filter(username, asset)
system_user_instances = SystemUserBackend.filter(username, asset)
instances = cls._merge_instances(admin_user_instances, system_user_instances)
return instances
@classmethod
def _merge_instances(cls, admin_user_instances, system_user_instances):
admin_user_instances_keyword_list = [
{'username': instance.username, 'asset': instance.asset}
for instance in admin_user_instances
]
instances = [
instance for instance in system_user_instances
if instance.keyword not in admin_user_instances_keyword_list
]
admin_user_instances.extend(instances)
return admin_user_instances
@classmethod
def create(cls, **kwargs):
raise cls.NotSupportError("Not support create")
# -*- coding: utf-8 -*-
#
import itertools
from assets.models import Asset
from ..base import BaseBackend
from .utils import construct_authbook_object
class SystemUserBackend(BaseBackend):
@classmethod
def filter(cls, username=None, asset=None, **kwargs):
instances = cls.construct_authbook_objects(username, asset)
return instances
@classmethod
def _distinct_system_users_by_username(cls, system_users):
system_users = sorted(
system_users,
key=lambda su: (su.username, su.priority, su.date_updated),
reverse=True,
)
results = itertools.groupby(system_users, key=lambda su: su.username)
system_users = [next(result[1]) for result in results]
return system_users
@classmethod
def _filter_system_users_by_username(cls, system_users, username):
_system_users = cls._distinct_system_users_by_username(system_users)
if username:
_system_users = [su for su in _system_users if username == su.username]
return _system_users
@classmethod
def _construct_authbook_objects(cls, system_users, asset):
instances = []
for system_user in system_users:
instance = construct_authbook_object(system_user, asset)
instances.append(instance)
return instances
@classmethod
def _get_assets_with_system_users(cls, asset=None):
"""
{ 'asset': set(<SystemUser>, <SystemUser>, ...) }
"""
if not asset:
_assets = Asset.objects.all().prefetch_related('systemuser_set')
else:
_assets = [asset]
assets = {asset: set(asset.systemuser_set.all()) for asset in _assets}
return assets
@classmethod
def construct_authbook_objects(cls, username, asset):
"""
:return: [<AuthBook>, <AuthBook>, ...]
"""
instances = []
assets = cls._get_assets_with_system_users(asset)
for _asset, _system_users in assets.items():
_system_users = cls._filter_system_users_by_username(_system_users, username)
_instances = cls._construct_authbook_objects(_system_users, _asset)
instances.extend(_instances)
return instances
@classmethod
def create(cls, **kwargs):
raise Exception("Not support create")
# -*- coding: utf-8 -*-
#
from assets.models import AuthBook
def construct_authbook_object(asset_user, asset):
"""
作用: 将<AssetUser>对象构造成为<AuthBook>对象并返回
:param asset_user: <AdminUser>或<SystemUser>对象
:param asset: <Asset>对象
:return: <AuthBook>对象
"""
fields = [
'id', 'name', 'username', 'comment', 'org_id',
'_password', '_private_key', '_public_key',
'date_created', 'date_updated', 'created_by'
]
obj = AuthBook(asset=asset, version=0, is_latest=True)
for field in fields:
value = getattr(asset_user, field)
setattr(obj, field, value)
return obj
# -*- coding: utf-8 -*-
#
from .base import BaseBackend
from .external.utils import get_backend
from .internal.asset_user import AssetUserBackend
class AssetUserManager(BaseBackend):
"""
资产用户管理器
"""
external_backend = get_backend()
internal_backend = AssetUserBackend
@classmethod
def filter(cls, username=None, asset=None, **kwargs):
external_instance = list(cls.external_backend.filter(username, asset))
internal_instance = list(cls.internal_backend.filter(username, asset))
instances = cls._merge_instances(external_instance, internal_instance)
return instances
@classmethod
def create(cls, **kwargs):
instance = cls.external_backend.create(**kwargs)
return instance
@classmethod
def _merge_instances(cls, external_instances, internal_instances):
external_instances_keyword_list = [
{'username': instance.username, 'asset': instance.asset}
for instance in external_instances
]
instances = [
instance for instance in internal_instances
if instance.keyword not in external_instances_keyword_list
]
external_instances.extend(instances)
return external_instances
...@@ -32,6 +32,18 @@ TEST_SYSTEM_USER_CONN_TASKS = [ ...@@ -32,6 +32,18 @@ TEST_SYSTEM_USER_CONN_TASKS = [
} }
] ]
ASSET_USER_CONN_CACHE_KEY = 'ASSET_USER_CONN_{}_{}'
TEST_ASSET_USER_CONN_TASKS = [
{
"name": "ping",
"action": {
"module": "ping",
}
}
]
TASK_OPTIONS = { TASK_OPTIONS = {
'timeout': 10, 'timeout': 10,
'forks': 10, 'forks': 10,
......
...@@ -97,24 +97,12 @@ class AssetBulkUpdateForm(OrgModelForm): ...@@ -97,24 +97,12 @@ class AssetBulkUpdateForm(OrgModelForm):
} }
) )
) )
port = forms.IntegerField(
label=_('Port'), required=False, min_value=1, max_value=65535,
)
admin_user = forms.ModelChoiceField(
required=False, queryset=AdminUser.objects,
label=_("Admin user"),
widget=forms.Select(
attrs={
'class': 'select2',
'data-placeholder': _('Admin user')
}
)
)
class Meta: class Meta:
model = Asset model = Asset
fields = [ fields = [
'assets', 'port', 'admin_user', 'labels', 'nodes', 'platform' 'assets', 'port', 'admin_user', 'labels', 'platform',
'protocol', 'domain',
] ]
widgets = { widgets = {
'labels': forms.SelectMultiple( 'labels': forms.SelectMultiple(
...@@ -125,6 +113,13 @@ class AssetBulkUpdateForm(OrgModelForm): ...@@ -125,6 +113,13 @@ class AssetBulkUpdateForm(OrgModelForm):
), ),
} }
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 重写其他字段为不再required
for name, field in self.fields.items():
if name != 'assets':
field.required = False
def save(self, commit=True): def save(self, commit=True):
changed_fields = [] changed_fields = []
for field in self._meta.fields: for field in self._meta.fields:
......
...@@ -66,6 +66,9 @@ class GatewayForm(PasswordAndKeyAuthForm, OrgModelForm): ...@@ -66,6 +66,9 @@ class GatewayForm(PasswordAndKeyAuthForm, OrgModelForm):
'name', 'ip', 'port', 'username', 'protocol', 'domain', 'password', 'name', 'ip', 'port', 'username', 'protocol', 'domain', 'password',
'private_key_file', 'is_active', 'comment', 'private_key_file', 'is_active', 'comment',
] ]
help_texts = {
'protocol': _("SSH gateway support proxy SSH,RDP,VNC")
}
widgets = { widgets = {
'name': forms.TextInput(attrs={'placeholder': _('Name')}), 'name': forms.TextInput(attrs={'placeholder': _('Name')}),
'username': forms.TextInput(attrs={'placeholder': _('Username')}), 'username': forms.TextInput(attrs={'placeholder': _('Username')}),
......
...@@ -35,8 +35,12 @@ class PasswordAndKeyAuthForm(forms.ModelForm): ...@@ -35,8 +35,12 @@ class PasswordAndKeyAuthForm(forms.ModelForm):
if private_key_file: if private_key_file:
key_string = private_key_file.read() key_string = private_key_file.read()
private_key_file.seek(0) private_key_file.seek(0)
key_string = key_string.decode()
if not validate_ssh_private_key(key_string, password): if not validate_ssh_private_key(key_string, password):
raise forms.ValidationError(_('Invalid private key')) msg = _('Invalid private key, Only support '
'RSA/DSA format key')
raise forms.ValidationError(msg)
return private_key_file return private_key_file
def validate_password_key(self): def validate_password_key(self):
...@@ -150,5 +154,6 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm): ...@@ -150,5 +154,6 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
'priority': _('1-100, High level will be using login asset as default, ' 'priority': _('1-100, High level will be using login asset as default, '
'if user was granted more than 2 system user'), 'if user was granted more than 2 system user'),
'login_mode': _('If you choose manual login mode, you do not ' 'login_mode': _('If you choose manual login mode, you do not '
'need to fill in the username and password.') 'need to fill in the username and password.'),
'sudo': _("Use comma split multi command, ex: /bin/whoami,/bin/ifconfig")
} }
# Generated by Django 2.1.7 on 2019-02-28 10:16
import assets.models.asset
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
replaces = [('assets', '0002_auto_20180105_1807'), ('assets', '0003_auto_20180109_2331'), ('assets', '0004_auto_20180125_1218'), ('assets', '0005_auto_20180126_1637'), ('assets', '0006_auto_20180130_1502'), ('assets', '0007_auto_20180225_1815'), ('assets', '0008_auto_20180306_1804'), ('assets', '0009_auto_20180307_1212')]
dependencies = [
('assets', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='adminuser',
options={'ordering': ['name'], 'verbose_name': 'Admin user'},
),
migrations.AlterModelOptions(
name='asset',
options={'verbose_name': 'Asset'},
),
migrations.AlterModelOptions(
name='assetgroup',
options={'ordering': ['name'], 'verbose_name': 'Asset group'},
),
migrations.AlterModelOptions(
name='cluster',
options={'ordering': ['name'], 'verbose_name': 'Cluster'},
),
migrations.AlterModelOptions(
name='systemuser',
options={'ordering': ['name'], 'verbose_name': 'System user'},
),
migrations.RemoveField(
model_name='asset',
name='cluster',
),
migrations.AlterField(
model_name='assetgroup',
name='created_by',
field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by'),
),
migrations.CreateModel(
name='Label',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('value', models.CharField(max_length=128, verbose_name='Value')),
('category', models.CharField(choices=[('S', 'System'), ('U', 'User')], default='U', max_length=128, verbose_name='Category')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('comment', models.TextField(blank=True, null=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
],
options={
'db_table': 'assets_label',
},
),
migrations.AlterUniqueTogether(
name='label',
unique_together={('name', 'value')},
),
migrations.AddField(
model_name='asset',
name='labels',
field=models.ManyToManyField(blank=True, related_name='assets', to='assets.Label', verbose_name='Labels'),
),
migrations.RemoveField(
model_name='asset',
name='cabinet_no',
),
migrations.RemoveField(
model_name='asset',
name='cabinet_pos',
),
migrations.RemoveField(
model_name='asset',
name='env',
),
migrations.RemoveField(
model_name='asset',
name='remote_card_ip',
),
migrations.RemoveField(
model_name='asset',
name='status',
),
migrations.RemoveField(
model_name='asset',
name='type',
),
migrations.CreateModel(
name='Node',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('key', models.CharField(max_length=64, unique=True, verbose_name='Key')),
('value', models.CharField(max_length=128, verbose_name='Value')),
('child_mark', models.IntegerField(default=0)),
('date_create', models.DateTimeField(auto_now_add=True)),
],
),
migrations.RemoveField(
model_name='asset',
name='groups',
),
migrations.RemoveField(
model_name='systemuser',
name='cluster',
),
migrations.AlterField(
model_name='asset',
name='admin_user',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.AdminUser', verbose_name='Admin user'),
),
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol'),
),
migrations.AddField(
model_name='asset',
name='nodes',
field=models.ManyToManyField(default=assets.models.asset.default_node, related_name='assets', to='assets.Node', verbose_name='Nodes'),
),
migrations.AddField(
model_name='systemuser',
name='nodes',
field=models.ManyToManyField(blank=True, to='assets.Node', verbose_name='Nodes'),
),
migrations.AlterField(
model_name='adminuser',
name='created_by',
field=models.CharField(max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(max_length=128, verbose_name='Username'),
),
migrations.AlterField(
model_name='asset',
name='platform',
field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'),
),
migrations.AlterField(
model_name='systemuser',
name='created_by',
field=models.CharField(max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(max_length=128, verbose_name='Username'),
),
]
# Generated by Django 2.1.7 on 2019-02-28 10:16
import assets.models.utils
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import uuid
# Functions from the following migrations need manual copying.
# Move them and any dependencies into this file, then update the
# RunPython operations to refer to the local versions:
# assets.migrations.0017_auto_20180702_1415
def migrate_win_to_ssh_protocol(apps, schema_editor):
asset_model = apps.get_model("assets", "Asset")
db_alias = schema_editor.connection.alias
asset_model.objects.using(db_alias).filter(platform__startswith='Win').update(protocol='rdp')
class Migration(migrations.Migration):
replaces = [('assets', '0010_auto_20180307_1749'), ('assets', '0011_auto_20180326_0957'), ('assets', '0012_auto_20180404_1302'), ('assets', '0013_auto_20180411_1135'), ('assets', '0014_auto_20180427_1245'), ('assets', '0015_auto_20180510_1235'), ('assets', '0016_auto_20180511_1203'), ('assets', '0017_auto_20180702_1415'), ('assets', '0018_auto_20180807_1116'), ('assets', '0019_auto_20180816_1320')]
dependencies = [
('assets', '0009_auto_20180307_1212'),
]
operations = [
migrations.AlterField(
model_name='node',
name='value',
field=models.CharField(max_length=128, unique=True, verbose_name='Value'),
),
migrations.CreateModel(
name='Domain',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
],
),
migrations.CreateModel(
name='Gateway',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('username', models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
('_password', models.CharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('_private_key', models.TextField(blank=True, max_length=4096, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key')),
('_public_key', models.TextField(blank=True, max_length=4096, verbose_name='SSH public key')),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_updated', models.DateTimeField(auto_now=True)),
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
('ip', models.GenericIPAddressField(db_index=True, verbose_name='IP')),
('port', models.IntegerField(default=22, verbose_name='Port')),
('protocol', models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol')),
('comment', models.CharField(blank=True, max_length=128, null=True, verbose_name='Comment')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Domain', verbose_name='Domain')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='asset',
name='domain',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.Domain', verbose_name='Domain'),
),
migrations.AddField(
model_name='systemuser',
name='assets',
field=models.ManyToManyField(blank=True, to='assets.Asset', verbose_name='Assets'),
),
migrations.AlterField(
model_name='systemuser',
name='sudo',
field=models.TextField(default='/bin/whoami', verbose_name='Sudo'),
),
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(max_length=32, 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(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(max_length=32, 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(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='node',
name='value',
field=models.CharField(max_length=128, verbose_name='Value'),
),
migrations.AddField(
model_name='asset',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=128, verbose_name='Protocol'),
),
migrations.AddField(
model_name='systemuser',
name='login_mode',
field=models.CharField(choices=[('auto', 'Automatic login'), ('manual', 'Manually login')], default='auto', max_length=10, verbose_name='Login mode'),
),
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='asset',
name='platform',
field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Windows2016', 'Windows(2016)'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'),
),
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=16, verbose_name='Protocol'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.RunPython(
code=migrate_win_to_ssh_protocol,
),
migrations.AddField(
model_name='adminuser',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='asset',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='domain',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='gateway',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='label',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='node',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='systemuser',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AlterField(
model_name='adminuser',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterField(
model_name='asset',
name='hostname',
field=models.CharField(max_length=128, verbose_name='Hostname'),
),
migrations.AlterField(
model_name='gateway',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterField(
model_name='systemuser',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterUniqueTogether(
name='adminuser',
unique_together={('name', 'org_id')},
),
migrations.AddField(
model_name='asset',
name='cpu_vcpus',
field=models.IntegerField(null=True, verbose_name='CPU vcpus'),
),
migrations.AlterUniqueTogether(
name='asset',
unique_together={('org_id', 'hostname')},
),
migrations.AlterUniqueTogether(
name='gateway',
unique_together={('name', 'org_id')},
),
migrations.AlterUniqueTogether(
name='systemuser',
unique_together={('name', 'org_id')},
),
migrations.AlterUniqueTogether(
name='label',
unique_together={('name', 'value', 'org_id')},
),
]
# Generated by Django 2.1.7 on 2019-03-25 12:35
import assets.models.utils
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0025_auto_20190221_1902'),
]
operations = [
migrations.CreateModel(
name='AuthBook',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
('_password', models.CharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('_private_key', models.TextField(blank=True, max_length=4096, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key')),
('_public_key', models.TextField(blank=True, max_length=4096, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_updated', models.DateTimeField(auto_now=True)),
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
('is_latest', models.BooleanField(default=False, verbose_name='Latest version')),
('version', models.IntegerField(default=1, verbose_name='Version')),
('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Asset', verbose_name='Asset')),
],
options={
'verbose_name': 'AuthBook',
},
),
migrations.AlterModelOptions(
name='node',
options={'ordering': ['key'], 'verbose_name': 'Node'},
),
]
...@@ -7,3 +7,4 @@ from .node import * ...@@ -7,3 +7,4 @@ from .node import *
from .asset import * from .asset import *
from .cmd_filter import * from .cmd_filter import *
from .utils import * from .utils import *
from .authbook import *
...@@ -197,6 +197,7 @@ class Asset(OrgModelMixin): ...@@ -197,6 +197,7 @@ class Asset(OrgModelMixin):
def get_auth_info(self): def get_auth_info(self):
if self.admin_user: if self.admin_user:
self.admin_user.load_specific_asset_auth(self)
return { return {
'username': self.admin_user.username, 'username': self.admin_user.username,
'password': self.admin_user.password, 'password': self.admin_user.password,
...@@ -232,6 +233,7 @@ class Asset(OrgModelMixin): ...@@ -232,6 +233,7 @@ class Asset(OrgModelMixin):
""" """
data = self.to_json() data = self.to_json()
if self.admin_user: if self.admin_user:
self.admin_user.load_specific_asset_auth(self)
admin_user = self.admin_user admin_user = self.admin_user
data.update({ data.update({
'username': admin_user.username, 'username': admin_user.username,
......
# -*- coding: utf-8 -*-
#
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.core.cache import cache
from orgs.mixins import OrgManager
from .base import AssetUser
from ..const import ASSET_USER_CONN_CACHE_KEY
__all__ = ['AuthBook']
class AuthBookQuerySet(models.QuerySet):
def latest_version(self):
return self.filter(is_latest=True)
class AuthBookManager(OrgManager):
pass
class AuthBook(AssetUser):
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset'))
is_latest = models.BooleanField(default=False, verbose_name=_('Latest version'))
version = models.IntegerField(default=1, verbose_name=_('Version'))
objects = AuthBookManager.from_queryset(AuthBookQuerySet)()
class Meta:
verbose_name = _('AuthBook')
def _set_latest(self):
self._remove_pre_obj_latest()
self.is_latest = True
self.save()
def _get_pre_obj(self):
pre_obj = self.__class__.objects.filter(
username=self.username, asset=self.asset).latest_version().first()
return pre_obj
def _remove_pre_obj_latest(self):
pre_obj = self._get_pre_obj()
if pre_obj:
pre_obj.is_latest = False
pre_obj.save()
def _set_version(self):
pre_obj = self._get_pre_obj()
if pre_obj:
self.version = pre_obj.version + 1
else:
self.version = 1
self.save()
def set_version_and_latest(self):
self._set_version()
self._set_latest()
@property
def _conn_cache_key(self):
return ASSET_USER_CONN_CACHE_KEY.format(self.id, self.asset.id)
@property
def connectivity(self):
value = cache.get(self._conn_cache_key, self.UNKNOWN)
return value
@connectivity.setter
def connectivity(self, value):
_connectivity = self.UNKNOWN
for host in value.get('dark', {}).keys():
if host == self.asset.hostname:
_connectivity = self.UNREACHABLE
for host in value.get('contacted', {}).keys():
if host == self.asset.hostname:
_connectivity = self.REACHABLE
cache.set(self._conn_cache_key, _connectivity, 3600)
@property
def keyword(self):
return {'username': self.username, 'asset': self.asset}
def __str__(self):
return '{}@{}'.format(self.username, self.asset)
...@@ -9,13 +9,17 @@ from django.db import models ...@@ -9,13 +9,17 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.conf import settings from django.conf import settings
from common.utils import get_signer, ssh_key_string_to_obj, ssh_key_gen from common.utils import (
get_signer, ssh_key_string_to_obj, ssh_key_gen, get_logger
)
from common.validators import alphanumeric from common.validators import alphanumeric
from orgs.mixins import OrgModelMixin from orgs.mixins import OrgModelMixin
from .utils import private_key_validator from .utils import private_key_validator
signer = get_signer() signer = get_signer()
logger = get_logger(__file__)
class AssetUser(OrgModelMixin): class AssetUser(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
...@@ -45,8 +49,8 @@ class AssetUser(OrgModelMixin): ...@@ -45,8 +49,8 @@ class AssetUser(OrgModelMixin):
@password.setter @password.setter
def password(self, password_raw): def password(self, password_raw):
raise AttributeError("Using set_auth do that") # raise AttributeError("Using set_auth do that")
# self._password = signer.sign(password_raw) self._password = signer.sign(password_raw)
@property @property
def private_key(self): def private_key(self):
...@@ -55,8 +59,8 @@ class AssetUser(OrgModelMixin): ...@@ -55,8 +59,8 @@ class AssetUser(OrgModelMixin):
@private_key.setter @private_key.setter
def private_key(self, private_key_raw): def private_key(self, private_key_raw):
raise AttributeError("Using set_auth do that") # raise AttributeError("Using set_auth do that")
# self._private_key = signer.sign(private_key_raw) self._private_key = signer.sign(private_key_raw)
@property @property
def private_key_obj(self): def private_key_obj(self):
...@@ -88,6 +92,11 @@ class AssetUser(OrgModelMixin): ...@@ -88,6 +92,11 @@ class AssetUser(OrgModelMixin):
else: else:
return None return None
@public_key.setter
def public_key(self, public_key_raw):
# raise AttributeError("Using set_auth do that")
self._public_key = signer.sign(public_key_raw)
@property @property
def public_key_obj(self): def public_key_obj(self):
if self.public_key: if self.public_key:
...@@ -115,6 +124,25 @@ class AssetUser(OrgModelMixin): ...@@ -115,6 +124,25 @@ class AssetUser(OrgModelMixin):
def get_auth(self, asset=None): def get_auth(self, asset=None):
pass pass
def load_specific_asset_auth(self, asset):
from ..backends.multi import AssetUserManager
try:
other = AssetUserManager.get(username=self.username, asset=asset)
except Exception as e:
logger.error(e, exc_info=True)
else:
self._merge_auth(other)
def _merge_auth(self, other):
if not other:
return
if other.password:
self.password = other.password
if other.public_key:
self.public_key = other.public_key
if other.private_key:
self.private_key = other.private_key
def clear_auth(self): def clear_auth(self):
self._password = '' self._password = ''
self._private_key = '' self._private_key = ''
......
...@@ -60,7 +60,9 @@ class Gateway(AssetUser): ...@@ -60,7 +60,9 @@ class Gateway(AssetUser):
unique_together = [('name', 'org_id')] unique_together = [('name', 'org_id')]
verbose_name = _("Gateway") verbose_name = _("Gateway")
def test_connective(self): def test_connective(self, local_port=None):
if local_port is None:
local_port = self.port
client = paramiko.SSHClient() client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
proxy = paramiko.SSHClient() proxy = paramiko.SSHClient()
...@@ -76,12 +78,11 @@ class Gateway(AssetUser): ...@@ -76,12 +78,11 @@ class Gateway(AssetUser):
paramiko.SSHException) as e: paramiko.SSHException) as e:
return False, str(e) return False, str(e)
try:
sock = proxy.get_transport().open_channel( sock = proxy.get_transport().open_channel(
'direct-tcpip', ('127.0.0.1', self.port), ('127.0.0.1', 0) 'direct-tcpip', ('127.0.0.1', local_port), ('127.0.0.1', 0)
) )
client.connect("127.0.0.1", port=local_port,
try:
client.connect("127.0.0.1", port=self.port,
username=self.username, username=self.username,
password=self.password, password=self.password,
key_filename=self.private_key_file, key_filename=self.private_key_file,
......
...@@ -29,6 +29,7 @@ class Node(OrgModelMixin): ...@@ -29,6 +29,7 @@ class Node(OrgModelMixin):
class Meta: class Meta:
verbose_name = _("Node") verbose_name = _("Node")
ordering = ['key']
def __str__(self): def __str__(self):
return self.full_value return self.full_value
...@@ -275,7 +276,8 @@ class Node(OrgModelMixin): ...@@ -275,7 +276,8 @@ class Node(OrgModelMixin):
@classmethod @classmethod
def default_node(cls): def default_node(cls):
defaults = {'value': 'Default'} defaults = {'value': 'Default'}
return cls.objects.get_or_create(defaults=defaults, key='1') obj, created = cls.objects.get_or_create(defaults=defaults, key='1')
return obj
def as_tree_node(self): def as_tree_node(self):
from common.tree import TreeNode from common.tree import TreeNode
......
...@@ -8,3 +8,4 @@ from .system_user import * ...@@ -8,3 +8,4 @@ from .system_user import *
from .node import * from .node import *
from .domain import * from .domain import *
from .cmd_filter import * from .cmd_filter import *
from .asset_user import *
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext as _
from rest_framework import serializers
from ..models import AuthBook
from ..backends.multi import AssetUserManager
__all__ = [
'AssetUserSerializer', 'AssetUserAuthInfoSerializer',
]
class AssetUserSerializer(serializers.ModelSerializer):
password = serializers.CharField(
max_length=256, allow_blank=True, allow_null=True, write_only=True,
required=False, help_text=_('Password')
)
public_key = serializers.CharField(
max_length=4096, allow_blank=True, allow_null=True, write_only=True,
required=False, help_text=_('Public key')
)
private_key = serializers.CharField(
max_length=4096, allow_blank=True, allow_null=True, write_only=True,
required=False, help_text=_('Private key')
)
class Meta:
model = AuthBook
read_only_fields = (
'date_created', 'date_updated', 'created_by',
'is_latest', 'version', 'connectivity',
)
fields = '__all__'
extra_kwargs = {
'username': {'required': True}
}
def get_field_names(self, declared_fields, info):
fields = super().get_field_names(declared_fields, info)
fields = [f for f in fields if not f.startswith('_') and f != 'id']
fields.extend(['connectivity'])
return fields
def create(self, validated_data):
kwargs = {
'name': validated_data.get('name'),
'username': validated_data.get('username'),
'asset': validated_data.get('asset'),
'comment': validated_data.get('comment', ''),
'org_id': validated_data.get('org_id', ''),
'password': validated_data.get('password'),
'public_key': validated_data.get('public_key'),
'private_key': validated_data.get('private_key')
}
instance = AssetUserManager.create(**kwargs)
return instance
class AssetUserAuthInfoSerializer(serializers.ModelSerializer):
class Meta:
model = AuthBook
fields = ['password', 'private_key', 'public_key']
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from rest_framework import serializers from rest_framework import serializers
from rest_framework_bulk.serializers import BulkListSerializer
from common.mixins import BulkSerializerMixin
from ..models import Asset, Node from ..models import Asset, Node
from .asset import AssetGrantedSerializer
__all__ = [ __all__ = [
...@@ -22,7 +19,7 @@ class NodeSerializer(serializers.ModelSerializer): ...@@ -22,7 +19,7 @@ class NodeSerializer(serializers.ModelSerializer):
'id', 'key', 'value', 'assets_amount', 'org_id', 'id', 'key', 'value', 'assets_amount', 'org_id',
] ]
read_only_fields = [ read_only_fields = [
'id', 'key', 'assets_amount', 'org_id', 'key', 'assets_amount', 'org_id',
] ]
def validate_value(self, data): def validate_value(self, data):
......
...@@ -5,9 +5,12 @@ from django.db.models.signals import post_save, m2m_changed, post_delete ...@@ -5,9 +5,12 @@ from django.db.models.signals import post_save, m2m_changed, post_delete
from django.dispatch import receiver from django.dispatch import receiver
from common.utils import get_logger from common.utils import get_logger
from .models import Asset, SystemUser, Node from .models import Asset, SystemUser, Node, AuthBook
from .tasks import update_assets_hardware_info_util, \ from .tasks import (
test_asset_connectivity_util, push_system_user_to_assets update_assets_hardware_info_util,
test_asset_connectivity_util,
push_system_user_to_assets
)
logger = get_logger(__file__) logger = get_logger(__file__)
...@@ -109,3 +112,10 @@ def on_node_assets_changed(sender, instance=None, **kwargs): ...@@ -109,3 +112,10 @@ def on_node_assets_changed(sender, instance=None, **kwargs):
def on_node_update_or_created(sender, instance=None, created=False, **kwargs): def on_node_update_or_created(sender, instance=None, created=False, **kwargs):
if instance and not created: if instance and not created:
instance.expire_full_value() instance.expire_full_value()
@receiver(post_save, sender=AuthBook)
def on_auth_book_created(sender, instance=None, created=False, **kwargs):
if created:
logger.debug('Receive create auth book object signal.')
instance.set_version_and_latest()
...@@ -26,20 +26,26 @@ disk_pattern = re.compile(r'^hd|sd|xvd|vd') ...@@ -26,20 +26,26 @@ disk_pattern = re.compile(r'^hd|sd|xvd|vd')
PERIOD_TASK = os.environ.get("PERIOD_TASK", "on") PERIOD_TASK = os.environ.get("PERIOD_TASK", "on")
def clean_hosts(assets): def check_asset_can_run_ansible(asset):
clean_assets = []
for asset in assets:
if not asset.is_active: if not asset.is_active:
msg = _("Asset has been disabled, skipped: {}").format(asset) msg = _("Asset has been disabled, skipped: {}").format(asset)
logger.info(msg) logger.info(msg)
continue return False
if not asset.support_ansible(): if not asset.support_ansible():
msg = _("Asset may not be support ansible, skipped: {}").format(asset) msg = _("Asset may not be support ansible, skipped: {}").format(asset)
logger.info(msg) logger.info(msg)
return False
return True
def clean_hosts(assets):
clean_assets = []
for asset in assets:
if not check_asset_can_run_ansible(asset):
continue continue
clean_assets.append(asset) clean_assets.append(asset)
if not clean_assets: if not clean_assets:
logger.info(_("No assets matched, stop task")) print(_("No assets matched, stop task"))
return clean_assets return clean_assets
...@@ -259,7 +265,7 @@ def test_system_user_connectivity_util(system_user, assets, task_name): ...@@ -259,7 +265,7 @@ def test_system_user_connectivity_util(system_user, assets, task_name):
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
task_name, hosts=hosts, tasks=tasks, pattern='all', task_name, hosts=hosts, tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, options=const.TASK_OPTIONS,
run_as=system_user, created_by=system_user.org_id, run_as=system_user.username, created_by=system_user.org_id,
) )
result = task.run() result = task.run()
set_system_user_connectivity_info(system_user, result) set_system_user_connectivity_info(system_user, result)
...@@ -341,6 +347,12 @@ def get_push_system_user_tasks(system_user): ...@@ -341,6 +347,12 @@ def get_push_system_user_tasks(system_user):
} }
}) })
if system_user.sudo: if system_user.sudo:
sudo = system_user.sudo.replace('\r\n', '\n').replace('\r', '\n')
sudo_list = sudo.split('\n')
sudo_tmp = []
for s in sudo_list:
sudo_tmp.append(s.strip(','))
sudo = ','.join(sudo_tmp)
tasks.append({ tasks.append({
'name': 'Set {} sudo setting'.format(system_user.username), 'name': 'Set {} sudo setting'.format(system_user.username),
'action': { 'action': {
...@@ -348,8 +360,7 @@ def get_push_system_user_tasks(system_user): ...@@ -348,8 +360,7 @@ def get_push_system_user_tasks(system_user):
'args': "dest=/etc/sudoers state=present regexp='^{0} ALL=' " 'args': "dest=/etc/sudoers state=present regexp='^{0} ALL=' "
"line='{0} ALL=(ALL) NOPASSWD: {1}' " "line='{0} ALL=(ALL) NOPASSWD: {1}' "
"validate='visudo -cf %s'".format( "validate='visudo -cf %s'".format(
system_user.username, system_user.username, sudo,
system_user.sudo,
) )
} }
}) })
...@@ -365,16 +376,18 @@ def push_system_user_util(system_user, assets, task_name): ...@@ -365,16 +376,18 @@ def push_system_user_util(system_user, assets, task_name):
logger.info(msg) logger.info(msg)
return return
tasks = get_push_system_user_tasks(system_user)
hosts = clean_hosts(assets) hosts = clean_hosts(assets)
if not hosts: if not hosts:
return {} return {}
for host in hosts:
system_user.load_specific_asset_auth(host)
tasks = get_push_system_user_tasks(system_user)
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all', task_name=task_name, hosts=[host], tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True, options=const.TASK_OPTIONS, run_as_admin=True,
created_by=system_user.org_id, created_by=system_user.org_id,
) )
return task.run() task.run()
@shared_task @shared_task
...@@ -410,6 +423,43 @@ def test_admin_user_connectability_period(): ...@@ -410,6 +423,43 @@ def test_admin_user_connectability_period():
pass pass
@shared_task
def set_asset_user_connectivity_info(asset_user, result):
summary = result[1]
asset_user.connectivity = summary
@shared_task
def test_asset_user_connectivity_util(asset_user, task_name):
"""
:param asset_user: <AuthBook>对象
:param task_name:
:return:
"""
from ops.utils import update_or_create_ansible_task
tasks = const.TEST_ASSET_USER_CONN_TASKS
if not check_asset_can_run_ansible(asset_user.asset):
return
task, created = update_or_create_ansible_task(
task_name, hosts=[asset_user.asset], tasks=tasks, pattern='all',
options=const.TASK_OPTIONS,
run_as=asset_user.username, created_by=asset_user.org_id
)
result = task.run()
set_asset_user_connectivity_info(asset_user, result)
@shared_task
def test_asset_users_connectivity_manual(asset_users):
"""
:param asset_users: <AuthBook>对象
"""
for asset_user in asset_users:
task_name = _("Test asset user connectivity: {}").format(asset_user)
test_asset_user_connectivity_util(asset_user, task_name)
# @shared_task # @shared_task
# @register_as_period_task(interval=3600) # @register_as_period_task(interval=3600)
# @after_app_ready_start # @after_app_ready_start
......
{% extends '_modal.html' %}
{% load i18n %}
{% block modal_id %}asset_user_auth_modal{% endblock %}
{% block modal_title%}{% trans "Update asset user auth" %}{% endblock %}
{% block modal_body %}
<form class="form-horizontal" role="form" onkeydown="if(event.keyCode==13){ $('#btn_asset_user_auth_modal_confirm').trigger('click'); return false;}">
{% csrf_token %}
<div class="form-group">
<label class="col-sm-2 control-label">{% trans "Hostname" %}</label>
<div class="col-sm-10">
<p class="form-control-static" id="id_hostname_p"></p>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">{% trans "Username" %}</label>
<div class="col-sm-10">
<p class="form-control-static" id="id_username_p"></p>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">{% trans "Password" %}</label>
<div class="col-sm-10">
<input class="form-control" id="id_password" type="password" name="password" placeholder="{% trans 'Please input password' %}"/>
</div>
</div>
</form>
{% endblock %}
{% block modal_confirm_id %}btn_asset_user_auth_modal_confirm{% endblock %}
{% extends '_modal.html' %}
{% load i18n %}
{% block modal_id %}gateway_test{% endblock %}
{% block modal_title%}{% trans "Test gateway test connection" %}{% endblock %}
{% block modal_body %}
{% load bootstrap3 %}
<form method="post" class="form-horizontal" action="" id="test_gateway_form" style="padding-top: 10px">
<div class="form-group">
<input id="gateway_id" name="gateway_id" hidden>
<label for="port" class="col-sm-2 control-label">{% trans 'SSH Port' %}</label>
<div class="col-sm-9" id="select2-container">
<input id="ssh_test_port" name="port" class="form-control">
<span class="help-block">{% trans 'If use nat, set the ssh real port' %}</span>
</div>
</div>
</form>
{% endblock %}
{% block modal_confirm_id %}btn_gateway_test{% endblock %}
\ No newline at end of file
...@@ -84,6 +84,7 @@ ...@@ -84,6 +84,7 @@
</div> </div>
</div> </div>
</div> </div>
{% include 'assets/_asset_user_auth_modal.html' %}
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
...@@ -109,9 +110,10 @@ function initTable() { ...@@ -109,9 +110,10 @@ function initTable() {
$(td).html('') $(td).html('')
} }
}}, }},
{targets: 4, createdCell: function (td, cellData) { {targets: 4, createdCell: function (td, cellData, rowData) {
var test_btn = ' <a class="btn btn-xs btn-info btn-test-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Test" %}</a>'.replace("{{ DEFAULT_PK }}", cellData); var test_btn = ' <a class="btn btn-xs btn-info btn-test-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Test" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
$(td).html(test_btn); var update_auth_btn = ' <a class="btn btn-xs btn-primary btn-update-asset-user-auth" data-aid="{{ DEFAULT_PK }}" data-hostname="hostname777">{% trans "Update auth" %}</a>'.replace("{{ DEFAULT_PK }}", cellData).replace("hostname777", rowData.hostname);
$(td).html(test_btn + update_auth_btn);
}} }}
], ],
...@@ -124,6 +126,15 @@ function initTable() { ...@@ -124,6 +126,15 @@ function initTable() {
jumpserver.initServerSideDataTable(options); jumpserver.initServerSideDataTable(options);
} }
function initAssetUserAuthModalForm(hostname, username){
$('#id_hostname_p').html(hostname);
$('#id_username_p').html(username);
$('#id_password').parent().removeClass('has-error');
$('#id_password').val('');
}
var assetId ;
$(document).ready(function () { $(document).ready(function () {
initTable(); initTable();
}) })
...@@ -156,5 +167,38 @@ $(document).ready(function () { ...@@ -156,5 +167,38 @@ $(document).ready(function () {
flash_message: false flash_message: false
}); });
}) })
.on('click', '.btn-update-asset-user-auth', function() {
assetId = $(this).data('aid');
var hostname = $(this).data('hostname');
var username = '{{ admin_user.username }}';
initAssetUserAuthModalForm(hostname, username);
$("#asset_user_auth_modal").modal();
})
.on('click', '#btn_asset_user_auth_modal_confirm', function(){
var password = $('#id_password').val();
if (password){
var data = {
'name': "{{ admin_user.username }}",
'asset': assetId,
'username': "{{ admin_user.username }}",
'password': password
};
formSubmit({
data: data,
url: "{% url 'api-assets:asset-user-list' %}",
method: 'POST',
success: function () {
toastr.success("{% trans 'Update successfully!' %}");
},
error: function () {
toastr.error("{% trans 'Update failed!' %}");
}
});
$("#asset_user_auth_modal").modal('hide');
}
else{
$('#id_password').parent().addClass('has-error');
}
})
</script> </script>
{% endblock %} {% endblock %}
{% extends 'base.html' %}
{% load common_tags %}
{% load static %}
{% load i18n %}
{% block custom_head_css_js %}
{% endblock %}
{% block content %}
<div class="wrapper wrapper-content animated fadeInRight">
<div class="row">
<div class="col-sm-12">
<div class="ibox float-e-margins">
<div class="panel-options">
<ul class="nav nav-tabs">
<li>
<a href="{% url 'assets:asset-detail' pk=asset.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Asset detail' %}</a>
</li>
<li class="active">
<a href="{% url 'assets:asset-user-list' pk=asset.id %}" class="text-center"><i class="fa fa-bar-chart-o"></i> {% trans 'Asset user list' %}</a>
</li>
</ul>
</div>
<div class="tab-content">
<div class="col-sm-8" style="padding-left: 0;">
<div class="ibox float-e-margins">
<div class="ibox-title">
<span style="float: left">{% trans 'Asset users of' %} <b>{{ asset.hostname }} </b></span>
<div class="ibox-tools">
<a class="collapse-link">
<i class="fa fa-chevron-up"></i>
</a>
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<i class="fa fa-wrench"></i>
</a>
<ul class="dropdown-menu dropdown-user">
</ul>
<a class="close-link">
<i class="fa fa-times"></i>
</a>
</div>
</div>
<div class="ibox-content">
<table class="table table-hover" id="asset_user_list">
<thead>
<tr>
<th class="text-center"><input type="checkbox" class="ipt_check_all"></th>
<th class="text-center">{% trans 'Username' %}</th>
<th class="text-center">{% trans 'Version' %}</th>
<th class="text-center">{% trans 'Reachable' %}</th>
<th class="text-center">{% trans 'Date updated' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-sm-4" style="padding-left: 0;padding-right: 0">
<div class="panel panel-primary">
<div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'Quick modify' %}
</div>
<div class="panel-body">
<table class="table">
<tbody>
{% if asset.protocol == 'ssh' %}
<tr class="no-borders-tr">
<td>{% trans 'Test connective' %}:</td>
<td>
<span class="pull-right">
<button type="button" class="btn btn-primary btn-xs" id="btn-bulk-test-connective" style="width: 54px">{% trans 'Test' %}</button>
</span>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% include 'assets/_asset_user_auth_modal.html' %}
{% endblock %}
{% block custom_foot_js %}
<script>
function initAssetUserAuthModalForm(hostname){
$('#id_hostname_p').html(hostname);
$('#id_username_p').html(username);
$('#id_password').parent().removeClass('has-error');
$('#id_password').val('');
}
function initAssetUserTable() {
var reachable = {{ asset.admin_user.REACHABLE }};
var unreachable = {{ asset.admin_user.UNREACHABLE }};
var options = {
ele: $('#asset_user_list'),
buttons: [],
order: [],
columnDefs: [
{targets: 3, createdCell: function (td, cellData) {
if (cellData === unreachable) {
$(td).html('<i class="fa fa-times text-danger"></i>')
} else if (cellData === reachable) {
$(td).html('<i class="fa fa-check text-navy"></i>')
} else {
$(td).html('')
}
}},
{targets: 4, createdCell: function (td, cellData) {
$(td).html(cellData.slice(0, -6));
}},
{targets: 5, createdCell: function (td, cellData) {
var update_auth_btn = ' <a class="btn btn-xs btn-primary btn-update-asset-user-auth" data-username="DEFAULT_USERNAME">{% trans "Update auth" %}</a>'.replace("DEFAULT_USERNAME", cellData);
{% if asset.protocol == 'ssh' %}
var test_btn = ' <a class="btn btn-xs btn-info btn-test-connective" data-username="DEFAULT_USERNAME">{% trans "Test" %}</a>'.replace("DEFAULT_USERNAME", cellData);
$(td).html(test_btn + update_auth_btn);
{% else %}
$(td).html(update_auth_btn);
{% endif %}
{#var check_btn = ' <a class="btn btn-xs btn-info btn-check-asset-user-auth" data-username="DEFAULT_USERNAME">{% trans "Check auth" %}</a>'.replace("DEFAULT_USERNAME", cellData);#}
}}
],
ajax_url: '{% url "api-assets:asset-user-list" %}' + '?asset_id={{ asset.id }}',
columns: [
{data: function (){return ''}}, {data: "username" },
{data: "version"}, {data: "connectivity"}, {data: "date_updated"},
{data: "username", orderable: false}
],
op_html: $('#actions').html()
};
jumpserver.initDataTable(options);
}
var username;
$(document).ready(function () {
initAssetUserTable();
})
{#.on('click', '.btn-check-asset-user-auth', function(){#}
{# var username = $(this).data('username');#}
{# var the_url = "{% url 'api-assets:asset-user-auth-info' %}" + '?asset_id={{ asset.id }}' + '&username=' + username;#}
{# $.ajax({#}
{# url: the_url,#}
{# method: 'GET',#}
{# success: function (data) {#}
{# alert("Password: " + data.password);#}
{# }#}
{# });#}
{# })#}
.on('click', '.btn-update-asset-user-auth', function() {
username = $(this).data('username');
var hostname = "{{ asset.hostname }}";
initAssetUserAuthModalForm(hostname, username);
$("#asset_user_auth_modal").modal();
})
.on('click', '#btn_asset_user_auth_modal_confirm', function(){
var password = $('#id_password').val();
if (password){
var data = {
'name': username,
'asset': "{{ asset.id }}",
'username': username,
'password': password
};
formSubmit({
data: data,
url: "{% url 'api-assets:asset-user-list' %}",
method: 'POST',
success: function () {
toastr.success("{% trans 'Update successfully!' %}");
},
error: function () {
toastr.error("{% trans 'Update failed!' %}");
}
});
$("#asset_user_auth_modal").modal('hide');
}
else{
$('#id_password').parent().addClass('has-error');
}
})
.on('click', '.btn-test-connective', function () {
var username = $(this).data('username');
var the_url = "{% url 'api-assets:asset-user-connective' %}" + "?asset_id={{ asset.id }}" + "&username=" + username;
var success = function (data) {
var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
};
APIUpdateAttr({
url: the_url,
method: 'GET',
success: success,
flash_message: false
});
})
.on('click', '#btn-bulk-test-connective', function () {
var the_url = "{% url 'api-assets:asset-user-connective' %}" + "?asset_id={{ asset.id }}";
var success = function (data) {
var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
};
APIUpdateAttr({
url: the_url,
method: 'GET',
success: success,
flash_message: false
});
})
</script>
{% endblock %}
\ No newline at end of file
...@@ -19,6 +19,9 @@ ...@@ -19,6 +19,9 @@
<li class="active"> <li class="active">
<a href="{% url 'assets:asset-detail' pk=asset.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Asset detail' %} </a> <a href="{% url 'assets:asset-detail' pk=asset.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Asset detail' %} </a>
</li> </li>
<li>
<a href="{% url 'assets:asset-user-list' pk=asset.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Asset user list' %} </a>
</li>
{% if user.is_superuser %} {% if user.is_superuser %}
<li class="pull-right"> <li class="pull-right">
<a class="btn btn-outline btn-default" href="{% url 'assets:asset-update' pk=asset.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a> <a class="btn btn-outline btn-default" href="{% url 'assets:asset-update' pk=asset.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
...@@ -32,7 +35,7 @@ ...@@ -32,7 +35,7 @@
</ul> </ul>
</div> </div>
<div class="tab-content"> <div class="tab-content">
<div class="col-sm-7" style="padding-left: 0"> <div class="col-sm-8" style="padding-left: 0">
<div class="ibox float-e-margins"> <div class="ibox float-e-margins">
<div class="ibox-title"> <div class="ibox-title">
<span class="label"><b>{{ asset.hostname }}</b></span> <span class="label"><b>{{ asset.hostname }}</b></span>
...@@ -139,7 +142,7 @@ ...@@ -139,7 +142,7 @@
</div> </div>
</div> </div>
{% if user.is_superuser or user.is_org_admin %} {% if user.is_superuser or user.is_org_admin %}
<div class="col-sm-5" style="padding-left: 0;padding-right: 0"> <div class="col-sm-4" style="padding-left: 0;padding-right: 0">
<div class="panel panel-primary"> <div class="panel panel-primary">
<div class="panel-heading"> <div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'Quick modify' %} <i class="fa fa-info-circle"></i> {% trans 'Quick modify' %}
......
...@@ -15,10 +15,16 @@ ...@@ -15,10 +15,16 @@
<div class="panel-options"> <div class="panel-options">
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li> <li>
<a href="{% url 'assets:domain-detail' pk=object.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a> <a href="{% url 'assets:domain-detail' pk=object.id %}"
class="text-center"><i
class="fa fa-laptop"></i> {% trans 'Detail' %}
</a>
</li> </li>
<li class="active"> <li class="active">
<a href="{% url 'assets:domain-detail' pk=object.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Gateway' %} </a> <a href="{% url 'assets:domain-detail' pk=object.id %}"
class="text-center"><i
class="fa fa-laptop"></i> {% trans 'Gateway' %}
</a>
</li> </li>
</ul> </ul>
</div> </div>
...@@ -33,7 +39,8 @@ ...@@ -33,7 +39,8 @@
<a class="collapse-link"> <a class="collapse-link">
<i class="fa fa-chevron-up"></i> <i class="fa fa-chevron-up"></i>
</a> </a>
<a class="dropdown-toggle" data-toggle="dropdown" href="#"> <a class="dropdown-toggle"
data-toggle="dropdown" href="#">
<i class="fa fa-wrench"></i> <i class="fa fa-wrench"></i>
</a> </a>
<ul class="dropdown-menu dropdown-user"> <ul class="dropdown-menu dropdown-user">
...@@ -45,13 +52,17 @@ ...@@ -45,13 +52,17 @@
</div> </div>
<div class="ibox-content"> <div class="ibox-content">
<div class="uc pull-left m-r-5"> <div class="uc pull-left m-r-5">
<a href="{% url 'assets:domain-gateway-create' pk=object.id %}" class="btn btn-sm btn-primary"> {% trans "Create gateway" %} </a> <a href="{% url 'assets:domain-gateway-create' pk=object.id %}"
class="btn btn-sm btn-primary"> {% trans "Create gateway" %} </a>
</div> </div>
<table class="table table-striped table-bordered table-hover " id="domain_list_table" > <table class="table table-striped table-bordered table-hover "
id="domain_list_table">
<thead> <thead>
<tr> <tr>
<th class="text-center"> <th class="text-center">
<input type="checkbox" id="check_all" class="ipt_check_all" > <input type="checkbox"
id="check_all"
class="ipt_check_all">
</th> </th>
<th class="text-center">{% trans 'Name' %}</th> <th class="text-center">{% trans 'Name' %}</th>
<th class="text-center">{% trans 'IP' %}</th> <th class="text-center">{% trans 'IP' %}</th>
...@@ -73,6 +84,7 @@ ...@@ -73,6 +84,7 @@
</div> </div>
</div> </div>
</div> </div>
{% include 'assets/_gateway_test_modal.html' %}
{% endblock %} {% endblock %}
{% block content_bottom_left %}{% endblock %} {% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
...@@ -84,7 +96,7 @@ function initTable() { ...@@ -84,7 +96,7 @@ function initTable() {
{targets: 7, createdCell: function (td, cellData, rowData) { {targets: 7, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "assets:domain-gateway-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData); var update_btn = '<a href="{% url "assets:domain-gateway-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData); var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
var test_btn = '<a class="btn btn-xs btn-warning m-l-xs btn-test" data-uid="{{ DEFAULT_PK }}">{% trans "Test connection" %}</a>'.replace('{{ DEFAULT_PK }}', cellData); var test_btn = '<a class="btn btn-xs btn-warning m-l-xs btn-test" data-uid="{{ DEFAULT_PK }}" data-port="PORT">{% trans "Test connection" %}</a>'.replace('{{ DEFAULT_PK }}', cellData).replace("PORT", rowData.port);
if(rowData.protocol === 'rdp'){ if(rowData.protocol === 'rdp'){
test_btn = '<a class="btn btn-xs btn-warning m-l-xs btn-test" disabled data-uid="{{ DEFAULT_PK }}">{% trans "Test connection" %}</a>'.replace('{{ DEFAULT_PK }}', cellData); test_btn = '<a class="btn btn-xs btn-warning m-l-xs btn-test" disabled data-uid="{{ DEFAULT_PK }}">{% trans "Test connection" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
} }
...@@ -114,12 +126,18 @@ $(document).ready(function(){ ...@@ -114,12 +126,18 @@ $(document).ready(function(){
$data_table.ajax.reload(); $data_table.ajax.reload();
}, 3000); }, 3000);
}).on('click', '.btn-test', function () { }).on('click', '.btn-test', function () {
var $this = $(this); $("#ssh_test_port").val($(this).data('port'));
var uid = $this.data('uid'); $("#gateway_id").val($(this).data('uid'));
$("#gateway_test").modal('show');
}).on('click', '#btn_gateway_test', function () {
var data = $("#test_gateway_form").serializeObject();
var uid = data.gateway_id;
var the_url = '{% url "api-assets:test-gateway-connective" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid); var the_url = '{% url "api-assets:test-gateway-connective" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
APIUpdateAttr({ APIUpdateAttr({
url: the_url, url: the_url,
method: "GET", method: "POST",
body: JSON.stringify({'port': parseInt(data.port)}),
success_message: "{% trans 'Can be connected' %}", success_message: "{% trans 'Can be connected' %}",
fail_message: "{% trans 'The connection fails' %}" fail_message: "{% trans 'The connection fails' %}"
}) })
......
...@@ -132,6 +132,7 @@ ...@@ -132,6 +132,7 @@
</div> </div>
</div> </div>
</div> </div>
{% include 'assets/_asset_user_auth_modal.html' %}
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
...@@ -155,14 +156,15 @@ function initAssetsTable() { ...@@ -155,14 +156,15 @@ function initAssetsTable() {
$(td).html('') $(td).html('')
} }
}}, }},
{targets: 4, createdCell: function (td, cellData) { {targets: 4, createdCell: function (td, cellData, rowData) {
var push_btn = ''; var push_btn = '';
{% if system_user.auto_push %} {% if system_user.auto_push %}
push_btn = '<a class="btn btn-xs btn-primary btn-push-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Push" %}</a>'.replace("{{ DEFAULT_PK }}", cellData); push_btn = '<a class="btn btn-xs btn-primary btn-push-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Push" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
{% endif %} {% endif %}
var test_btn = ' <a class="btn btn-xs btn-info btn-test-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Test" %}</a>'.replace("{{ DEFAULT_PK }}", cellData); var test_btn = ' <a class="btn btn-xs btn-info btn-test-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Test" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
{#var unbound_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-asset-unbound" data-uid="{{ DEFAULT_PK }}">{% trans "Unbound" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);#} {#var unbound_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-asset-unbound" data-uid="{{ DEFAULT_PK }}">{% trans "Unbound" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);#}
$(td).html(push_btn + test_btn); var update_auth_btn = ' <a class="btn btn-xs btn-primary btn-update-asset-user-auth" data-aid="{{ DEFAULT_PK }}" data-hostname="hostname777">{% trans "Update auth" %}</a>'.replace("{{ DEFAULT_PK }}", cellData).replace("hostname777", rowData.hostname);
$(td).html(push_btn + test_btn + update_auth_btn);
}} }}
], ],
ajax_url: '{% url "api-assets:system-user-assets" pk=system_user.id %}', ajax_url: '{% url "api-assets:system-user-assets" pk=system_user.id %}',
...@@ -202,6 +204,15 @@ function updateSystemUserNode(nodes) { ...@@ -202,6 +204,15 @@ function updateSystemUserNode(nodes) {
} }
jumpserver.nodes_selected = {}; jumpserver.nodes_selected = {};
function initAssetUserAuthModalForm(hostname, username){
$('#id_hostname_p').html(hostname);
$('#id_username_p').html(username);
$('#id_password').parent().removeClass('has-error');
$('#id_password').val('');
}
var assetId;
$(document).ready(function () { $(document).ready(function () {
$('.select2').select2() $('.select2').select2()
.on('select2:select', function(evt) { .on('select2:select', function(evt) {
...@@ -315,6 +326,38 @@ $(document).ready(function () { ...@@ -315,6 +326,38 @@ $(document).ready(function () {
error: error error: error
}) })
}) })
.on('click', '.btn-update-asset-user-auth', function() {
assetId = $(this).data('aid');
var hostname = $(this).data('hostname');
var username = '{{ system_user.username }}';
initAssetUserAuthModalForm(hostname, username);
$("#asset_user_auth_modal").modal();
})
.on('click', '#btn_asset_user_auth_modal_confirm', function(){
var password = $('#id_password').val();
if (password){
var data = {
'name': "{{ system_user.username }}",
'asset': assetId,
'username': "{{ system_user.username }}",
'password': password
};
formSubmit({
data: data,
url: "{% url 'api-assets:asset-user-list' %}",
method: 'POST',
success: function () {
toastr.success("{% trans 'Update successfully!' %}");
},
error: function () {
toastr.error("{% trans 'Update failed!' %}");
}
});
$("#asset_user_auth_modal").modal('hide');
}
else{
$('#id_password').parent().addClass('has-error');
}
})
</script> </script>
{% endblock %} {% endblock %}
...@@ -17,7 +17,6 @@ ...@@ -17,7 +17,6 @@
<div class="file-manager "> <div class="file-manager ">
<div id="assetTree" class="ztree"> <div id="assetTree" class="ztree">
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
</div> </div>
...@@ -46,6 +45,7 @@ ...@@ -46,6 +45,7 @@
<th class="text-center">{% trans 'IP' %}</th> <th class="text-center">{% trans 'IP' %}</th>
<th class="text-center">{% trans 'Active' %}</th> <th class="text-center">{% trans 'Active' %}</th>
<th class="text-center">{% trans 'System users' %}</th> <th class="text-center">{% trans 'System users' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
...@@ -62,16 +62,19 @@ ...@@ -62,16 +62,19 @@
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
var treeUrl = "{% url 'api-perms:my-nodes-assets-as-tree' %}?show_assets=0&cache_policy=1";
var zTree, asset_table, show=0; var zTree, asset_table, show=0;
var inited = false; var inited = false;
var url; var url;
function initTable() { function initTable() {
if (inited){ if (inited){
return return
} else { } else {
inited = true; inited = true;
} }
url = "{% url 'api-perms:my-assets' %}"; url = "{% url 'api-perms:my-assets' %}?cache_policy=1";
var options = { var options = {
ele: $('#user_assets_table'), ele: $('#user_assets_table'),
columnDefs: [ columnDefs: [
...@@ -92,13 +95,18 @@ function initTable() { ...@@ -92,13 +95,18 @@ function initTable() {
users.push(data.name); users.push(data.name);
}); });
$(td).html(users.join(', ')) $(td).html(users.join(', '))
}},
{targets: 5, createdCell: function (td, cellData) {
var conn_btn = '<a href="{% url "luna-view" %}?login_to=' + cellData +'" class="btn btn-xs btn-primary">{% trans "Connect" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
$(td).html(conn_btn)
}} }}
], ],
ajax_url: url, ajax_url: url,
columns: [ columns: [
{data: "id"}, {data: "hostname" }, {data: "ip" }, {data: "id"}, {data: "hostname" }, {data: "ip" },
{data: "is_active", orderable: false }, {data: "is_active", orderable: false },
{data: "system_users_granted", orderable: false} {data: "system_users_granted", orderable: false},
{data: "id", orderable: false}
] ]
}; };
asset_table = jumpserver.initServerSideDataTable(options); asset_table = jumpserver.initServerSideDataTable(options);
...@@ -106,7 +114,7 @@ function initTable() { ...@@ -106,7 +114,7 @@ function initTable() {
} }
function onSelected(event, treeNode) { function onSelected(event, treeNode) {
url = '{% url "api-perms:my-node-assets" node_id=DEFAULT_PK %}'; url = '{% url "api-perms:my-node-assets" node_id=DEFAULT_PK %}?cache_policy=1';
var node_id = treeNode.meta.node.id; var node_id = treeNode.meta.node.id;
url = url.replace("{{ DEFAULT_PK }}", node_id); url = url.replace("{{ DEFAULT_PK }}", node_id);
setCookie('node_selected', treeNode.id); setCookie('node_selected', treeNode.id);
...@@ -118,7 +126,7 @@ function initTree() { ...@@ -118,7 +126,7 @@ function initTree() {
var setting = { var setting = {
view: { view: {
dblClickExpand: false, dblClickExpand: false,
showLine: true showLine: true,
}, },
data: { data: {
simpleData: { simpleData: {
...@@ -131,10 +139,14 @@ function initTree() { ...@@ -131,10 +139,14 @@ function initTree() {
}; };
var zNodes = []; var zNodes = [];
$.get("{% url 'api-perms:my-nodes-assets-as-tree' %}?show_assets=0", function(data, status){ $.get(treeUrl, function(data, status){
zNodes = data; zNodes = data;
$.fn.zTree.init($("#assetTree"), setting, zNodes); $.fn.zTree.init($("#assetTree"), setting, zNodes);
zTree = $.fn.zTree.getZTreeObj("assetTree"); zTree = $.fn.zTree.getZTreeObj("assetTree");
rootNodeAddDom(zTree, function () {
treeUrl = treeUrl.replace('cache_policy=1', 'cache_policy=2');
initTree();
});
}); });
} }
......
...@@ -17,6 +17,7 @@ router.register(r'nodes', api.NodeViewSet, 'node') ...@@ -17,6 +17,7 @@ router.register(r'nodes', api.NodeViewSet, 'node')
router.register(r'domain', api.DomainViewSet, 'domain') router.register(r'domain', api.DomainViewSet, 'domain')
router.register(r'gateway', api.GatewayViewSet, 'gateway') router.register(r'gateway', api.GatewayViewSet, 'gateway')
router.register(r'cmd-filter', api.CommandFilterViewSet, 'cmd-filter') router.register(r'cmd-filter', api.CommandFilterViewSet, 'cmd-filter')
router.register(r'asset-user', api.AssetUserViewSet, 'asset-user')
cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filter', lookup='filter') cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filter', lookup='filter')
cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule') cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule')
...@@ -31,6 +32,12 @@ urlpatterns = [ ...@@ -31,6 +32,12 @@ urlpatterns = [
path('assets/<uuid:pk>/gateway/', path('assets/<uuid:pk>/gateway/',
api.AssetGatewayApi.as_view(), name='asset-gateway'), api.AssetGatewayApi.as_view(), name='asset-gateway'),
path('asset-user/auth-info/',
api.AssetUserAuthInfoApi.as_view(), name='asset-user-auth-info'),
path('asset-user/test-connective/',
api.AssetUserTestConnectiveApi.as_view(), name='asset-user-connective'),
path('admin-user/<uuid:pk>/nodes/', path('admin-user/<uuid:pk>/nodes/',
api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'), api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'),
path('admin-user/<uuid:pk>/auth/', path('admin-user/<uuid:pk>/auth/',
...@@ -42,6 +49,8 @@ urlpatterns = [ ...@@ -42,6 +49,8 @@ urlpatterns = [
path('system-user/<uuid:pk>/auth-info/', path('system-user/<uuid:pk>/auth-info/',
api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'), api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
path('system-user/<uuid:pk>/asset/<uuid:aid>/auth-info/',
api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'),
path('system-user/<uuid:pk>/assets/', path('system-user/<uuid:pk>/assets/',
api.SystemUserAssetsListView.as_view(), name='system-user-assets'), api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
path('system-user/<uuid:pk>/push/', path('system-user/<uuid:pk>/push/',
...@@ -79,6 +88,7 @@ urlpatterns = [ ...@@ -79,6 +88,7 @@ urlpatterns = [
path('gateway/<uuid:pk>/test-connective/', path('gateway/<uuid:pk>/test-connective/',
api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'), api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
] ]
urlpatterns += router.urls + cmd_filter_router.urls urlpatterns += router.urls + cmd_filter_router.urls
......
...@@ -15,6 +15,8 @@ urlpatterns = [ ...@@ -15,6 +15,8 @@ urlpatterns = [
path('asset/<uuid:pk>/update/', views.AssetUpdateView.as_view(), name='asset-update'), path('asset/<uuid:pk>/update/', views.AssetUpdateView.as_view(), name='asset-update'),
path('asset/<uuid:pk>/delete/', views.AssetDeleteView.as_view(), name='asset-delete'), path('asset/<uuid:pk>/delete/', views.AssetDeleteView.as_view(), name='asset-delete'),
path('asset/update/', views.AssetBulkUpdateView.as_view(), name='asset-bulk-update'), path('asset/update/', views.AssetBulkUpdateView.as_view(), name='asset-bulk-update'),
# Asset user view
path('asset/<uuid:pk>/asset-user/', views.AssetUserListView.as_view(), name='asset-user-list'),
# User asset view # User asset view
path('user-asset/', views.UserAssetListView.as_view(), name='user-asset-list'), path('user-asset/', views.UserAssetListView.as_view(), name='user-asset-list'),
......
...@@ -34,7 +34,7 @@ from ..models import Asset, AdminUser, SystemUser, Label, Node, Domain ...@@ -34,7 +34,7 @@ from ..models import Asset, AdminUser, SystemUser, Label, Node, Domain
__all__ = [ __all__ = [
'AssetListView', 'AssetCreateView', 'AssetUpdateView', 'AssetListView', 'AssetCreateView', 'AssetUpdateView', 'AssetUserListView',
'UserAssetListView', 'AssetBulkUpdateView', 'AssetDetailView', 'UserAssetListView', 'AssetBulkUpdateView', 'AssetDetailView',
'AssetDeleteView', 'AssetExportView', 'BulkImportAssetView', 'AssetDeleteView', 'AssetExportView', 'BulkImportAssetView',
] ]
...@@ -56,6 +56,20 @@ class AssetListView(AdminUserRequiredMixin, TemplateView): ...@@ -56,6 +56,20 @@ class AssetListView(AdminUserRequiredMixin, TemplateView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class AssetUserListView(AdminUserRequiredMixin, DetailView):
model = Asset
context_object_name = 'asset'
template_name = 'assets/asset_asset_user_list.html'
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),
'action': _('Asset user list'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class UserAssetListView(LoginRequiredMixin, TemplateView): class UserAssetListView(LoginRequiredMixin, TemplateView):
template_name = 'assets/user_asset_list.html' template_name = 'assets/user_asset_list.html'
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from users.models import LoginLog
\ No newline at end of file
...@@ -38,14 +38,4 @@ class Migration(migrations.Migration): ...@@ -38,14 +38,4 @@ class Migration(migrations.Migration):
('datetime', models.DateTimeField(auto_now=True)), ('datetime', models.DateTimeField(auto_now=True)),
], ],
), ),
migrations.CreateModel(
name='UserLoginLog',
fields=[
],
options={
'proxy': True,
'indexes': [],
},
bases=('users.loginlog',),
),
] ]
# Generated by Django 2.1.7 on 2019-02-28 09:15
from django.db import migrations, models, connection
import django.utils.timezone
import uuid
class Migration(migrations.Migration):
dependencies = [
('audits', '0004_operatelog_passwordchangelog_userloginlog'),
('users', '0019_auto_20190304_1459'),
]
state_operations = [
migrations.CreateModel(
name='UserLoginLog',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True,
serialize=False)),
('username',
models.CharField(max_length=128, verbose_name='Username')),
('type',
models.CharField(choices=[('W', 'Web'), ('T', 'Terminal')],
max_length=2, verbose_name='Login type')),
('ip', models.GenericIPAddressField(verbose_name='Login ip')),
('city', models.CharField(blank=True, max_length=254, null=True,
verbose_name='Login city')),
('user_agent',
models.CharField(blank=True, max_length=254, null=True,
verbose_name='User agent')),
('mfa', models.SmallIntegerField(
choices=[(0, 'Disabled'), (1, 'Enabled'), (2, '-')],
default=2, verbose_name='MFA')),
('reason', models.SmallIntegerField(
choices=[(0, '-'), (1, 'Username/password check failed'),
(2, 'MFA authentication failed'),
(3, 'Username does not exist'),
(4, 'Password expired')], default=0,
verbose_name='Reason')),
('status', models.BooleanField(
choices=[(True, 'Success'), (False, 'Failed')],
default=True, max_length=2, verbose_name='Status')),
('datetime',
models.DateTimeField(default=django.utils.timezone.now,
verbose_name='Date login')),
],
options={
'ordering': ['-datetime', 'username'],
},
),
]
operations = [
migrations.SeparateDatabaseAndState(state_operations=state_operations)
]
import uuid import uuid
from django.db import models from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from orgs.mixins import OrgModelMixin from orgs.mixins import OrgModelMixin
from .hands import LoginLog
__all__ = [ __all__ = [
'FTPLog', 'OperateLog', 'PasswordChangeLog', 'UserLoginLog', 'FTPLog', 'OperateLog', 'PasswordChangeLog', 'UserLoginLog',
...@@ -55,6 +56,67 @@ class PasswordChangeLog(models.Model): ...@@ -55,6 +56,67 @@ class PasswordChangeLog(models.Model):
return "{} change {}'s password".format(self.change_by, self.user) return "{} change {}'s password".format(self.change_by, self.user)
class UserLoginLog(LoginLog): class UserLoginLog(models.Model):
LOGIN_TYPE_CHOICE = (
('W', 'Web'),
('T', 'Terminal'),
)
MFA_DISABLED = 0
MFA_ENABLED = 1
MFA_UNKNOWN = 2
MFA_CHOICE = (
(MFA_DISABLED, _('Disabled')),
(MFA_ENABLED, _('Enabled')),
(MFA_UNKNOWN, _('-')),
)
REASON_NOTHING = 0
REASON_PASSWORD = 1
REASON_MFA = 2
REASON_NOT_EXIST = 3
REASON_PASSWORD_EXPIRED = 4
REASON_CHOICE = (
(REASON_NOTHING, _('-')),
(REASON_PASSWORD, _('Username/password check failed')),
(REASON_MFA, _('MFA authentication failed')),
(REASON_NOT_EXIST, _("Username does not exist")),
(REASON_PASSWORD_EXPIRED, _("Password expired")),
)
STATUS_CHOICE = (
(True, _('Success')),
(False, _('Failed'))
)
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
username = models.CharField(max_length=128, verbose_name=_('Username'))
type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=2, verbose_name=_('Login type'))
ip = models.GenericIPAddressField(verbose_name=_('Login ip'))
city = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('Login city'))
user_agent = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('User agent'))
mfa = models.SmallIntegerField(default=MFA_UNKNOWN, choices=MFA_CHOICE, verbose_name=_('MFA'))
reason = models.SmallIntegerField(default=0, choices=REASON_CHOICE, verbose_name=_('Reason'))
status = models.BooleanField(max_length=2, default=True, choices=STATUS_CHOICE, verbose_name=_('Status'))
datetime = models.DateTimeField(default=timezone.now, verbose_name=_('Date login'))
@classmethod
def get_login_logs(cls, date_form=None, date_to=None, user=None, keyword=None):
login_logs = cls.objects.all()
if date_form and date_to:
login_logs = login_logs.filter(
datetime__gt=date_form, datetime__lt=date_to
)
if user:
login_logs = login_logs.filter(username=user)
if keyword:
login_logs = login_logs.filter(
Q(ip__contains=keyword) |
Q(city__contains=keyword) |
Q(username__contains=keyword)
)
return login_logs
class Meta: class Meta:
proxy = True ordering = ['-datetime', 'username']
# -*- coding: utf-8 -*-
#
import datetime
from django.utils import timezone
from django.conf import settings
from celery import shared_task
from ops.celery.decorator import register_as_period_task
from .models import UserLoginLog
@register_as_period_task(interval=3600*24)
@shared_task
def clean_login_log_period():
now = timezone.now()
try:
days = int(settings.LOGIN_LOG_KEEP_DAYS)
except ValueError:
days = 90
expired_day = now - datetime.timedelta(days=days)
UserLoginLog.objects.filter(datetime__lt=expired_day).delete()
...@@ -17,10 +17,10 @@ ...@@ -17,10 +17,10 @@
<div class="form-group" id="date"> <div class="form-group" id="date">
<div class="input-daterange input-group" id="datepicker"> <div class="input-daterange input-group" id="datepicker">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span> <span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_from" value="{{ date_from|date:'Y-m-d'}}"> <input type="text" id="id_date_from" class="input-sm form-control" style="width: 100px;" name="date_from" value="{{ date_from|date:'Y-m-d'}}">
{# <input type="text" class="input-sm form-control" style="width: 100px;" name="date_from" >#} {# <input type="text" class="input-sm form-control" style="width: 100px;" name="date_from" >#}
<span class="input-group-addon">to</span> <span class="input-group-addon">to</span>
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_to" value="{{ date_to|date:'Y-m-d'}}"> <input type="text" id="id_date_to" class="input-sm form-control" style="width: 100px;" name="date_to" value="{{ date_to|date:'Y-m-d'}}">
</div> </div>
</div> </div>
<div class="input-group"> <div class="input-group">
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
</select> </select>
</div> </div>
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control input-sm" name="keyword" placeholder="{% trans 'Search' %}" value="{{ keyword }}"> <input type="text" id="search" class="form-control input-sm" name="keyword" placeholder="{% trans 'Search' %}" value="{{ keyword }}">
</div> </div>
<div class="input-group"> <div class="input-group">
<div class="input-group-btn"> <div class="input-group-btn">
...@@ -43,8 +43,10 @@ ...@@ -43,8 +43,10 @@
</div> </div>
</form> </form>
{% endblock %} {% endblock %}
{% block table_container %}
{% block table_head %} <table class="table table-striped table-bordered table-hover " id="login_log_table" >
<thead>
<tr>
<th class="text-center">{% trans 'ID' %}</th> <th class="text-center">{% trans 'ID' %}</th>
<th class="text-center">{% trans 'Username' %}</th> <th class="text-center">{% trans 'Username' %}</th>
<th class="text-center">{% trans 'Type' %}</th> <th class="text-center">{% trans 'Type' %}</th>
...@@ -55,9 +57,10 @@ ...@@ -55,9 +57,10 @@
<th class="text-center">{% trans 'Reason' %}</th> <th class="text-center">{% trans 'Reason' %}</th>
<th class="text-center">{% trans 'Status' %}</th> <th class="text-center">{% trans 'Status' %}</th>
<th class="text-center">{% trans 'Date' %}</th> <th class="text-center">{% trans 'Date' %}</th>
{% endblock %} </tr>
<thead>
{% block table_body %} <tbody>
{% for login_log in object_list %} {% for login_log in object_list %}
<tr class="gradeX"> <tr class="gradeX">
<td class="text-center">{{ forloop.counter }}</td> <td class="text-center">{{ forloop.counter }}</td>
...@@ -74,8 +77,24 @@ ...@@ -74,8 +77,24 @@
<td class="text-center">{{ login_log.datetime }}</td> <td class="text-center">{{ login_log.datetime }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody>
</table>
<div id="actions" class="" style="margin-top: -20px">
<div class="input-group">
<select class="form-control m-b" style="width: auto" id="slct_bulk_update">
<option value="export">{% trans 'Export' %}</option>
</select>
<div class="input-group-btn pull-left" style="padding-left: 5px;">
<button id='btn_bulk_update' style="height: 32px;" class="btn btn-sm btn-primary btn_export">
{% trans 'Submit' %}
</button>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script> <script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
<script> <script>
...@@ -95,6 +114,29 @@ ...@@ -95,6 +114,29 @@
width: 'auto' width: 'auto'
}); });
}) })
.on('click', '.btn_export', function () {
var date_form = $('#id_date_from').val();
var date_to = $('#id_date_to').val();
var user = $('.select2 option:selected').val();
var keyword = $('#search').val();
$.ajax({
url: "{% url "audits:login-log-export" %}",
method: 'POST',
data: JSON.stringify({
'date_form':date_form,
'date_to':date_to,
'user':user,
'keyword':keyword
}),
dataType: "json",
success: function (data, textStatus) {
window.open(data.redirect)
},
error: function () {
toastr.error('Export failed');
}
})
})
</script> </script>
{% endblock %} {% endblock %}
...@@ -14,4 +14,5 @@ urlpatterns = [ ...@@ -14,4 +14,5 @@ urlpatterns = [
path('operate-log/', views.OperateLogListView.as_view(), name='operate-log-list'), path('operate-log/', views.OperateLogListView.as_view(), name='operate-log-list'),
path('password-change-log/', views.PasswordChangeLogList.as_view(), name='password-change-log-list'), path('password-change-log/', views.PasswordChangeLogList.as_view(), name='password-change-log-list'),
path('command-execution-log/', views.CommandExecutionListView.as_view(), name='command-execution-log-list'), path('command-execution-log/', views.CommandExecutionListView.as_view(), name='command-execution-log-list'),
path('login-log/export/', views.LoginLogExportView.as_view(), name='login-log-export'),
] ]
import csv
import codecs
from django.http import HttpResponse
def get_excel_response(filename):
excel_response = HttpResponse(content_type='text/csv')
excel_response[
'Content-Disposition'] = 'attachment; filename="%s"' % filename
excel_response.write(codecs.BOM_UTF8)
return excel_response
def write_content_to_excel(response, header=None, login_logs=None, fields=None):
writer = csv.writer(response, dialect='excel', quoting=csv.QUOTE_MINIMAL)
if header:
writer.writerow(header)
if login_logs:
for log in login_logs:
data = [getattr(log, field.name) for field in fields]
writer.writerow(data)
return response
\ No newline at end of file
import csv
import json
import uuid
import codecs
from django.conf import settings from django.conf import settings
from django.urls import reverse
from django.utils import timezone
from django.core.cache import cache
from django.http import HttpResponse, JsonResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import ListView from django.views.generic import ListView
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q from django.db.models import Q
from audits.utils import get_excel_response, write_content_to_excel
from common.mixins import DatetimeSearchMixin from common.mixins import DatetimeSearchMixin
from common.permissions import AdminUserRequiredMixin from common.permissions import AdminUserRequiredMixin
from orgs.utils import current_org from orgs.utils import current_org
from ops.views import CommandExecutionListView as UserCommandExecutionListView from ops.views import CommandExecutionListView as UserCommandExecutionListView
from users.models import User
from .models import FTPLog, OperateLog, PasswordChangeLog, UserLoginLog from .models import FTPLog, OperateLog, PasswordChangeLog, UserLoginLog
...@@ -222,14 +236,49 @@ class CommandExecutionListView(UserCommandExecutionListView): ...@@ -222,14 +236,49 @@ class CommandExecutionListView(UserCommandExecutionListView):
return users return users
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = super().get_context_data(**kwargs)
context.update({
'app': _('Audits'), 'app': _('Audits'),
'action': _('Command execution list'), 'action': _('Command execution log'),
'date_from': self.date_from, 'date_from': self.date_from,
'date_to': self.date_to, 'date_to': self.date_to,
'user_list': self.get_user_list(), 'user_list': self.get_user_list(),
'keyword': self.keyword, 'keyword': self.keyword,
'user_id': self.user_id, 'user_id': self.user_id,
} })
kwargs.update(context) return super().get_context_data(**context)
return super().get_context_data(**kwargs)
@method_decorator(csrf_exempt, name='dispatch')
class LoginLogExportView(LoginRequiredMixin, View):
def get(self, request):
fields = [
field for field in UserLoginLog._meta.fields
]
filename = 'login-logs-{}.csv'.format(
timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S')
)
excel_response = get_excel_response(filename)
header = [field.verbose_name for field in fields]
login_logs = cache.get(request.GET.get('spm', ''), [])
response = write_content_to_excel(excel_response, login_logs=login_logs,
header=header, fields=fields)
return response
def post(self, request):
try:
date_form = json.loads(request.body).get('date_form', [])
date_to = json.loads(request.body).get('date_to', [])
user = json.loads(request.body).get('user', [])
keyword = json.loads(request.body).get('keyword', [])
login_logs = UserLoginLog.get_login_logs(
date_form=date_form, date_to=date_to, user=user, keyword=keyword)
except ValueError:
return HttpResponse('Json object not valid', status=400)
spm = uuid.uuid4().hex
cache.set(spm, login_logs, 300)
url = reverse('audits:login-log-export') + '?spm=%s' % spm
return JsonResponse({'redirect': url})
\ No newline at end of file
# -*- coding: utf-8 -*-
#
from .auth import *
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import uuid import uuid
from django.core.cache import cache from django.core.cache import cache
...@@ -14,16 +15,21 @@ from rest_framework.views import APIView ...@@ -14,16 +15,21 @@ from rest_framework.views import APIView
from common.utils import get_logger, get_request_ip from common.utils import get_logger, get_request_ip
from common.permissions import IsOrgAdminOrAppUser from common.permissions import IsOrgAdminOrAppUser
from orgs.mixins import RootOrgViewMixin from orgs.mixins import RootOrgViewMixin
from ..serializers import UserSerializer from users.serializers import UserSerializer
from ..tasks import write_login_log_async from users.models import User
from ..models import User, LoginLog from assets.models import Asset, SystemUser
from ..utils import check_user_valid, check_otp_code, \ from audits.models import UserLoginLog as LoginLog
increase_login_failed_count, is_block_login, \ from users.utils import (
clean_failed_count check_user_valid, check_otp_code, increase_login_failed_count,
from ..hands import Asset, SystemUser is_block_login, clean_failed_count
)
from ..signals import post_auth_success, post_auth_failed
logger = get_logger(__name__) logger = get_logger(__name__)
__all__ = [
'UserAuthApi', 'UserConnectionTokenApi', 'UserOtpAuthApi',
]
class UserAuthApi(RootOrgViewMixin, APIView): class UserAuthApi(RootOrgViewMixin, APIView):
...@@ -46,37 +52,22 @@ class UserAuthApi(RootOrgViewMixin, APIView): ...@@ -46,37 +52,22 @@ class UserAuthApi(RootOrgViewMixin, APIView):
username = request.data.get('username', '') username = request.data.get('username', '')
exist = User.objects.filter(username=username).first() exist = User.objects.filter(username=username).first()
reason = LoginLog.REASON_PASSWORD if exist else LoginLog.REASON_NOT_EXIST reason = LoginLog.REASON_PASSWORD if exist else LoginLog.REASON_NOT_EXIST
data = { self.send_auth_signal(success=False, username=username, reason=reason)
'username': username,
'mfa': LoginLog.MFA_UNKNOWN,
'reason': reason,
'status': False
}
self.write_login_log(request, data)
increase_login_failed_count(username, ip) increase_login_failed_count(username, ip)
return Response({'msg': msg}, status=401) return Response({'msg': msg}, status=401)
if user.password_has_expired: if user.password_has_expired:
data = { self.send_auth_signal(
'username': user.username, success=False, username=username,
'mfa': int(user.otp_enabled), reason=LoginLog.REASON_PASSWORD_EXPIRED
'reason': LoginLog.REASON_PASSWORD_EXPIRED, )
'status': False
}
self.write_login_log(request, data)
msg = _("The user {} password has expired, please update.".format( msg = _("The user {} password has expired, please update.".format(
user.username)) user.username))
logger.info(msg) logger.info(msg)
return Response({'msg': msg}, status=401) return Response({'msg': msg}, status=401)
if not user.otp_enabled: if not user.otp_enabled:
data = { self.send_auth_signal(success=True, user=user)
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_NOTHING,
'status': True
}
self.write_login_log(request, data)
# 登陆成功,清除原来的缓存计数 # 登陆成功,清除原来的缓存计数
clean_failed_count(username, ip) clean_failed_count(username, ip)
token = user.create_bearer_token(request) token = user.create_bearer_token(request)
...@@ -91,7 +82,7 @@ class UserAuthApi(RootOrgViewMixin, APIView): ...@@ -91,7 +82,7 @@ class UserAuthApi(RootOrgViewMixin, APIView):
'code': 101, 'code': 101,
'msg': _('Please carry seed value and ' 'msg': _('Please carry seed value and '
'conduct MFA secondary certification'), 'conduct MFA secondary certification'),
'otp_url': reverse('api-users:user-otp-auth'), 'otp_url': reverse('api-auth:user-otp-auth'),
'seed': seed, 'seed': seed,
'user': self.serializer_class(user).data 'user': self.serializer_class(user).data
}, status=300 }, status=300
...@@ -108,22 +99,14 @@ class UserAuthApi(RootOrgViewMixin, APIView): ...@@ -108,22 +99,14 @@ class UserAuthApi(RootOrgViewMixin, APIView):
) )
return user, msg return user, msg
@staticmethod def send_auth_signal(self, success=True, user=None, username='', reason=''):
def write_login_log(request, data): if success:
login_ip = request.data.get('remote_addr', None) post_auth_success.send(sender=self.__class__, user=user, request=self.request)
login_type = request.data.get('login_type', '') else:
user_agent = request.data.get('HTTP_USER_AGENT', '') post_auth_failed.send(
sender=self.__class__, username=username,
if not login_ip: request=self.request, reason=reason
login_ip = get_request_ip(request) )
tmp_data = {
'ip': login_ip,
'type': login_type,
'user_agent': user_agent,
}
data.update(tmp_data)
write_login_log_async.delay(**data)
class UserConnectionTokenApi(RootOrgViewMixin, APIView): class UserConnectionTokenApi(RootOrgViewMixin, APIView):
...@@ -167,29 +150,6 @@ class UserConnectionTokenApi(RootOrgViewMixin, APIView): ...@@ -167,29 +150,6 @@ class UserConnectionTokenApi(RootOrgViewMixin, APIView):
return super().get_permissions() return super().get_permissions()
class UserToken(APIView):
permission_classes = (AllowAny,)
def post(self, request):
if not request.user.is_authenticated:
username = request.data.get('username', '')
email = request.data.get('email', '')
password = request.data.get('password', '')
public_key = request.data.get('public_key', '')
user, msg = check_user_valid(
username=username, email=email,
password=password, public_key=public_key)
else:
user = request.user
msg = None
if user:
token = user.create_bearer_token(request)
return Response({'Token': token, 'Keyword': 'Bearer'}, status=200)
else:
return Response({'error': msg}, status=406)
class UserOtpAuthApi(RootOrgViewMixin, APIView): class UserOtpAuthApi(RootOrgViewMixin, APIView):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
serializer_class = UserSerializer serializer_class = UserSerializer
...@@ -197,52 +157,25 @@ class UserOtpAuthApi(RootOrgViewMixin, APIView): ...@@ -197,52 +157,25 @@ class UserOtpAuthApi(RootOrgViewMixin, APIView):
def post(self, request): def post(self, request):
otp_code = request.data.get('otp_code', '') otp_code = request.data.get('otp_code', '')
seed = request.data.get('seed', '') seed = request.data.get('seed', '')
user = cache.get(seed, None) user = cache.get(seed, None)
if not user: if not user:
return Response( return Response(
{'msg': _('Please verify the user name and password first')}, {'msg': _('Please verify the user name and password first')},
status=401 status=401
) )
if not check_otp_code(user.otp_secret_key, otp_code): if not check_otp_code(user.otp_secret_key, otp_code):
data = { self.send_auth_signal(success=False, username=user.username, reason=LoginLog.REASON_MFA)
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_MFA,
'status': False
}
self.write_login_log(request, data)
return Response({'msg': _('MFA certification failed')}, status=401) return Response({'msg': _('MFA certification failed')}, status=401)
self.send_auth_signal(success=True, user=user)
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_NOTHING,
'status': True
}
self.write_login_log(request, data)
token = user.create_bearer_token(request) token = user.create_bearer_token(request)
return Response( data = {'token': token, 'user': self.serializer_class(user).data}
{ return Response(data)
'token': token,
'user': self.serializer_class(user).data
}
)
@staticmethod def send_auth_signal(self, success=True, user=None, username='', reason=''):
def write_login_log(request, data): if success:
login_ip = request.data.get('remote_addr', None) post_auth_success.send(sender=self.__class__, user=user, request=self.request)
login_type = request.data.get('login_type', '') else:
user_agent = request.data.get('HTTP_USER_AGENT', '') post_auth_failed.send(
sender=self.__class__, username=username,
if not login_ip: request=self.request, reason=reason
login_ip = get_request_ip(request) )
tmp_data = {
'ip': login_ip,
'type': login_type,
'user_agent': user_agent
}
data.update(tmp_data)
write_login_log_async.delay(**data)
...@@ -8,13 +8,13 @@ from django.core.cache import cache ...@@ -8,13 +8,13 @@ from django.core.cache import cache
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.six import text_type from django.utils.six import text_type
from django.utils.translation import ugettext_lazy as _ from django.contrib.auth import get_user_model
from rest_framework import HTTP_HEADER_ENCODING from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import authentication, exceptions from rest_framework import authentication, exceptions
from rest_framework.authentication import CSRFCheck from rest_framework.authentication import CSRFCheck
from common.utils import get_object_or_none, make_signature, http_to_unixtime from common.utils import get_object_or_none, make_signature, http_to_unixtime
from .models import User, AccessKey, PrivateToken from ..models import AccessKey, PrivateToken
def get_request_date_header(request): def get_request_date_header(request):
...@@ -42,7 +42,6 @@ class AccessKeyAuthentication(authentication.BaseAuthentication): ...@@ -42,7 +42,6 @@ class AccessKeyAuthentication(authentication.BaseAuthentication):
失败 失败
""" """
keyword = 'Sign' keyword = 'Sign'
model = AccessKey
def authenticate(self, request): def authenticate(self, request):
auth = authentication.get_authorization_header(request).split() auth = authentication.get_authorization_header(request).split()
...@@ -109,7 +108,7 @@ class AccessKeyAuthentication(authentication.BaseAuthentication): ...@@ -109,7 +108,7 @@ class AccessKeyAuthentication(authentication.BaseAuthentication):
class AccessTokenAuthentication(authentication.BaseAuthentication): class AccessTokenAuthentication(authentication.BaseAuthentication):
keyword = 'Bearer' keyword = 'Bearer'
model = User model = get_user_model()
expiration = settings.TOKEN_EXPIRATION or 3600 expiration = settings.TOKEN_EXPIRATION or 3600
def authenticate(self, request): def authenticate(self, request):
...@@ -133,10 +132,9 @@ class AccessTokenAuthentication(authentication.BaseAuthentication): ...@@ -133,10 +132,9 @@ class AccessTokenAuthentication(authentication.BaseAuthentication):
raise exceptions.AuthenticationFailed(msg) raise exceptions.AuthenticationFailed(msg)
return self.authenticate_credentials(token) return self.authenticate_credentials(token)
@staticmethod def authenticate_credentials(self, token):
def authenticate_credentials(token):
user_id = cache.get(token) user_id = cache.get(token)
user = get_object_or_none(User, id=user_id) user = get_object_or_none(self.model, id=user_id)
if not user: if not user:
msg = _('Invalid token or cache refreshed.') msg = _('Invalid token or cache refreshed.')
......
...@@ -16,8 +16,13 @@ class LDAPAuthorizationBackend(LDAPBackend): ...@@ -16,8 +16,13 @@ class LDAPAuthorizationBackend(LDAPBackend):
""" """
def authenticate(self, request=None, username=None, password=None, **kwargs): def authenticate(self, request=None, username=None, password=None, **kwargs):
logger.info('Authentication LDAP backend')
if not username:
logger.info('Authenticate failed: username is None')
return None
ldap_user = LDAPUser(self, username=username.strip(), request=request) ldap_user = LDAPUser(self, username=username.strip(), request=request)
user = self.authenticate_ldap_user(ldap_user, password) user = self.authenticate_ldap_user(ldap_user, password)
logger.info('Authenticate user: {}'.format(user))
return user return user
def get_user(self, user_id): def get_user(self, user_id):
...@@ -83,7 +88,10 @@ class LDAPUser(_LDAPUser): ...@@ -83,7 +88,10 @@ class LDAPUser(_LDAPUser):
def _populate_user_from_attributes(self): def _populate_user_from_attributes(self):
super()._populate_user_from_attributes() super()._populate_user_from_attributes()
if not hasattr(self._user, 'email') or '@' not in self._user.email: if not hasattr(self._user, 'email') or '@' not in self._user.email:
if '@' not in self._user.username:
email = '{}@{}'.format(self._user.username, settings.EMAIL_SUFFIX) email = '{}@{}'.format(self._user.username, settings.EMAIL_SUFFIX)
else:
email = self._user.username
setattr(self._user, 'email', email) setattr(self._user, 'email', email)
......
# -*- coding: utf-8 -*-
#
from .backends import *
from .middleware import *
from .utils import *
...@@ -4,16 +4,19 @@ ...@@ -4,16 +4,19 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.conf import settings from django.conf import settings
from . import client
from common.utils import get_logger from common.utils import get_logger
from authentication.openid.models import OIDT_ACCESS_TOKEN from .utils import new_client
from .models import OIDT_ACCESS_TOKEN
UserModel = get_user_model() UserModel = get_user_model()
logger = get_logger(__file__) logger = get_logger(__file__)
client = new_client()
BACKEND_OPENID_AUTH_CODE = \
'authentication.openid.backends.OpenIDAuthorizationCodeBackend' __all__ = [
'OpenIDAuthorizationCodeBackend', 'OpenIDAuthorizationPasswordBackend',
]
class BaseOpenIDAuthorizationBackend(object): class BaseOpenIDAuthorizationBackend(object):
...@@ -39,41 +42,41 @@ class BaseOpenIDAuthorizationBackend(object): ...@@ -39,41 +42,41 @@ class BaseOpenIDAuthorizationBackend(object):
class OpenIDAuthorizationCodeBackend(BaseOpenIDAuthorizationBackend): class OpenIDAuthorizationCodeBackend(BaseOpenIDAuthorizationBackend):
def authenticate(self, request, **kwargs): def authenticate(self, request, **kwargs):
logger.info('1.openid code backend') logger.info('Authentication OpenID code backend')
code = kwargs.get('code') code = kwargs.get('code')
redirect_uri = kwargs.get('redirect_uri') redirect_uri = kwargs.get('redirect_uri')
if not code or not redirect_uri: if not code or not redirect_uri:
logger.info('Authenticate failed: No code or No redirect uri')
return None return None
try: try:
oidt_profile = client.update_or_create_from_code( oidt_profile = client.update_or_create_from_code(
code=code, code=code, redirect_uri=redirect_uri
redirect_uri=redirect_uri
) )
except Exception as e: except Exception as e:
logger.error(e) logger.info('Authenticate failed: get oidt_profile: {}'.format(e))
else: else:
# Check openid user single logout or not with access_token # Check openid user single logout or not with access_token
request.session[OIDT_ACCESS_TOKEN] = oidt_profile.access_token request.session[OIDT_ACCESS_TOKEN] = oidt_profile.access_token
user = oidt_profile.user user = oidt_profile.user
logger.info('Authenticate success: user -> {}'.format(user))
return user if self.user_can_authenticate(user) else None return user if self.user_can_authenticate(user) else None
class OpenIDAuthorizationPasswordBackend(BaseOpenIDAuthorizationBackend): class OpenIDAuthorizationPasswordBackend(BaseOpenIDAuthorizationBackend):
def authenticate(self, request, username=None, password=None, **kwargs): def authenticate(self, request, username=None, password=None, **kwargs):
logger.info('2.openid password backend') logger.info('Authentication OpenID password backend')
if not settings.AUTH_OPENID: if not settings.AUTH_OPENID:
logger.info('Authenticate failed: AUTH_OPENID is False')
return None return None
elif not username: elif not username:
logger.info('Authenticate failed: Not username')
return None return None
try: try:
...@@ -82,9 +85,10 @@ class OpenIDAuthorizationPasswordBackend(BaseOpenIDAuthorizationBackend): ...@@ -82,9 +85,10 @@ class OpenIDAuthorizationPasswordBackend(BaseOpenIDAuthorizationBackend):
) )
except Exception as e: except Exception as e:
logger.error(e) logger.info('Authenticate failed: get oidt_profile: {}'.format(e))
else: else:
user = oidt_profile.user user = oidt_profile.user
logger.info('Authenticate success: user -> {}'.format(user))
return user if self.user_can_authenticate(user) else None return user if self.user_can_authenticate(user) else None
...@@ -6,12 +6,13 @@ from django.contrib.auth import logout ...@@ -6,12 +6,13 @@ from django.contrib.auth import logout
from django.utils.deprecation import MiddlewareMixin from django.utils.deprecation import MiddlewareMixin
from django.contrib.auth import BACKEND_SESSION_KEY from django.contrib.auth import BACKEND_SESSION_KEY
from . import client
from common.utils import get_logger from common.utils import get_logger
from .backends import BACKEND_OPENID_AUTH_CODE from .utils import new_client
from authentication.openid.models import OIDT_ACCESS_TOKEN from .models import OIDT_ACCESS_TOKEN
BACKEND_OPENID_AUTH_CODE = 'OpenIDAuthorizationCodeBackend'
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = ['OpenIDAuthenticationMiddleware']
class OpenIDAuthenticationMiddleware(MiddlewareMixin): class OpenIDAuthenticationMiddleware(MiddlewareMixin):
...@@ -20,22 +21,22 @@ class OpenIDAuthenticationMiddleware(MiddlewareMixin): ...@@ -20,22 +21,22 @@ class OpenIDAuthenticationMiddleware(MiddlewareMixin):
""" """
def process_request(self, request): def process_request(self, request):
# Don't need openid auth if AUTH_OPENID is False # Don't need openid auth if AUTH_OPENID is False
if not settings.AUTH_OPENID: if not settings.AUTH_OPENID:
return return
# Don't need check single logout if user not authenticated # Don't need check single logout if user not authenticated
if not request.user.is_authenticated: if not request.user.is_authenticated:
return return
elif request.session[BACKEND_SESSION_KEY].endswith(
elif request.session[BACKEND_SESSION_KEY] != BACKEND_OPENID_AUTH_CODE: BACKEND_OPENID_AUTH_CODE):
return return
# Check openid user single logout or not with access_token # Check openid user single logout or not with access_token
client = new_client()
try: try:
client.openid_connect_client.userinfo( client.openid_connect_client.userinfo(
token=request.session.get(OIDT_ACCESS_TOKEN)) token=request.session.get(OIDT_ACCESS_TOKEN)
)
except Exception as e: except Exception as e:
logout(request) logout(request)
......
...@@ -5,7 +5,8 @@ from django.db import transaction ...@@ -5,7 +5,8 @@ from django.db import transaction
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from keycloak.realm import KeycloakRealm from keycloak.realm import KeycloakRealm
from keycloak.keycloak_openid import KeycloakOpenID from keycloak.keycloak_openid import KeycloakOpenID
from ..signals import post_create_openid_user
from .signals import post_create_openid_user
OIDT_ACCESS_TOKEN = 'oidt_access_token' OIDT_ACCESS_TOKEN = 'oidt_access_token'
...@@ -38,10 +39,6 @@ class Client(object): ...@@ -38,10 +39,6 @@ class Client(object):
self.openid_connect_client = self.new_openid_connect_client() self.openid_connect_client = self.new_openid_connect_client()
def new_realm(self): def new_realm(self):
"""
:param authentication.openid.models.Realm realm:
:return keycloak.realm.Realm:
"""
return KeycloakRealm( return KeycloakRealm(
server_url=self.server_url, server_url=self.server_url,
realm_name=self.realm_name, realm_name=self.realm_name,
...@@ -76,7 +73,7 @@ class Client(object): ...@@ -76,7 +73,7 @@ class Client(object):
:param str username: authentication username :param str username: authentication username
:param str password: authentication password :param str password: authentication password
:return: authentication.models.OpenIDTokenProfile :return: OpenIDTokenProfile
""" """
token_response = self.openid_client.token( token_response = self.openid_client.token(
username=username, password=password username=username, password=password
...@@ -93,7 +90,7 @@ class Client(object): ...@@ -93,7 +90,7 @@ class Client(object):
:param str code: authentication code :param str code: authentication code
:param str redirect_uri: :param str redirect_uri:
:rtype: authentication.models.OpenIDTokenProfile :rtype: OpenIDTokenProfile
""" """
token_response = self.openid_connect_client.authorization_code( token_response = self.openid_connect_client.authorization_code(
...@@ -114,7 +111,7 @@ class Client(object): ...@@ -114,7 +111,7 @@ class Client(object):
- refresh_expires_in - refresh_expires_in
:param dict token_response: :param dict token_response:
:rtype: authentication.openid.models.OpenIDTokenProfile :rtype: OpenIDTokenProfile
""" """
userinfo = self.openid_connect_client.userinfo( userinfo = self.openid_connect_client.userinfo(
......
from django.dispatch import Signal
post_create_openid_user = Signal(providing_args=('user',))
post_openid_login_success = Signal(providing_args=('user', 'request'))
# -*- coding: utf-8 -*-
#
from django.urls import path
from . import views
urlpatterns = [
path('login/', views.OpenIDLoginView.as_view(), name='openid-login'),
path('login/complete/', views.OpenIDLoginCompleteView.as_view(),
name='openid-login-complete'),
]
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
from django.conf import settings from django.conf import settings
from .models import Client from .models import Client
__all__ = ['new_client']
def new_client(): def new_client():
""" """
...@@ -15,6 +17,3 @@ def new_client(): ...@@ -15,6 +17,3 @@ def new_client():
client_id=settings.AUTH_OPENID_CLIENT_ID, client_id=settings.AUTH_OPENID_CLIENT_ID,
client_secret=settings.AUTH_OPENID_CLIENT_SECRET client_secret=settings.AUTH_OPENID_CLIENT_SECRET
) )
client = new_client()
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
import logging import logging
from django.urls import reverse
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.views.generic.base import RedirectView from django.views.generic.base import RedirectView
...@@ -14,43 +13,35 @@ from django.http.response import ( ...@@ -14,43 +13,35 @@ from django.http.response import (
HttpResponseRedirect HttpResponseRedirect
) )
from . import client from .utils import new_client
from .models import Nonce from .models import Nonce
from users.models import LoginLog from .signals import post_openid_login_success
from users.tasks import write_login_log_async
from common.utils import get_request_ip
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
client = new_client()
__all__ = ['OpenIDLoginView', 'OpenIDLoginCompleteView']
def get_base_site_url():
return settings.BASE_SITE_URL
class OpenIDLoginView(RedirectView):
class LoginView(RedirectView):
def get_redirect_url(self, *args, **kwargs): def get_redirect_url(self, *args, **kwargs):
redirect_uri = settings.BASE_SITE_URL + str(settings.LOGIN_COMPLETE_URL)
nonce = Nonce( nonce = Nonce(
redirect_uri=get_base_site_url() + reverse( redirect_uri=redirect_uri,
"authentication:openid-login-complete"),
next_path=self.request.GET.get('next') next_path=self.request.GET.get('next')
) )
cache.set(str(nonce.state), nonce, 24*3600) cache.set(str(nonce.state), nonce, 24*3600)
self.request.session['openid_state'] = str(nonce.state) self.request.session['openid_state'] = str(nonce.state)
authorization_url = client.openid_connect_client.\ authorization_url = client.openid_connect_client.\
authorization_url( authorization_url(
redirect_uri=nonce.redirect_uri, scope='code', redirect_uri=nonce.redirect_uri, scope='code',
state=str(nonce.state) state=str(nonce.state)
) )
return authorization_url return authorization_url
class LoginCompleteView(RedirectView): class OpenIDLoginCompleteView(RedirectView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if 'error' in request.GET: if 'error' in request.GET:
...@@ -79,24 +70,8 @@ class LoginCompleteView(RedirectView): ...@@ -79,24 +70,8 @@ class LoginCompleteView(RedirectView):
return HttpResponseBadRequest() return HttpResponseBadRequest()
login(self.request, user) login(self.request, user)
post_openid_login_success.send(
data = { sender=self.__class__, user=user, request=self.request
'username': user.username, )
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_NOTHING,
'status': True
}
self.write_login_log(data)
return HttpResponseRedirect(nonce.next_path or '/') return HttpResponseRedirect(nonce.next_path or '/')
def write_login_log(self, data):
login_ip = get_request_ip(self.request)
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
tmp_data = {
'ip': login_ip,
'type': 'W',
'user_agent': user_agent
}
data.update(tmp_data)
write_login_log_async.delay(**data)
# -*- coding: utf-8 -*-
#
# -*- coding: utf-8 -*-
#
from django import forms
from django.contrib.auth.forms import AuthenticationForm
from django.utils.translation import gettext_lazy as _
from captcha.fields import CaptchaField
class UserLoginForm(AuthenticationForm):
username = forms.CharField(label=_('Username'), max_length=100)
password = forms.CharField(
label=_('Password'), widget=forms.PasswordInput,
max_length=128, strip=False
)
def confirm_login_allowed(self, user):
if not user.is_staff:
raise forms.ValidationError(
self.error_messages['inactive'],
code='inactive',)
class UserLoginCaptchaForm(UserLoginForm):
captcha = CaptchaField()
class UserCheckOtpCodeForm(forms.Form):
otp_code = forms.CharField(label=_('MFA code'), max_length=6)
# Generated by Django 2.1.7 on 2019-02-28 08:07
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('users', '0019_auto_20190304_1459'),
]
state_operations = [
migrations.CreateModel(
name='AccessKey',
fields=[
('id',
models.UUIDField(default=uuid.uuid4, editable=False,
primary_key=True, serialize=False,
verbose_name='AccessKeyID')),
('secret',
models.UUIDField(default=uuid.uuid4, editable=False,
verbose_name='AccessKeySecret')),
('user', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='access_keys',
to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
),
migrations.CreateModel(
name='PrivateToken',
fields=[
('key',
models.CharField(max_length=40, primary_key=True,
serialize=False, verbose_name='Key')),
('created', models.DateTimeField(auto_now_add=True,
verbose_name='Created')),
('user', models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name='auth_token',
to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'Private Token',
},
),
]
operations = [
migrations.SeparateDatabaseAndState(state_operations=state_operations)
]
import uuid
from django.db import models
from django.utils.translation import ugettext_lazy as _
from rest_framework.authtoken.models import Token
from django.conf import settings
class AccessKey(models.Model):
id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True,
default=uuid.uuid4, editable=False)
secret = models.UUIDField(verbose_name='AccessKeySecret',
default=uuid.uuid4, editable=False)
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='User',
on_delete=models.CASCADE, related_name='access_keys')
def get_id(self):
return str(self.id)
def get_secret(self):
return str(self.secret)
def get_full_value(self):
return '{}:{}'.format(self.id, self.secret)
def __str__(self):
return str(self.id)
class PrivateToken(Token):
"""Inherit from auth token, otherwise migration is boring"""
class Meta:
verbose_name = _('Private Token')
\ No newline at end of file
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from .models import AccessKey
__all__ = ['AccessKeySerializer']
class AccessKeySerializer(serializers.ModelSerializer):
class Meta:
model = AccessKey
fields = ['id', 'secret']
read_only_fields = ['id', 'secret']
from django.dispatch import Signal from django.dispatch import Signal
post_create_openid_user = Signal(providing_args=('user',)) post_auth_success = Signal(providing_args=('user', 'request'))
post_auth_failed = Signal(providing_args=('username', 'request', 'reason'))
...@@ -2,9 +2,16 @@ from django.http.request import QueryDict ...@@ -2,9 +2,16 @@ from django.http.request import QueryDict
from django.conf import settings from django.conf import settings
from django.dispatch import receiver from django.dispatch import receiver
from django.contrib.auth.signals import user_logged_out from django.contrib.auth.signals import user_logged_out
from django.utils import timezone
from django_auth_ldap.backend import populate_user from django_auth_ldap.backend import populate_user
from .openid import client
from .signals import post_create_openid_user from common.utils import get_request_ip
from .backends.openid import new_client
from .backends.openid.signals import (
post_create_openid_user, post_openid_login_success
)
from .tasks import write_login_log_async
from .signals import post_auth_success, post_auth_failed
@receiver(user_logged_out) @receiver(user_logged_out)
...@@ -17,6 +24,7 @@ def on_user_logged_out(sender, request, user, **kwargs): ...@@ -17,6 +24,7 @@ def on_user_logged_out(sender, request, user, **kwargs):
'redirect_uri': settings.BASE_SITE_URL 'redirect_uri': settings.BASE_SITE_URL
}) })
client = new_client()
openid_logout_url = "%s?%s" % ( openid_logout_url = "%s?%s" % (
client.openid_connect_client.get_url( client.openid_connect_client.get_url(
name='end_session_endpoint'), name='end_session_endpoint'),
...@@ -33,8 +41,46 @@ def on_post_create_openid_user(sender, user=None, **kwargs): ...@@ -33,8 +41,46 @@ def on_post_create_openid_user(sender, user=None, **kwargs):
user.save() user.save()
@receiver(post_openid_login_success)
def on_openid_login_success(sender, user=None, request=None, **kwargs):
post_auth_success.send(sender=sender, user=user, request=request)
@receiver(populate_user) @receiver(populate_user)
def on_ldap_create_user(sender, user, ldap_user, **kwargs): def on_ldap_create_user(sender, user, ldap_user, **kwargs):
if user and user.name != 'admin': if user and user.name != 'admin':
user.source = user.SOURCE_LDAP user.source = user.SOURCE_LDAP
user.save() user.save()
def generate_data(username, request):
if not request.user.is_anonymous and request.user.is_app:
login_ip = request.data.get('remote_addr', None)
login_type = request.data.get('login_type', '')
user_agent = request.data.get('HTTP_USER_AGENT', '')
else:
login_ip = get_request_ip(request)
user_agent = request.META.get('HTTP_USER_AGENT', '')
login_type = 'W'
data = {
'username': username,
'ip': login_ip,
'type': login_type,
'user_agent': user_agent,
'datetime': timezone.now()
}
return data
@receiver(post_auth_success)
def on_user_auth_success(sender, user, request, **kwargs):
data = generate_data(user.username, request)
data.update({'mfa': int(user.otp_enabled), 'status': True})
write_login_log_async.delay(**data)
@receiver(post_auth_failed)
def on_user_auth_failed(sender, username, request, reason, **kwargs):
data = generate_data(username, request)
data.update({'reason': reason, 'status': False})
write_login_log_async.delay(**data)
# -*- coding: utf-8 -*-
#
from celery import shared_task
from ops.celery.decorator import register_as_period_task
from django.contrib.sessions.models import Session
from django.utils import timezone
from .utils import write_login_log
@shared_task
def write_login_log_async(*args, **kwargs):
write_login_log(*args, **kwargs)
@register_as_period_task(interval=3600*24)
@shared_task
def clean_django_sessions():
Session.objects.filter(expire_date__lt=timezone.now()).delete()
# coding:utf-8
#
from __future__ import absolute_import
from django.urls import path
from .. import api
app_name = 'authentication'
urlpatterns = [
# path('token/', api.UserToken.as_view(), name='user-token'),
path('auth/', api.UserAuthApi.as_view(), name='user-auth'),
path('connection-token/',
api.UserConnectionTokenApi.as_view(), name='connection-token'),
path('otp/auth/', api.UserOtpAuthApi.as_view(), name='user-otp-auth'),
]
# coding:utf-8 # coding:utf-8
# #
from django.urls import path from __future__ import absolute_import
from authentication.openid import views
from django.urls import path, include
from .. import views
app_name = 'authentication' app_name = 'authentication'
urlpatterns = [ urlpatterns = [
# openid # openid
path('openid/login/', views.LoginView.as_view(), name='openid-login'), path('openid/', include(('authentication.backends.openid.urls', 'authentication'), namespace='openid')),
path('openid/login/complete/', views.LoginCompleteView.as_view(),
name='openid-login-complete'),
# other # login
path('login/', views.UserLoginView.as_view(), name='login'),
path('login/otp/', views.UserLoginOtpView.as_view(), name='login-otp'),
path('logout/', views.UserLogoutView.as_view(), name='logout'),
] ]
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext as _
from common.utils import get_ip_city, validate_ip
def write_login_log(*args, **kwargs):
from audits.models import UserLoginLog
default_city = _("Unknown")
ip = kwargs.get('ip', '')
if not (ip and validate_ip(ip)):
ip = ip[:15]
city = default_city
else:
city = get_ip_city(ip) or default_city
kwargs.update({'ip': ip, 'city': city})
UserLoginLog.objects.create(**kwargs)
# -*- coding: utf-8 -*-
#
from .login import *
# ~*~ coding: utf-8 ~*~
#
from __future__ import unicode_literals
import os
from django.core.cache import cache
from django.contrib.auth import login as auth_login, logout as auth_logout
from django.http import HttpResponse
from django.shortcuts import reverse, redirect
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView
from django.conf import settings
from common.utils import get_request_ip
from users.models import User
from audits.models import UserLoginLog as LoginLog
from users.utils import (
check_otp_code, is_block_login, clean_failed_count, get_user_or_tmp_user,
set_tmp_user_to_cache, increase_login_failed_count,
redirect_user_first_login_or_index,
)
from ..signals import post_auth_success, post_auth_failed
from .. import forms
__all__ = [
'UserLoginView', 'UserLoginOtpView', 'UserLogoutView',
]
@method_decorator(sensitive_post_parameters(), name='dispatch')
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(never_cache, name='dispatch')
class UserLoginView(FormView):
form_class = forms.UserLoginForm
form_class_captcha = forms.UserLoginCaptchaForm
redirect_field_name = 'next'
key_prefix_captcha = "_LOGIN_INVALID_{}"
def get_template_names(self):
template_name = 'users/login.html'
if not settings.XPACK_ENABLED:
return template_name
from xpack.plugins.license.models import License
if not License.has_valid_license():
return template_name
template_name = 'users/new_login.html'
return template_name
def get(self, request, *args, **kwargs):
if request.user.is_staff:
return redirect(redirect_user_first_login_or_index(
request, self.redirect_field_name)
)
request.session.set_test_cookie()
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
# limit login authentication
ip = get_request_ip(request)
username = self.request.POST.get('username')
if is_block_login(username, ip):
return self.render_to_response(self.get_context_data(block_login=True))
return super().post(request, *args, **kwargs)
def form_valid(self, form):
if not self.request.session.test_cookie_worked():
return HttpResponse(_("Please enable cookies and try again."))
user = form.get_user()
# user password expired
if user.password_has_expired:
reason = LoginLog.REASON_PASSWORD_EXPIRED
self.send_auth_signal(success=False, username=user.username, reason=reason)
return self.render_to_response(self.get_context_data(password_expired=True))
set_tmp_user_to_cache(self.request, user)
username = form.cleaned_data.get('username')
ip = get_request_ip(self.request)
# 登陆成功,清除缓存计数
clean_failed_count(username, ip)
return redirect(self.get_success_url())
def form_invalid(self, form):
# write login failed log
username = form.cleaned_data.get('username')
exist = User.objects.filter(username=username).first()
reason = LoginLog.REASON_PASSWORD if exist else LoginLog.REASON_NOT_EXIST
# limit user login failed count
ip = get_request_ip(self.request)
increase_login_failed_count(username, ip)
# show captcha
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
self.send_auth_signal(success=False, username=username, reason=reason)
old_form = form
form = self.form_class_captcha(data=form.data)
form._errors = old_form.errors
return super().form_invalid(form)
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
else:
return self.form_class
def get_success_url(self):
user = get_user_or_tmp_user(self.request)
if user.otp_enabled and user.otp_secret_key:
# 1,2,mfa_setting & T
return reverse('authentication:login-otp')
elif user.otp_enabled and not user.otp_secret_key:
# 1,2,mfa_setting & F
return reverse('users:user-otp-enable-authentication')
elif not user.otp_enabled:
# 0 & T,F
auth_login(self.request, user)
self.send_auth_signal(success=True, user=user)
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
def get_context_data(self, **kwargs):
context = {
'demo_mode': os.environ.get("DEMO_MODE"),
'AUTH_OPENID': settings.AUTH_OPENID,
}
kwargs.update(context)
return super().get_context_data(**kwargs)
def send_auth_signal(self, success=True, user=None, username='', reason=''):
if success:
post_auth_success.send(sender=self.__class__, user=user, request=self.request)
else:
post_auth_failed.send(
sender=self.__class__, username=username,
request=self.request, reason=reason
)
class UserLoginOtpView(FormView):
template_name = 'users/login_otp.html'
form_class = forms.UserCheckOtpCodeForm
redirect_field_name = 'next'
def form_valid(self, form):
user = get_user_or_tmp_user(self.request)
otp_code = form.cleaned_data.get('otp_code')
otp_secret_key = user.otp_secret_key
if check_otp_code(otp_secret_key, otp_code):
auth_login(self.request, user)
self.send_auth_signal(success=True, user=user)
return redirect(self.get_success_url())
else:
self.send_auth_signal(
success=False, username=user.username,
reason=LoginLog.REASON_MFA
)
form.add_error(
'otp_code', _('MFA code invalid, or ntp sync server time')
)
return super().form_invalid(form)
def get_success_url(self):
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
def send_auth_signal(self, success=True, user=None, username='', reason=''):
if success:
post_auth_success.send(sender=self.__class__, user=user, request=self.request)
else:
post_auth_failed.send(
sender=self.__class__, username=username,
request=self.request, reason=reason
)
@method_decorator(never_cache, name='dispatch')
class UserLogoutView(TemplateView):
template_name = 'flash_message_standalone.html'
def get(self, request, *args, **kwargs):
auth_logout(request)
next_uri = request.COOKIES.get("next")
if next_uri:
return redirect(next_uri)
response = super().get(request, *args, **kwargs)
return response
def get_context_data(self, **kwargs):
context = {
'title': _('Logout success'),
'messages': _('Logout success, return login page'),
'interval': 1,
'redirect_url': reverse('authentication:login'),
'auto_redirect': True,
}
kwargs.update(context)
return super().get_context_data(**kwargs)
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import os import os
import json
import jms_storage
import uuid import uuid
from rest_framework.views import Response, APIView from rest_framework.views import Response
from rest_framework import generics from rest_framework import generics, serializers
from ldap3 import Server, Connection
from django.core.mail import send_mail
from django.core.cache import cache from django.core.cache import cache
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from .permissions import IsOrgAdmin, IsSuperUser
from .serializers import (
MailTestSerializer, LDAPTestSerializer, OutputSerializer
)
from .models import Setting
class MailTestingAPI(APIView):
permission_classes = (IsOrgAdmin,)
serializer_class = MailTestSerializer
success_message = _("Test mail sent to {}, please check")
def post(self, request):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
email_host_user = serializer.validated_data["EMAIL_HOST_USER"]
for k, v in serializer.validated_data.items():
if k.startswith('EMAIL'):
setattr(settings, k, v)
try:
subject = "Test"
message = "Test smtp setting"
send_mail(subject, message, email_host_user, [email_host_user])
except Exception as e:
return Response({"error": str(e)}, status=401)
return Response({"msg": self.success_message.format(email_host_user)})
else:
return Response({"error": str(serializer.errors)}, status=401)
class LDAPTestingAPI(APIView):
permission_classes = (IsOrgAdmin,)
serializer_class = LDAPTestSerializer
success_message = _("Test ldap success")
def post(self, request):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
host = serializer.validated_data["AUTH_LDAP_SERVER_URI"]
bind_dn = serializer.validated_data["AUTH_LDAP_BIND_DN"]
password = serializer.validated_data["AUTH_LDAP_BIND_PASSWORD"]
use_ssl = serializer.validated_data.get("AUTH_LDAP_START_TLS", False)
search_ougroup = serializer.validated_data["AUTH_LDAP_SEARCH_OU"]
search_filter = serializer.validated_data["AUTH_LDAP_SEARCH_FILTER"]
attr_map = serializer.validated_data["AUTH_LDAP_USER_ATTR_MAP"]
try:
attr_map = json.loads(attr_map)
except json.JSONDecodeError:
return Response({"error": "AUTH_LDAP_USER_ATTR_MAP not valid"}, status=401)
server = Server(host, use_ssl=use_ssl)
conn = Connection(server, bind_dn, password)
try:
conn.bind()
except Exception as e:
return Response({"error": str(e)}, status=401)
users = []
for search_ou in str(search_ougroup).split("|"):
ok = conn.search(search_ou, search_filter % ({"user": "*"}),
attributes=list(attr_map.values()))
if not ok:
return Response({"error": _("Search no entry matched in ou {}").format(search_ou)}, status=401)
for entry in conn.entries:
user = {}
for attr, mapping in attr_map.items():
if hasattr(entry, mapping):
user[attr] = getattr(entry, mapping)
users.append(user)
if len(users) > 0:
return Response({"msg": _("Match {} s users").format(len(users))})
else:
return Response({"error": "Have user but attr mapping error"}, status=401)
else:
return Response({"error": str(serializer.errors)}, status=401)
class ReplayStorageCreateAPI(APIView):
permission_classes = (IsSuperUser,)
def post(self, request):
storage_data = request.data
if storage_data.get('TYPE') == 'ceph':
port = storage_data.get('PORT')
if port.isdigit():
storage_data['PORT'] = int(storage_data.get('PORT'))
storage_name = storage_data.pop('NAME')
data = {storage_name: storage_data}
if not self.is_valid(storage_data):
return Response({
"error": _("Error: Account invalid (Please make sure the "
"information such as Access key or Secret key is correct)")},
status=401
)
Setting.save_storage('TERMINAL_REPLAY_STORAGE', data)
return Response({"msg": _('Create succeed')}, status=200)
@staticmethod class OutputSerializer(serializers.Serializer):
def is_valid(storage_data): output = serializers.CharField()
if storage_data.get('TYPE') == 'server': is_end = serializers.BooleanField()
return True mark = serializers.CharField()
storage = jms_storage.get_object_storage(storage_data)
target = 'tests.py'
src = os.path.join(settings.BASE_DIR, 'common', target)
return storage.is_valid(src, target)
class ReplayStorageDeleteAPI(APIView):
permission_classes = (IsSuperUser,)
def post(self, request):
storage_name = str(request.data.get('name'))
Setting.delete_storage('TERMINAL_REPLAY_STORAGE', storage_name)
return Response({"msg": _('Delete succeed')}, status=200)
class CommandStorageCreateAPI(APIView):
permission_classes = (IsSuperUser,)
def post(self, request):
storage_data = request.data
storage_name = storage_data.pop('NAME')
data = {storage_name: storage_data}
if not self.is_valid(storage_data):
return Response(
{"error": _("Error: Account invalid (Please make sure the "
"information such as Access key or Secret key is correct)")},
status=401
)
Setting.save_storage('TERMINAL_COMMAND_STORAGE', data)
return Response({"msg": _('Create succeed')}, status=200)
@staticmethod
def is_valid(storage_data):
if storage_data.get('TYPE') == 'server':
return True
try:
storage = jms_storage.get_log_storage(storage_data)
except Exception:
return False
return storage.ping()
class CommandStorageDeleteAPI(APIView):
permission_classes = (IsSuperUser,)
def post(self, request):
storage_name = str(request.data.get('name'))
Setting.delete_storage('TERMINAL_COMMAND_STORAGE', storage_name)
return Response({"msg": _('Delete succeed')}, status=200)
class DjangoSettingsAPI(APIView):
def get(self, request):
if not settings.DEBUG:
return Response("Not in debug mode")
data = {}
for i in [settings, getattr(settings, '_wrapped')]:
if not i:
continue
for k, v in i.__dict__.items():
if k and k.isupper():
try:
json.dumps(v)
data[k] = v
except (json.JSONDecodeError, TypeError):
data[k] = str(v)
return Response(data)
class LogTailApi(generics.RetrieveAPIView): class LogTailApi(generics.RetrieveAPIView):
......
from __future__ import unicode_literals from __future__ import unicode_literals
import sys
from django.apps import AppConfig from django.apps import AppConfig
from django.dispatch import receiver
from django.db.backends.signals import connection_created
@receiver(connection_created, dispatch_uid="my_unique_identifier")
def on_db_connection_ready(sender, **kwargs):
from .signals import django_ready
if 'migrate' not in sys.argv:
django_ready.send(CommonConfig)
class CommonConfig(AppConfig): class CommonConfig(AppConfig):
name = 'common' name = 'common'
def ready(self):
from . import signals_handler
from .signals import django_ready
django_ready.send(self.__class__)
return super().ready()
# -*- coding: utf-8 -*-
#
from .form import *
from .model import *
from .serializer import *
...@@ -2,16 +2,19 @@ ...@@ -2,16 +2,19 @@
# #
import json import json
from django.db import models
from django import forms from django import forms
from django.utils import six from django.utils import six
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from rest_framework import serializers from ..utils import get_signer
from .utils import get_signer
signer = get_signer() signer = get_signer()
__all__ = [
'FormDictField', 'FormEncryptCharField', 'FormEncryptDictField',
'FormEncryptMixin',
]
class FormDictField(forms.Field): class FormDictField(forms.Field):
widget = forms.Textarea widget = forms.Textarea
...@@ -44,38 +47,6 @@ class FormDictField(forms.Field): ...@@ -44,38 +47,6 @@ class FormDictField(forms.Field):
return self.to_python(initial) != self.to_python(data) return self.to_python(initial) != self.to_python(data)
class StringIDField(serializers.Field):
def to_representation(self, value):
return {"pk": value.pk, "name": value.__str__()}
class StringManyToManyField(serializers.RelatedField):
def to_representation(self, value):
return value.__str__()
class EncryptMixin:
def from_db_value(self, value, expression, connection, context):
if value is not None:
return signer.unsign(value)
return super().from_db_value(self, value, expression, connection, context)
def get_prep_value(self, value):
if value is None:
return value
return signer.sign(value)
class EncryptTextField(EncryptMixin, models.TextField):
description = _("Encrypt field using Secret Key")
class EncryptCharField(EncryptMixin, models.CharField):
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 2048
super().__init__(*args, **kwargs)
class FormEncryptMixin: class FormEncryptMixin:
pass pass
...@@ -88,19 +59,5 @@ class FormEncryptDictField(FormEncryptMixin, FormDictField): ...@@ -88,19 +59,5 @@ class FormEncryptDictField(FormEncryptMixin, FormDictField):
pass pass
class ChoiceDisplayField(serializers.ChoiceField):
def __init__(self, *args, **kwargs):
super(ChoiceDisplayField, self).__init__(*args, **kwargs)
self.choice_strings_to_display = {
six.text_type(key): value for key, value in self.choices.items()
}
def to_representation(self, value):
if value is None:
return value
return {
'value': self.choice_strings_to_values.get(six.text_type(value),
value),
'display': self.choice_strings_to_display.get(six.text_type(value),
value),
}
# -*- coding: utf-8 -*-
#
import json
from django.db import models
from django.utils.translation import ugettext_lazy as _
from ..utils import get_signer
__all__ = [
'JsonMixin', 'JsonDictMixin', 'JsonListMixin', 'JsonTypeMixin',
'JsonCharField', 'JsonTextField', 'JsonListCharField', 'JsonListTextField',
'JsonDictCharField', 'JsonDictTextField', 'EncryptCharField',
'EncryptTextField', 'EncryptMixin',
]
signer = get_signer()
class JsonMixin:
tp = None
@staticmethod
def json_decode(data):
try:
return json.loads(data)
except (TypeError, json.JSONDecodeError):
return None
@staticmethod
def json_encode(data):
return json.dumps(data)
def from_db_value(self, value, expression, connection, context):
if value is None:
return value
return self.json_decode(value)
def to_python(self, value):
if value is None:
return value
if not isinstance(value, str) or not value.startswith('"'):
return value
else:
return self.json_decode(value)
def get_prep_value(self, value):
if value is None:
return value
return self.json_encode(value)
class JsonTypeMixin(JsonMixin):
tp = dict
def from_db_value(self, value, expression, connection, context):
value = super().from_db_value(value, expression, connection, context)
if not isinstance(value, self.tp):
value = self.tp()
return value
def to_python(self, value):
data = super().to_python(value)
if not isinstance(data, self.tp):
data = self.tp()
return data
def get_prep_value(self, value):
if not isinstance(value, self.tp):
value = self.tp()
return self.json_encode(value)
class JsonDictMixin(JsonTypeMixin):
tp = dict
class JsonDictCharField(JsonDictMixin, models.CharField):
description = _("Marshal dict data to char field")
class JsonDictTextField(JsonDictMixin, models.TextField):
description = _("Marshal dict data to text field")
class JsonListMixin(JsonTypeMixin):
tp = list
class JsonStrListMixin(JsonListMixin):
pass
class JsonListCharField(JsonListMixin, models.CharField):
description = _("Marshal list data to char field")
class JsonListTextField(JsonListMixin, models.TextField):
description = _("Marshal list data to text field")
class JsonCharField(JsonMixin, models.CharField):
description = _("Marshal data to char field")
class JsonTextField(JsonMixin, models.TextField):
description = _("Marshal data to text field")
class EncryptMixin:
def from_db_value(self, value, expression, connection, context):
if value is not None:
return signer.unsign(value)
return None
def get_prep_value(self, value):
if value is None:
return value
return signer.sign(value)
class EncryptTextField(EncryptMixin, models.TextField):
description = _("Encrypt field using Secret Key")
class EncryptCharField(EncryptMixin, models.CharField):
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 2048
super().__init__(*args, **kwargs)
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from django.utils import six
__all__ = ['StringIDField', 'StringManyToManyField', 'ChoiceDisplayField']
class StringIDField(serializers.Field):
def to_representation(self, value):
return {"pk": value.pk, "name": value.__str__()}
class StringManyToManyField(serializers.RelatedField):
def to_representation(self, value):
return value.__str__()
class ChoiceDisplayField(serializers.ChoiceField):
def __init__(self, *args, **kwargs):
super(ChoiceDisplayField, self).__init__(*args, **kwargs)
self.choice_strings_to_display = {
six.text_type(key): value for key, value in self.choices.items()
}
def to_representation(self, value):
if value is None:
return value
return {
'value': self.choice_strings_to_values.get(six.text_type(value), value),
'display': self.choice_strings_to_display.get(six.text_type(value), value),
}
class DictField(serializers.DictField):
def to_representation(self, value):
if not value or not isinstance(value, dict):
value = {}
return super().to_representation(value)
# Generated by Django 2.1.7 on 2019-03-04 07:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('common', '0005_auto_20190221_1902'),
]
database_operations = [
migrations.AlterModelTable('setting', 'settings_setting')
]
state_operations = [migrations.DeleteModel('setting')]
operations = [
migrations.SeparateDatabaseAndState(
database_operations=database_operations,
state_operations=state_operations
)
]
...@@ -2,7 +2,6 @@ from django.core.mail import send_mail ...@@ -2,7 +2,6 @@ from django.core.mail import send_mail
from django.conf import settings from django.conf import settings
from celery import shared_task from celery import shared_task
from .utils import get_logger from .utils import get_logger
from .models import Setting
logger = get_logger(__file__) logger = get_logger(__file__)
......
# -*- coding: utf-8 -*-
#
from .common import *
from .django import *
from .encode import *
from .http import *
from .ipip import *
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import re import re
import sys
from collections import OrderedDict from collections import OrderedDict
from six import string_types
import base64
import os
from itertools import chain from itertools import chain
import logging import logging
import datetime import datetime
import time
import hashlib
from email.utils import formatdate
import calendar
import threading
from io import StringIO
import uuid import uuid
from functools import wraps from functools import wraps
import copy import copy
import ipaddress
import paramiko
import sshpubkeys
from itsdangerous import TimedJSONWebSignatureSerializer, JSONWebSignatureSerializer, \
BadSignature, SignatureExpired
from django.shortcuts import reverse as dj_reverse
from django.conf import settings
from django.utils import timezone
UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}') UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}')
ipip_db = None
def reverse(view_name, urlconf=None, args=None, kwargs=None,
current_app=None, external=False):
url = dj_reverse(view_name, urlconf=urlconf, args=args,
kwargs=kwargs, current_app=current_app)
if external:
site_url = settings.SITE_URL
url = site_url.strip('/') + url
return url
def get_object_or_none(model, **kwargs):
try:
obj = model.objects.get(**kwargs)
except model.DoesNotExist:
return None
return obj
class Singleton(type):
def __init__(cls, *args, **kwargs):
cls.__instance = None
super().__init__(*args, **kwargs)
def __call__(cls, *args, **kwargs):
if cls.__instance is None:
cls.__instance = super().__call__(*args, **kwargs)
return cls.__instance
else:
return cls.__instance
class Signer(metaclass=Singleton):
"""用来加密,解密,和基于时间戳的方式验证token"""
def __init__(self, secret_key=None):
self.secret_key = secret_key
def sign(self, value):
s = JSONWebSignatureSerializer(self.secret_key, algorithm_name='HS256')
return s.dumps(value).decode()
def unsign(self, value):
if value is None:
return value
s = JSONWebSignatureSerializer(self.secret_key, algorithm_name='HS256')
try:
return s.loads(value)
except BadSignature:
return {}
def sign_t(self, value, expires_in=3600):
s = TimedJSONWebSignatureSerializer(self.secret_key, expires_in=expires_in)
return str(s.dumps(value), encoding="utf8")
def unsign_t(self, value):
s = TimedJSONWebSignatureSerializer(self.secret_key)
try:
return s.loads(value)
except (BadSignature, SignatureExpired):
return {}
def date_expired_default():
try:
years = int(settings.DEFAULT_EXPIRED_YEARS)
except TypeError:
years = 70
return timezone.now() + timezone.timedelta(days=365*years)
def combine_seq(s1, s2, callback=None): def combine_seq(s1, s2, callback=None):
...@@ -146,88 +60,6 @@ def timesince(dt, since='', default="just now"): ...@@ -146,88 +60,6 @@ def timesince(dt, since='', default="just now"):
return default return default
def ssh_key_string_to_obj(text, password=None):
key = None
try:
key = paramiko.RSAKey.from_private_key(StringIO(text), password=password)
except paramiko.SSHException:
pass
try:
key = paramiko.DSSKey.from_private_key(StringIO(text), password=password)
except paramiko.SSHException:
pass
return key
def ssh_pubkey_gen(private_key=None, username='jumpserver', hostname='localhost', password=None):
if isinstance(private_key, bytes):
private_key = private_key.decode("utf-8")
if isinstance(private_key, string_types):
private_key = ssh_key_string_to_obj(private_key, password=password)
if not isinstance(private_key, (paramiko.RSAKey, paramiko.DSSKey)):
raise IOError('Invalid private key')
public_key = "%(key_type)s %(key_content)s %(username)s@%(hostname)s" % {
'key_type': private_key.get_name(),
'key_content': private_key.get_base64(),
'username': username,
'hostname': hostname,
}
return public_key
def ssh_key_gen(length=2048, type='rsa', password=None, username='jumpserver', hostname=None):
"""Generate user ssh private and public key
Use paramiko RSAKey generate it.
:return private key str and public key str
"""
if hostname is None:
hostname = os.uname()[1]
f = StringIO()
try:
if type == 'rsa':
private_key_obj = paramiko.RSAKey.generate(length)
elif type == 'dsa':
private_key_obj = paramiko.DSSKey.generate(length)
else:
raise IOError('SSH private key must be `rsa` or `dsa`')
private_key_obj.write_private_key(f, password=password)
private_key = f.getvalue()
public_key = ssh_pubkey_gen(private_key_obj, username=username, hostname=hostname)
return private_key, public_key
except IOError:
raise IOError('These is error when generate ssh key.')
def validate_ssh_private_key(text, password=None):
if isinstance(text, bytes):
try:
text = text.decode("utf-8")
except UnicodeDecodeError:
return False
key = ssh_key_string_to_obj(text, password=password)
if key is None:
return False
else:
return True
def validate_ssh_public_key(text):
ssh = sshpubkeys.SSHKey(text)
try:
ssh.parse()
except (sshpubkeys.InvalidKeyException, UnicodeDecodeError):
return False
except NotImplementedError as e:
return False
return True
def setattr_bulk(seq, key, value): def setattr_bulk(seq, key, value):
def set_attr(obj): def set_attr(obj):
setattr(obj, key, value) setattr(obj, key, value)
...@@ -243,70 +75,6 @@ def set_or_append_attr_bulk(seq, key, value): ...@@ -243,70 +75,6 @@ def set_or_append_attr_bulk(seq, key, value):
setattr(obj, key, value) setattr(obj, key, value)
def content_md5(data):
"""计算data的MD5值,经过Base64编码并返回str类型。
返回值可以直接作为HTTP Content-Type头部的值
"""
if isinstance(data, str):
data = hashlib.md5(data.encode('utf-8'))
value = base64.b64encode(data.hexdigest().encode('utf-8'))
return value.decode('utf-8')
_STRPTIME_LOCK = threading.Lock()
_GMT_FORMAT = "%a, %d %b %Y %H:%M:%S GMT"
_ISO8601_FORMAT = "%Y-%m-%dT%H:%M:%S.000Z"
def to_unixtime(time_string, format_string):
time_string = time_string.decode("ascii")
with _STRPTIME_LOCK:
return int(calendar.timegm(time.strptime(time_string, format_string)))
def http_date(timeval=None):
"""返回符合HTTP标准的GMT时间字符串,用strftime的格式表示就是"%a, %d %b %Y %H:%M:%S GMT"。
但不能使用strftime,因为strftime的结果是和locale相关的。
"""
return formatdate(timeval, usegmt=True)
def http_to_unixtime(time_string):
"""把HTTP Date格式的字符串转换为UNIX时间(自1970年1月1日UTC零点的秒数)。
HTTP Date形如 `Sat, 05 Dec 2015 11:10:29 GMT` 。
"""
return to_unixtime(time_string, _GMT_FORMAT)
def iso8601_to_unixtime(time_string):
"""把ISO8601时间字符串(形如,2012-02-24T06:07:48.000Z)转换为UNIX时间,精确到秒。"""
return to_unixtime(time_string, _ISO8601_FORMAT)
def make_signature(access_key_secret, date=None):
if isinstance(date, bytes):
date = bytes.decode(date)
if isinstance(date, int):
date_gmt = http_date(date)
elif date is None:
date_gmt = http_date(int(time.time()))
else:
date_gmt = date
data = str(access_key_secret) + "\n" + date_gmt
return content_md5(data)
def encrypt_password(password, salt=None):
from passlib.hash import sha512_crypt
if password:
return sha512_crypt.using(rounds=5000).hash(password, salt=salt)
return None
def capacity_convert(size, expect='auto', rate=1000): def capacity_convert(size, expect='auto', rate=1000):
""" """
:param size: '100MB', '1G' :param size: '100MB', '1G'
...@@ -374,11 +142,6 @@ def is_uuid(seq): ...@@ -374,11 +142,6 @@ def is_uuid(seq):
return True return True
def get_signer():
signer = Signer(settings.SECRET_KEY)
return signer
def get_request_ip(request): def get_request_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',') x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')
if x_forwarded_for and x_forwarded_for[0]: if x_forwarded_for and x_forwarded_for[0]:
...@@ -388,22 +151,13 @@ def get_request_ip(request): ...@@ -388,22 +151,13 @@ def get_request_ip(request):
return login_ip return login_ip
def get_command_storage_setting(): def validate_ip(ip):
default = settings.DEFAULT_TERMINAL_COMMAND_STORAGE try:
value = settings.TERMINAL_COMMAND_STORAGE ipaddress.ip_address(ip)
if not value: return True
return default except ValueError:
value.update(default) pass
return value return False
def get_replay_storage_setting():
default = settings.DEFAULT_TERMINAL_REPLAY_STORAGE
value = settings.TERMINAL_REPLAY_STORAGE
if not value:
return default
value.update(default)
return value
def with_cache(func): def with_cache(func):
......
# -*- coding: utf-8 -*-
#
import re
from django.shortcuts import reverse as dj_reverse
from django.conf import settings
from django.utils import timezone
UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}')
def reverse(view_name, urlconf=None, args=None, kwargs=None,
current_app=None, external=False):
url = dj_reverse(view_name, urlconf=urlconf, args=args,
kwargs=kwargs, current_app=current_app)
if external:
site_url = settings.SITE_URL
url = site_url.strip('/') + url
return url
def get_object_or_none(model, **kwargs):
try:
obj = model.objects.get(**kwargs)
except model.DoesNotExist:
return None
return obj
def date_expired_default():
try:
years = int(settings.DEFAULT_EXPIRED_YEARS)
except TypeError:
years = 70
return timezone.now() + timezone.timedelta(days=365*years)
def get_command_storage_setting():
default = settings.DEFAULT_TERMINAL_COMMAND_STORAGE
value = settings.TERMINAL_COMMAND_STORAGE
if not value:
return default
value.update(default)
return value
def get_replay_storage_setting():
default = settings.DEFAULT_TERMINAL_REPLAY_STORAGE
value = settings.TERMINAL_REPLAY_STORAGE
if not value:
return default
value.update(default)
return value
# -*- coding: utf-8 -*-
#
import re
from six import string_types
import base64
import os
import time
import hashlib
from io import StringIO
import paramiko
import sshpubkeys
from itsdangerous import (
TimedJSONWebSignatureSerializer, JSONWebSignatureSerializer,
BadSignature, SignatureExpired
)
from django.conf import settings
from .http import http_date
UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}')
class Singleton(type):
def __init__(cls, *args, **kwargs):
cls.__instance = None
super().__init__(*args, **kwargs)
def __call__(cls, *args, **kwargs):
if cls.__instance is None:
cls.__instance = super().__call__(*args, **kwargs)
return cls.__instance
else:
return cls.__instance
class Signer(metaclass=Singleton):
"""用来加密,解密,和基于时间戳的方式验证token"""
def __init__(self, secret_key=None):
self.secret_key = secret_key
def sign(self, value):
s = JSONWebSignatureSerializer(self.secret_key, algorithm_name='HS256')
return s.dumps(value).decode()
def unsign(self, value):
if value is None:
return value
s = JSONWebSignatureSerializer(self.secret_key, algorithm_name='HS256')
try:
return s.loads(value)
except BadSignature:
return {}
def sign_t(self, value, expires_in=3600):
s = TimedJSONWebSignatureSerializer(self.secret_key, expires_in=expires_in)
return str(s.dumps(value), encoding="utf8")
def unsign_t(self, value):
s = TimedJSONWebSignatureSerializer(self.secret_key)
try:
return s.loads(value)
except (BadSignature, SignatureExpired):
return {}
def ssh_key_string_to_obj(text, password=None):
key = None
try:
key = paramiko.RSAKey.from_private_key(StringIO(text), password=password)
except paramiko.SSHException:
pass
try:
key = paramiko.DSSKey.from_private_key(StringIO(text), password=password)
except paramiko.SSHException:
pass
return key
def ssh_pubkey_gen(private_key=None, username='jumpserver', hostname='localhost', password=None):
if isinstance(private_key, bytes):
private_key = private_key.decode("utf-8")
if isinstance(private_key, string_types):
private_key = ssh_key_string_to_obj(private_key, password=password)
if not isinstance(private_key, (paramiko.RSAKey, paramiko.DSSKey)):
raise IOError('Invalid private key')
public_key = "%(key_type)s %(key_content)s %(username)s@%(hostname)s" % {
'key_type': private_key.get_name(),
'key_content': private_key.get_base64(),
'username': username,
'hostname': hostname,
}
return public_key
def ssh_key_gen(length=2048, type='rsa', password=None, username='jumpserver', hostname=None):
"""Generate user ssh private and public key
Use paramiko RSAKey generate it.
:return private key str and public key str
"""
if hostname is None:
hostname = os.uname()[1]
f = StringIO()
try:
if type == 'rsa':
private_key_obj = paramiko.RSAKey.generate(length)
elif type == 'dsa':
private_key_obj = paramiko.DSSKey.generate(length)
else:
raise IOError('SSH private key must be `rsa` or `dsa`')
private_key_obj.write_private_key(f, password=password)
private_key = f.getvalue()
public_key = ssh_pubkey_gen(private_key_obj, username=username, hostname=hostname)
return private_key, public_key
except IOError:
raise IOError('These is error when generate ssh key.')
def validate_ssh_private_key(text, password=None):
if isinstance(text, bytes):
try:
text = text.decode("utf-8")
except UnicodeDecodeError:
return False
key = ssh_key_string_to_obj(text, password=password)
if key is None:
return False
else:
return True
def validate_ssh_public_key(text):
ssh = sshpubkeys.SSHKey(text)
try:
ssh.parse()
except (sshpubkeys.InvalidKeyException, UnicodeDecodeError):
return False
except NotImplementedError as e:
return False
return True
def content_md5(data):
"""计算data的MD5值,经过Base64编码并返回str类型。
返回值可以直接作为HTTP Content-Type头部的值
"""
if isinstance(data, str):
data = hashlib.md5(data.encode('utf-8'))
value = base64.b64encode(data.hexdigest().encode('utf-8'))
return value.decode('utf-8')
def make_signature(access_key_secret, date=None):
if isinstance(date, bytes):
date = bytes.decode(date)
if isinstance(date, int):
date_gmt = http_date(date)
elif date is None:
date_gmt = http_date(int(time.time()))
else:
date_gmt = date
data = str(access_key_secret) + "\n" + date_gmt
return content_md5(data)
def encrypt_password(password, salt=None):
from passlib.hash import sha512_crypt
if password:
return sha512_crypt.using(rounds=5000).hash(password, salt=salt)
return None
def get_signer():
signer = Signer(settings.SECRET_KEY)
return signer
# -*- coding: utf-8 -*-
#
import time
from email.utils import formatdate
import calendar
import threading
_STRPTIME_LOCK = threading.Lock()
_GMT_FORMAT = "%a, %d %b %Y %H:%M:%S GMT"
_ISO8601_FORMAT = "%Y-%m-%dT%H:%M:%S.000Z"
def to_unixtime(time_string, format_string):
time_string = time_string.decode("ascii")
with _STRPTIME_LOCK:
return int(calendar.timegm(time.strptime(time_string, format_string)))
def http_date(timeval=None):
"""返回符合HTTP标准的GMT时间字符串,用strftime的格式表示就是"%a, %d %b %Y %H:%M:%S GMT"。
但不能使用strftime,因为strftime的结果是和locale相关的。
"""
return formatdate(timeval, usegmt=True)
def http_to_unixtime(time_string):
"""把HTTP Date格式的字符串转换为UNIX时间(自1970年1月1日UTC零点的秒数)。
HTTP Date形如 `Sat, 05 Dec 2015 11:10:29 GMT` 。
"""
return to_unixtime(time_string, _GMT_FORMAT)
def iso8601_to_unixtime(time_string):
"""把ISO8601时间字符串(形如,2012-02-24T06:07:48.000Z)转换为UNIX时间,精确到秒。"""
return to_unixtime(time_string, _ISO8601_FORMAT)
# -*- coding: utf-8 -*-
#
from .ipdb import *
# -*- coding: utf-8 -*-
#
import os
import ipdb
ipip_db = None
def get_ip_city(ip):
global ipip_db
if ipip_db is None:
ipip_db_path = os.path.join(os.path.dirname(__file__), 'ipipfree.ipdb')
ipip_db = ipdb.City(ipip_db_path)
info = list(set(ipip_db.find(ip, 'CN')))
if '' in info:
info.remove('')
return ' '.join(info)
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
[{"model": "users.usergroup", "pk": "f3c6b021-59a9-43e7-b022-c2e9bfac84d7", "fields": {"is_discard": false, "discard_time": null, "name": "Default", "comment": "Default user group", "date_created": "2017-12-12T08:13:20.906Z", "created_by": "System"}}, {"model": "users.loginlog", "pk": "328c7d0f-214d-4665-8c7b-f3063718530e", "fields": {"username": "admin", "type": "ST", "ip": "127.0.0.1", "city": "Unknown", "user_agent": "", "datetime": "2017-12-12T08:10:28.973Z"}}, {"model": "users.loginlog", "pk": "a72fa02e-3b2c-40a0-8cb1-0c2f98d8f248", "fields": {"username": "admin", "type": "ST", "ip": "127.0.0.1", "city": "Unknown", "user_agent": "", "datetime": "2017-12-12T08:10:28.980Z"}}, {"model": "assets.cluster", "pk": "a950b8aa-073b-45ab-b72e-5bdfbb614653", "fields": {"name": "Default", "admin_user": null, "bandwidth": "", "contact": "", "phone": "", "address": "", "intranet": "", "extranet": "", "date_created": "2017-12-12T08:13:20.919Z", "operator": "", "created_by": "System", "comment": "Default Cluster"}}, {"model": "assets.assetgroup", "pk": "d742a7be-faf1-4c29-ae0a-e6aa640ab395", "fields": {"name": "Default", "created_by": "", "date_created": "2017-12-12T08:13:20.923Z", "comment": "Default asset group"}}, {"model": "captcha.captchastore", "pk": 1, "fields": {"challenge": "EQEI", "response": "eqei", "hashkey": "c993e8e245252eb8ca40a57c67a63ee9c61dce5c", "expiration": "2017-12-12T08:17:50.235Z"}}, {"model": "users.user", "pk": "61c39c1f-5b57-4268-8180-b6dda235aadd", "fields": {"password": "pbkdf2_sha256$36000$yhYWUEo4DNqj$SpxtdIOm9nwRG+X76jUUlGvdDcLaMBl7Z+rJ8sfSMcU=", "last_login": null, "first_name": "", "last_name": "", "is_active": true, "date_joined": "2017-12-12T08:13:20.827Z", "username": "admin", "name": "Administrator", "email": "admin@jumpserver.org", "role": "Admin", "avatar": "", "wechat": "", "phone": null, "enable_otp": false, "secret_key_otp": "", "_private_key": "", "_public_key": "", "comment": "Administrator is the super user of system", "is_first_login": false, "date_expired": "2087-11-25T08:13:20.827Z", "created_by": "System", "user_permissions": [], "groups": ["f3c6b021-59a9-43e7-b022-c2e9bfac84d7"]}}]
\ No newline at end of file
...@@ -193,7 +193,7 @@ class Config(dict): ...@@ -193,7 +193,7 @@ class Config(dict):
if self.root_path: if self.root_path:
filename = os.path.join(self.root_path, filename) filename = os.path.join(self.root_path, filename)
try: try:
with open(filename) as f: with open(filename, 'rt', encoding='utf8') as f:
obj = yaml.load(f) obj = yaml.load(f)
except IOError as e: except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR): if silent and e.errno in (errno.ENOENT, errno.EISDIR):
...@@ -268,25 +268,36 @@ class Config(dict): ...@@ -268,25 +268,36 @@ class Config(dict):
rv[key] = v rv[key] = v
return rv return rv
def convert_type(self, k, v):
default_value = self.defaults.get(k)
if default_value is None:
return v
tp = type(default_value)
try:
v = tp(v)
except Exception:
pass
return v
def __repr__(self): def __repr__(self):
return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self)) return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self))
def __getitem__(self, item): def __getitem__(self, item):
# 先从设置的来
try: try:
value = super().__getitem__(item) value = super().__getitem__(item)
except KeyError: except KeyError:
value = None value = None
if value is not None: if value is not None:
return value return self.convert_type(item, value)
# 其次从环境变量来
value = os.environ.get(item, None) value = os.environ.get(item, None)
if value is not None: if value is not None:
if value.isdigit(): if value.lower() == 'false':
value = int(value)
elif value.lower() == 'false':
value = False value = False
elif value.lower() == 'true': elif value.lower() == 'true':
value = True value = True
return value return self.convert_type(item, value)
return self.defaults.get(item) return self.defaults.get(item)
def __getattr__(self, item): def __getattr__(self, item):
...@@ -311,6 +322,7 @@ defaults = { ...@@ -311,6 +322,7 @@ defaults = {
'REDIS_PASSWORD': '', 'REDIS_PASSWORD': '',
'REDIS_DB_CELERY': 3, 'REDIS_DB_CELERY': 3,
'REDIS_DB_CACHE': 4, 'REDIS_DB_CACHE': 4,
'REDIS_DB_SESSION': 5,
'CAPTCHA_TEST_MODE': None, 'CAPTCHA_TEST_MODE': None,
'TOKEN_EXPIRATION': 3600 * 24, 'TOKEN_EXPIRATION': 3600 * 24,
'DISPLAY_PER_PAGE': 25, 'DISPLAY_PER_PAGE': 25,
...@@ -347,6 +359,8 @@ defaults = { ...@@ -347,6 +359,8 @@ defaults = {
'RADIUS_SECRET': '', 'RADIUS_SECRET': '',
'HTTP_BIND_HOST': '0.0.0.0', 'HTTP_BIND_HOST': '0.0.0.0',
'HTTP_LISTEN_PORT': 8080, 'HTTP_LISTEN_PORT': 8080,
'LOGIN_LOG_KEEP_DAYS': 90,
'ASSETS_PERM_CACHE_TIME': 3600,
} }
......
# -*- coding: utf-8 -*-
#
from django.shortcuts import render
from django.http import JsonResponse
def handler404(request, *args, **argv):
if request.content_type.find('application/json') > -1:
response = JsonResponse({'error': 'Not found'}, status=404)
else:
response = render(request, '404.html', status=404)
return response
def handler500(request, *args, **argv):
if request.content_type.find('application/json') > -1:
response = JsonResponse({'error': 'Server internal error'}, status=500)
else:
response = render(request, '500.html', status=500)
return response
...@@ -22,6 +22,10 @@ from .conf import load_user_config ...@@ -22,6 +22,10 @@ from .conf import load_user_config
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_DIR = os.path.dirname(BASE_DIR) PROJECT_DIR = os.path.dirname(BASE_DIR)
sys.path.append(PROJECT_DIR)
from apps import __version__
VERSION = __version__
CONFIG = load_user_config() CONFIG = load_user_config()
LOG_DIR = os.path.join(PROJECT_DIR, 'logs') LOG_DIR = os.path.join(PROJECT_DIR, 'logs')
JUMPSERVER_LOG_FILE = os.path.join(LOG_DIR, 'jumpserver.log') JUMPSERVER_LOG_FILE = os.path.join(LOG_DIR, 'jumpserver.log')
...@@ -59,6 +63,7 @@ INSTALLED_APPS = [ ...@@ -59,6 +63,7 @@ INSTALLED_APPS = [
'assets.apps.AssetsConfig', 'assets.apps.AssetsConfig',
'perms.apps.PermsConfig', 'perms.apps.PermsConfig',
'ops.apps.OpsConfig', 'ops.apps.OpsConfig',
'settings.apps.SettingsConfig',
'common.apps.CommonConfig', 'common.apps.CommonConfig',
'terminal.apps.TerminalConfig', 'terminal.apps.TerminalConfig',
'audits.apps.AuditsConfig', 'audits.apps.AuditsConfig',
...@@ -99,7 +104,7 @@ MIDDLEWARE = [ ...@@ -99,7 +104,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'authentication.openid.middleware.OpenIDAuthenticationMiddleware', # openid 'authentication.backends.openid.middleware.OpenIDAuthenticationMiddleware',
'jumpserver.middleware.TimezoneMiddleware', 'jumpserver.middleware.TimezoneMiddleware',
'jumpserver.middleware.DemoMiddleware', 'jumpserver.middleware.DemoMiddleware',
'jumpserver.middleware.RequestMiddleware', 'jumpserver.middleware.RequestMiddleware',
...@@ -134,17 +139,28 @@ TEMPLATES = [ ...@@ -134,17 +139,28 @@ TEMPLATES = [
# WSGI_APPLICATION = 'jumpserver.wsgi.applications' # WSGI_APPLICATION = 'jumpserver.wsgi.applications'
LOGIN_REDIRECT_URL = reverse_lazy('index') LOGIN_REDIRECT_URL = reverse_lazy('index')
LOGIN_URL = reverse_lazy('users:login') LOGIN_URL = reverse_lazy('authentication:login')
SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN
CSRF_COOKIE_DOMAIN = CONFIG.CSRF_COOKIE_DOMAIN CSRF_COOKIE_DOMAIN = CONFIG.CSRF_COOKIE_DOMAIN
SESSION_COOKIE_AGE = CONFIG.SESSION_COOKIE_AGE SESSION_COOKIE_AGE = CONFIG.SESSION_COOKIE_AGE
SESSION_EXPIRE_AT_BROWSER_CLOSE = CONFIG.SESSION_EXPIRE_AT_BROWSER_CLOSE SESSION_EXPIRE_AT_BROWSER_CLOSE = CONFIG.SESSION_EXPIRE_AT_BROWSER_CLOSE
SESSION_ENGINE = 'redis_sessions.session'
SESSION_REDIS = {
'host': CONFIG.REDIS_HOST,
'port': CONFIG.REDIS_PORT,
'password': CONFIG.REDIS_PASSWORD,
'db': CONFIG.REDIS_DB_SESSION,
'prefix': 'auth_session',
'socket_timeout': 1,
'retry_on_timeout': False
}
MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
# Database # Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases # https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DB_OPTIONS = {}
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.{}'.format(CONFIG.DB_ENGINE), 'ENGINE': 'django.db.backends.{}'.format(CONFIG.DB_ENGINE),
...@@ -154,8 +170,13 @@ DATABASES = { ...@@ -154,8 +170,13 @@ DATABASES = {
'USER': CONFIG.DB_USER, 'USER': CONFIG.DB_USER,
'PASSWORD': CONFIG.DB_PASSWORD, 'PASSWORD': CONFIG.DB_PASSWORD,
'ATOMIC_REQUESTS': True, 'ATOMIC_REQUESTS': True,
'OPTIONS': DB_OPTIONS
} }
} }
DB_CA_PATH = os.path.join(PROJECT_DIR, 'data', 'ca.pem')
if CONFIG.DB_ENGINE == 'mysql' and os.path.isfile(DB_CA_PATH):
DB_OPTIONS['ssl'] = {'ca': DB_CA_PATH}
# Password validation # Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
...@@ -342,10 +363,10 @@ REST_FRAMEWORK = { ...@@ -342,10 +363,10 @@ REST_FRAMEWORK = {
), ),
'DEFAULT_AUTHENTICATION_CLASSES': ( 'DEFAULT_AUTHENTICATION_CLASSES': (
# 'rest_framework.authentication.BasicAuthentication', # 'rest_framework.authentication.BasicAuthentication',
'users.authentication.AccessKeyAuthentication', 'authentication.backends.api.AccessKeyAuthentication',
'users.authentication.AccessTokenAuthentication', 'authentication.backends.api.AccessTokenAuthentication',
'users.authentication.PrivateTokenAuthentication', 'authentication.backends.api.PrivateTokenAuthentication',
'users.authentication.SessionAuthentication', 'authentication.backends.api.SessionAuthentication',
), ),
'DEFAULT_FILTER_BACKENDS': ( 'DEFAULT_FILTER_BACKENDS': (
'django_filters.rest_framework.DjangoFilterBackend', 'django_filters.rest_framework.DjangoFilterBackend',
...@@ -394,7 +415,7 @@ AUTH_LDAP_CONNECTION_OPTIONS = { ...@@ -394,7 +415,7 @@ AUTH_LDAP_CONNECTION_OPTIONS = {
} }
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 1 AUTH_LDAP_GROUP_CACHE_TIMEOUT = 1
AUTH_LDAP_ALWAYS_UPDATE_USER = True AUTH_LDAP_ALWAYS_UPDATE_USER = True
AUTH_LDAP_BACKEND = 'authentication.ldap.backends.LDAPAuthorizationBackend' AUTH_LDAP_BACKEND = 'authentication.backends.ldap.LDAPAuthorizationBackend'
if AUTH_LDAP: if AUTH_LDAP:
AUTHENTICATION_BACKENDS.insert(0, AUTH_LDAP_BACKEND) AUTHENTICATION_BACKENDS.insert(0, AUTH_LDAP_BACKEND)
...@@ -408,18 +429,19 @@ AUTH_OPENID_REALM_NAME = CONFIG.AUTH_OPENID_REALM_NAME ...@@ -408,18 +429,19 @@ AUTH_OPENID_REALM_NAME = CONFIG.AUTH_OPENID_REALM_NAME
AUTH_OPENID_CLIENT_ID = CONFIG.AUTH_OPENID_CLIENT_ID AUTH_OPENID_CLIENT_ID = CONFIG.AUTH_OPENID_CLIENT_ID
AUTH_OPENID_CLIENT_SECRET = CONFIG.AUTH_OPENID_CLIENT_SECRET AUTH_OPENID_CLIENT_SECRET = CONFIG.AUTH_OPENID_CLIENT_SECRET
AUTH_OPENID_BACKENDS = [ AUTH_OPENID_BACKENDS = [
'authentication.openid.backends.OpenIDAuthorizationPasswordBackend', 'authentication.backends.openid.backends.OpenIDAuthorizationPasswordBackend',
'authentication.openid.backends.OpenIDAuthorizationCodeBackend', 'authentication.backends.openid.backends.OpenIDAuthorizationCodeBackend',
] ]
if AUTH_OPENID: if AUTH_OPENID:
LOGIN_URL = reverse_lazy("authentication:openid-login") LOGIN_URL = reverse_lazy("authentication:openid:openid-login")
LOGIN_COMPLETE_URL = reverse_lazy("authentication:openid:openid-login-complete")
AUTHENTICATION_BACKENDS.insert(0, AUTH_OPENID_BACKENDS[0]) AUTHENTICATION_BACKENDS.insert(0, AUTH_OPENID_BACKENDS[0])
AUTHENTICATION_BACKENDS.insert(0, AUTH_OPENID_BACKENDS[1]) AUTHENTICATION_BACKENDS.insert(0, AUTH_OPENID_BACKENDS[1])
# Radius Auth # Radius Auth
AUTH_RADIUS = CONFIG.AUTH_RADIUS AUTH_RADIUS = CONFIG.AUTH_RADIUS
AUTH_RADIUS_BACKEND = 'authentication.radius.backends.RadiusBackend' AUTH_RADIUS_BACKEND = 'authentication.backends.radius.RadiusBackend'
RADIUS_SERVER = CONFIG.RADIUS_SERVER RADIUS_SERVER = CONFIG.RADIUS_SERVER
RADIUS_PORT = CONFIG.RADIUS_PORT RADIUS_PORT = CONFIG.RADIUS_PORT
RADIUS_SECRET = CONFIG.RADIUS_SECRET RADIUS_SECRET = CONFIG.RADIUS_SECRET
...@@ -444,8 +466,8 @@ CELERY_ACCEPT_CONTENT = ['json', 'pickle'] ...@@ -444,8 +466,8 @@ CELERY_ACCEPT_CONTENT = ['json', 'pickle']
CELERY_RESULT_EXPIRES = 3600 CELERY_RESULT_EXPIRES = 3600
# CELERY_WORKER_LOG_FORMAT = '%(asctime)s [%(module)s %(levelname)s] %(message)s' # CELERY_WORKER_LOG_FORMAT = '%(asctime)s [%(module)s %(levelname)s] %(message)s'
# CELERY_WORKER_LOG_FORMAT = '%(message)s' # CELERY_WORKER_LOG_FORMAT = '%(message)s'
CELERY_WORKER_TASK_LOG_FORMAT = '%(task_id)s %(task_name)s %(message)s' # CELERY_WORKER_TASK_LOG_FORMAT = '%(task_id)s %(task_name)s %(message)s'
# CELERY_WORKER_TASK_LOG_FORMAT = '%(message)s' CELERY_WORKER_TASK_LOG_FORMAT = '%(message)s'
# CELERY_WORKER_LOG_FORMAT = '%(asctime)s [%(module)s %(levelname)s] %(message)s' # CELERY_WORKER_LOG_FORMAT = '%(asctime)s [%(module)s %(levelname)s] %(message)s'
CELERY_WORKER_LOG_FORMAT = '%(message)s' CELERY_WORKER_LOG_FORMAT = '%(message)s'
CELERY_TASK_EAGER_PROPAGATES = True CELERY_TASK_EAGER_PROPAGATES = True
...@@ -526,6 +548,7 @@ TERMINAL_ASSET_LIST_PAGE_SIZE = CONFIG.TERMINAL_ASSET_LIST_PAGE_SIZE ...@@ -526,6 +548,7 @@ TERMINAL_ASSET_LIST_PAGE_SIZE = CONFIG.TERMINAL_ASSET_LIST_PAGE_SIZE
TERMINAL_SESSION_KEEP_DURATION = CONFIG.TERMINAL_SESSION_KEEP_DURATION TERMINAL_SESSION_KEEP_DURATION = CONFIG.TERMINAL_SESSION_KEEP_DURATION
TERMINAL_HOST_KEY = CONFIG.TERMINAL_HOST_KEY TERMINAL_HOST_KEY = CONFIG.TERMINAL_HOST_KEY
TERMINAL_HEADER_TITLE = CONFIG.TERMINAL_HEADER_TITLE TERMINAL_HEADER_TITLE = CONFIG.TERMINAL_HEADER_TITLE
TERMINAL_TELNET_REGEX = CONFIG.TERMINAL_TELNET_REGEX
# Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html # Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html
BOOTSTRAP3 = { BOOTSTRAP3 = {
...@@ -555,4 +578,10 @@ SWAGGER_SETTINGS = { ...@@ -555,4 +578,10 @@ SWAGGER_SETTINGS = {
# Default email suffix # Default email suffix
EMAIL_SUFFIX = CONFIG.EMAIL_SUFFIX EMAIL_SUFFIX = CONFIG.EMAIL_SUFFIX
TERMINAL_TELNET_REGEX = CONFIG.TERMINAL_TELNET_REGEX LOGIN_LOG_KEEP_DAYS = CONFIG.LOGIN_LOG_KEEP_DAYS
# User or user group permission cache time, default 3600 seconds
ASSETS_PERM_CACHE_TIME = CONFIG.ASSETS_PERM_CACHE_TIME
# Asset user auth external backend, default AuthBook backend
BACKEND_ASSET_USER_AUTH_VAULT = False
...@@ -10,9 +10,7 @@ from django.views.i18n import JavaScriptCatalog ...@@ -10,9 +10,7 @@ from django.views.i18n import JavaScriptCatalog
from .views import IndexView, LunaView, I18NView from .views import IndexView, LunaView, I18NView
from .swagger import get_swagger_view from .swagger import get_swagger_view
api_v1 = [
api_v1_patterns = [
path('api/', include([
path('users/v1/', include('users.urls.api_urls', namespace='api-users')), path('users/v1/', include('users.urls.api_urls', namespace='api-users')),
path('assets/v1/', include('assets.urls.api_urls', namespace='api-assets')), path('assets/v1/', include('assets.urls.api_urls', namespace='api-assets')),
path('perms/v1/', include('perms.urls.api_urls', namespace='api-perms')), path('perms/v1/', include('perms.urls.api_urls', namespace='api-perms')),
...@@ -20,17 +18,16 @@ api_v1_patterns = [ ...@@ -20,17 +18,16 @@ api_v1_patterns = [
path('ops/v1/', include('ops.urls.api_urls', namespace='api-ops')), path('ops/v1/', include('ops.urls.api_urls', namespace='api-ops')),
path('audits/v1/', include('audits.urls.api_urls', namespace='api-audits')), path('audits/v1/', include('audits.urls.api_urls', namespace='api-audits')),
path('orgs/v1/', include('orgs.urls.api_urls', namespace='api-orgs')), path('orgs/v1/', include('orgs.urls.api_urls', namespace='api-orgs')),
path('common/v1/', include('common.urls.api_urls', namespace='api-common')), path('settings/v1/', include('settings.urls.api_urls', namespace='api-settings')),
])) path('authentication/v1/', include('authentication.urls.api_urls', namespace='api-auth')),
] ]
api_v2_patterns = [ api_v2 = [
path('api/', include([
path('terminal/v2/', include('terminal.urls.api_urls_v2', namespace='api-terminal-v2')), path('terminal/v2/', include('terminal.urls.api_urls_v2', namespace='api-terminal-v2')),
path('users/v2/', include('users.urls.api_urls_v2', namespace='api-users-v2')), path('users/v2/', include('users.urls.api_urls_v2', namespace='api-users-v2')),
]))
] ]
app_view_patterns = [ app_view_patterns = [
path('users/', include('users.urls.views_urls', namespace='users')), path('users/', include('users.urls.views_urls', namespace='users')),
path('assets/', include('assets.urls.views_urls', namespace='assets')), path('assets/', include('assets.urls.views_urls', namespace='assets')),
...@@ -44,30 +41,42 @@ app_view_patterns = [ ...@@ -44,30 +41,42 @@ app_view_patterns = [
if settings.XPACK_ENABLED: if settings.XPACK_ENABLED:
app_view_patterns.append(path('xpack/', include('xpack.urls', namespace='xpack'))) app_view_patterns.append(path('xpack/', include('xpack.urls.view_urls', namespace='xpack')))
api_v1.append(path('xpack/v1/', include('xpack.urls.api_urls', namespace='api-xpack')))
js_i18n_patterns = i18n_patterns( js_i18n_patterns = i18n_patterns(
path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'), path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
) )
api_v1_patterns = [
path('api/', include(api_v1))
]
api_v2_patterns = [
path('api/', include(api_v2))
]
urlpatterns = [ urlpatterns = [
path('', IndexView.as_view(), name='index'), path('', IndexView.as_view(), name='index'),
path('', include(api_v2_patterns)), path('', include(api_v2_patterns)),
path('', include(api_v1_patterns)), path('', include(api_v1_patterns)),
path('luna/', LunaView.as_view(), name='luna-error'), path('luna/', LunaView.as_view(), name='luna-view'),
path('i18n/<str:lang>/', I18NView.as_view(), name='i18n-switch'), path('i18n/<str:lang>/', I18NView.as_view(), name='i18n-switch'),
path('settings/', include('common.urls.view_urls', namespace='settings')), path('settings/', include('settings.urls.view_urls', namespace='settings')),
path('common/', include('common.urls.view_urls', namespace='common')),
# path('api/v2/', include(api_v2_patterns)), # path('api/v2/', include(api_v2_patterns)),
# External apps url # External apps url
path('captcha/', include('captcha.urls')), path('captcha/', include('captcha.urls')),
] ]
urlpatterns += app_view_patterns urlpatterns += app_view_patterns
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \
+ static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += js_i18n_patterns urlpatterns += js_i18n_patterns
handler404 = 'jumpserver.error_views.handler404'
handler500 = 'jumpserver.error_views.handler500'
if settings.DEBUG: if settings.DEBUG:
urlpatterns += [ urlpatterns += [
re_path('^swagger(?P<format>\.json|\.yaml)$', re_path('^swagger(?P<format>\.json|\.yaml)$',
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -6,12 +6,15 @@ from celery import Celery ...@@ -6,12 +6,15 @@ from celery import Celery
# set the default Django settings module for the 'celery' program. # set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'jumpserver.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'jumpserver.settings')
from jumpserver import settings
from django.conf import settings # from django.conf import settings
app = Celery('jumpserver') app = Celery('jumpserver')
configs = {k: v for k, v in settings.__dict__.items() if k.startswith('CELERY')}
# Using a string here means the worker will not have to # Using a string here means the worker will not have to
# pickle the object when using Windows. # pickle the object when using Windows.
app.config_from_object('django.conf:settings', namespace='CELERY') # app.config_from_object('django.conf:settings', namespace='CELERY')
app.namespace = 'CELERY'
app.conf.update(configs)
app.autodiscover_tasks(lambda: [app_config.split('.')[0] for app_config in settings.INSTALLED_APPS]) app.autodiscover_tasks(lambda: [app_config.split('.')[0] for app_config in settings.INSTALLED_APPS])
...@@ -143,7 +143,7 @@ class CeleryTaskFileHandler(CeleryTaskLoggerHandler): ...@@ -143,7 +143,7 @@ class CeleryTaskFileHandler(CeleryTaskLoggerHandler):
def emit(self, record): def emit(self, record):
msg = self.format(record) msg = self.format(record)
if not self.f: if not self.f or self.f.closed:
return return
self.f.write(msg) self.f.write(msg)
self.f.write(self.terminator) self.f.write(self.terminator)
......
...@@ -4,6 +4,7 @@ import json ...@@ -4,6 +4,7 @@ import json
import os import os
from django.conf import settings from django.conf import settings
from django.utils.timezone import get_current_timezone
from django.db.utils import ProgrammingError, OperationalError from django.db.utils import ProgrammingError, OperationalError
from django_celery_beat.models import PeriodicTask, IntervalSchedule, CrontabSchedule from django_celery_beat.models import PeriodicTask, IntervalSchedule, CrontabSchedule
...@@ -49,7 +50,7 @@ def create_or_update_celery_periodic_tasks(tasks): ...@@ -49,7 +50,7 @@ def create_or_update_celery_periodic_tasks(tasks):
raise SyntaxError("crontab is not valid") raise SyntaxError("crontab is not valid")
kwargs = dict( kwargs = dict(
minute=minute, hour=hour, day_of_week=week, minute=minute, hour=hour, day_of_week=week,
day_of_month=day, month_of_year=month, day_of_month=day, month_of_year=month, timezone=get_current_timezone()
) )
contabs = CrontabSchedule.objects.filter( contabs = CrontabSchedule.objects.filter(
**kwargs **kwargs
......
...@@ -4,43 +4,17 @@ ...@@ -4,43 +4,17 @@
from .ansible.inventory import BaseInventory from .ansible.inventory import BaseInventory
from assets.utils import get_assets_by_id_list, get_system_user_by_id from assets.utils import get_assets_by_id_list, get_system_user_by_id
from common.utils import get_logger
__all__ = [ __all__ = [
'JMSInventory' 'JMSInventory', 'JMSCustomInventory',
] ]
class JMSInventory(BaseInventory): logger = get_logger(__file__)
"""
JMS Inventory is the manager with jumpserver assets, so you can
write you own manager, construct you inventory
"""
def __init__(self, assets, run_as_admin=False, run_as=None, become_info=None):
"""
:param host_id_list: ["test1", ]
:param run_as_admin: True 是否使用管理用户去执行, 每台服务器的管理用户可能不同
:param run_as: 是否统一使用某个系统用户去执行
:param become_info: 是否become成某个用户去执行
"""
self.assets = assets
self.using_admin = run_as_admin
self.run_as = run_as
self.become_info = become_info
host_list = []
for asset in assets: class JMSBaseInventory(BaseInventory):
info = self.convert_to_ansible(asset, run_as_admin=run_as_admin)
host_list.append(info)
if run_as:
run_user_info = self.get_run_user_info()
for host in host_list:
host.update(run_user_info)
if become_info:
for host in host_list:
host.update(become_info)
super().__init__(host_list=host_list)
def convert_to_ansible(self, asset, run_as_admin=False): def convert_to_ansible(self, asset, run_as_admin=False):
info = { info = {
...@@ -69,13 +43,6 @@ class JMSInventory(BaseInventory): ...@@ -69,13 +43,6 @@ class JMSInventory(BaseInventory):
info["groups"].append("domain_"+asset.domain.name) info["groups"].append("domain_"+asset.domain.name)
return info return info
def get_run_user_info(self):
system_user = self.run_as
if not system_user:
return {}
else:
return system_user._to_secret_json()
@staticmethod @staticmethod
def make_proxy_command(asset): def make_proxy_command(asset):
gateway = asset.domain.random_gateway() gateway = asset.domain.random_gateway()
...@@ -97,3 +64,89 @@ class JMSInventory(BaseInventory): ...@@ -97,3 +64,89 @@ class JMSInventory(BaseInventory):
" ".join(proxy_command_list) " ".join(proxy_command_list)
) )
return {"ansible_ssh_common_args": proxy_command} return {"ansible_ssh_common_args": proxy_command}
class JMSInventory(JMSBaseInventory):
"""
JMS Inventory is the manager with jumpserver assets, so you can
write you own manager, construct you inventory,
user_info is obtained from admin_user or asset_user
"""
def __init__(self, assets, run_as_admin=False, run_as=None, become_info=None):
"""
:param host_id_list: ["test1", ]
:param run_as_admin: True 是否使用管理用户去执行, 每台服务器的管理用户可能不同
:param run_as: 用户名(添加了统一的资产用户管理器之后AssetUserManager加上之后修改为username)
:param become_info: 是否become成某个用户去执行
"""
self.assets = assets
self.using_admin = run_as_admin
self.run_as = run_as
self.become_info = become_info
host_list = []
for asset in assets:
info = self.convert_to_ansible(asset, run_as_admin=run_as_admin)
host_list.append(info)
if run_as:
for host in host_list:
run_user_info = self.get_run_user_info(host)
host.update(run_user_info)
if become_info:
for host in host_list:
host.update(become_info)
super().__init__(host_list=host_list)
def get_run_user_info(self, host):
from assets.backends.multi import AssetUserManager
if not self.run_as:
return {}
try:
asset = self.assets.get(id=host.get('id'))
run_user = AssetUserManager.get(self.run_as, asset)
except Exception as e:
logger.error(e, exc_info=True)
return {}
else:
return run_user._to_secret_json()
class JMSCustomInventory(JMSBaseInventory):
"""
JMS Custom Inventory is the manager with jumpserver assets,
user_info is obtained from custom parameter
"""
def __init__(self, assets, username, password=None, public_key=None, private_key=None):
"""
"""
self.assets = assets
self.username = username
self.password = password
self.public_key = public_key
self.private_key = private_key
host_list = []
for asset in assets:
info = self.convert_to_ansible(asset)
host_list.append(info)
for host in host_list:
run_user_info = self.get_run_user_info()
host.update(run_user_info)
super().__init__(host_list=host_list)
def get_run_user_info(self):
return {
'username': self.username,
'password': self.password,
'public_key': self.public_key,
'private_key': self.private_key
}
# Generated by Django 2.1.7 on 2019-03-18 02:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ops', '0005_auto_20181219_1807'),
]
operations = [
migrations.AlterField(
model_name='adhoc',
name='run_as',
field=models.CharField(default='', max_length=64, null=True, verbose_name='Username'),
),
]
...@@ -149,7 +149,7 @@ class AdHoc(models.Model): ...@@ -149,7 +149,7 @@ class AdHoc(models.Model):
_options: ansible options, more see ops.ansible.runner.Options _options: ansible options, more see ops.ansible.runner.Options
_hosts: ["hostname1", "hostname2"], hostname must be unique key of cmdb _hosts: ["hostname1", "hostname2"], hostname must be unique key of cmdb
run_as_admin: if true, then need get every host admin user run it, because every host may be have different admin user, so we choise host level run_as_admin: if true, then need get every host admin user run it, because every host may be have different admin user, so we choise host level
run_as: if not run as admin, it run it as a system/common user from cmdb run_as: username(Add the uniform AssetUserManager <AssetUserManager> and change it to username)
_become: May be using become [sudo, su] options. {method: "sudo", user: "user", pass: "pass"] _become: May be using become [sudo, su] options. {method: "sudo", user: "user", pass: "pass"]
pattern: Even if we set _hosts, We only use that to make inventory, We also can set `patter` to run task on match hosts pattern: Even if we set _hosts, We only use that to make inventory, We also can set `patter` to run task on match hosts
""" """
...@@ -161,7 +161,7 @@ class AdHoc(models.Model): ...@@ -161,7 +161,7 @@ class AdHoc(models.Model):
_hosts = models.TextField(blank=True, verbose_name=_('Hosts')) # ['hostname1', 'hostname2'] _hosts = models.TextField(blank=True, verbose_name=_('Hosts')) # ['hostname1', 'hostname2']
hosts = models.ManyToManyField('assets.Asset', verbose_name=_("Host")) hosts = models.ManyToManyField('assets.Asset', verbose_name=_("Host"))
run_as_admin = models.BooleanField(default=False, verbose_name=_('Run as admin')) run_as_admin = models.BooleanField(default=False, verbose_name=_('Run as admin'))
run_as = models.ForeignKey('assets.SystemUser', null=True, on_delete=models.CASCADE) run_as = models.CharField(max_length=64, default='', null=True, verbose_name=_('Username'))
_become = models.CharField(max_length=1024, default='', verbose_name=_("Become")) _become = models.CharField(max_length=1024, default='', verbose_name=_("Become"))
created_by = models.CharField(max_length=64, default='', null=True, verbose_name=_('Create by')) created_by = models.CharField(max_length=64, default='', null=True, verbose_name=_('Create by'))
date_created = models.DateTimeField(auto_now_add=True, db_index=True) date_created = models.DateTimeField(auto_now_add=True, db_index=True)
...@@ -233,6 +233,7 @@ class AdHoc(models.Model): ...@@ -233,6 +233,7 @@ class AdHoc(models.Model):
history.summary = summary history.summary = summary
return raw, summary return raw, summary
except Exception as e: except Exception as e:
logger.error(e, exc_info=True)
return {}, {"dark": {"all": str(e)}, "contacted": []} return {}, {"dark": {"all": str(e)}, "contacted": []}
finally: finally:
history.date_finished = timezone.now() history.date_finished = timezone.now()
......
...@@ -31,7 +31,7 @@ class CommandExecution(models.Model): ...@@ -31,7 +31,7 @@ class CommandExecution(models.Model):
@property @property
def inventory(self): def inventory(self):
return JMSInventory(self.hosts.all(), run_as=self.run_as) return JMSInventory(self.hosts.all(), run_as=self.run_as.username)
@property @property
def result(self): def result(self):
......
...@@ -75,7 +75,7 @@ class CommandExecutionSerializer(serializers.ModelSerializer): ...@@ -75,7 +75,7 @@ class CommandExecutionSerializer(serializers.ModelSerializer):
'is_finished', 'date_created', 'date_finished' 'is_finished', 'date_created', 'date_finished'
] ]
read_only_fields = [ read_only_fields = [
'id', 'result', 'is_finished', 'log_url', 'date_created', 'result', 'is_finished', 'log_url', 'date_created',
'date_finished' 'date_finished'
] ]
......
...@@ -81,6 +81,8 @@ def clean_celery_tasks_period(): ...@@ -81,6 +81,8 @@ def clean_celery_tasks_period():
settings.CELERY_LOG_DIR, expire_days settings.CELERY_LOG_DIR, expire_days
) )
subprocess.call(command, shell=True) subprocess.call(command, shell=True)
command = "echo > {}".format(os.path.join(settings.LOG_DIR, 'celery.log'))
subprocess.call(command, shell=True)
@shared_task @shared_task
......
...@@ -110,9 +110,9 @@ function initTree() { ...@@ -110,9 +110,9 @@ function initTree() {
onCheck: onCheck onCheck: onCheck
} }
}; };
var url = "{% url 'api-perms:my-nodes-assets-as-tree' %}"; var url = "{% url 'api-perms:my-nodes-assets-as-tree' %}?cache_policy=1";
if (systemUserId) { if (systemUserId) {
url += '?system_user=' + systemUserId url += '&system_user=' + systemUserId
} }
$.get(url, function(data, status){ $.get(url, function(data, status){
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet"> <link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script> <script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
<style> <style>
#search_btn { #search_btn {
margin-bottom: 0; margin-bottom: 0;
} }
...@@ -19,7 +20,7 @@ ...@@ -19,7 +20,7 @@
{% endblock %} {% endblock %}
{% block table_search %} {% block table_search %}
<form id="search_form" method="get" action="" class="pull-right form-inline"> <form id="search_form" method="get" action="" class="pull-right form-inline" style="padding-bottom: 5px">
<div class="form-group" id="date"> <div class="form-group" id="date">
<div class="input-daterange input-group" id="datepicker"> <div class="input-daterange input-group" id="datepicker">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span> <span class="input-group-addon"><i class="fa fa-calendar"></i></span>
...@@ -67,7 +68,7 @@ ...@@ -67,7 +68,7 @@
<tbody> <tbody>
{% for object in object_list %} {% for object in object_list %}
<tr class="gradeX"> <tr class="gradeX">
<td>{{ forloop.counter }}</td> <td class="text-center">{{ forloop.counter }}</td>
<td class="text-center hosts" value="{{ object.get_hosts_names }}"></td> <td class="text-center hosts" value="{{ object.get_hosts_names }}"></td>
<td class="text-center">{{ object.user.name }}</td> <td class="text-center">{{ object.user.name }}</td>
<td class="text-center">{{ object.command| truncatechars:16 }}</td> <td class="text-center">{{ object.command| truncatechars:16 }}</td>
......
...@@ -9,14 +9,6 @@ ...@@ -9,14 +9,6 @@
{% block table_search %} {% block table_search %}
<form id="search_form" method="get" action="" class="pull-right form-inline"> <form id="search_form" method="get" action="" class="pull-right form-inline">
<div class="form-group" id="date">
<div class="input-daterange input-group" id="datepicker">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_from" value="{{ date_from|date:'Y-m-d' }}">
<span class="input-group-addon">to</span>
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_to" value="{{ date_to|date:'Y-m-d' }}">
</div>
</div>
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control input-sm" name="keyword" placeholder="{% trans 'Search' %}" value="{{ keyword }}"> <input type="text" class="form-control input-sm" name="keyword" placeholder="{% trans 'Search' %}" value="{{ keyword }}">
</div> </div>
......
...@@ -33,11 +33,6 @@ class TaskListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): ...@@ -33,11 +33,6 @@ class TaskListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
queryset = queryset.filter(created_by='') queryset = queryset.filter(created_by='')
self.keyword = self.request.GET.get('keyword', '') self.keyword = self.request.GET.get('keyword', '')
queryset = queryset.filter(
date_created__gt=self.date_from,
date_created__lt=self.date_to
)
if self.keyword: if self.keyword:
queryset = queryset.filter( queryset = queryset.filter(
name__icontains=self.keyword, name__icontains=self.keyword,
......
...@@ -16,7 +16,7 @@ class OrgSerializer(ModelSerializer): ...@@ -16,7 +16,7 @@ class OrgSerializer(ModelSerializer):
model = Organization model = Organization
list_serializer_class = BulkListSerializer list_serializer_class = BulkListSerializer
fields = '__all__' fields = '__all__'
read_only_fields = ['id', 'created_by', 'date_created'] read_only_fields = ['created_by', 'date_created']
class OrgReadSerializer(ModelSerializer): class OrgReadSerializer(ModelSerializer):
......
...@@ -14,8 +14,12 @@ class SwitchOrgView(DetailView): ...@@ -14,8 +14,12 @@ class SwitchOrgView(DetailView):
pk = kwargs.get('pk') pk = kwargs.get('pk')
self.object = Organization.get_instance(pk) self.object = Organization.get_instance(pk)
request.session['oid'] = self.object.id.__str__() request.session['oid'] = self.object.id.__str__()
referer = request.META.get('HTTP_REFERER', reverse('index')) host = request.get_host()
referer = request.META.get('HTTP_REFERER')
if referer.find(host) != -1:
return redirect(referer) return redirect(referer)
else:
return redirect('index')
class SwitchToAOrgView(View): class SwitchToAOrgView(View):
......
# -*- coding: utf-8 -*-
#
from .permission import *
from .user_permission import *
from .user_group_permission import *
# -*- coding: utf-8 -*-
#
from django.utils import timezone
from django.db.models import Q
from rest_framework.views import Response
from rest_framework.generics import RetrieveUpdateAPIView
from rest_framework import viewsets
from rest_framework.pagination import LimitOffsetPagination
from common.permissions import IsOrgAdmin
from common.utils import get_object_or_none
from ..models import AssetPermission
from ..hands import (
User, UserGroup, Asset, Node, SystemUser,
)
from .. import serializers
__all__ = [
'AssetPermissionViewSet', 'AssetPermissionRemoveUserApi',
'AssetPermissionAddUserApi', 'AssetPermissionRemoveAssetApi',
'AssetPermissionAddAssetApi',
]
class AssetPermissionViewSet(viewsets.ModelViewSet):
"""
资产授权列表的增删改查api
"""
queryset = AssetPermission.objects.all()
serializer_class = serializers.AssetPermissionCreateUpdateSerializer
pagination_class = LimitOffsetPagination
filter_fields = ['name']
permission_classes = (IsOrgAdmin,)
def get_serializer_class(self):
if self.action in ("list", 'retrieve'):
return serializers.AssetPermissionListSerializer
return self.serializer_class
def filter_valid(self, queryset):
valid = self.request.query_params.get('is_valid', None)
if valid is None:
return queryset
if valid in ['0', 'N', 'false', 'False']:
valid = False
else:
valid = True
now = timezone.now()
if valid:
queryset = queryset.filter(is_active=True).filter(
date_start__lt=now, date_expired__gt=now,
)
else:
queryset = queryset.filter(
Q(is_active=False) |
Q(date_start__gt=now) |
Q(date_expired__lt=now)
)
return queryset
def filter_system_user(self, queryset):
system_user_id = self.request.query_params.get('system_user_id')
system_user_name = self.request.query_params.get('system_user')
if system_user_id:
system_user = get_object_or_none(SystemUser, pk=system_user_id)
elif system_user_name:
system_user = get_object_or_none(SystemUser, name=system_user_name)
else:
return queryset
if not system_user:
return queryset.none()
queryset = queryset.filter(system_users=system_user)
return queryset
def filter_node(self, queryset):
node_id = self.request.query_params.get('node_id')
node_name = self.request.query_params.get('node')
if node_id:
node = get_object_or_none(Node, pk=node_id)
elif node_name:
node = get_object_or_none(Node, name=node_name)
else:
return queryset
if not node:
return queryset.none()
nodes = node.get_ancestor(with_self=True)
queryset = queryset.filter(nodes__in=nodes)
return queryset
def filter_asset(self, queryset):
asset_id = self.request.query_params.get('asset_id')
hostname = self.request.query_params.get('hostname')
ip = self.request.query_params.get('ip')
if asset_id:
assets = Asset.objects.filter(pk=asset_id)
elif hostname:
assets = Asset.objects.filter(hostname=hostname)
elif ip:
assets = Asset.objects.filter(ip=ip)
else:
return queryset
if not assets:
return queryset.none()
inherit_nodes = set()
for asset in assets:
for node in asset.nodes.all():
inherit_nodes.update(set(node.get_ancestor(with_self=True)))
queryset = queryset.filter(Q(assets__in=assets) | Q(nodes__in=inherit_nodes))
return queryset
def filter_user(self, queryset):
user_id = self.request.query_params.get('user_id')
username = self.request.query_params.get('username')
if user_id:
user = get_object_or_none(User, pk=user_id)
elif username:
user = get_object_or_none(User, username=username)
else:
return queryset
if not user:
return queryset.none()
def filter_user_group(self, queryset):
user_group_id = self.request.query_params.get('user_group_id')
user_group_name = self.request.query_params.get('user_group')
if user_group_id:
group = get_object_or_none(UserGroup, pk=user_group_id)
elif user_group_name:
group = get_object_or_none(UserGroup, name=user_group_name)
else:
return queryset
if not group:
return queryset.none()
queryset = queryset.filter(user_groups=group)
return queryset
def filter_keyword(self, queryset):
keyword = self.request.query_params.get('search')
if not keyword:
return queryset
queryset = queryset.filter(name__icontains=keyword)
return queryset
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
queryset = self.filter_valid(queryset)
queryset = self.filter_keyword(queryset)
queryset = self.filter_asset(queryset)
queryset = self.filter_node(queryset)
queryset = self.filter_system_user(queryset)
queryset = self.filter_user_group(queryset)
return queryset
def get_queryset(self):
return self.queryset.all()
class AssetPermissionRemoveUserApi(RetrieveUpdateAPIView):
"""
将用户从授权中移除,Detail页面会调用
"""
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetPermissionUpdateUserSerializer
queryset = AssetPermission.objects.all()
def update(self, request, *args, **kwargs):
perm = self.get_object()
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
users = serializer.validated_data.get('users')
if users:
perm.users.remove(*tuple(users))
return Response({"msg": "ok"})
else:
return Response({"error": serializer.errors})
class AssetPermissionAddUserApi(RetrieveUpdateAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetPermissionUpdateUserSerializer
queryset = AssetPermission.objects.all()
def update(self, request, *args, **kwargs):
perm = self.get_object()
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
users = serializer.validated_data.get('users')
if users:
perm.users.add(*tuple(users))
return Response({"msg": "ok"})
else:
return Response({"error": serializer.errors})
class AssetPermissionRemoveAssetApi(RetrieveUpdateAPIView):
"""
将用户从授权中移除,Detail页面会调用
"""
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetPermissionUpdateAssetSerializer
queryset = AssetPermission.objects.all()
def update(self, request, *args, **kwargs):
perm = self.get_object()
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
assets = serializer.validated_data.get('assets')
if assets:
perm.assets.remove(*tuple(assets))
return Response({"msg": "ok"})
else:
return Response({"error": serializer.errors})
class AssetPermissionAddAssetApi(RetrieveUpdateAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetPermissionUpdateAssetSerializer
queryset = AssetPermission.objects.all()
def update(self, request, *args, **kwargs):
perm = self.get_object()
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
assets = serializer.validated_data.get('assets')
if assets:
perm.assets.add(*tuple(assets))
return Response({"msg": "ok"})
else:
return Response({"error": serializer.errors})
# -*- coding: utf-8 -*-
#
from django.shortcuts import get_object_or_404
from rest_framework.generics import (
ListAPIView, get_object_or_404,
)
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from common.tree import TreeNodeSerializer
from orgs.utils import set_to_root_org
from ..utils import (
AssetPermissionUtil, parse_asset_to_tree_node, parse_node_to_tree_node
)
from ..hands import (
AssetGrantedSerializer, UserGroup, Node, NodeSerializer
)
from .. import serializers
__all__ = [
'UserGroupGrantedAssetsApi', 'UserGroupGrantedNodesApi',
'UserGroupGrantedNodesWithAssetsApi', 'UserGroupGrantedNodeAssetsApi',
'UserGroupGrantedNodesWithAssetsAsTreeApi',
]
class UserGroupGrantedAssetsApi(ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = AssetGrantedSerializer
def get_queryset(self):
user_group_id = self.kwargs.get('pk', '')
queryset = []
if not user_group_id:
return queryset
user_group = get_object_or_404(UserGroup, id=user_group_id)
util = AssetPermissionUtil(user_group)
assets = util.get_assets()
for k, v in assets.items():
k.system_users_granted = v
queryset.append(k)
return queryset
class UserGroupGrantedNodesApi(ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = NodeSerializer
def get_queryset(self):
group_id = self.kwargs.get('pk', '')
queryset = []
if group_id:
group = get_object_or_404(UserGroup, id=group_id)
util = AssetPermissionUtil(group)
nodes = util.get_nodes_with_assets()
return nodes.keys()
return queryset
class UserGroupGrantedNodesWithAssetsApi(ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeGrantedSerializer
def get_queryset(self):
user_group_id = self.kwargs.get('pk', '')
queryset = []
if not user_group_id:
return queryset
user_group = get_object_or_404(UserGroup, id=user_group_id)
util = AssetPermissionUtil(user_group)
nodes = util.get_nodes_with_assets()
for node, _assets in nodes.items():
assets = _assets.keys()
for asset, system_users in _assets.items():
asset.system_users_granted = system_users
node.assets_granted = assets
queryset.append(node)
return queryset
class UserGroupGrantedNodesWithAssetsAsTreeApi(ListAPIView):
serializer_class = TreeNodeSerializer
permission_classes = (IsOrgAdminOrAppUser,)
show_assets = True
system_user_id = None
def change_org_if_need(self):
if self.request.user.is_superuser or \
self.request.user.is_app or \
self.kwargs.get('pk') is None:
set_to_root_org()
def get(self, request, *args, **kwargs):
self.show_assets = request.query_params.get('show_assets', '1') == '1'
self.system_user_id = request.query_params.get('system_user')
return super().get(request, *args, **kwargs)
def get_queryset(self):
self.change_org_if_need()
user_group_id = self.kwargs.get('pk', '')
queryset = []
group = get_object_or_404(UserGroup, id=user_group_id)
util = AssetPermissionUtil(group)
if self.system_user_id:
util.filter_permission_with_system_user(system_user=self.system_user_id)
nodes = util.get_nodes_with_assets()
for node, assets in nodes.items():
data = parse_node_to_tree_node(node)
queryset.append(data)
if not self.show_assets:
continue
for asset, system_users in assets.items():
data = parse_asset_to_tree_node(node, asset, system_users)
queryset.append(data)
queryset = sorted(queryset)
return queryset
class UserGroupGrantedNodeAssetsApi(ListAPIView):
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = AssetGrantedSerializer
def get_queryset(self):
user_group_id = self.kwargs.get('pk', '')
node_id = self.kwargs.get('node_id')
user_group = get_object_or_404(UserGroup, id=user_group_id)
node = get_object_or_404(Node, id=node_id)
util = AssetPermissionUtil(user_group)
nodes = util.get_nodes_with_assets()
assets = nodes.get(node, [])
for asset, system_users in assets.items():
asset.system_users_granted = system_users
return assets
# ~*~ coding: utf-8 ~*~ # -*- coding: utf-8 -*-
# #
from hashlib import md5
from django.core.cache import cache
from django.conf import settings
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.db.models import Q
from rest_framework.views import APIView, Response from rest_framework.views import APIView, Response
from rest_framework.generics import ( from rest_framework.generics import (
ListAPIView, get_object_or_404, RetrieveUpdateAPIView ListAPIView, get_object_or_404,
) )
from rest_framework import viewsets
from rest_framework.pagination import LimitOffsetPagination from rest_framework.pagination import LimitOffsetPagination
from common.permissions import IsValidUser, IsOrgAdmin, IsOrgAdminOrAppUser from common.permissions import IsValidUser, IsOrgAdminOrAppUser
from common.tree import TreeNode, TreeNodeSerializer from common.tree import TreeNodeSerializer
from common.utils import get_object_or_none from common.utils import get_logger
from orgs.mixins import RootOrgViewMixin
from orgs.utils import set_to_root_org from orgs.utils import set_to_root_org
from .utils import ( from ..utils import (
AssetPermissionUtil, parse_asset_to_tree_node, parse_node_to_tree_node AssetPermissionUtil, parse_asset_to_tree_node, parse_node_to_tree_node
) )
from .models import AssetPermission from ..hands import (
from .hands import ( AssetGrantedSerializer, User, Asset, Node,
AssetGrantedSerializer, User, UserGroup, Asset, Node,
SystemUser, NodeSerializer SystemUser, NodeSerializer
) )
from . import serializers from .. import serializers
from .mixins import AssetsFilterMixin from ..mixins import AssetsFilterMixin
logger = get_logger(__name__)
__all__ = [ __all__ = [
'AssetPermissionViewSet', 'UserGrantedAssetsApi', 'UserGrantedNodesApi', 'UserGrantedAssetsApi', 'UserGrantedNodesApi',
'UserGrantedNodesWithAssetsApi', 'UserGrantedNodeAssetsApi', 'UserGroupGrantedAssetsApi', 'UserGrantedNodesWithAssetsApi', 'UserGrantedNodeAssetsApi',
'UserGroupGrantedNodesApi', 'UserGroupGrantedNodesWithAssetsApi', 'UserGroupGrantedNodeAssetsApi', 'ValidateUserAssetPermissionApi', 'UserGrantedNodeChildrenApi',
'ValidateUserAssetPermissionApi', 'AssetPermissionRemoveUserApi', 'AssetPermissionAddUserApi',
'AssetPermissionRemoveAssetApi', 'AssetPermissionAddAssetApi', 'UserGrantedNodeChildrenApi',
'UserGrantedNodesWithAssetsAsTreeApi', 'UserGrantedNodesWithAssetsAsTreeApi',
] ]
class AssetPermissionViewSet(viewsets.ModelViewSet): class UserPermissionMixin:
""" cache_policy = '0'
资产授权列表的增删改查api RESP_CACHE_KEY = '_PERMISSION_RESPONSE_CACHE_{}'
""" CACHE_TIME = settings.ASSETS_PERM_CACHE_TIME
queryset = AssetPermission.objects.all()
serializer_class = serializers.AssetPermissionCreateUpdateSerializer
pagination_class = LimitOffsetPagination
filter_fields = ['name']
permission_classes = (IsOrgAdmin,)
def get_serializer_class(self):
if self.action in ("list", 'retrieve'):
return serializers.AssetPermissionListSerializer
return self.serializer_class
def filter_valid(self, queryset):
valid = self.request.query_params.get('is_valid', None)
if valid is None:
return queryset
if valid in ['0', 'N', 'false', 'False']:
valid = False
else:
valid = True
now = timezone.now()
if valid:
queryset = queryset.filter(is_active=True).filter(
date_start__lt=now, date_expired__gt=now,
)
else:
queryset = queryset.filter(
Q(is_active=False) |
Q(date_start__gt=now) |
Q(date_expired__lt=now)
)
return queryset
def filter_system_user(self, queryset):
system_user_id = self.request.query_params.get('system_user_id')
system_user_name = self.request.query_params.get('system_user')
if system_user_id:
system_user = get_object_or_none(SystemUser, pk=system_user_id)
elif system_user_name:
system_user = get_object_or_none(SystemUser, name=system_user_name)
else:
return queryset
if not system_user:
return queryset.none()
queryset = queryset.filter(system_users=system_user)
return queryset
def filter_node(self, queryset):
node_id = self.request.query_params.get('node_id')
node_name = self.request.query_params.get('node')
if node_id:
node = get_object_or_none(Node, pk=node_id)
elif node_name:
node = get_object_or_none(Node, name=node_name)
else:
return queryset
if not node:
return queryset.none()
nodes = node.get_ancestor(with_self=True)
queryset = queryset.filter(nodes__in=nodes)
return queryset
def filter_asset(self, queryset):
asset_id = self.request.query_params.get('asset_id')
hostname = self.request.query_params.get('hostname')
ip = self.request.query_params.get('ip')
if asset_id:
assets = Asset.objects.filter(pk=asset_id)
elif hostname:
assets = Asset.objects.filter(hostname=hostname)
elif ip:
assets = Asset.objects.filter(ip=ip)
else:
return queryset
if not assets:
return queryset.none()
inherit_nodes = set()
for asset in assets:
for node in asset.nodes.all():
inherit_nodes.update(set(node.get_ancestor(with_self=True)))
queryset = queryset.filter(Q(assets__in=assets) | Q(nodes__in=inherit_nodes))
return queryset
def filter_user(self, queryset):
user_id = self.request.query_params.get('user_id')
username = self.request.query_params.get('username')
if user_id:
user = get_object_or_none(User, pk=user_id)
elif username:
user = get_object_or_none(User, username=username)
else:
return queryset
if not user:
return queryset.none()
def filter_user_group(self, queryset):
user_group_id = self.request.query_params.get('user_group_id')
user_group_name = self.request.query_params.get('user_group')
if user_group_id:
group = get_object_or_none(UserGroup, pk=user_group_id)
elif user_group_name:
group = get_object_or_none(UserGroup, name=user_group_name)
else:
return queryset
if not group:
return queryset.none()
queryset = queryset.filter(user_groups=group)
return queryset
def filter_keyword(self, queryset): @staticmethod
keyword = self.request.query_params.get('search') def change_org_if_need(request, kwargs):
if not keyword: if request.user.is_authenticated and \
return queryset request.user.is_superuser or \
queryset = queryset.filter(name__icontains=keyword) request.user.is_app or \
return queryset kwargs.get('pk') is None:
set_to_root_org()
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
queryset = self.filter_valid(queryset)
queryset = self.filter_keyword(queryset)
queryset = self.filter_asset(queryset)
queryset = self.filter_node(queryset)
queryset = self.filter_system_user(queryset)
queryset = self.filter_user_group(queryset)
return queryset
def get_queryset(self): def get_object(self):
return self.queryset.all() return None
def get(self, request, *args, **kwargs):
self.change_org_if_need(request, kwargs)
self.cache_policy = request.GET.get('cache_policy', '0')
class UserGrantedAssetsApi(AssetsFilterMixin, ListAPIView): obj = self.get_object()
if obj is None:
return super().get(request, *args, **kwargs)
request_path_md5 = md5(request.get_full_path().encode()).hexdigest()
obj_id = str(obj.id)
expire_cache_key = '{}_{}'.format(obj_id, '*')
if self.CACHE_TIME <= 0 or \
self.cache_policy in AssetPermissionUtil.CACHE_POLICY_MAP[0]:
return super().get(request, *args, **kwargs)
elif self.cache_policy in AssetPermissionUtil.CACHE_POLICY_MAP[2]:
self.expire_cache_response(expire_cache_key)
util = AssetPermissionUtil(obj, cache_policy=self.cache_policy)
meta_cache_id = util.cache_meta.get('id')
cache_id = '{}_{}_{}'.format(obj_id, request_path_md5, meta_cache_id)
# 没有数据缓冲
if not meta_cache_id:
response = super().get(request, *args, **kwargs)
self.set_cache_response(cache_id, response)
return response
# 从响应缓冲里获取响应
response = self.get_cache_response(cache_id)
if not response:
response = super().get(request, *args, **kwargs)
self.set_cache_response(cache_id, response)
return response
def get_cache_response(self, _id):
if not _id:
return None
key = self.RESP_CACHE_KEY.format(_id)
data = cache.get(key)
if not data:
return None
return Response(data)
def expire_cache_response(self, _id):
key = self.RESP_CACHE_KEY.format(_id)
cache.delete(key)
def set_cache_response(self, _id, response):
key = self.RESP_CACHE_KEY.format(_id)
cache.set(key, response.data, self.CACHE_TIME)
class UserGrantedAssetsApi(UserPermissionMixin, AssetsFilterMixin, ListAPIView):
""" """
用户授权的所有资产 用户授权的所有资产
""" """
...@@ -179,28 +108,24 @@ class UserGrantedAssetsApi(AssetsFilterMixin, ListAPIView): ...@@ -179,28 +108,24 @@ class UserGrantedAssetsApi(AssetsFilterMixin, ListAPIView):
serializer_class = AssetGrantedSerializer serializer_class = AssetGrantedSerializer
pagination_class = LimitOffsetPagination pagination_class = LimitOffsetPagination
def change_org_if_need(self): def get_object(self):
if self.request.user.is_superuser or \
self.request.user.is_app or \
self.kwargs.get('pk') is None:
set_to_root_org()
def get_queryset(self):
self.change_org_if_need()
user_id = self.kwargs.get('pk', '') user_id = self.kwargs.get('pk', '')
queryset = []
if user_id: if user_id:
user = get_object_or_404(User, id=user_id) user = get_object_or_404(User, id=user_id)
else: else:
user = self.request.user user = self.request.user
return user
util = AssetPermissionUtil(user) def get_queryset(self):
for k, v in util.get_assets().items(): queryset = []
user = self.get_object()
util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
assets = util.get_assets()
for k, v in assets.items():
system_users_granted = [s for s in v if s.protocol == k.protocol] system_users_granted = [s for s in v if s.protocol == k.protocol]
k.system_users_granted = system_users_granted k.system_users_granted = system_users_granted
queryset.append(k) queryset.append(k)
return queryset return queryset
def get_permissions(self): def get_permissions(self):
...@@ -209,27 +134,24 @@ class UserGrantedAssetsApi(AssetsFilterMixin, ListAPIView): ...@@ -209,27 +134,24 @@ class UserGrantedAssetsApi(AssetsFilterMixin, ListAPIView):
return super().get_permissions() return super().get_permissions()
class UserGrantedNodesApi(ListAPIView): class UserGrantedNodesApi(UserPermissionMixin, ListAPIView):
""" """
查询用户授权的所有节点的API, 如果是超级用户或者是 app,切换到root org 查询用户授权的所有节点的API, 如果是超级用户或者是 app,切换到root org
""" """
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = NodeSerializer serializer_class = NodeSerializer
def change_org_if_need(self): def get_object(self):
if self.request.user.is_superuser or \
self.request.user.is_app or \
self.kwargs.get('pk') is None:
set_to_root_org()
def get_queryset(self):
self.change_org_if_need()
user_id = self.kwargs.get('pk', '') user_id = self.kwargs.get('pk', '')
if user_id: if user_id:
user = get_object_or_404(User, id=user_id) user = get_object_or_404(User, id=user_id)
else: else:
user = self.request.user user = self.request.user
util = AssetPermissionUtil(user) return user
def get_queryset(self):
user = self.get_object()
util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
nodes = util.get_nodes_with_assets() nodes = util.get_nodes_with_assets()
return nodes.keys() return nodes.keys()
...@@ -239,34 +161,31 @@ class UserGrantedNodesApi(ListAPIView): ...@@ -239,34 +161,31 @@ class UserGrantedNodesApi(ListAPIView):
return super().get_permissions() return super().get_permissions()
class UserGrantedNodesWithAssetsApi(AssetsFilterMixin, ListAPIView): class UserGrantedNodesWithAssetsApi(UserPermissionMixin, AssetsFilterMixin, ListAPIView):
""" """
用户授权的节点并带着节点下资产的api 用户授权的节点并带着节点下资产的api
""" """
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.NodeGrantedSerializer serializer_class = serializers.NodeGrantedSerializer
def change_org_if_need(self): def get_object(self):
if self.request.user.is_superuser or \
self.request.user.is_app or \
self.kwargs.get('pk') is None:
set_to_root_org()
def get_queryset(self):
self.change_org_if_need()
user_id = self.kwargs.get('pk', '') user_id = self.kwargs.get('pk', '')
queryset = []
if not user_id: if not user_id:
user = self.request.user user = self.request.user
else: else:
user = get_object_or_404(User, id=user_id) user = get_object_or_404(User, id=user_id)
return user
util = AssetPermissionUtil(user) def get_queryset(self):
queryset = []
user = self.get_object()
util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
nodes = util.get_nodes_with_assets() nodes = util.get_nodes_with_assets()
for node, _assets in nodes.items(): for node, _assets in nodes.items():
assets = _assets.keys() assets = _assets.keys()
for k, v in _assets.items(): for k, v in _assets.items():
system_users_granted = [s for s in v if s.protocol == k.protocol] system_users_granted = [s for s in v if
s.protocol == k.protocol]
k.system_users_granted = system_users_granted k.system_users_granted = system_users_granted
node.assets_granted = assets node.assets_granted = assets
queryset.append(node) queryset.append(node)
...@@ -283,18 +202,12 @@ class UserGrantedNodesWithAssetsApi(AssetsFilterMixin, ListAPIView): ...@@ -283,18 +202,12 @@ class UserGrantedNodesWithAssetsApi(AssetsFilterMixin, ListAPIView):
return super().get_permissions() return super().get_permissions()
class UserGrantedNodesWithAssetsAsTreeApi(ListAPIView): class UserGrantedNodesWithAssetsAsTreeApi(UserPermissionMixin, ListAPIView):
serializer_class = TreeNodeSerializer serializer_class = TreeNodeSerializer
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
show_assets = True show_assets = True
system_user_id = None system_user_id = None
def change_org_if_need(self):
if self.request.user.is_superuser or \
self.request.user.is_app or \
self.kwargs.get('pk') is None:
set_to_root_org()
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.show_assets = request.query_params.get('show_assets', '1') == '1' self.show_assets = request.query_params.get('show_assets', '1') == '1'
self.system_user_id = request.query_params.get('system_user') self.system_user_id = request.query_params.get('system_user')
...@@ -305,17 +218,22 @@ class UserGrantedNodesWithAssetsAsTreeApi(ListAPIView): ...@@ -305,17 +218,22 @@ class UserGrantedNodesWithAssetsAsTreeApi(ListAPIView):
self.permission_classes = (IsValidUser,) self.permission_classes = (IsValidUser,)
return super().get_permissions() return super().get_permissions()
def get_queryset(self): def get_object(self):
self.change_org_if_need()
user_id = self.kwargs.get('pk', '') user_id = self.kwargs.get('pk', '')
queryset = []
if not user_id: if not user_id:
user = self.request.user user = self.request.user
else: else:
user = get_object_or_404(User, id=user_id) user = get_object_or_404(User, id=user_id)
util = AssetPermissionUtil(user) return user
def get_queryset(self):
queryset = []
user = self.get_object()
util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
if self.system_user_id: if self.system_user_id:
util.filter_permission_with_system_user(system_user=self.system_user_id) util.filter_permission_with_system_user(
system_user=self.system_user_id
)
nodes = util.get_nodes_with_assets() nodes = util.get_nodes_with_assets()
for node, assets in nodes.items(): for node, assets in nodes.items():
data = parse_node_to_tree_node(node) data = parse_node_to_tree_node(node)
...@@ -329,7 +247,7 @@ class UserGrantedNodesWithAssetsAsTreeApi(ListAPIView): ...@@ -329,7 +247,7 @@ class UserGrantedNodesWithAssetsAsTreeApi(ListAPIView):
return queryset return queryset
class UserGrantedNodeAssetsApi(AssetsFilterMixin, ListAPIView): class UserGrantedNodeAssetsApi(UserPermissionMixin, AssetsFilterMixin, ListAPIView):
""" """
查询用户授权的节点下的资产的api, 与上面api不同的是,只返回某个节点下的资产 查询用户授权的节点下的资产的api, 与上面api不同的是,只返回某个节点下的资产
""" """
...@@ -337,22 +255,19 @@ class UserGrantedNodeAssetsApi(AssetsFilterMixin, ListAPIView): ...@@ -337,22 +255,19 @@ class UserGrantedNodeAssetsApi(AssetsFilterMixin, ListAPIView):
serializer_class = AssetGrantedSerializer serializer_class = AssetGrantedSerializer
pagination_class = LimitOffsetPagination pagination_class = LimitOffsetPagination
def change_org_if_need(self): def get_object(self):
if self.request.user.is_superuser or \
self.request.user.is_app or \
self.kwargs.get('pk') is None:
set_to_root_org()
def get_queryset(self):
self.change_org_if_need()
user_id = self.kwargs.get('pk', '') user_id = self.kwargs.get('pk', '')
node_id = self.kwargs.get('node_id')
if user_id: if user_id:
user = get_object_or_404(User, id=user_id) user = get_object_or_404(User, id=user_id)
else: else:
user = self.request.user user = self.request.user
util = AssetPermissionUtil(user) return user
def get_queryset(self):
user = self.get_object()
node_id = self.kwargs.get('node_id')
util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
node = get_object_or_404(Node, id=node_id) node = get_object_or_404(Node, id=node_id)
nodes = util.get_nodes_with_assets() nodes = util.get_nodes_with_assets()
assets = nodes.get(node, []) assets = nodes.get(node, [])
...@@ -368,228 +283,19 @@ class UserGrantedNodeAssetsApi(AssetsFilterMixin, ListAPIView): ...@@ -368,228 +283,19 @@ class UserGrantedNodeAssetsApi(AssetsFilterMixin, ListAPIView):
return super().get_permissions() return super().get_permissions()
class UserGroupGrantedAssetsApi(ListAPIView): class UserGrantedNodeChildrenApi(UserPermissionMixin, ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = AssetGrantedSerializer
def get_queryset(self):
user_group_id = self.kwargs.get('pk', '')
queryset = []
if not user_group_id:
return queryset
user_group = get_object_or_404(UserGroup, id=user_group_id)
util = AssetPermissionUtil(user_group)
assets = util.get_assets()
for k, v in assets.items():
k.system_users_granted = v
queryset.append(k)
return queryset
class UserGroupGrantedNodesApi(ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = NodeSerializer
def get_queryset(self):
group_id = self.kwargs.get('pk', '')
queryset = []
if group_id:
group = get_object_or_404(UserGroup, id=group_id)
util = AssetPermissionUtil(group)
nodes = util.get_nodes_with_assets()
return nodes.keys()
return queryset
class UserGroupGrantedNodesWithAssetsApi(ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeGrantedSerializer
def get_queryset(self):
user_group_id = self.kwargs.get('pk', '')
queryset = []
if not user_group_id:
return queryset
user_group = get_object_or_404(UserGroup, id=user_group_id)
util = AssetPermissionUtil(user_group)
nodes = util.get_nodes_with_assets()
for node, _assets in nodes.items():
assets = _assets.keys()
for asset, system_users in _assets.items():
asset.system_users_granted = system_users
node.assets_granted = assets
queryset.append(node)
return queryset
class UserGroupGrantedNodesWithAssetsAsTreeApi(ListAPIView):
serializer_class = TreeNodeSerializer
permission_classes = (IsOrgAdminOrAppUser,)
show_assets = True
system_user_id = None
def change_org_if_need(self):
if self.request.user.is_superuser or \
self.request.user.is_app or \
self.kwargs.get('pk') is None:
set_to_root_org()
def get(self, request, *args, **kwargs):
self.show_assets = request.query_params.get('show_assets', '1') == '1'
self.system_user_id = request.query_params.get('system_user')
return super().get(request, *args, **kwargs)
def get_queryset(self):
self.change_org_if_need()
user_group_id = self.kwargs.get('pk', '')
queryset = []
group = get_object_or_404(UserGroup, id=user_group_id)
util = AssetPermissionUtil(group)
if self.system_user_id:
util.filter_permission_with_system_user(system_user=self.system_user_id)
nodes = util.get_nodes_with_assets()
for node, assets in nodes.items():
data = parse_node_to_tree_node(node)
queryset.append(data)
if not self.show_assets:
continue
for asset, system_users in assets.items():
data = parse_asset_to_tree_node(node, asset, system_users)
queryset.append(data)
queryset = sorted(queryset)
return queryset
class UserGroupGrantedNodeAssetsApi(ListAPIView):
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = AssetGrantedSerializer
def get_queryset(self):
user_group_id = self.kwargs.get('pk', '')
node_id = self.kwargs.get('node_id')
user_group = get_object_or_404(UserGroup, id=user_group_id)
node = get_object_or_404(Node, id=node_id)
util = AssetPermissionUtil(user_group)
nodes = util.get_nodes_with_assets()
assets = nodes.get(node, [])
for asset, system_users in assets.items():
asset.system_users_granted = system_users
return assets
class ValidateUserAssetPermissionApi(RootOrgViewMixin, APIView):
permission_classes = (IsOrgAdminOrAppUser,)
@staticmethod
def get(request):
user_id = request.query_params.get('user_id', '')
asset_id = request.query_params.get('asset_id', '')
system_id = request.query_params.get('system_user_id', '')
user = get_object_or_404(User, id=user_id)
asset = get_object_or_404(Asset, id=asset_id)
system_user = get_object_or_404(SystemUser, id=system_id)
util = AssetPermissionUtil(user)
assets_granted = util.get_assets()
if system_user in assets_granted.get(asset, []):
return Response({'msg': True}, status=200)
else:
return Response({'msg': False}, status=403)
class AssetPermissionRemoveUserApi(RetrieveUpdateAPIView):
"""
将用户从授权中移除,Detail页面会调用
"""
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetPermissionUpdateUserSerializer
queryset = AssetPermission.objects.all()
def update(self, request, *args, **kwargs):
perm = self.get_object()
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
users = serializer.validated_data.get('users')
if users:
perm.users.remove(*tuple(users))
return Response({"msg": "ok"})
else:
return Response({"error": serializer.errors})
class AssetPermissionAddUserApi(RetrieveUpdateAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetPermissionUpdateUserSerializer
queryset = AssetPermission.objects.all()
def update(self, request, *args, **kwargs):
perm = self.get_object()
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
users = serializer.validated_data.get('users')
if users:
perm.users.add(*tuple(users))
return Response({"msg": "ok"})
else:
return Response({"error": serializer.errors})
class AssetPermissionRemoveAssetApi(RetrieveUpdateAPIView):
""" """
将用户从授权中移除,Detail页面会调用 获取用户自己授权节点下子节点的api
""" """
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetPermissionUpdateAssetSerializer
queryset = AssetPermission.objects.all()
def update(self, request, *args, **kwargs):
perm = self.get_object()
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
assets = serializer.validated_data.get('assets')
if assets:
perm.assets.remove(*tuple(assets))
return Response({"msg": "ok"})
else:
return Response({"error": serializer.errors})
class AssetPermissionAddAssetApi(RetrieveUpdateAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetPermissionUpdateAssetSerializer
queryset = AssetPermission.objects.all()
def update(self, request, *args, **kwargs):
perm = self.get_object()
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
assets = serializer.validated_data.get('assets')
if assets:
perm.assets.add(*tuple(assets))
return Response({"msg": "ok"})
else:
return Response({"error": serializer.errors})
class UserGrantedNodeChildrenApi(ListAPIView):
permission_classes = (IsValidUser,) permission_classes = (IsValidUser,)
serializer_class = serializers.AssetPermissionNodeSerializer serializer_class = serializers.AssetPermissionNodeSerializer
def change_org_if_need(self): def get_object(self):
if self.request.user.is_superuser or \ return self.request.user
self.request.user.is_app or \
self.kwargs.get('pk') is None:
set_to_root_org()
def get_children_queryset(self): def get_children_queryset(self):
util = AssetPermissionUtil(self.request.user) user = self.get_object()
util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
node_id = self.request.query_params.get('id') node_id = self.request.query_params.get('id')
nodes_granted = util.get_nodes_with_assets() nodes_granted = util.get_nodes_with_assets()
if not nodes_granted: if not nodes_granted:
...@@ -629,7 +335,8 @@ class UserGrantedNodeChildrenApi(ListAPIView): ...@@ -629,7 +335,8 @@ class UserGrantedNodeChildrenApi(ListAPIView):
return queryset return queryset
def get_search_queryset(self, keyword): def get_search_queryset(self, keyword):
util = AssetPermissionUtil(self.request.user) user = self.get_object()
util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
nodes_granted = util.get_nodes_with_assets() nodes_granted = util.get_nodes_with_assets()
queryset = [] queryset = []
for node, assets in nodes_granted.items(): for node, assets in nodes_granted.items():
...@@ -655,9 +362,30 @@ class UserGrantedNodeChildrenApi(ListAPIView): ...@@ -655,9 +362,30 @@ class UserGrantedNodeChildrenApi(ListAPIView):
return queryset return queryset
def get_queryset(self): def get_queryset(self):
self.change_org_if_need()
keyword = self.request.query_params.get('search') keyword = self.request.query_params.get('search')
if keyword: if keyword:
return self.get_search_queryset(keyword) return self.get_search_queryset(keyword)
else: else:
return self.get_children_queryset() return self.get_children_queryset()
class ValidateUserAssetPermissionApi(UserPermissionMixin, APIView):
permission_classes = (IsOrgAdminOrAppUser,)
def get(self, request, *args, **kwargs):
user_id = request.query_params.get('user_id', '')
asset_id = request.query_params.get('asset_id', '')
system_id = request.query_params.get('system_user_id', '')
user = get_object_or_404(User, id=user_id)
asset = get_object_or_404(Asset, id=asset_id)
system_user = get_object_or_404(SystemUser, id=system_id)
util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
assets_granted = util.get_assets()
if system_user in assets_granted.get(asset, []):
return Response({'msg': True}, status=200)
else:
return Response({'msg': False}, status=403)
# Generated by Django 2.1.7 on 2019-02-28 10:21
import common.utils.django
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import uuid
# Functions from the following migrations need manual copying.
# Move them and any dependencies into this file, then update the
# RunPython operations to refer to the local versions:
# perms.migrations.0005_migrate_data_20180411_1144
def migrate_node_permissions(apps, schema_editor):
node_perm_model = apps.get_model("perms", "NodePermission")
asset_perm_model = apps.get_model("perms", "AssetPermission")
db_alias = schema_editor.connection.alias
for old in node_perm_model.objects.using(db_alias).all():
perm = asset_perm_model.objects.using(db_alias).create(
name="{}-{}-{}".format(
old.node.value,
old.user_group.name,
old.system_user.name
),
is_active=old.is_active,
date_expired=old.date_expired,
created_by=old.date_expired,
date_created=old.date_created,
comment=old.comment,
)
perm.user_groups.add(old.user_group)
perm.nodes.add(old.node)
perm.system_users.add(old.system_user)
def migrate_system_assets_relation(apps, schema_editor):
system_user_model = apps.get_model("assets", "SystemUser")
db_alias = schema_editor.connection.alias
for s in system_user_model.objects.using(db_alias).all():
nodes = list(s.nodes.all())
s.nodes.set([])
s.nodes.set(nodes)
class Migration(migrations.Migration):
replaces = [('perms', '0002_auto_20171228_0025'), ('perms', '0003_auto_20180225_1815'), ('perms', '0004_auto_20180411_1135'), ('perms', '0005_migrate_data_20180411_1144'), ('perms', '0006_auto_20180606_1505'), ('perms', '0007_auto_20180807_1116'), ('perms', '0008_auto_20180816_1652'), ('perms', '0009_auto_20180903_1132')]
dependencies = [
('users', '0002_auto_20171225_1157'),
('assets', '0007_auto_20180225_1815'),
('assets', '0013_auto_20180411_1135'),
('users', '0004_auto_20180125_1218'),
('perms', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='assetpermission',
name='user_groups',
field=models.ManyToManyField(blank=True, related_name='asset_permissions', to='users.UserGroup', verbose_name='User group'),
),
migrations.AddField(
model_name='assetpermission',
name='users',
field=models.ManyToManyField(blank=True, related_name='asset_permissions', to=settings.AUTH_USER_MODEL, verbose_name='User'),
),
migrations.CreateModel(
name='NodePermission',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('is_active', models.BooleanField(default=True, verbose_name='Active')),
('date_expired', models.DateTimeField(default=common.utils.django.date_expired_default, verbose_name='Date expired')),
('created_by', models.CharField(blank=True, max_length=128, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('node', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Node', verbose_name='Node')),
('system_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.SystemUser', verbose_name='System user')),
('user_group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.UserGroup', verbose_name='User group')),
],
options={
'verbose_name': 'Asset permission',
},
),
migrations.AlterUniqueTogether(
name='nodepermission',
unique_together={('node', 'user_group', 'system_user')},
),
migrations.RemoveField(
model_name='assetpermission',
name='asset_groups',
),
migrations.AddField(
model_name='assetpermission',
name='date_start',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Date start'),
),
migrations.AddField(
model_name='assetpermission',
name='nodes',
field=models.ManyToManyField(blank=True, related_name='granted_by_permissions', to='assets.Node', verbose_name='Nodes'),
),
migrations.RunPython(
code=migrate_node_permissions,
),
migrations.RunPython(
code=migrate_system_assets_relation,
),
migrations.AlterField(
model_name='assetpermission',
name='date_expired',
field=models.DateTimeField(db_index=True, default=common.utils.django.date_expired_default, verbose_name='Date expired'),
),
migrations.AlterField(
model_name='assetpermission',
name='date_start',
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='Date start'),
),
migrations.AddField(
model_name='assetpermission',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='nodepermission',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AlterField(
model_name='assetpermission',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterUniqueTogether(
name='assetpermission',
unique_together={('org_id', 'name')},
),
migrations.AlterUniqueTogether(
name='nodepermission',
unique_together=set(),
),
migrations.AlterField(
model_name='assetpermission',
name='org_id',
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='nodepermission',
name='org_id',
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterModelOptions(
name='assetpermission',
options={'verbose_name': 'Asset permission'},
),
]
...@@ -5,7 +5,6 @@ from django.utils.translation import ugettext_lazy as _ ...@@ -5,7 +5,6 @@ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone from django.utils import timezone
from common.utils import date_expired_default, set_or_append_attr_bulk from common.utils import date_expired_default, set_or_append_attr_bulk
from orgs.mixins import OrgModelMixin, OrgManager from orgs.mixins import OrgModelMixin, OrgManager
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.db.models.signals import m2m_changed from django.db.models.signals import m2m_changed, post_save, post_delete
from django.dispatch import receiver from django.dispatch import receiver
from common.utils import get_logger from common.utils import get_logger
from .utils import AssetPermissionUtil
from .models import AssetPermission from .models import AssetPermission
logger = get_logger(__file__) logger = get_logger(__file__)
@receiver(post_save, sender=AssetPermission)
def on_permission_update(sender, **kwargs):
AssetPermissionUtil.expire_all_cache()
@receiver(post_delete, sender=AssetPermission)
def on_permission_delete(sender, **kwargs):
AssetPermissionUtil.expire_all_cache()
@receiver(m2m_changed, sender=AssetPermission.nodes.through) @receiver(m2m_changed, sender=AssetPermission.nodes.through)
def on_permission_nodes_changed(sender, instance=None, **kwargs): def on_permission_nodes_changed(sender, instance=None, **kwargs):
if isinstance(instance, AssetPermission) and kwargs['action'] == 'post_add': if isinstance(instance, AssetPermission) and kwargs['action'] == 'post_add':
......
# coding: utf-8 # coding: utf-8
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import uuid
from collections import defaultdict from collections import defaultdict
from django.utils import timezone
from django.db.models import Q from django.db.models import Q
from django.core.cache import cache
from django.conf import settings
from common.utils import get_logger from common.utils import get_logger
from common.tree import TreeNode from common.tree import TreeNode
...@@ -97,10 +101,20 @@ class AssetPermissionUtil: ...@@ -97,10 +101,20 @@ class AssetPermissionUtil:
"SystemUser": get_node_permissions, "SystemUser": get_node_permissions,
} }
def __init__(self, obj): CACHE_KEY = '_ASSET_PERM_CACHE_{}_{}'
CACHE_META_KEY = '_ASSET_PERM_META_KEY_{}'
CACHE_TIME = settings.ASSETS_PERM_CACHE_TIME
CACHE_POLICY_MAP = (('0', 'never'), ('1', 'using'), ('2', 'refresh'))
def __init__(self, obj, cache_policy='0'):
self.object = obj self.object = obj
self.obj_id = str(obj.id)
self._permissions = None self._permissions = None
self._assets = None self._assets = None
self.cache_policy = cache_policy
self.node_key = self.CACHE_KEY.format(self.obj_id, 'NODES_WITH_ASSETS')
self.asset_key = self.CACHE_KEY.format(self.obj_id, 'ASSETS')
self.system_key = self.CACHE_KEY.format(self.obj_id, 'SYSTEM_USER')
@property @property
def permissions(self): def permissions(self):
...@@ -141,7 +155,7 @@ class AssetPermissionUtil: ...@@ -141,7 +155,7 @@ class AssetPermissionUtil:
) )
return assets return assets
def get_assets(self): def get_assets_without_cache(self):
if self._assets: if self._assets:
return self._assets return self._assets
assets = self.get_assets_direct() assets = self.get_assets_direct()
...@@ -155,25 +169,120 @@ class AssetPermissionUtil: ...@@ -155,25 +169,120 @@ class AssetPermissionUtil:
self._assets = assets self._assets = assets
return self._assets return self._assets
def get_nodes_with_assets(self): def get_assets_from_cache(self):
cached = cache.get(self.asset_key)
if not cached:
self.update_cache()
cached = cache.get(self.asset_key)
return cached
def get_assets(self):
if self.CACHE_TIME <= 0 or self.cache_policy in self.CACHE_POLICY_MAP[1]:
return self.get_assets_from_cache()
elif self.cache_policy in self.CACHE_POLICY_MAP[2]:
self.expire_cache()
return self.get_assets_from_cache()
else:
self.expire_cache()
return self.get_assets_without_cache()
def get_nodes_with_assets_without_cache(self):
""" """
返回节点并且包含资产 返回节点并且包含资产
{"node": {"assets": set("system_user")}} {"node": {"assets": set("system_user")}}
:return: :return:
""" """
assets = self.get_assets() assets = self.get_assets_without_cache()
tree = GenerateTree() tree = GenerateTree()
for asset, system_users in assets.items(): for asset, system_users in assets.items():
tree.add_asset(asset, system_users) tree.add_asset(asset, system_users)
return tree.get_nodes() return tree.get_nodes()
def get_system_users(self): def get_nodes_with_assets_from_cache(self):
cached = cache.get(self.node_key)
if not cached:
self.update_cache()
cached = cache.get(self.node_key)
return cached
def get_nodes_with_assets(self):
if self.CACHE_TIME <= 0 or self.cache_policy in self.CACHE_POLICY_MAP[1]:
return self.get_nodes_with_assets_from_cache()
elif self.cache_policy in self.CACHE_POLICY_MAP[2]:
self.expire_cache()
return self.get_nodes_with_assets_from_cache()
else:
return self.get_nodes_with_assets_without_cache()
def get_system_user_without_cache(self):
system_users = set() system_users = set()
permissions = self.permissions.prefetch_related('system_users') permissions = self.permissions.prefetch_related('system_users')
for perm in permissions: for perm in permissions:
system_users.update(perm.system_users.all()) system_users.update(perm.system_users.all())
return system_users return system_users
def get_system_user_from_cache(self):
cached = cache.get(self.system_key)
if not cached:
self.update_cache()
cached = cache.get(self.system_key)
return cached
def get_system_users(self):
if self.CACHE_TIME <= 0 or self.cache_policy in self.CACHE_POLICY_MAP[1]:
return self.get_system_user_from_cache()
elif self.cache_policy in self.CACHE_POLICY_MAP[2]:
self.expire_cache()
return self.get_system_user_from_cache()
else:
return self.get_system_user_without_cache()
@property
def cache_meta(self):
key = self.CACHE_META_KEY.format(str(self.object.id))
return cache.get(key) or {}
def set_cache_meta(self):
key = self.CACHE_META_KEY.format(str(self.object.id))
meta = {
'id': str(uuid.uuid4()),
'datetime': timezone.now(),
'object': str(self.object)
}
cache.set(key, meta, self.CACHE_TIME)
def expire_cache_meta(self):
key = self.CACHE_META_KEY.format(str(self.object.id))
cache.delete(key)
def update_cache(self):
assets = self.get_assets_without_cache()
nodes = self.get_nodes_with_assets_without_cache()
system_users = self.get_system_user_without_cache()
cache.set(self.asset_key, assets, self.CACHE_TIME)
cache.set(self.node_key, nodes, self.CACHE_TIME)
cache.set(self.system_key, system_users, self.CACHE_TIME)
self.set_cache_meta()
def expire_cache(self):
"""
因为 获取用户的节点,资产,系统用户等都能会缓存,这里会清理所有与该对象有关的
缓存,以免造成不统一的情况
:return:
"""
key = self.CACHE_KEY.format(str(self.object.id), '*')
cache.delete_pattern(key)
self.expire_cache_meta()
def expire_all_cache_meta(self):
key = self.CACHE_META_KEY.format('*')
cache.delete_pattern(key)
@classmethod
def expire_all_cache(cls):
key = cls.CACHE_KEY.format('*', '*')
cache.delete_pattern(key)
def is_obj_attr_has(obj, val, attrs=("hostname", "ip", "comment")): def is_obj_attr_has(obj, val, attrs=("hostname", "ip", "comment")):
if not attrs: if not attrs:
......
from django.contrib import admin
# Register your models here.
# -*- coding: utf-8 -*-
#
import os
import json
import jms_storage
from ldap3 import Server, Connection
from rest_framework.views import Response, APIView
from django.conf import settings
from django.core.mail import send_mail
from django.utils.translation import ugettext_lazy as _
from .models import Setting
from .utils import get_ldap_users_list, save_user
from common.permissions import IsOrgAdmin, IsSuperUser
from .serializers import MailTestSerializer, LDAPTestSerializer
class MailTestingAPI(APIView):
permission_classes = (IsOrgAdmin,)
serializer_class = MailTestSerializer
success_message = _("Test mail sent to {}, please check")
def post(self, request):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
email_host_user = serializer.validated_data["EMAIL_HOST_USER"]
for k, v in serializer.validated_data.items():
if k.startswith('EMAIL'):
setattr(settings, k, v)
try:
subject = "Test"
message = "Test smtp setting"
send_mail(subject, message, email_host_user, [email_host_user])
except Exception as e:
return Response({"error": str(e)}, status=401)
return Response({"msg": self.success_message.format(email_host_user)})
else:
return Response({"error": str(serializer.errors)}, status=401)
class LDAPTestingAPI(APIView):
permission_classes = (IsOrgAdmin,)
serializer_class = LDAPTestSerializer
success_message = _("Test ldap success")
def post(self, request):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
host = serializer.validated_data["AUTH_LDAP_SERVER_URI"]
bind_dn = serializer.validated_data["AUTH_LDAP_BIND_DN"]
password = serializer.validated_data["AUTH_LDAP_BIND_PASSWORD"]
use_ssl = serializer.validated_data.get("AUTH_LDAP_START_TLS", False)
search_ougroup = serializer.validated_data["AUTH_LDAP_SEARCH_OU"]
search_filter = serializer.validated_data["AUTH_LDAP_SEARCH_FILTER"]
attr_map = serializer.validated_data["AUTH_LDAP_USER_ATTR_MAP"]
try:
attr_map = json.loads(attr_map)
except json.JSONDecodeError:
return Response({"error": "AUTH_LDAP_USER_ATTR_MAP not valid"}, status=401)
server = Server(host, use_ssl=use_ssl)
conn = Connection(server, bind_dn, password)
try:
conn.bind()
except Exception as e:
return Response({"error": str(e)}, status=401)
users = []
for search_ou in str(search_ougroup).split("|"):
ok = conn.search(search_ou, search_filter % ({"user": "*"}),
attributes=list(attr_map.values()))
if not ok:
return Response({"error": _("Search no entry matched in ou {}").format(search_ou)}, status=401)
for entry in conn.entries:
user = {}
for attr, mapping in attr_map.items():
if hasattr(entry, mapping):
user[attr] = getattr(entry, mapping)
users.append(user)
if len(users) > 0:
return Response({"msg": _("Match {} s users").format(len(users))})
else:
return Response({"error": "Have user but attr mapping error"}, status=401)
else:
return Response({"error": str(serializer.errors)}, status=401)
class LDAPSyncAPI(APIView):
permission_classes = (IsOrgAdmin,)
def get(self, request):
ldap_users_list = get_ldap_users_list()
if not isinstance(ldap_users_list, list):
return Response(ldap_users_list, status=401)
return Response(ldap_users_list)
class LDAPConfirmSyncAPI(APIView):
permission_classes = (IsOrgAdmin,)
def post(self, request):
user_names = request.data.get('user_names', '')
if not user_names:
error = _('User is not currently selected, please check the user '
'you want to import')
return Response({'error': error}, status=401)
ldap_users_list = get_ldap_users_list(user_names=user_names)
if not isinstance(ldap_users_list, list):
return Response(ldap_users_list, status=401)
save_result = save_user(ldap_users_list)
if 'error' in save_result.keys():
return Response(save_result, status=401)
return Response(save_result)
class ReplayStorageCreateAPI(APIView):
permission_classes = (IsSuperUser,)
def post(self, request):
storage_data = request.data
if storage_data.get('TYPE') == 'ceph':
port = storage_data.get('PORT')
if port.isdigit():
storage_data['PORT'] = int(storage_data.get('PORT'))
storage_name = storage_data.pop('NAME')
data = {storage_name: storage_data}
if not self.is_valid(storage_data):
return Response({
"error": _("Error: Account invalid (Please make sure the "
"information such as Access key or Secret key is correct)")},
status=401
)
Setting.save_storage('TERMINAL_REPLAY_STORAGE', data)
return Response({"msg": _('Create succeed')}, status=200)
@staticmethod
def is_valid(storage_data):
if storage_data.get('TYPE') == 'server':
return True
storage = jms_storage.get_object_storage(storage_data)
target = 'tests.py'
src = os.path.join(settings.BASE_DIR, 'common', target)
return storage.is_valid(src, target)
class ReplayStorageDeleteAPI(APIView):
permission_classes = (IsSuperUser,)
def post(self, request):
storage_name = str(request.data.get('name'))
Setting.delete_storage('TERMINAL_REPLAY_STORAGE', storage_name)
return Response({"msg": _('Delete succeed')}, status=200)
class CommandStorageCreateAPI(APIView):
permission_classes = (IsSuperUser,)
def post(self, request):
storage_data = request.data
storage_name = storage_data.pop('NAME')
data = {storage_name: storage_data}
if not self.is_valid(storage_data):
return Response(
{"error": _("Error: Account invalid (Please make sure the "
"information such as Access key or Secret key is correct)")},
status=401
)
Setting.save_storage('TERMINAL_COMMAND_STORAGE', data)
return Response({"msg": _('Create succeed')}, status=200)
@staticmethod
def is_valid(storage_data):
if storage_data.get('TYPE') == 'server':
return True
try:
storage = jms_storage.get_log_storage(storage_data)
except Exception:
return False
return storage.ping()
class CommandStorageDeleteAPI(APIView):
permission_classes = (IsSuperUser,)
def post(self, request):
storage_name = str(request.data.get('name'))
Setting.delete_storage('TERMINAL_COMMAND_STORAGE', storage_name)
return Response({"msg": _('Delete succeed')}, status=200)
class DjangoSettingsAPI(APIView):
def get(self, request):
if not settings.DEBUG:
return Response("Not in debug mode")
data = {}
for i in [settings, getattr(settings, '_wrapped')]:
if not i:
continue
for k, v in i.__dict__.items():
if k and k.isupper():
try:
json.dumps(v)
data[k] = v
except (json.JSONDecodeError, TypeError):
data[k] = str(v)
return Response(data)
from django.apps import AppConfig
class SettingsConfig(AppConfig):
name = 'settings'
def ready(self):
from . import signals_handler
...@@ -6,8 +6,9 @@ from django.utils.translation import ugettext_lazy as _ ...@@ -6,8 +6,9 @@ from django.utils.translation import ugettext_lazy as _
from django.db import transaction from django.db import transaction
from .models import Setting, settings from .models import Setting, settings
from .fields import FormDictField, FormEncryptCharField, \ from common.fields import (
FormEncryptMixin FormDictField, FormEncryptCharField, FormEncryptMixin
)
class BaseForm(forms.Form): class BaseForm(forms.Form):
...@@ -104,7 +105,8 @@ class LDAPSettingForm(BaseForm): ...@@ -104,7 +105,8 @@ class LDAPSettingForm(BaseForm):
) )
AUTH_LDAP_SEARCH_OU = forms.CharField( AUTH_LDAP_SEARCH_OU = forms.CharField(
label=_("User OU"), label=_("User OU"),
help_text=_("Use | split User OUs") help_text=_("Use | split User OUs"),
required=False,
) )
AUTH_LDAP_SEARCH_FILTER = forms.CharField( AUTH_LDAP_SEARCH_FILTER = forms.CharField(
label=_("User search filter"), label=_("User search filter"),
......
# Generated by Django 2.1.7 on 2019-02-26 03:11
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('common', '0006_auto_20190304_1515'),
]
state_operations = [
migrations.CreateModel(
name='Setting',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True,
serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128, unique=True,
verbose_name='Name')),
('value', models.TextField(verbose_name='Value')),
('category',
models.CharField(default='default', max_length=128)),
('encrypted', models.BooleanField(default=False)),
('enabled',
models.BooleanField(default=True, verbose_name='Enabled')),
('comment', models.TextField(verbose_name='Comment')),
],
options={
'verbose_name': 'Setting',
'db_table': 'settings_setting',
},
),
]
operations = [
migrations.SeparateDatabaseAndState(state_operations=state_operations)
]
...@@ -6,7 +6,7 @@ from django.db.utils import ProgrammingError, OperationalError ...@@ -6,7 +6,7 @@ from django.db.utils import ProgrammingError, OperationalError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.conf import settings from django.conf import settings
from .utils import get_signer from common.utils import get_signer
signer = get_signer() signer = get_signer()
...@@ -122,5 +122,5 @@ class Setting(models.Model): ...@@ -122,5 +122,5 @@ class Setting(models.Model):
settings.AUTHENTICATION_BACKENDS = old_setting settings.AUTHENTICATION_BACKENDS = old_setting
class Meta: class Meta:
db_table = "settings" db_table = "settings_setting"
verbose_name = _("Setting") verbose_name = _("Setting")
...@@ -20,7 +20,4 @@ class LDAPTestSerializer(serializers.Serializer): ...@@ -20,7 +20,4 @@ class LDAPTestSerializer(serializers.Serializer):
AUTH_LDAP_START_TLS = serializers.BooleanField(required=False) AUTH_LDAP_START_TLS = serializers.BooleanField(required=False)
class OutputSerializer(serializers.Serializer):
output = serializers.CharField()
is_end = serializers.BooleanField()
mark = serializers.CharField()
...@@ -4,14 +4,14 @@ import json ...@@ -4,14 +4,14 @@ import json
from django.dispatch import receiver from django.dispatch import receiver
from django.db.models.signals import post_save, pre_save from django.db.models.signals import post_save, pre_save
from django.conf import LazySettings, empty from django.conf import LazySettings, empty, global_settings
from django.db.utils import ProgrammingError, OperationalError from django.db.utils import ProgrammingError, OperationalError
from django.core.cache import cache from django.core.cache import cache
from jumpserver.utils import current_request from jumpserver.utils import current_request
from common.utils import get_logger, ssh_key_gen
from common.signals import django_ready
from .models import Setting from .models import Setting
from .utils import get_logger, ssh_key_gen
from .signals import django_ready
logger = get_logger(__file__) logger = get_logger(__file__)
...@@ -25,11 +25,18 @@ def refresh_settings_on_changed(sender, instance=None, **kwargs): ...@@ -25,11 +25,18 @@ def refresh_settings_on_changed(sender, instance=None, **kwargs):
@receiver(django_ready, dispatch_uid="my_unique_identifier") @receiver(django_ready, dispatch_uid="my_unique_identifier")
def monkey_patch_settings(sender, **kwargs): def monkey_patch_settings(sender, **kwargs):
cache_key_prefix = '_SETTING_' cache_key_prefix = '_SETTING_'
uncached_settings = [ custom_need_cache_settings = [
'CACHES', 'DEBUG', 'SECRET_KEY', 'INSTALLED_APPS', 'AUTHENTICATION_BACKENDS'
'ROOT_URLCONF', 'TEMPLATES', 'DATABASES', '_wrapped',
'CELERY_LOG_DIR'
] ]
custom_no_cache_settings = [
'BASE_DIR', 'VERSION', 'AUTH_OPENID'
]
django_settings = dir(global_settings)
uncached_settings = [i for i in django_settings if i.isupper()]
uncached_settings = [i for i in uncached_settings if not i.startswith('EMAIL')]
uncached_settings = [i for i in uncached_settings if not i.startswith('SESSION_REDIS')]
uncached_settings = [i for i in uncached_settings if i not in custom_need_cache_settings]
uncached_settings.extend(custom_no_cache_settings)
def monkey_patch_getattr(self, name): def monkey_patch_getattr(self, name):
if name not in uncached_settings: if name not in uncached_settings:
...@@ -58,6 +65,7 @@ def monkey_patch_settings(sender, **kwargs): ...@@ -58,6 +65,7 @@ def monkey_patch_settings(sender, **kwargs):
cache.delete(key) cache.delete(key)
try: try:
cache.delete_pattern(cache_key_prefix+'*')
LazySettings.__getattr__ = monkey_patch_getattr LazySettings.__getattr__ = monkey_patch_getattr
LazySettings.__setattr__ = monkey_patch_setattr LazySettings.__setattr__ = monkey_patch_setattr
LazySettings.__delattr__ = monkey_patch_delattr LazySettings.__delattr__ = monkey_patch_delattr
......
{% extends '_modal.html' %}
{% load i18n %}
{% load static %}
{% block modal_class %}modal-lg{% endblock %}
{% block modal_id %}ldap_list_users_modal{% endblock %}
{% block modal_title%}{% trans "Ldap users" %}{% endblock %}
{% block modal_body %}
<link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet">
<script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>
<script src="{% static 'js/jquery.form.min.js' %}"></script>
<style>
.inmodal .modal-header {
padding: 10px 10px;
text-align: center;
}
#assetTree2.ztree * {
background-color: #f8fafb;
}
#assetTree2.ztree {
background-color: #f8fafb;
}
</style>
<div class="wrapper wrapper-content">
<div class="row">
<div class="col-lg-12 animated fadeInRight" id="split-right">
<div class="mail-box-header">
<table class="table table-striped table-bordered table-hover " id="ldap_list_users_table" style="width: 100%">
<thead>
<tr>
<th class="text-center"><input type="checkbox" class="ipt_check_all"></th>
<th class="text-center">{% trans 'Username' %}</th>
<th class="text-center">{% trans 'Name' %}</th>
<th class="text-center">{% trans 'Email' %}</th>
<th class="text-center">{% trans 'Is imported' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
var ldap_users_table = 0;
function initLdapTable() {
if(ldap_users_table){
return
}
var options = {
ele: $('#ldap_list_users_table'),
ajax_url: '{% url "api-settings:ldap-sync" %}',
columns: [
{data: "username" },{data: "username" }, {data: "name" },
{data:"email"}, {data:'is_imported'}
],
pageLength: 10
};
ldap_users_table = jumpserver.initDataTable(options);
return ldap_users_table
}
$(document).ready(function(){
}).on('show.bs.modal', function () {
initLdapTable();
})
.on('click','.close_btn1',function () {
window.location.reload()
})
.on('click','.close_btn2',function () {
window.location.reload()
})
</script>
{% endblock %}
{% block modal_button %}
{{ block.super }}
{% endblock %}
{% block modal_confirm_id %}btn_ldap_modal_confirm{% endblock %}
...@@ -159,10 +159,10 @@ $(document).ready(function() { ...@@ -159,10 +159,10 @@ $(document).ready(function() {
data[name] = value data[name] = value
} }
}); });
var url = "{% url 'api-common:command-storage-create' %}"; var url = "{% url 'api-settings:command-storage-create' %}";
var success = function(data, textStatus) { var success = function(data, textStatus) {
console.log(data, textStatus); console.log(data, textStatus);
location = "{% url 'common:terminal-setting' %}"; location = "{% url 'settings:terminal-setting' %}";
}; };
var error = function(data, textStatus) { var error = function(data, textStatus) {
var error_msg = data.responseJSON.error; var error_msg = data.responseJSON.error;
......
...@@ -84,7 +84,7 @@ $(document).ready(function () { ...@@ -84,7 +84,7 @@ $(document).ready(function () {
data[field.name] = field.value; data[field.name] = field.value;
}); });
var the_url = "{% url 'api-common:mail-testing' %}"; var the_url = "{% url 'api-settings:mail-testing' %}";
function error(message) { function error(message) {
toastr.error(message) toastr.error(message)
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
<div class="tab-content"> <div class="tab-content">
<div class="col-sm-12" style="padding-left:0"> <div class="col-sm-12" style="padding-left:0">
<div class="ibox-content" style="border-width: 0;padding-top: 40px;"> <div class="ibox-content" style="border-width: 0;padding-top: 40px;">
<form action="" method="post" class="form-horizontal"> <form id="ldap_form" action="" method="post" class="form-horizontal">
{% if form.non_field_errors %} {% if form.non_field_errors %}
<div class="alert alert-danger"> <div class="alert alert-danger">
{{ form.non_field_errors }} {{ form.non_field_errors }}
...@@ -61,6 +61,8 @@ ...@@ -61,6 +61,8 @@
<button class="btn btn-default btn-test" type="button"> {% trans 'Test connection' %}</button> <button class="btn btn-default btn-test" type="button"> {% trans 'Test connection' %}</button>
<button class="btn btn-default" type="reset"> {% trans 'Reset' %}</button> <button class="btn btn-default" type="reset"> {% trans 'Reset' %}</button>
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button> <button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
{# <button class="btn btn-primary sync_button " data-toggle="modal" data-target="#sync_users_modal" type="button">{% trans 'Synchronization' %}</button>#}
<button class="btn btn-primary sync_button " data-toggle="modal" data-target="#ldap_list_users_modal" type="button">{% trans 'Sync User' %}</button>
</div> </div>
</div> </div>
</form> </form>
...@@ -72,10 +74,12 @@ ...@@ -72,10 +74,12 @@
</div> </div>
</div> </div>
</div> </div>
{% include 'settings/_ldap_list_users_modal.html' %}
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
$(document).ready(function () { $(document).ready(function () {
}) })
.on("click", ".btn-test", function () { .on("click", ".btn-test", function () {
var data = {}; var data = {};
...@@ -84,7 +88,7 @@ $(document).ready(function () { ...@@ -84,7 +88,7 @@ $(document).ready(function () {
data[field.name] = field.value; data[field.name] = field.value;
}); });
var the_url = "{% url 'api-common:ldap-testing' %}"; var the_url = "{% url 'api-settings:ldap-testing' %}";
function error(message) { function error(message) {
toastr.error(message) toastr.error(message)
...@@ -102,5 +106,30 @@ $(document).ready(function () { ...@@ -102,5 +106,30 @@ $(document).ready(function () {
error: error error: error
}); });
}) })
.on("click","#btn_ldap_modal_confirm",function () {
var user_names=[];
var cheked = $("tbody input[type='checkbox']:checked").each(function () {
user_names.push($(this).attr('id'));
});
var the_url = "{% url "api-settings:ldap-comfirm-sync" %}";
function error(message) {
toastr.error(message)
}
function success(message) {
toastr.success(message.msg)
}
APIUpdateAttr({
url: the_url,
body: JSON.stringify({'user_names':user_names}),
method: "POST",
flash_message: false,
success: success,
error: error
});
})
</script> </script>
{% endblock %} {% endblock %}
...@@ -251,9 +251,9 @@ $(document).ready(function() { ...@@ -251,9 +251,9 @@ $(document).ready(function() {
var name = $(id_field).attr('name'); var name = $(id_field).attr('name');
data[name] = $(id_field).val(); data[name] = $(id_field).val();
}); });
var url = "{% url 'api-common:replay-storage-create' %}"; var url = "{% url 'api-settings:replay-storage-create' %}";
var success = function(data, textStatus) { var success = function(data, textStatus) {
location = "{% url 'common:terminal-setting' %}"; location = "{% url 'settings:terminal-setting' %}";
submitBtn.removeClass('disabled'); submitBtn.removeClass('disabled');
submitBtn.html(origin_text); submitBtn.html(origin_text);
}; };
......
...@@ -92,7 +92,7 @@ ...@@ -92,7 +92,7 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<a href="{% url 'common:command-storage-create' %}" class="btn btn-primary btn-xs">{% trans 'Add' %}</a> <a href="{% url 'settings:command-storage-create' %}" class="btn btn-primary btn-xs">{% trans 'Add' %}</a>
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
<h3>{% trans "Replay storage" %}</h3> <h3>{% trans "Replay storage" %}</h3>
...@@ -114,7 +114,7 @@ ...@@ -114,7 +114,7 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<a href="{% url 'common:replay-storage-create' %}" class="btn btn-primary btn-xs">{% trans 'Add' %}</a> <a href="{% url 'settings:replay-storage-create' %}" class="btn btn-primary btn-xs">{% trans 'Add' %}</a>
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
</form> </form>
...@@ -174,12 +174,12 @@ $(document).ready(function () { ...@@ -174,12 +174,12 @@ $(document).ready(function () {
}) })
.on('click', '.btn-del-replay', function(){ .on('click', '.btn-del-replay', function(){
var $this = $(this); var $this = $(this);
var the_url = "{% url 'api-common:replay-storage-delete' %}"; var the_url = "{% url 'api-settings:replay-storage-delete' %}";
deleteStorage($this, the_url); deleteStorage($this, the_url);
}) })
.on('click', '.btn-del-command', function() { .on('click', '.btn-del-command', function() {
var $this = $(this); var $this = $(this);
var the_url = "{% url 'api-common:command-storage-delete' %}"; var the_url = "{% url 'api-settings:command-storage-delete' %}";
deleteStorage($this, the_url) deleteStorage($this, the_url)
}); });
......
from django.test import TestCase
# Create your tests here.
...@@ -9,6 +9,8 @@ app_name = 'common' ...@@ -9,6 +9,8 @@ app_name = 'common'
urlpatterns = [ urlpatterns = [
path('mail/testing/', api.MailTestingAPI.as_view(), name='mail-testing'), path('mail/testing/', api.MailTestingAPI.as_view(), name='mail-testing'),
path('ldap/testing/', api.LDAPTestingAPI.as_view(), name='ldap-testing'), path('ldap/testing/', api.LDAPTestingAPI.as_view(), name='ldap-testing'),
path('ldap/sync/', api.LDAPSyncAPI.as_view(), name='ldap-sync'),
path('ldap/comfirm/sync/', api.LDAPConfirmSyncAPI.as_view(), name='ldap-comfirm-sync'),
path('terminal/replay-storage/create/', api.ReplayStorageCreateAPI.as_view(), name='replay-storage-create'), path('terminal/replay-storage/create/', api.ReplayStorageCreateAPI.as_view(), name='replay-storage-create'),
path('terminal/replay-storage/delete/', api.ReplayStorageDeleteAPI.as_view(), name='replay-storage-delete'), path('terminal/replay-storage/delete/', api.ReplayStorageDeleteAPI.as_view(), name='replay-storage-delete'),
path('terminal/command-storage/create/', api.CommandStorageCreateAPI.as_view(), name='command-storage-create'), path('terminal/command-storage/create/', api.CommandStorageCreateAPI.as_view(), name='command-storage-create'),
......
# -*- coding: utf-8 -*-
#
from ldap3 import Server, Connection
from django.utils.translation import ugettext_lazy as _
from .models import settings
from users.models import User
def ldap_conn(host, use_ssl, bind_dn, password):
server = Server(host, use_ssl=use_ssl)
conn = Connection(server, bind_dn, password)
return conn
def ldap_search(conn, search_ougroup, search_filter, attr_map, user_names=None):
users_list = []
for search_ou in str(search_ougroup).split("|"):
ok = conn.search(search_ou, search_filter % ({"user": "*"}),
attributes=list(attr_map.values()))
if not ok:
error = _("Search no entry matched in ou {}").format(search_ou)
return {"error": error}
ldap_map_users(conn, attr_map, users_list, user_names)
if len(users_list) > 0:
return users_list
return {"error": _("Have user but attr mapping error")}
def get_ldap_users_list(user_names=None):
ldap_setting = get_ldap_setting()
conn = ldap_conn(ldap_setting['host'], ldap_setting['use_ssl'],
ldap_setting['bind_dn'], ldap_setting['password'])
try:
conn.bind()
except Exception as e:
return {"error": str(e)}
result_search = ldap_search(conn, ldap_setting['search_ougroup'],
ldap_setting['search_filter'],
ldap_setting['attr_map'], user_names=user_names)
return result_search
def ldap_map_users(conn, attr_map, users, user_names=None):
for entry in conn.entries:
user = entry_user(entry, attr_map)
if user_names:
if user.get('username', '') in user_names:
users.append(user)
else:
users.append(user)
def entry_user(entry, attr_map):
user = {}
user['is_imported'] = _('No')
for attr, mapping in attr_map.items():
if not hasattr(entry, mapping):
continue
value = getattr(entry, mapping).value
user[attr] = value if value else ''
if attr != 'username':
continue
if User.objects.filter(username=user[attr]):
user['is_imported'] = _('Yes')
return user
def get_ldap_setting():
host = settings.AUTH_LDAP_SERVER_URI
bind_dn = settings.AUTH_LDAP_BIND_DN
password = settings.AUTH_LDAP_BIND_PASSWORD
use_ssl = settings.AUTH_LDAP_START_TLS
search_ougroup = settings.AUTH_LDAP_SEARCH_OU
search_filter = settings.AUTH_LDAP_SEARCH_FILTER
attr_map = settings.AUTH_LDAP_USER_ATTR_MAP
auth_ldap = settings.AUTH_LDAP
ldap_setting = {
'host': host, 'bind_dn': bind_dn, 'password': password,
'search_ougroup': search_ougroup, 'search_filter': search_filter,
'attr_map': attr_map, 'auth_ldap': auth_ldap, 'use_ssl': use_ssl,
}
return ldap_setting
def save_user(users):
exist = []
username_list = [item.get('username') for item in users]
for name in username_list:
if User.objects.filter(username=name).exclude(source='ldap'):
exist.append(name)
users = [user for user in users if (user.get('username') not in exist)]
result_save = save(users, exist)
return result_save
def save(users, exist):
fail_user = []
for item in users:
item = set_default_item(item)
user = User.objects.filter(username=item['username'], source='ldap')
user = user.first()
if not user:
try:
user = User.objects.create(**item)
except Exception as e:
fail_user.append(item.get('username'))
continue
for key, value in item.items():
user.key = value
user.save()
get_msg = get_messages(users, exist, fail_user)
return get_msg
def set_default_item(item):
item['source'] = 'ldap'
if not item.get('email', ''):
if '@' in item['username']:
item['email'] = item['username']
else:
item['email'] = item['username'] + '@' + settings.EMAIL_SUFFIX
if 'is_imported' in item.keys():
item.pop('is_imported')
return item
def get_messages(users, exist, fail_user):
if exist:
info = _("Import {} users successfully; import {} users failed, the "
"database already exists with the same name")
msg = info.format(len(users), str(exist))
if fail_user:
info = _("Import {} users successfully; import {} users failed, "
"the database already exists with the same name; import {}"
"users failed, Because’TypeError' object has no attribute "
"'keys'")
msg = info.format(len(users)-len(fail_user), str(exist), str(fail_user))
else:
msg = _("Import {} users successfully").format(len(users))
if fail_user:
info = _("Import {} users successfully;import {} users failed, "
"Because’TypeError' object has no attribute 'keys'")
msg = info.format(len(users)-len(fail_user), str(fail_user))
return {'msg': msg}
\ No newline at end of file
...@@ -3,15 +3,15 @@ from django.shortcuts import render, redirect ...@@ -3,15 +3,15 @@ from django.shortcuts import render, redirect
from django.contrib import messages from django.contrib import messages
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from common.permissions import SuperUserRequiredMixin
from common import utils
from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \ from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \
TerminalSettingForm, SecuritySettingForm TerminalSettingForm, SecuritySettingForm
from common.permissions import SuperUserRequiredMixin
from . import utils
class BasicSettingView(SuperUserRequiredMixin, TemplateView): class BasicSettingView(SuperUserRequiredMixin, TemplateView):
form_class = BasicSettingForm form_class = BasicSettingForm
template_name = "common/basic_setting.html" template_name = "settings/basic_setting.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = {
...@@ -37,7 +37,7 @@ class BasicSettingView(SuperUserRequiredMixin, TemplateView): ...@@ -37,7 +37,7 @@ class BasicSettingView(SuperUserRequiredMixin, TemplateView):
class EmailSettingView(SuperUserRequiredMixin, TemplateView): class EmailSettingView(SuperUserRequiredMixin, TemplateView):
form_class = EmailSettingForm form_class = EmailSettingForm
template_name = "common/email_setting.html" template_name = "settings/email_setting.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = {
...@@ -63,7 +63,7 @@ class EmailSettingView(SuperUserRequiredMixin, TemplateView): ...@@ -63,7 +63,7 @@ class EmailSettingView(SuperUserRequiredMixin, TemplateView):
class LDAPSettingView(SuperUserRequiredMixin, TemplateView): class LDAPSettingView(SuperUserRequiredMixin, TemplateView):
form_class = LDAPSettingForm form_class = LDAPSettingForm
template_name = "common/ldap_setting.html" template_name = "settings/ldap_setting.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = {
...@@ -89,7 +89,7 @@ class LDAPSettingView(SuperUserRequiredMixin, TemplateView): ...@@ -89,7 +89,7 @@ class LDAPSettingView(SuperUserRequiredMixin, TemplateView):
class TerminalSettingView(SuperUserRequiredMixin, TemplateView): class TerminalSettingView(SuperUserRequiredMixin, TemplateView):
form_class = TerminalSettingForm form_class = TerminalSettingForm
template_name = "common/terminal_setting.html" template_name = "settings/terminal_setting.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
command_storage = utils.get_command_storage_setting() command_storage = utils.get_command_storage_setting()
...@@ -119,7 +119,7 @@ class TerminalSettingView(SuperUserRequiredMixin, TemplateView): ...@@ -119,7 +119,7 @@ class TerminalSettingView(SuperUserRequiredMixin, TemplateView):
class ReplayStorageCreateView(SuperUserRequiredMixin, TemplateView): class ReplayStorageCreateView(SuperUserRequiredMixin, TemplateView):
template_name = 'common/replay_storage_create.html' template_name = 'settings/replay_storage_create.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = {
...@@ -131,7 +131,7 @@ class ReplayStorageCreateView(SuperUserRequiredMixin, TemplateView): ...@@ -131,7 +131,7 @@ class ReplayStorageCreateView(SuperUserRequiredMixin, TemplateView):
class CommandStorageCreateView(SuperUserRequiredMixin, TemplateView): class CommandStorageCreateView(SuperUserRequiredMixin, TemplateView):
template_name = 'common/command_storage_create.html' template_name = 'settings/command_storage_create.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = {
...@@ -144,7 +144,7 @@ class CommandStorageCreateView(SuperUserRequiredMixin, TemplateView): ...@@ -144,7 +144,7 @@ class CommandStorageCreateView(SuperUserRequiredMixin, TemplateView):
class SecuritySettingView(SuperUserRequiredMixin, TemplateView): class SecuritySettingView(SuperUserRequiredMixin, TemplateView):
form_class = SecuritySettingForm form_class = SecuritySettingForm
template_name = "common/security_setting.html" template_name = "settings/security_setting.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = {
......
...@@ -450,3 +450,7 @@ div.dataTables_wrapper div.dataTables_filter { ...@@ -450,3 +450,7 @@ div.dataTables_wrapper div.dataTables_filter {
content:"*"; content:"*";
color:red; color:red;
} }
#tree-refresh .fa-refresh {
font: normal normal normal 14px/1 FontAwesome !important;
}
\ No newline at end of file
...@@ -555,6 +555,17 @@ jumpserver.initServerSideDataTable = function (options) { ...@@ -555,6 +555,17 @@ jumpserver.initServerSideDataTable = function (options) {
processing: true, processing: true,
ajax: { ajax: {
url: options.ajax_url , url: options.ajax_url ,
error: function(jqXHR, textStatus, errorThrown) {
var msg = gettext("Unknown error occur");
if (jqXHR.responseJSON) {
if (jqXHR.responseJSON.error) {
msg = jqXHR.responseJSON.error
} else if (jqXHR.responseJSON.msg) {
msg = jqXHR.responseJSON.msg
}
}
alert(msg)
},
data: function (data) { data: function (data) {
delete data.columns; delete data.columns;
if (data.length !== null){ if (data.length !== null){
...@@ -922,3 +933,16 @@ function initSelectedAssets2Table(){ ...@@ -922,3 +933,16 @@ function initSelectedAssets2Table(){
}); });
} }
} }
function rootNodeAddDom(ztree, callback) {
var refreshIcon = "<a id='tree-refresh'><i class='fa fa-refresh'></i></a>";
var rootNode = ztree.getNodes()[0];
var $rootNodeRef = $("#" + rootNode.tId + "_a");
$rootNodeRef.after(refreshIcon);
var refreshIconRef = $('#tree-refresh');
refreshIconRef.bind('click', function () {
ztree.destroy();
callback()
})
}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
/**
* Created by liuzheng on 3/3/16.
*/
var rowHeight = 1;
var colWidth = 1;
function WSSHClient() {
}
WSSHClient.prototype._generateEndpoint = function (options) {
console.log(options);
if (window.location.protocol == 'https:') {
var protocol = 'wss://';
} else {
var protocol = 'ws://';
}
var endpoint = protocol + document.URL.match(RegExp('//(.*?)/'))[1] + '/ws/applications' + document.URL.match(/(\?.*)/);
return endpoint;
};
WSSHClient.prototype.connect = function (options) {
var endpoint = this._generateEndpoint(options);
if (window.WebSocket) {
this._connection = new WebSocket(endpoint);
}
else if (window.MozWebSocket) {
this._connection = MozWebSocket(endpoint);
}
else {
options.onError('WebSocket Not Supported');
return;
}
this._connection.onopen = function () {
options.onConnect();
};
this._connection.onmessage = function (evt) {
try {
options.onData(evt.data);
} catch (e) {
var data = JSON.parse(evt.data.toString());
options.onError(data.error);
}
};
this._connection.onclose = function (evt) {
options.onClose();
};
};
WSSHClient.prototype.send = function (data) {
this._connection.send(JSON.stringify({'data': data}));
};
function openTerminal(options) {
var client = new WSSHClient();
var rowHeight, colWidth;
try {
rowHeight = localStorage.getItem('term-row');
colWidth = localStorage.getItem('term-col');
} catch (err) {
rowHeight = 35;
colWidth = 100
}
if (rowHeight) {
} else {
rowHeight = 35
}
if (colWidth) {
} else {
colWidth = 100
}
var term = new Terminal({
rows: rowHeight,
cols: colWidth,
useStyle: true,
screenKeys: true
});
term.open();
term.on('data', function (data) {
client.send(data)
});
$('.applications').detach().appendTo('#term');
//term.resize(colWidth, rowHeight);
term.write('Connecting...');
client.connect($.extend(options, {
onError: function (error) {
term.write('Error: ' + error + '\r\n');
},
onConnect: function () {
// Erase our connecting message
client.send({'resize': {'rows': rowHeight, 'cols': colWidth}});
term.write('\r');
},
onClose: function () {
term.write('Connection Reset By Peer');
},
onData: function (data) {
term.write(data);
}
}));
//rowHeight = 0.0 + 1.00 * $('.applications').height() / 24;
//colWidth = 0.0 + 1.00 * $('.applications').width() / 80;
return {'term': term, 'client': client};
}
//function resize() {
// $('.applications').css('width', window.innerWidth - 25);
// console.log(window.innerWidth);
// console.log(window.innerWidth - 10);
// var rows = Math.floor(window.innerHeight / rowHeight) - 2;
// var cols = Math.floor(window.innerWidth / colWidth) - 1;
//
// return {rows: rows, cols: cols};
//}
$(document).ready(function () {
var options = {};
$('#ssh').show();
var term_client = openTerminal(options);
console.log(rowHeight);
// by liuzheng712 because it will bring record bug
//window.onresize = function () {
// var geom = resize();
// console.log(geom);
// term_client.term.resize(geom.cols, geom.rows);
// term_client.client.send({'resize': {'rows': geom.rows, 'cols': geom.cols}});
// $('#ssh').show();
//}
try {
$('#term-row')[0].value = localStorage.getItem('term-row');
$('#term-col')[0].value = localStorage.getItem('term-col');
} catch (err) {
$('#term-row')[0].value = 35;
$('#term-col')[0].value = 100;
}
$('#col-row').click(function () {
var col = $('#term-col').val();
var row = $('#term-row').val();
localStorage.setItem('term-col', col);
localStorage.setItem('term-row', row);
term_client.term.resize(col, row);
term_client.client.send({'resize': {'rows': row, 'cols': col}});
$('#ssh').show();
});
$(".applications").mouseleave(function () {
$(".termChangBar").slideDown();
});
$(".applications").mouseenter(function () {
$(".termChangBar").slideUp();
})
});
\ No newline at end of file
/*
WSSH Javascript Client
Usage:
var client = new WSSHClient();
client.connect({
// Connection and authentication parameters
username: 'root',
hostname: 'localhost',
authentication_method: 'password', // can either be password or private_key
password: 'secretpassword', // do not provide when using private_key
key_passphrase: 'secretpassphrase', // *may* be provided if the private_key is encrypted
// Callbacks
onError: function(error) {
// Called upon an error
console.error(error);
},
onConnect: function() {
// Called after a successful connection to the server
console.debug('Connected!');
client.send('ls\n'); // You can send data back to the server by using WSSHClient.send()
},
onClose: function() {
// Called when the remote closes the connection
console.debug('Connection Reset By Peer');
},
onData: function(data) {
// Called when data is received from the server
console.debug('Received: ' + data);
}
});
*/
function WSSHClient() {
}
WSSHClient.prototype._generateEndpoint = function(options) {
console.log(options);
if (window.location.protocol == 'https:') {
var protocol = 'wss://';
} else {
var protocol = 'ws://';
}
var endpoint = protocol + window.location.host + ':8080' + '/applications';
return endpoint;
};
WSSHClient.prototype.connect = function(options) {
var endpoint = this._generateEndpoint(options);
if (window.WebSocket) {
this._connection = new WebSocket(endpoint);
}
else if (window.MozWebSocket) {
this._connection = MozWebSocket(endpoint);
}
else {
options.onError('WebSocket Not Supported');
return ;
}
this._connection.onopen = function() {
options.onConnect();
};
this._connection.onmessage = function (evt) {
var data = JSON.parse(evt.data.toString());
if (data.error !== undefined) {
options.onError(data.error);
}
else {
options.onData(data.data);
}
};
this._connection.onclose = function(evt) {
options.onClose();
};
};
WSSHClient.prototype.send = function(data) {
this._connection.send(JSON.stringify({'data': data}));
};
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Not found</title>
</head>
<body>
<h1>Not found</h1>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Server error</title>
</head>
<body>
<h1>Server error occur, contact administrator</h1>
</body>
</html>
\ No newline at end of file
...@@ -94,10 +94,10 @@ ...@@ -94,10 +94,10 @@
<li><a id="switch_user"><i class="fa fa-exchange"></i><span> {% trans 'User page' %}</span></a></li> <li><a id="switch_user"><i class="fa fa-exchange"></i><span> {% trans 'User page' %}</span></a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
<li><a href="{% url 'users:logout' %}"><i class="fa fa-sign-out"></i> {% trans 'Logout' %}</a></li> <li><a href="{% url 'authentication:logout' %}"><i class="fa fa-sign-out"></i> {% trans 'Logout' %}</a></li>
</ul> </ul>
{% else %} {% else %}
<a href="{% url 'users:login' %}"> <a href="{% url 'authentication:login' %}">
<i class="fa fa-sign-in"></i>{% trans 'Login' %} <i class="fa fa-sign-in"></i>{% trans 'Login' %}
</a> </a>
{% endif %} {% endif %}
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<div class="modal-dialog {% block modal_class %}{% endblock %}"> <div class="modal-dialog {% block modal_class %}{% endblock %}">
<div class="modal-content animated fadeIn"> <div class="modal-content animated fadeIn">
<div class="modal-header"> <div class="modal-header">
<button data-dismiss="modal" class="close" type="button"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button> <button data-dismiss="modal" class="close close_btn1" type="button"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
<h4 class="modal-title">{% block modal_title %}{% endblock %}</h4> <h4 class="modal-title">{% block modal_title %}{% endblock %}</h4>
<small>{% block modal_comment %}{% endblock %}</small> <small>{% block modal_comment %}{% endblock %}</small>
</div> </div>
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
{% block modal_button %} {% block modal_button %}
<button data-dismiss="modal" class="btn btn-white" type="button">{% trans "Close" %}</button> <button data-dismiss="modal" class="btn btn-white close_btn2" type="button">{% trans "Close" %}</button>
<button class="btn btn-primary" type="button" id="{% block modal_confirm_id %}{% endblock %}">{% trans 'Confirm' %}</button> <button class="btn btn-primary" type="button" id="{% block modal_confirm_id %}{% endblock %}">{% trans 'Confirm' %}</button>
{% endblock %} {% endblock %}
</div> </div>
......
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
</a> </a>
<ul class="nav nav-second-level"> <ul class="nav nav-second-level">
<li id="task"><a href="{% url 'ops:task-list' %}">{% trans 'Task list' %}</a></li> <li id="task"><a href="{% url 'ops:task-list' %}">{% trans 'Task list' %}</a></li>
<li id="command-execution"><a href="{% url 'ops:command-execution-start' %}">{% trans 'Command execution' %}</a></li> <li id="command-execution"><a href="{% url 'ops:command-execution-start' %}">{% trans 'Batch command' %}</a></li>
</ul> </ul>
</li> </li>
<li id="audits"> <li id="audits">
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
<li id="ftp-log"><a href="{% url 'audits:ftp-log-list' %}">{% trans 'FTP log' %}</a></li> <li id="ftp-log"><a href="{% url 'audits:ftp-log-list' %}">{% trans 'FTP log' %}</a></li>
<li id="operate-log"><a href="{% url 'audits:operate-log-list' %}">{% trans 'Operate log' %}</a></li> <li id="operate-log"><a href="{% url 'audits:operate-log-list' %}">{% trans 'Operate log' %}</a></li>
<li id="password-change-log"><a href="{% url 'audits:password-change-log-list' %}">{% trans 'Password change log' %}</a></li> <li id="password-change-log"><a href="{% url 'audits:password-change-log-list' %}">{% trans 'Password change log' %}</a></li>
<li id="command-execution-log"><a href="{% url 'audits:command-execution-log-list' %}">{% trans 'Command execution' %}</a></li> <li id="command-execution-log"><a href="{% url 'audits:command-execution-log-list' %}">{% trans 'Batch command' %}</a></li>
</ul> </ul>
</li> </li>
{% if XPACK_PLUGINS %} {% if XPACK_PLUGINS %}
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .terminal import *
from .session import *
from .task import *
...@@ -9,17 +9,17 @@ from django.http import HttpResponseNotFound ...@@ -9,17 +9,17 @@ from django.http import HttpResponseNotFound
from django.conf import settings from django.conf import settings
from rest_framework.pagination import LimitOffsetPagination from rest_framework.pagination import LimitOffsetPagination
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.views import Response from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
import jms_storage import jms_storage
from common.utils import is_uuid from common.utils import is_uuid
from common.permissions import IsOrgAdminOrAppUser from common.permissions import IsOrgAdminOrAppUser
from ...hands import SystemUser from ..hands import SystemUser
from ...models import Terminal, Session from ..models import Terminal, Session
from ...serializers import v1 as serializers from .. import serializers
from ...backends import get_command_storage, get_multi_command_storage, \ from ..backends import get_command_storage, get_multi_command_storage, \
SessionCommandSerializer SessionCommandSerializer
__all__ = ['SessionViewSet', 'SessionReplayViewSet', 'CommandViewSet'] __all__ = ['SessionViewSet', 'SessionReplayViewSet', 'CommandViewSet']
......
...@@ -6,8 +6,8 @@ from rest_framework_bulk import BulkModelViewSet ...@@ -6,8 +6,8 @@ from rest_framework_bulk import BulkModelViewSet
from common.utils import get_object_or_none from common.utils import get_object_or_none
from common.permissions import IsOrgAdminOrAppUser from common.permissions import IsOrgAdminOrAppUser
from ...models import Session, Task from ..models import Session, Task
from ...serializers import v1 as serializers from .. import serializers
__all__ = ['TaskViewSet', 'KillSessionAPI'] __all__ = ['TaskViewSet', 'KillSessionAPI']
......
...@@ -14,8 +14,8 @@ from rest_framework.permissions import AllowAny ...@@ -14,8 +14,8 @@ from rest_framework.permissions import AllowAny
from common.utils import get_object_or_none from common.utils import get_object_or_none
from common.permissions import IsAppUser, IsOrgAdminOrAppUser, IsSuperUser from common.permissions import IsAppUser, IsOrgAdminOrAppUser, IsSuperUser
from ...models import Terminal, Status, Session from ..models import Terminal, Status, Session
from ...serializers import v1 as serializers from .. import serializers
__all__ = [ __all__ = [
'TerminalViewSet', 'TerminalTokenApi', 'StatusViewSet', 'TerminalConfig', 'TerminalViewSet', 'TerminalTokenApi', 'StatusViewSet', 'TerminalConfig',
......
...@@ -7,8 +7,8 @@ from rest_framework.response import Response ...@@ -7,8 +7,8 @@ from rest_framework.response import Response
from common.permissions import IsSuperUser, WithBootstrapToken from common.permissions import IsSuperUser, WithBootstrapToken
from ...models import Terminal from ..models import Terminal
from ...serializers import v2 as serializers from .. import serializers_v2 as serializers
__all__ = ['TerminalViewSet', 'TerminalRegistrationApi'] __all__ = ['TerminalViewSet', 'TerminalRegistrationApi']
......
...@@ -66,9 +66,13 @@ class CommandStore(CommandBase): ...@@ -66,9 +66,13 @@ class CommandStore(CommandBase):
date_from_default = timezone.now() - datetime.timedelta(days=7) date_from_default = timezone.now() - datetime.timedelta(days=7)
date_to_default = timezone.now() date_to_default = timezone.now()
date_from = date_from if date_from else date_from_default if not date_from and not session:
date_to = date_to if date_to else date_to_default date_from = date_from_default
if not date_to and not session:
date_to = date_to_default
if date_from is not None:
filter_kwargs['timestamp__gte'] = int(date_from.timestamp()) filter_kwargs['timestamp__gte'] = int(date_from.timestamp())
if date_to is not None:
filter_kwargs['timestamp__lte'] = int(date_to.timestamp()) filter_kwargs['timestamp__lte'] = int(date_to.timestamp())
if user: if user:
......
# Generated by Django 2.1.7 on 2019-02-28 10:23
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
replaces = [('terminal', '0002_auto_20171228_0025'), ('terminal', '0003_auto_20171230_0308'), ('terminal', '0004_session_remote_addr'), ('terminal', '0005_auto_20180122_1154'), ('terminal', '0006_auto_20180123_1037'), ('terminal', '0007_session_date_last_active'), ('terminal', '0008_auto_20180307_1603'), ('terminal', '0009_auto_20180326_0957')]
dependencies = [
('terminal', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='session',
name='terminal',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='terminal.Terminal'),
),
migrations.AddField(
model_name='status',
name='terminal',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='terminal.Terminal'),
),
migrations.AddField(
model_name='task',
name='terminal',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='terminal.Terminal'),
),
migrations.AddField(
model_name='terminal',
name='user',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='terminal', to=settings.AUTH_USER_MODEL, verbose_name='Application User'),
),
migrations.AlterField(
model_name='terminal',
name='name',
field=models.CharField(max_length=32, verbose_name='Name'),
),
migrations.AlterField(
model_name='command',
name='asset',
field=models.CharField(db_index=True, max_length=128, verbose_name='Asset'),
),
migrations.AlterField(
model_name='command',
name='system_user',
field=models.CharField(db_index=True, max_length=64, verbose_name='System user'),
),
migrations.AlterField(
model_name='command',
name='user',
field=models.CharField(db_index=True, max_length=64, verbose_name='User'),
),
migrations.AddField(
model_name='session',
name='remote_addr',
field=models.CharField(blank=True, max_length=15, null=True, verbose_name='Remote addr'),
),
migrations.AddField(
model_name='terminal',
name='command_storage',
field=models.CharField(default='default', max_length=128, verbose_name='Command storage'),
),
migrations.AddField(
model_name='terminal',
name='replay_storage',
field=models.CharField(default='default', max_length=128, verbose_name='Replay storage'),
),
migrations.AddField(
model_name='session',
name='date_last_active',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Date last active'),
),
migrations.AlterField(
model_name='session',
name='date_start',
field=models.DateTimeField(db_index=True, verbose_name='Date start'),
),
]
...@@ -188,6 +188,14 @@ class Session(OrgModelMixin): ...@@ -188,6 +188,14 @@ class Session(OrgModelMixin):
local_path = rel_path local_path = rel_path
return local_path return local_path
def can_replay(self):
if self.has_replay:
return True
version = settings.VERSION.split('.')
if [int(i) for i in version] > [1, 4, 8]:
return False
return True
def save_to_storage(self, f): def save_to_storage(self, f):
local_path = self.get_local_path() local_path = self.get_local_path()
try: try:
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .v1 import *
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .terminal import * from .terminal import *
from .session import *
from .task import *
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
from rest_framework import serializers from rest_framework import serializers
from common.utils import get_request_ip from common.utils import get_request_ip
from users.serializers.v2 import ServiceAccountSerializer from users.serializers_v2 import ServiceAccountSerializer
from ..models import Terminal from ..models import Terminal
...@@ -21,7 +21,7 @@ class TerminalSerializer(serializers.ModelSerializer): ...@@ -21,7 +21,7 @@ class TerminalSerializer(serializers.ModelSerializer):
'replay_storage', 'user', 'is_accepted', 'is_deleted', 'replay_storage', 'user', 'is_accepted', 'is_deleted',
'date_created', 'comment' 'date_created', 'comment'
] ]
read_only_fields = ['id', 'remote_addr', 'user', 'date_created'] read_only_fields = ['remote_addr', 'user', 'date_created']
def is_valid(self, raise_exception=False): def is_valid(self, raise_exception=False):
valid = super().is_valid(raise_exception=raise_exception) valid = super().is_valid(raise_exception=raise_exception)
......
...@@ -101,9 +101,8 @@ ...@@ -101,9 +101,8 @@
<td class="text-center">{{ session.date_start|time_util_with_seconds:session.date_end }}</td> <td class="text-center">{{ session.date_start|time_util_with_seconds:session.date_end }}</td>
<td> <td>
{% if session.is_finished %} {% if session.is_finished %}
<a onclick="window.open('/luna/replay/{{ session.id }}','luna', 'height=600, width=800, top=400, left=400, toolbar=no, menubar=no, scrollbars=no, location=no, status=no')" class="btn btn-xs btn-warning btn-replay" >{% trans "Replay" %}</a> <a {% if not session.can_replay %} disabled="" {% endif %} onclick="window.open('/luna/replay/{{ session.id }}','luna', 'height=600, width=800, top=400, left=400, toolbar=no, menubar=no, scrollbars=no, location=no, status=no')" class="btn btn-xs btn-warning btn-replay" >{% trans "Replay" %}</a>
{% else %} {% else %}
<!--<a onclick="window.open('/luna/monitor/{{ session.id }}','luna', 'height=600, width=800, top=0, left=0, toolbar=no, menubar=no, scrollbars=no, location=no, status=no')" class="btn btn-xs btn-warning btn-monitor" >{% trans "Monitor" %}</a>-->
{% if session.protocol == 'ssh' %} {% if session.protocol == 'ssh' %}
<a class="btn btn-xs btn-danger btn-term" value="{{ session.id }}" terminal="{{ session.terminal.id }}" >{% trans "Terminate" %}</a> <a class="btn btn-xs btn-danger btn-term" value="{{ session.id }}" terminal="{{ session.terminal.id }}" >{% trans "Terminate" %}</a>
{% else %} {% else %}
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
from django.urls import path, include from django.urls import path, include
from rest_framework_bulk.routes import BulkRouter from rest_framework_bulk.routes import BulkRouter
from ..api import v1 as api from .. import api
app_name = 'terminal' app_name = 'terminal'
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
from django.urls import path from django.urls import path
from rest_framework_bulk.routes import BulkRouter from rest_framework_bulk.routes import BulkRouter
from ..api import v2 as api from .. import api_v2 as api
app_name = 'terminal' app_name = 'terminal'
......
...@@ -2,5 +2,4 @@ ...@@ -2,5 +2,4 @@
# #
from .user import * from .user import *
from .auth import *
from .group import * from .group import *
...@@ -11,8 +11,9 @@ from rest_framework.permissions import IsAuthenticated ...@@ -11,8 +11,9 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination from rest_framework.pagination import LimitOffsetPagination
from common.permissions import IsOrgAdmin, IsCurrentUserOrReadOnly, \ from common.permissions import (
IsOrgAdminOrAppUser IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser
)
from common.mixins import IDInFilterMixin from common.mixins import IDInFilterMixin
from common.utils import get_logger from common.utils import get_logger
from orgs.utils import current_org from orgs.utils import current_org
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
from rest_framework import viewsets from rest_framework import viewsets
from common.permissions import WithBootstrapToken from common.permissions import WithBootstrapToken
from ...serializers import v2 as serializers from .. import serializers_v2 as serializers
class ServiceAccountRegistrationViewSet(viewsets.ModelViewSet): class ServiceAccountRegistrationViewSet(viewsets.ModelViewSet):
......
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
from django import forms from django import forms
from django.contrib.auth.forms import AuthenticationForm
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from captcha.fields import CaptchaField
from common.utils import validate_ssh_public_key from common.utils import validate_ssh_public_key
from orgs.mixins import OrgModelForm from orgs.mixins import OrgModelForm
...@@ -11,24 +9,6 @@ from orgs.utils import current_org ...@@ -11,24 +9,6 @@ from orgs.utils import current_org
from .models import User, UserGroup from .models import User, UserGroup
class UserLoginForm(AuthenticationForm):
username = forms.CharField(label=_('Username'), max_length=100)
password = forms.CharField(
label=_('Password'), widget=forms.PasswordInput,
max_length=128, strip=False
)
def confirm_login_allowed(self, user):
if not user.is_staff:
raise forms.ValidationError(
self.error_messages['inactive'],
code='inactive',)
class UserLoginCaptchaForm(UserLoginForm):
captcha = CaptchaField()
class UserCheckPasswordForm(forms.Form): class UserCheckPasswordForm(forms.Form):
username = forms.CharField(label=_('Username'), max_length=100) username = forms.CharField(label=_('Username'), max_length=100)
password = forms.CharField( password = forms.CharField(
......
# Generated by Django 2.1.7 on 2019-03-04 06:59
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('users', '0018_auto_20190107_1912'),
]
database_operations = [
migrations.AlterModelTable(name='accesskey', table='authentication_accesskey'),
migrations.AlterModelTable(name='privatetoken', table='authentication_privatetoken'),
migrations.AlterModelTable(name='loginlog', table='audits_userloginlog'),
]
state_operations = [
migrations.DeleteModel('accesskey'),
migrations.DeleteModel('privatetoken'),
migrations.DeleteModel('loginlog'),
]
operations = [
migrations.SeparateDatabaseAndState(
database_operations=database_operations,
state_operations=state_operations)
]
...@@ -4,5 +4,4 @@ ...@@ -4,5 +4,4 @@
from .user import * from .user import *
from .group import * from .group import *
from .authentication import *
from .utils import * from .utils import *
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
import uuid
from django.db import models
from django.utils.translation import ugettext_lazy as _
from rest_framework.authtoken.models import Token
from .user import User
__all__ = ['AccessKey', 'PrivateToken', 'LoginLog']
class AccessKey(models.Model):
id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True,
default=uuid.uuid4, editable=False)
secret = models.UUIDField(verbose_name='AccessKeySecret',
default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, verbose_name='User',
on_delete=models.CASCADE, related_name='access_keys')
def get_id(self):
return str(self.id)
def get_secret(self):
return str(self.secret)
def get_full_value(self):
return '{}:{}'.format(self.id, self.secret)
def __str__(self):
return str(self.id)
class PrivateToken(Token):
"""Inherit from auth token, otherwise migration is boring"""
class Meta:
verbose_name = _('Private Token')
class LoginLog(models.Model):
LOGIN_TYPE_CHOICE = (
('W', 'Web'),
('T', 'Terminal'),
)
MFA_DISABLED = 0
MFA_ENABLED = 1
MFA_UNKNOWN = 2
MFA_CHOICE = (
(MFA_DISABLED, _('Disabled')),
(MFA_ENABLED, _('Enabled')),
(MFA_UNKNOWN, _('-')),
)
REASON_NOTHING = 0
REASON_PASSWORD = 1
REASON_MFA = 2
REASON_NOT_EXIST = 3
REASON_PASSWORD_EXPIRED = 4
REASON_CHOICE = (
(REASON_NOTHING, _('-')),
(REASON_PASSWORD, _('Username/password check failed')),
(REASON_MFA, _('MFA authentication failed')),
(REASON_NOT_EXIST, _("Username does not exist")),
(REASON_PASSWORD_EXPIRED, _("Password expired")),
)
STATUS_CHOICE = (
(True, _('Success')),
(False, _('Failed'))
)
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
username = models.CharField(max_length=128, verbose_name=_('Username'))
type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=2, verbose_name=_('Login type'))
ip = models.GenericIPAddressField(verbose_name=_('Login ip'))
city = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('Login city'))
user_agent = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('User agent'))
mfa = models.SmallIntegerField(default=MFA_UNKNOWN, choices=MFA_CHOICE, verbose_name=_('MFA'))
reason = models.SmallIntegerField(default=REASON_NOTHING, choices=REASON_CHOICE, verbose_name=_('Reason'))
status = models.BooleanField(max_length=2, default=True, choices=STATUS_CHOICE, verbose_name=_('Status'))
datetime = models.DateTimeField(auto_now_add=True, verbose_name=_('Date login'))
class Meta:
ordering = ['-datetime', 'username']
...@@ -16,7 +16,6 @@ from django.utils import timezone ...@@ -16,7 +16,6 @@ from django.utils import timezone
from django.shortcuts import reverse from django.shortcuts import reverse
from common.utils import get_signer, date_expired_default from common.utils import get_signer, date_expired_default
from orgs.utils import current_org
__all__ = ['User'] __all__ = ['User']
...@@ -104,6 +103,8 @@ class User(AbstractUser): ...@@ -104,6 +103,8 @@ class User(AbstractUser):
verbose_name=_('Date password last updated') verbose_name=_('Date password last updated')
) )
user_cache_key_prefix = '_User_{}'
def __str__(self): def __str__(self):
return '{0.name}({0.username})'.format(self) return '{0.name}({0.username})'.format(self)
...@@ -281,23 +282,25 @@ class User(AbstractUser): ...@@ -281,23 +282,25 @@ class User(AbstractUser):
self.role = 'Admin' self.role = 'Admin'
self.is_active = True self.is_active = True
super().save(*args, **kwargs) super().save(*args, **kwargs)
self.expire_user_cache()
@property @property
def private_token(self): def private_token(self):
return self.create_private_token() from authentication.models import PrivateToken
def create_private_token(self):
from .authentication import PrivateToken
try: try:
token = PrivateToken.objects.get(user=self) token = PrivateToken.objects.get(user=self)
except PrivateToken.DoesNotExist: except PrivateToken.DoesNotExist:
token = self.create_private_token()
return token
def create_private_token(self):
from authentication.models import PrivateToken
token = PrivateToken.objects.create(user=self) token = PrivateToken.objects.create(user=self)
return token.key return token
def refresh_private_token(self): def refresh_private_token(self):
from .authentication import PrivateToken self.private_token.delete()
PrivateToken.objects.filter(user=self).delete() return self.create_private_token()
return PrivateToken.objects.create(user=self)
def create_bearer_token(self, request=None): def create_bearer_token(self, request=None):
expiration = settings.TOKEN_EXPIRATION or 3600 expiration = settings.TOKEN_EXPIRATION or 3600
...@@ -308,7 +311,8 @@ class User(AbstractUser): ...@@ -308,7 +311,8 @@ class User(AbstractUser):
if not isinstance(remote_addr, bytes): if not isinstance(remote_addr, bytes):
remote_addr = remote_addr.encode("utf-8") remote_addr = remote_addr.encode("utf-8")
remote_addr = base64.b16encode(remote_addr) # .replace(b'=', '') remote_addr = base64.b16encode(remote_addr) # .replace(b'=', '')
token = cache.get('%s_%s' % (self.id, remote_addr)) cache_key = '%s_%s' % (self.id, remote_addr)
token = cache.get(cache_key)
if not token: if not token:
token = uuid.uuid4().hex token = uuid.uuid4().hex
cache.set(token, self.id, expiration) cache.set(token, self.id, expiration)
...@@ -319,8 +323,7 @@ class User(AbstractUser): ...@@ -319,8 +323,7 @@ class User(AbstractUser):
pass pass
def create_access_key(self): def create_access_key(self):
from . import AccessKey access_key = self.access_keys.create()
access_key = AccessKey.objects.create(user=self)
return access_key return access_key
@property @property
...@@ -332,11 +335,6 @@ class User(AbstractUser): ...@@ -332,11 +335,6 @@ class User(AbstractUser):
return True return True
return False return False
def check_public_key(self, public_key):
if self.ssh_public_key == public_key:
return True
return False
def avatar_url(self): def avatar_url(self):
admin_default = settings.STATIC_URL + "img/avatar/admin.png" admin_default = settings.STATIC_URL + "img/avatar/admin.png"
user_default = settings.STATIC_URL + "img/avatar/user.png" user_default = settings.STATIC_URL + "img/avatar/user.png"
...@@ -422,8 +420,26 @@ class User(AbstractUser): ...@@ -422,8 +420,26 @@ class User(AbstractUser):
def delete(self, using=None, keep_parents=False): def delete(self, using=None, keep_parents=False):
if self.pk == 1 or self.username == 'admin': if self.pk == 1 or self.username == 'admin':
return return
self.expire_user_cache()
return super(User, self).delete() return super(User, self).delete()
def expire_user_cache(self):
key = self.user_cache_key_prefix.format(self.id)
cache.delete(key)
@classmethod
def get_user_or_from_cache(cls, uid):
key = cls.user_cache_key_prefix.format(uid)
user = cache.get(key)
if user:
return user
try:
user = cls.objects.get(id=uid)
cache.set(key, user, 3600)
except cls.DoesNotExist:
user = None
return user
class Meta: class Meta:
ordering = ['username'] ordering = ['username']
verbose_name = _("User") verbose_name = _("User")
......
...@@ -54,7 +54,7 @@ class UserGroupSerializer(BulkSerializerMixin, serializers.ModelSerializer): ...@@ -54,7 +54,7 @@ class UserGroupSerializer(BulkSerializerMixin, serializers.ModelSerializer):
model = UserGroup model = UserGroup
list_serializer_class = BulkListSerializer list_serializer_class = BulkListSerializer
fields = '__all__' fields = '__all__'
read_only_fields = ['id', 'created_by'] read_only_fields = ['created_by']
@staticmethod @staticmethod
def get_users(obj): def get_users(obj):
......
# -*- coding: utf-8 -*-
#
from .user import *
...@@ -2,15 +2,11 @@ ...@@ -2,15 +2,11 @@
# #
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from rest_framework import serializers from rest_framework import serializers
from ..models import User, AccessKey from ..models import User
from authentication.serializers import AccessKeySerializer
class AccessKeySerializer(serializers.ModelSerializer): __all__ = ['ServiceAccountSerializer']
class Meta:
model = AccessKey
fields = ['id', 'secret']
read_only_fields = ['id', 'secret']
class ServiceAccountSerializer(serializers.ModelSerializer): class ServiceAccountSerializer(serializers.ModelSerializer):
...@@ -19,7 +15,7 @@ class ServiceAccountSerializer(serializers.ModelSerializer): ...@@ -19,7 +15,7 @@ class ServiceAccountSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
fields = ['id', 'name', 'access_key'] fields = ['id', 'name', 'access_key']
read_only_fields = ['id', 'access_key'] read_only_fields = ['access_key']
def get_username(self): def get_username(self):
return self.initial_data.get('name') return self.initial_data.get('name')
......
...@@ -2,4 +2,3 @@ from django.dispatch import Signal ...@@ -2,4 +2,3 @@ from django.dispatch import Signal
post_user_create = Signal(providing_args=('user',)) post_user_create = Signal(providing_args=('user',))
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import datetime
from django.utils import timezone
from django.conf import settings
from celery import shared_task from celery import shared_task
from ops.celery.utils import create_or_update_celery_periodic_tasks from ops.celery.utils import create_or_update_celery_periodic_tasks
from ops.celery.decorator import after_app_ready_start from ops.celery.decorator import after_app_ready_start, register_as_period_task
from .models import User
from common.utils import get_logger from common.utils import get_logger
from .utils import write_login_log, send_password_expiration_reminder_mail from .models import User
from .utils import send_password_expiration_reminder_mail
logger = get_logger(__file__) logger = get_logger(__file__)
@shared_task
def write_login_log_async(*args, **kwargs):
write_login_log(*args, **kwargs)
@shared_task @shared_task
def check_password_expired(): def check_password_expired():
users = User.objects.exclude(role=User.ROLE_APP) users = User.objects.exclude(role=User.ROLE_APP)
...@@ -43,3 +41,6 @@ def check_password_expired_periodic(): ...@@ -43,3 +41,6 @@ def check_password_expired_periodic():
} }
} }
create_or_update_celery_periodic_tasks(tasks) create_or_update_celery_periodic_tasks(tasks)
...@@ -88,7 +88,7 @@ ...@@ -88,7 +88,7 @@
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
<p class="text-muted text-center">{% trans "More login options" %}</p> <p class="text-muted text-center">{% trans "More login options" %}</p>
<div> <div>
<button type="button" class="btn btn-default btn-sm btn-block" onclick="location.href='{% url 'authentication:openid-login' %}'"> <button type="button" class="btn btn-default btn-sm btn-block" onclick="location.href='{% url 'authentication:openid:openid-login' %}'">
<i class="fa fa-openid"></i> <i class="fa fa-openid"></i>
{% trans 'Keycloak' %} {% trans 'Keycloak' %}
</button> </button>
......
...@@ -32,27 +32,53 @@ ...@@ -32,27 +32,53 @@
<script src="{% static 'js/bootstrap.min.js' %}"></script> <script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/plugins/datatables/datatables.min.js' %}"></script> <script src="{% static 'js/plugins/datatables/datatables.min.js' %}"></script>
{# <script src="{% static 'js/angular.min.js' %}"></script>#}
<style> <style>
.box-1{
height: 472px;
width: 984px;
margin-right: auto;
margin-left: auto;
margin-top: calc((100vh - 470px)/2);
}
.box-2{
height: 100%;
width: 50%;
float: right;
}
.box-3{
text-align: center;
background-color: white;
height: 100%;
width: 50%;
}
.captcha { .captcha {
float: right; float: right;
} }
</style> </style>
</head> </head>
<body> <body style="height: 100%">
<div>
<div class="login-dialog"> <div class="box-1">
<div class=""> <div class="box-2">
<div class="row" style="height: 472px"> {% if interface.login_image %}
<div class="col-md-4 col-md-offset-2 input_shadow-1" style="text-align: center;background-color: white; padding-right: 0px;height: 100%"> <img src="{{ MEDIA_URL }}{{ interface.login_image }}" style="height: 100%; width: 100%"/>
{% else %}
<img src="{% static 'img/login/login_image_1.png' %}" style=" height: 100%; width: 100%"/>
{% endif %}
</div>
<div class="box-3">
<div style="background-color: white"> <div style="background-color: white">
{% if interface.login_title %} {% if interface.login_title %}
<div style="margin-top: 40px"> <div style="margin-top: 40px;padding-top: 50px;">
<span style="font-size: 24px;font-weight:400;color: #151515;letter-spacing: 0;">{{ interface.login_title }}</span> <span style="font-size: 24px;font-weight:400;color: #151515;letter-spacing: 0;">{{ interface.login_title }}</span>
</div> </div>
{% else %} {% else %}
<div style="margin-top: 40px"> <div style="margin-top: 40px;padding-top: 50px;">
<span style="font-size: 24px;font-weight:400;color: #151515;letter-spacing: 0;">{% trans 'Welcome to the Jumpserver open source fortress' %}</span> <span style="font-size: 24px;font-weight:400;color: #151515;letter-spacing: 0;">{% trans 'Welcome to the Jumpserver open source fortress' %}</span>
</div> </div>
{% endif %} {% endif %}
...@@ -81,15 +107,12 @@ ...@@ -81,15 +107,12 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" name="{{ form.username.html_name }}" <input type="text" class="form-control" name="{{ form.username.html_name }}" placeholder="{% trans 'Username' %}" required="" value="{% if form.username.value %}{{ form.username.value }}{% endif %}" style="height: 35px">
placeholder="{% trans 'Username' %}" required=""
value="{% if form.username.value %}{{ form.username.value }}{% endif %}" style="height: 35px">
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="password" class="form-control" name="{{ form.password.html_name }}" <input type="password" class="form-control" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
placeholder="{% trans 'Password' %}" required="">
</div> </div>
<div class="form-group" style="height: 50px;margin-bottom: 0px"> <div class="form-group" style="height: 50px;margin-bottom: 0">
{{ form.captcha }} {{ form.captcha }}
</div> </div>
<div class="form-group" style="margin-top: 10px"> <div class="form-group" style="margin-top: 10px">
...@@ -104,23 +127,11 @@ ...@@ -104,23 +127,11 @@
</div> </div>
<div class="col-md-1"></div> <div class="col-md-1"></div>
</div> </div>
</div>
</div> </div>
</div> </div>
<div class="col-md-4 " style="padding-left: 0px; height: 100%">
{% if interface.login_image %}
<img src="{{ MEDIA_URL }}{{ interface.login_image }}" style="width: 100%; height: 100%;" class="input_shadow-1" />
{% else %}
<img src="{% static 'img/login/login-image.jpg' %}" style="width: 100%; height: 100%;" class="input_shadow-1" />
{% endif %}
</div> </div>
</div>
<div class="col-md-2"></div>
</div> </div>
</div> </div>
</div>
</body> </body>
</html> </html>
\ No newline at end of file
...@@ -64,6 +64,7 @@ var zTree; ...@@ -64,6 +64,7 @@ var zTree;
var inited = false; var inited = false;
var url; var url;
var asset_table; var asset_table;
var treeUrl = "{% url 'api-perms:user-nodes-assets-as-tree' pk=object.id %}?show_assets=0&cache_policy=1";
function initTable() { function initTable() {
if (inited){ if (inited){
...@@ -71,7 +72,7 @@ function initTable() { ...@@ -71,7 +72,7 @@ function initTable() {
} else { } else {
inited = true; inited = true;
} }
url = "{% url 'api-perms:user-assets' pk=object.id %}"; url = "{% url 'api-perms:user-assets' pk=object.id %}?cache_policy=1";
var options = { var options = {
ele: $('#user_assets_table'), ele: $('#user_assets_table'),
columnDefs: [ columnDefs: [
...@@ -106,13 +107,14 @@ function initTable() { ...@@ -106,13 +107,14 @@ function initTable() {
} }
function onSelected(event, treeNode) { function onSelected(event, treeNode) {
url = '{% url "api-perms:user-node-assets" pk=object.id node_id=DEFAULT_PK %}'; url = '{% url "api-perms:user-node-assets" pk=object.id node_id=DEFAULT_PK %}?cache_policy=1';
var node_id = treeNode.meta.node.id; var node_id = treeNode.meta.node.id;
url = url.replace("{{ DEFAULT_PK }}", node_id); url = url.replace("{{ DEFAULT_PK }}", node_id);
asset_table.ajax.url(url); asset_table.ajax.url(url);
asset_table.ajax.reload(); asset_table.ajax.reload();
} }
function initTree() { function initTree() {
var setting = { var setting = {
view: { view: {
...@@ -129,9 +131,13 @@ function initTree() { ...@@ -129,9 +131,13 @@ function initTree() {
} }
}; };
$.get("{% url 'api-perms:user-nodes-assets-as-tree' pk=object.id %}?show_assets=0", function(data, status) { $.get(treeUrl, function(data, status) {
$.fn.zTree.init($("#assetTree"), setting, data); $.fn.zTree.init($("#assetTree"), setting, data);
zTree = $.fn.zTree.getZTreeObj("assetTree"); zTree = $.fn.zTree.getZTreeObj("assetTree");
rootNodeAddDom(zTree, function () {
treeUrl = treeUrl.replace('cache_policy=1', 'cache_policy=2');
initTree();
});
}); });
} }
......
...@@ -5,6 +5,8 @@ from __future__ import absolute_import ...@@ -5,6 +5,8 @@ from __future__ import absolute_import
from django.urls import path from django.urls import path
from rest_framework_bulk.routes import BulkRouter from rest_framework_bulk.routes import BulkRouter
from authentication import api as auth_api
from .. import api from .. import api
app_name = 'users' app_name = 'users'
...@@ -15,11 +17,12 @@ router.register(r'groups', api.UserGroupViewSet, 'user-group') ...@@ -15,11 +17,12 @@ router.register(r'groups', api.UserGroupViewSet, 'user-group')
urlpatterns = [ urlpatterns = [
# path('token/', api.UserToken.as_view(), name='user-token'), path('connection-token/', auth_api.UserConnectionTokenApi.as_view(),
path('connection-token/', api.UserConnectionTokenApi.as_view(), name='connection-token'), name='connection-token'),
path('auth/', auth_api.UserAuthApi.as_view(), name='user-auth'),
path('otp/auth/', auth_api.UserOtpAuthApi.as_view(), name='user-otp-auth'),
path('profile/', api.UserProfileApi.as_view(), name='user-profile'), path('profile/', api.UserProfileApi.as_view(), name='user-profile'),
path('auth/', api.UserAuthApi.as_view(), name='user-auth'),
path('otp/auth/', api.UserOtpAuthApi.as_view(), name='user-otp-auth'),
path('otp/reset/', api.UserResetOTPApi.as_view(), name='my-otp-reset'), path('otp/reset/', api.UserResetOTPApi.as_view(), name='my-otp-reset'),
path('users/<uuid:pk>/otp/reset/', api.UserResetOTPApi.as_view(), name='user-reset-otp'), path('users/<uuid:pk>/otp/reset/', api.UserResetOTPApi.as_view(), name='user-reset-otp'),
path('users/<uuid:pk>/password/', api.UserChangePasswordApi.as_view(), name='change-user-password'), path('users/<uuid:pk>/password/', api.UserChangePasswordApi.as_view(), name='change-user-password'),
......
...@@ -5,7 +5,7 @@ from __future__ import absolute_import ...@@ -5,7 +5,7 @@ from __future__ import absolute_import
from django.urls import path, include from django.urls import path, include
from rest_framework_bulk.routes import BulkRouter from rest_framework_bulk.routes import BulkRouter
from ..api import v2 as api from .. import api_v2 as api
app_name = 'users' app_name = 'users'
......
...@@ -9,8 +9,6 @@ app_name = 'users' ...@@ -9,8 +9,6 @@ app_name = 'users'
urlpatterns = [ urlpatterns = [
# Login view # Login view
path('login/', views.UserLoginView.as_view(), name='login'), path('login/', views.UserLoginView.as_view(), name='login'),
path('logout/', views.UserLogoutView.as_view(), name='logout'),
path('login/otp/', views.UserLoginOtpView.as_view(), name='login-otp'),
path('password/forgot/', views.UserForgotPasswordView.as_view(), name='forgot-password'), path('password/forgot/', views.UserForgotPasswordView.as_view(), name='forgot-password'),
path('password/forgot/sendmail-success/', views.UserForgotPasswordSendmailSuccessView.as_view(), name='forgot-password-sendmail-success'), path('password/forgot/sendmail-success/', views.UserForgotPasswordSendmailSuccessView.as_view(), name='forgot-password-sendmail-success'),
path('password/reset/', views.UserResetPasswordView.as_view(), name='reset-password'), path('password/reset/', views.UserResetPasswordView.as_view(), name='reset-password'),
...@@ -48,7 +46,4 @@ urlpatterns = [ ...@@ -48,7 +46,4 @@ urlpatterns = [
path('user-group/<uuid:pk>/update/', views.UserGroupUpdateView.as_view(), name='user-group-update'), path('user-group/<uuid:pk>/update/', views.UserGroupUpdateView.as_view(), name='user-group-update'),
path('user-group/<uuid:pk>/assets/', views.UserGroupGrantedAssetView.as_view(), name='user-group-granted-asset'), path('user-group/<uuid:pk>/assets/', views.UserGroupGrantedAssetView.as_view(), name='user-group-granted-asset'),
# Login log
# Abandon
path('login-log/', views.LoginLogListView.as_view(), name='login-log-list'),
] ]
...@@ -6,9 +6,7 @@ import re ...@@ -6,9 +6,7 @@ import re
import pyotp import pyotp
import base64 import base64
import logging import logging
import uuid
import requests
import ipaddress import ipaddress
from django.http import Http404 from django.http import Http404
from django.conf import settings from django.conf import settings
...@@ -19,10 +17,8 @@ from django.core.cache import cache ...@@ -19,10 +17,8 @@ from django.core.cache import cache
from datetime import datetime from datetime import datetime
from common.tasks import send_mail_async from common.tasks import send_mail_async
from common.utils import reverse, get_object_or_none from common.utils import reverse, get_object_or_none, get_ip_city
from common.forms import SecuritySettingForm from .models import User
from common.models import Setting
from .models import User, LoginLog
logger = logging.getLogger('jumpserver') logger = logging.getLogger('jumpserver')
...@@ -66,7 +62,7 @@ def send_user_created_mail(user): ...@@ -66,7 +62,7 @@ def send_user_created_mail(user):
'rest_password_token': user.generate_reset_token(), 'rest_password_token': user.generate_reset_token(),
'forget_password_url': reverse('users:forgot-password', external=True), 'forget_password_url': reverse('users:forgot-password', external=True),
'email': user.email, 'email': user.email,
'login_url': reverse('users:login', external=True), 'login_url': reverse('authentication:login', external=True),
} }
if settings.DEBUG: if settings.DEBUG:
try: try:
...@@ -102,7 +98,7 @@ def send_reset_password_mail(user): ...@@ -102,7 +98,7 @@ def send_reset_password_mail(user):
'rest_password_token': user.generate_reset_token(), 'rest_password_token': user.generate_reset_token(),
'forget_password_url': reverse('users:forgot-password', external=True), 'forget_password_url': reverse('users:forgot-password', external=True),
'email': user.email, 'email': user.email,
'login_url': reverse('users:login', external=True), 'login_url': reverse('authentication:login', external=True),
} }
if settings.DEBUG: if settings.DEBUG:
logger.debug(message) logger.debug(message)
...@@ -140,7 +136,7 @@ def send_password_expiration_reminder_mail(user): ...@@ -140,7 +136,7 @@ def send_password_expiration_reminder_mail(user):
'update_password_url': reverse('users:user-password-update', external=True), 'update_password_url': reverse('users:user-password-update', external=True),
'forget_password_url': reverse('users:forgot-password', external=True), 'forget_password_url': reverse('users:forgot-password', external=True),
'email': user.email, 'email': user.email,
'login_url': reverse('users:login', external=True), 'login_url': reverse('authentication:login', external=True),
} }
if settings.DEBUG: if settings.DEBUG:
logger.debug(message) logger.debug(message)
...@@ -162,7 +158,7 @@ def send_reset_ssh_key_mail(user): ...@@ -162,7 +158,7 @@ def send_reset_ssh_key_mail(user):
</br> </br>
""") % { """) % {
'name': user.name, 'name': user.name,
'login_url': reverse('users:login', external=True), 'login_url': reverse('authentication:login', external=True),
} }
if settings.DEBUG: if settings.DEBUG:
logger.debug(message) logger.debug(message)
...@@ -202,51 +198,6 @@ def check_user_valid(**kwargs): ...@@ -202,51 +198,6 @@ def check_user_valid(**kwargs):
return None, _('Password or SSH public key invalid') return None, _('Password or SSH public key invalid')
def validate_ip(ip):
try:
ipaddress.ip_address(ip)
return True
except ValueError:
pass
return False
def write_login_log(*args, **kwargs):
ip = kwargs.get('ip', '')
if not (ip and validate_ip(ip)):
ip = ip[:15]
city = "Unknown"
else:
city = get_ip_city(ip)
kwargs.update({'ip': ip, 'city': city})
LoginLog.objects.create(**kwargs)
def get_ip_city(ip, timeout=10):
# Taobao ip api: http://ip.taobao.com/service/getIpInfo.php?ip=8.8.8.8
# Sina ip api: http://int.dpool.sina.com.cn/iplookup/iplookup.php?ip=8.8.8.8&format=json
url = 'http://ip.taobao.com/service/getIpInfo.php?ip=%s' % ip
try:
r = requests.get(url, timeout=timeout)
except:
r = None
city = 'Unknown'
if r and r.status_code == 200:
try:
data = r.json()
if not isinstance(data, int) and data['code'] == 0:
country = data['data']['country']
_city = data['data']['city']
if country == 'XX':
city = _city
else:
city = ' '.join([country, _city])
except ValueError:
pass
return city
def get_user_or_tmp_user(request): def get_user_or_tmp_user(request):
user = request.user user = request.user
tmp_user = get_tmp_user_from_cache(request) tmp_user = get_tmp_user_from_cache(request)
......
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
from __future__ import unicode_literals from __future__ import unicode_literals
import os
from django.core.cache import cache
from django.shortcuts import render from django.shortcuts import render
from django.contrib.auth import login as auth_login, logout as auth_logout
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView from django.views.generic import RedirectView
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.http import HttpResponseRedirect, HttpResponse from django.http import HttpResponseRedirect
from django.shortcuts import reverse, redirect from django.shortcuts import reverse, redirect
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView
from django.conf import settings from django.conf import settings
from django.urls import reverse_lazy
from formtools.wizard.views import SessionWizardView from formtools.wizard.views import SessionWizardView
from common.utils import get_object_or_none, get_request_ip from common.utils import get_object_or_none
from ..models import User, LoginLog from ..models import User
from ..utils import send_reset_password_mail, check_otp_code, \ from ..utils import (
redirect_user_first_login_or_index, get_user_or_tmp_user, \ send_reset_password_mail, get_password_check_rules, check_password_rules
set_tmp_user_to_cache, get_password_check_rules, check_password_rules, \ )
is_block_login, increase_login_failed_count, clean_failed_count
from ..tasks import write_login_log_async
from .. import forms from .. import forms
__all__ = [ __all__ = [
'UserLoginView', 'UserLoginOtpView', 'UserLogoutView', 'UserLoginView', 'UserForgotPasswordSendmailSuccessView',
'UserForgotPasswordView', 'UserForgotPasswordSendmailSuccessView', 'UserResetPasswordSuccessView', 'UserResetPasswordSuccessView',
'UserResetPasswordView', 'UserResetPasswordSuccessView', 'UserResetPasswordView', 'UserForgotPasswordView', 'UserFirstLoginView',
'UserFirstLoginView', 'LoginLogListView'
] ]
@method_decorator(sensitive_post_parameters(), name='dispatch') class UserLoginView(RedirectView):
@method_decorator(csrf_protect, name='dispatch') urls = reverse_lazy('authentication:login')
@method_decorator(never_cache, name='dispatch')
class UserLoginView(FormView):
form_class = forms.UserLoginForm
form_class_captcha = forms.UserLoginCaptchaForm
redirect_field_name = 'next'
key_prefix_captcha = "_LOGIN_INVALID_{}"
def get_template_names(self):
template_name = 'users/login.html'
if not settings.XPACK_ENABLED:
return template_name
from xpack.plugins.license.models import License
if not License.has_valid_license():
return template_name
template_name = 'users/new_login.html'
return template_name
def get(self, request, *args, **kwargs):
if request.user.is_staff:
return redirect(redirect_user_first_login_or_index(
request, self.redirect_field_name)
)
request.session.set_test_cookie()
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
# limit login authentication
ip = get_request_ip(request)
username = self.request.POST.get('username')
if is_block_login(username, ip):
return self.render_to_response(self.get_context_data(block_login=True))
return super().post(request, *args, **kwargs)
def form_valid(self, form):
if not self.request.session.test_cookie_worked():
return HttpResponse(_("Please enable cookies and try again."))
user = form.get_user()
# user password expired
if user.password_has_expired:
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_PASSWORD_EXPIRED,
'status': False
}
self.write_login_log(data)
return self.render_to_response(self.get_context_data(password_expired=True))
set_tmp_user_to_cache(self.request, user)
username = form.cleaned_data.get('username')
ip = get_request_ip(self.request)
# 登陆成功,清除缓存计数
clean_failed_count(username, ip)
return redirect(self.get_success_url())
def form_invalid(self, form):
# write login failed log
username = form.cleaned_data.get('username')
exist = User.objects.filter(username=username).first()
reason = LoginLog.REASON_PASSWORD if exist else LoginLog.REASON_NOT_EXIST
data = {
'username': username,
'mfa': LoginLog.MFA_UNKNOWN,
'reason': reason,
'status': False
}
self.write_login_log(data)
# limit user login failed count
ip = get_request_ip(self.request)
increase_login_failed_count(username, ip)
# show captcha
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
old_form = form
form = self.form_class_captcha(data=form.data)
form._errors = old_form.errors
return super().form_invalid(form)
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
else:
return self.form_class
def get_success_url(self):
user = get_user_or_tmp_user(self.request)
if user.otp_enabled and user.otp_secret_key:
# 1,2,mfa_setting & T
return reverse('users:login-otp')
elif user.otp_enabled and not user.otp_secret_key:
# 1,2,mfa_setting & F
return reverse('users:user-otp-enable-authentication')
elif not user.otp_enabled:
# 0 & T,F
auth_login(self.request, user)
data = {
'username': self.request.user.username,
'mfa': int(self.request.user.otp_enabled),
'reason': LoginLog.REASON_NOTHING,
'status': True
}
self.write_login_log(data)
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
def get_context_data(self, **kwargs):
context = {
'demo_mode': os.environ.get("DEMO_MODE"),
'AUTH_OPENID': settings.AUTH_OPENID,
}
kwargs.update(context)
return super().get_context_data(**kwargs)
def write_login_log(self, data):
login_ip = get_request_ip(self.request)
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
tmp_data = {
'ip': login_ip,
'type': 'W',
'user_agent': user_agent
}
data.update(tmp_data)
write_login_log_async.delay(**data)
class UserLoginOtpView(FormView):
template_name = 'users/login_otp.html'
form_class = forms.UserCheckOtpCodeForm
redirect_field_name = 'next'
def form_valid(self, form):
user = get_user_or_tmp_user(self.request)
otp_code = form.cleaned_data.get('otp_code')
otp_secret_key = user.otp_secret_key
if check_otp_code(otp_secret_key, otp_code):
auth_login(self.request, user)
data = {
'username': self.request.user.username,
'mfa': int(self.request.user.otp_enabled),
'reason': LoginLog.REASON_NOTHING,
'status': True
}
self.write_login_log(data)
return redirect(self.get_success_url())
else:
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_MFA,
'status': False
}
self.write_login_log(data)
form.add_error('otp_code', _('MFA code invalid, or ntp sync server time'))
return super().form_invalid(form)
def get_success_url(self):
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
def write_login_log(self, data):
login_ip = get_request_ip(self.request)
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
tmp_data = {
'ip': login_ip,
'type': 'W',
'user_agent': user_agent
}
data.update(tmp_data)
write_login_log_async.delay(**data)
@method_decorator(never_cache, name='dispatch')
class UserLogoutView(TemplateView):
template_name = 'flash_message_standalone.html'
def get(self, request, *args, **kwargs):
auth_logout(request)
next_uri = request.COOKIES.get("next")
if next_uri:
return redirect(next_uri)
response = super().get(request, *args, **kwargs)
return response
def get_context_data(self, **kwargs):
context = {
'title': _('Logout success'),
'messages': _('Logout success, return login page'),
'interval': 1,
'redirect_url': reverse('users:login'),
'auto_redirect': True,
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class UserForgotPasswordView(TemplateView): class UserForgotPasswordView(TemplateView):
...@@ -267,7 +58,7 @@ class UserForgotPasswordSendmailSuccessView(TemplateView): ...@@ -267,7 +58,7 @@ class UserForgotPasswordSendmailSuccessView(TemplateView):
'title': _('Send reset password message'), 'title': _('Send reset password message'),
'messages': _('Send reset password mail success, ' 'messages': _('Send reset password mail success, '
'login your mail box and follow it '), 'login your mail box and follow it '),
'redirect_url': reverse('users:login'), 'redirect_url': reverse('authentication:login'),
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
...@@ -280,7 +71,7 @@ class UserResetPasswordSuccessView(TemplateView): ...@@ -280,7 +71,7 @@ class UserResetPasswordSuccessView(TemplateView):
context = { context = {
'title': _('Reset password success'), 'title': _('Reset password success'),
'messages': _('Reset password success, return to login page'), 'messages': _('Reset password success, return to login page'),
'redirect_url': reverse('users:login'), 'redirect_url': reverse('authentication:login'),
'auto_redirect': True, 'auto_redirect': True,
} }
kwargs.update(context) kwargs.update(context)
...@@ -291,7 +82,7 @@ class UserResetPasswordView(TemplateView): ...@@ -291,7 +82,7 @@ class UserResetPasswordView(TemplateView):
template_name = 'users/reset_password.html' template_name = 'users/reset_password.html'
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
token = request.GET.get('token') token = request.GET.get('token', '')
user = User.validate_reset_token(token) user = User.validate_reset_token(token)
if not user: if not user:
kwargs.update({'errors': _('Token invalid or expired')}) kwargs.update({'errors': _('Token invalid or expired')})
...@@ -386,8 +177,3 @@ class UserFirstLoginView(LoginRequiredMixin, SessionWizardView): ...@@ -386,8 +177,3 @@ class UserFirstLoginView(LoginRequiredMixin, SessionWizardView):
form.fields["otp_level"].initial = self.request.user.otp_level form.fields["otp_level"].initial = self.request.user.otp_level
return form return form
class LoginLogListView(ListView):
def get(self, request, *args, **kwargs):
return redirect(reverse('audits:login-log-list'))
...@@ -574,7 +574,7 @@ class UserOtpSettingsSuccessView(TemplateView): ...@@ -574,7 +574,7 @@ class UserOtpSettingsSuccessView(TemplateView):
'title': title, 'title': title,
'messages': describe, 'messages': describe,
'interval': 1, 'interval': 1,
'redirect_url': reverse('users:login'), 'redirect_url': reverse('authentication:login'),
'auto_redirect': True, 'auto_redirect': True,
} }
kwargs.update(context) kwargs.update(context)
......
...@@ -61,6 +61,14 @@ REDIS_PORT: 6379 ...@@ -61,6 +61,14 @@ REDIS_PORT: 6379
# AUTH_OPENID_REALM_NAME: realm-name # AUTH_OPENID_REALM_NAME: realm-name
# AUTH_OPENID_CLIENT_ID: client-id # AUTH_OPENID_CLIENT_ID: client-id
# AUTH_OPENID_CLIENT_SECRET: client-secret # AUTH_OPENID_CLIENT_SECRET: client-secret
#
# Use Radius authorization
# 使用Radius来认证
# AUTH_RADIUS: false
# RADIUS_SERVER: localhost
# RADIUS_PORT: 1812
# RADIUS_SECRET:
# OTP settings # OTP settings
# OTP/MFA 配置 # OTP/MFA 配置
......
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = Jumpserver
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
\ No newline at end of file
## 说明
文档已移动到docs分支,该目录中不是最新文档, 请提交到docs分支
## 访问在线文档
[访问](https://docs.jumpserver.org)
资产管理模块
=============
这里介绍资产管理模块功能。
.. toctree::
:maxdepth: 1
asset_list
asset_admin_user
asset_system_user
asset_label
\ No newline at end of file
管理文档
=========
这里介绍管理员功能。
.. toctree::
:maxdepth: 1
admin_instruction
admin_user
admin_asset
admin_permission
admin_work_center
admin_session
admin_system_settings
架构说明
=================
.. image:: _static/img/structure.png
:alt: 组件架构图
组件说明
=================
Jumpserver
`````````````
现指 Jumpserver 管理后台,是核心组件(Core), 使用 Django Class Based View 风格开发,支持 Restful API。
`Github <https://github.com/jumpserver/jumpserver.git>`_
Coco
````````
实现了 SSH Server 和 Web Terminal Server 的组件,提供 SSH 和 WebSocket 接口, 使用 Paramiko 和 Flask 开发。
`Github <https://github.com/jumpserver/coco.git>`__
Luna
````````
现在是 Web Terminal 前端,计划前端页面都由该项目提供,Jumpserver 只提供 API,不再负责后台渲染html等。
`Github <https://github.com/jumpserver/luna.git>`__
Guacamole
```````````
Apache 跳板机项目,Jumpserver 使用其组件实现 RDP 功能,Jumpserver 并没有修改其代码而是添加了额外的插件,支持 Jumpserver 调用。
Jumpserver-Python-SDK
```````````````````````
Jumpserver API Python SDK,Coco 目前使用该 SDK 与 Jumpserver API 交互。
`Github <https://github.com/jumpserver/jumpserver-python-sdk.git>`__
权限管理模块
=============
这里介绍权限管理功能。
.. toctree::
:maxdepth: 1
permission_asset_authorized
会话管理模块
==============
这里介绍会话管理功能。
.. toctree::
:maxdepth: 1
session_history
session_online
session_command
session_web_terminal
session_terminal
\ No newline at end of file
系统设置
=============
这里介绍系统设置的功能。
.. contents:: Topics
.. _view_system_settings:
查看系统设置
`````````````
点击页面左侧“系统设置”按钮,进入系统设置页面,产看基本设置、邮件设置、LDAP 设置和终端设置等内容。
.. _basic_settings:
基本设置
`````````
点击页面上边的"基本设置" TAB ,进入基本设置页面,编辑当前站点 URL、用户想到 URL、Email 主题前缀等信息,点击“提交”按钮,基本设置完成。
.. _email_settings:
邮件设置
`````````
点击页面上边的"邮件设置" TAB ,进入邮件设置页面,编辑 SMTP 主机、SMTP 端口、SMTP 账号、SMTP 密码和使用 SSL 或者 TSL 等信息,点击“测试连接”按钮,测试是否正确设置,点击“提交”按钮,邮件设置完成。
.. _ladp_settings:
LDAP 设置
````````````
点击页面上边的" LDAP 设置" TAB ,进入 LDAP 设置页面,编辑 LDAP 地址、DN、用户 OU、用户过滤器、LDAP 属性映射和是否使用 SSL、是否启用 LDAP 认证等信息,点击“测试连接”按钮,测试是否正确设置,点击“提交”按钮,完成 LDAP 设置。
.. _terminal_settings:
终端设置
````````````
点击页面上边的“终端设置” TAB ,进入终端设置页面,编辑终端信息,点击“提交”按钮,终端设置完成。
\ No newline at end of file
用户管理模块
=============
这里介绍用户管理功能。
.. toctree::
:maxdepth: 1
user
user_group
login_log
\ No newline at end of file
作业中心模块
==============
这里介绍作业中心功能。
.. toctree::
:maxdepth: 1
work_center_list
\ No newline at end of file
REST API规范约定
----------------
这里仅考虑 REST API 的基本情况。参考
`RESTful API 设计指南`_
`Github API 文档`_
协议
~~~~
API 与用户的通信协议,总是使用 HTTPS 协议。
域名
~~~~
这版 API 相对简单, 没有前后端分离, 没有独立 APP, 所以放在主域名下
::
https://example.org/api/
版本
~~~~
将 API 的版本号放入 URL 中,由于一个项目多个 APP 所以 Jumpserver 使用以下风格,将版本号放到 APP 后面
::
https://example.com/api/:app:/:version:/:resource:
https://example.com/api/assets/v1.0/assets [GET, POST]
https://example.com/api/assets/v1.0/assets/1 [GET, PUT, DELETE]
路径
~~~~
路径又称“终点”(endpoint),表示 API 的具体网址。
在 RESTful 架构中,每个网址代表一种资源(Resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的“集合”(Collection),所以 API 中的名词也应该使用复数。
举例来说 Cmdb 中的 Assets 列表, IDC 列表。
::
https://example.com/api/:app:/:version:/:resource:
https://example.com/api/assets/v1.0/assets [GET, POST]
https://example.com/api/assets/v1.0/assets/1 [GET, PUT, DELETE]
https://example.com/api/assets/v1.0/idcs [GET, POST]
一般性的增删查改(CRUD)API,完全使用 HTTP Method 加上 URL 提供的语义,URL 中的可变部分(比如上面提到的),一般用来传递该API操作的核心实体对象的唯一 ID,如果有更多的参数需要提供,GET 方法请使用 URL Parameter(例如:“?client_id=xxxxx&app_id=xxxxxx”),PUT/POST/DELETE 方法请使用请求体传递参数。
HTTP Method
~~~~~~~~~~~
对于资源的具体操作类型,由 HTTP 动词表示。
常用的HTTP动词有下面五个(括号里是对应的 SQL 命令)。
- GET(SELECT):从服务器取出资源(一项或多项)。
- POST(CREATE):在服务器新建一个资源。
- PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源, 幂等
- PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
- DELETE(DELETE):从服务器删除资源。
.. _RESTful API 设计指南: http://www.ruanyifeng.com/blog/2014/05/restful_api.html
.. _Github API 文档: https://developer.github.com/v3/
过滤信息
~~~~~~~~
常见参数约定
::
?keyword=localhost 模糊搜索
?limit=10:指定返回记录的数量
?offset=10:指定返回记录的开始位置。
?page=2&per_page=100:指定第几页,以及每页的记录数。
?sort=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
?asset_id=1:指定筛选条件
状态码
~~~~~~
服务器向用户返回的状态码和提示信息,常见的有以下一些(方括号中是该状态码对应的HTTP动词)。
- 200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
- 201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
- 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
- 204 NO CONTENT - [DELETE]:用户删除数据成功。
- 400 INVALID REQUEST -
[POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
- 401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
- 403 Forbidden - [*]
表示用户得到授权(与401错误相对),但是访问是被禁止的。
- 404 NOT FOUND -
[*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
- 406 Not Acceptable -
[GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
- 410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
- 422 Unprocesable entity - [POST/PUT/PATCH]
当创建一个对象时,发生一个验证错误。
- 500 INTERNAL SERVER ERROR -
[*]:服务器发生错误,用户将无法判断发出的请求是否成功。
错误处理
~~~~~~~~
如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将 error 作为键名,出错信息作为键值即可。
::
{
error: "Invalid API key"
}
返回结果
~~~~~~~~
针对不同操作,服务器向用户返回的结果应该符合以下规范。
::
GET /collection:返回资源对象的列表(数组)
GET /collection/resource:返回单个资源对象
POST /collection:返回新生成的资源对象
PUT /collection/resource:返回完整的资源对象
PATCH /collection/resource:返回完整的资源对象
DELETE /collection/resource:返回一个空文档
Hypermedia API
~~~~~~~~~~~~~~
RESTful
API 最好做到 Hypermedia,即返回结果中提供链接,连向其他 API 方法,使得用户不查文档,也知道下一步应该做什么。
比如,当用户向 api.example.com 的根目录发出请求,会得到这样一个文档。
::
{"link": {
"rel": "collection https://www.example.com/zoos",
"href": "https://api.example.com/zoos",
"title": "List of zoos",
"type": "application/vnd.yourformat+json"
}}
上面代码表示,文档中有一个 Link 属性,用户读取这个属性就知道下一步该调用什么 API 了。
- rel 表示这个 API 与当前网址的关系(Collection 关系,并给出该 Collection 的网址)
- href 表示 API 的路径
- title 表示 API 的标题
- type 表示返回类型
Hypermedia API 的设计被称为 HATEOAS。 Github API 就是这种设计.
其它
~~~~
(1)API 的身份认证应该使用 OAuth 2.0 框架。
(2)服务器返回的数据格式,应该尽量使用 JSON。
\ No newline at end of file
管理用户
==========
这里介绍管理用户的功能。
.. contents:: Topics
.. _view_admin_user_list:
查看管理用户列表
````````````````
点击页面左侧“资产管理“菜单下的“管理用户”按钮,进入管理用户列表页面,查看管理用户的名称、资产数等信息。
.. _create_admin_user:
创建管理用户
````````````
点击页面左上角的“创建管理用户“按钮,进入创建管理用户界面,填写名称、用户名、密码、ssh私钥等信息,点击“提交”按钮,完成管理用户创建。
.. _update_admin_user:
更新管理用户
````````````
点击页面右边动作栏的“更新”按钮,进入更新管理用户页面,编辑管理用户的信息,点击“提交”按钮,完成管理用户更新。
.. _delete_admin_user:
删除管理用户
````````````
点击页面右边动作栏的“删除”按钮,弹出删除确认框,点击"确认"按钮,管理用户删除完成。
\ No newline at end of file
标签管理
============
这里介绍标签管理的功能。
.. contents:: Topics
.. _view_label_list:
查看标签列表
````````````````
点击页面左边“资产管理”菜单下的“标签管理”按钮,进入标签列表页面,产看标签的名称、值、资产数等信息。
.. _create_label:
创建标签
````````````
点击页面左上角“创建标签”按钮,进入创建标签页面,填写标签的名称、值等信息,选择资产,点击“提交”按钮,标签创建完成。
.. _update_label:
更新标签
````````````
点击页面右边动作栏的“更新”按钮,进入更新标签页面,编辑标签信息,点击“提交”按钮,标签更新完成。
.. _delete_label:
删除标签
`````````
点击页面右边动作栏的“删除”按钮,弹出删除确认框,点击“确认”按钮,完成标签删除。
\ No newline at end of file
资产列表
===========
这里介绍资产列表的功能。
.. contents:: Topics
.. _view_asset_list:
查看资产列表
`````````````
点击页面左侧的“资产管理”菜单下的“资产列表”按钮,查看当前所有的资产列表。
.. _create_asset:
创建资产
````````````
点击页面左上角的“创建资产”按钮,进入资产创建页面,填写资产信息,点击“提交”按钮,完成资产创建。
.. _update_asset:
更新资产
````````````
点击页面右边的“更新”按钮,进入编辑资产页面,更新资产信息,点击“提交”按钮,完成资产更新。
.. _delete_asset:
删除资产
`````````
点击页面右边的“删除”按钮,弹出删除确认框,点击“确认”按钮,完成资产删除。
.. _batch_operation:
批量操作
````````````
选中资产,选择页面左下角批量操作选项,点击“提交”按钮,完成批量操作。
\ No newline at end of file
系统用户
===========
这里介绍系统用户功能。
.. contents:: Topics
.. _view_admin_system_user:
查看系统用户
````````````
点击页面左侧“资产管理“菜单下的”系统用户“按钮,进入系统用户列表页面,查看系统用户的名称,资产数和连接数等信息。
.. _create_admin_system_user:
创建系统用户
````````````
点击页面左上角的“创建系统用户“按钮,进入创建系统用户页面,填写系统用户的基本信息、认证信息和其它信息,点击“提交“按钮,完成系统用户创建。
.. _update_admin_system_user:
更新系统用户
`````````````
点击页面动作栏的“更新”按钮,进入更新系统用户页面,编辑系统用户信息,点击“提交”按钮,系统用户更新完成。
.. _delete_admin_system_user:
删除系统用户
`````````````
点击页面动作栏的“删除”按钮,弹出删除确认框,点击“删除”按钮,完成删除系统用户。
\ No newline at end of file
# -*- coding: utf-8 -*-
#
# Configuration file for the Sphinx documentation builder.
#
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/stable/config
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
import sphinx_rtd_theme
# -- Project information -----------------------------------------------------
project = 'Jumpserver'
copyright = '北京堆栈科技有限公司 © 2014-2018'
author = 'Jumpserver team'
# The short X.Y version
version = ''
# The full version, including alpha/beta/rc tags
release = '0.5.0'
# -- General configuration ---------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.viewcode',
'sphinx.ext.githubpages',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = 'zh_CN'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path .
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
html_show_sourcelink = False
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
# html_theme = 'alabaster'
html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
html_theme_options = {
'logo_only': True,
'display_version': True
}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself. Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'Jumpserver 文档'
# -- Options for LaTeX output ------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'Jumpserver.tex', 'Jumpserver Documentation',
'Jumpserver team', 'manual'),
]
# -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'Jumpserver', 'Jumpserver Documentation',
[author], 1)
]
# -- Options for Texinfo output ----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'Jumpserver', 'Jumpserver 文档',
author, 'Jumpserver', ' Jumpserver是全球首款完全开源的堡垒机,是符合 4A 的专业运维审计系统',
'Miscellaneous'),
]
# -- Extension configuration -------------------------------------------------
html_logo = '_static/img/logo-text.png'
联系方式
+++++++++++++++++++++++++
商业支持
~~~~~~~~~~~
`阿里云市场购买: <https://market.aliyun.com/products/53690006/cmgj026011.html>`_
QQ 群
~~~~~~~~
群1: 390139816 (推荐)
群2: 399218702 (满)
群3: 552054376 (满)
Github
~~~~~~~~
https://github.com/jumpserver/jumpserver.git
官网
~~~~~~~~
http://www.jumpserver.org
Demo
~~~~~~~~
http://demo.jumpserver.org
邮件
~~~~~~~~
support@fit2cloud.com (#替换为@)
\ No newline at end of file
贡献者
=============
感谢以下朋友为 Jumpserver 做出的贡献,世界因你们而不同,排名不分先后
- **小彧 <李磊>** Django 资深开发者,为用户模块贡献了很多代码
- **sofia <周小侠>** 资深前端工程师, 前端代码贡献者
- **liuz <刘正> 全栈工程师** 编写了 Web Terminal 大部分代码
- **jiaxiangkong <陈尚委>** Jumpserver 测试运营
- **halcyon <王墉>** DevOps 资深开发者, 0.3.2 核心开发者之一
- **yumaojun03 <喻茂峻>** DevOps 资深开发者,擅长 Python、Go 以及 PaaS 平台开发
- **kelianchun <柯连春>** DevOps 资产开发者,修复了很多 Bugs
- **q4speed <莫鹍>** 架构师,贡献了 0.5.0 Windows 远程桌面登录大部分代码
- **ZhangFengyi <张峰毅>** 贡献了 0.5.0 新版文档
- **Aaron3S <沈晨阳>** 贡献了 0.5.0 新版文档
- **liqiang-fit2cloud <张立强>** 0.5.0 版本测试,给资产树设计贡献了很多建议
\ No newline at end of file
开发文档
======================================
.. toctree::
:maxdepth: 1
:caption: 开发文档
api_style_guide
python_style_guide
project_structure
FAQ
==========
1. Windows 无法连接
::
(1). 如果白屏 可能是nginx设置的不对,也可能运行guacamole的docker容器有问题,总之请求到不了guacamole
(2). 如果显示没有权限 可能是你在 终端管理里没有接受 guacamole的注册,请接受一下,如果还是不行,就删除刚才的注册,重启guacamole的docker重新注册
(3). 如果显示未知问题 可能是你的资产填写的端口不对,或者授权的系统用户的协议不是rdp
2. 用户、系统用户、管理用户的关系
::
用户:每个公司的同事创建一个用户账号,用来登录Jumpserver
系统用户:使用来登录到服务器的用户,如 web, dba, root等
管理用户:是服务器上已存在的特权用户,Ansible用来获取硬件信息, 如 root, 或者其它拥有 sudo NOPASSWD: ALL权限的用户
.. jumpserver documentation master file, created by
sphinx-quickstart on Mon Feb 26 23:28:27 2018.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Jumpserver 文档
======================================
目录:
.. toctree::
:maxdepth: 2
introduce
installation
admin_guide
user_guide
development
contributor
contact
snapshot
faq
安装文档
++++++++++++++++++++++++
.. toctree::
:maxdepth: 1
quickstart
step_by_step
upgrade
总体介绍
==================
欢迎来到 Jumpserver 文档。
Jumpserver 是全球首款完全开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 的专业运维审计系统。
Jumpserver 使用 Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 解决方案,交互界面美观、用户体验好。
Jumpserver 采纳分布式架构,支持多机房跨区域部署,中心节点提供 API,各机房部署登录节点,可横向扩展、无并发访问限制。
改变世界,从一点点开始。
\ No newline at end of file
登录日志
==========
这里介绍登录日志的功能。
点击页面左侧“用户管理”菜单下的“登录日志”按钮,进入登录日志页面。
\ No newline at end of file
资产授权
=========
这里介绍资产授权的相关的功能。
.. contents:: Topics
.. _view_asset_authorized:
查看资产授权规则列表
````````````````````
资产授权页面默认展示资产授权列表。点击左侧资产节点树下的节点,右侧展示此节点下的资产授权规则。
.. _create_asset_authorized:
创建授权规则
````````````
在左侧资产节点树下选择要创建授权规则的节点,点击页面右侧创建授权规则进入创建授权规则页面,填写授权规则信息,点击提交,完成创建授权规则。
.. _update_asset_authorized:
更新授权规则
````````````
在左侧资产节点树下选择要更新授权规则的节点,在右侧授权规则列表中找到要更新的授权规则,点击“动作”标题下的“更新”按钮进入授权规则更新页面,填写授权规则信息,点击提交,完成创建授权规则。
.. _delete_asset_authorized:
删除授权规则
````````````
在左侧资产节点树下选择要删除授权规则的节点,在右侧授权规则列表中找到要删除的授权规则,点击“动作”标题下的“删除”按钮,弹出确认删除页面,点击确认,完成删除授权规则。
项目骨架
--------
说明如下:
::
.
├── config-example.py // 配置文件样例
├── docs // 所有 DOC 文件放到该目录
│ └── README.md
├── LICENSE
├── README.md
├── install // 安装说明
├── logs // 日志目录
├── apps // 管理后台目录,也是各 APP 所在目录
│ └── assets // APP 目录
│ │ ├── admin.py
│ │ ├── apps.py // 新版本 Django APP 设置文件
│ │ ├── api.py // API 文件
│ │ ├── __init__.py // 对外暴露的接口,放到该文件中,方便别的 APP 引用
│ │ ├── migrations // Models Migrations 版本控制目录
│ │ │ └── __init__.py
│ │ ├── models.py // 数据模型目录
│ │ ├── static // APP 下静态资源目录,如果需要
│ │ │ └── assets // 多一层目录,防止资源重名
│ │ │ └── some_image.png
│ │ ├── templates // APP 下模板目录
│ │ │ └── assets // 多一层目录,防止资源重名
│ │ │ └── asset_list.html
│ │ ├── templatetags // 模板标签目录
│ │ ├── tests.py // 测试用例文件
│ │ ├── urls.py // Urlconf 文件
│ │ ├── utils.py // 将 Views 和 API 可复用的代码放在这里, API 和 Views 只是请求和返回不同
│ │ └── views.py // Views 文件
│ ├── common
│ │ ├── templatetags // 通用 Template Tag
│ │ ├── utils.py // 通用的函数方法
│ │ └── views.py
│ ├── fixtures // 初始化数据目录
│ │ ├── init.json // 初始化项目数据库
│ │ └── fake.json // 生成大量测试数据
│ ├── jumpserver // 项目设置目录
│ │ ├── __init__.py
│ │ ├── settings.py // 项目设置文件
│ │ ├── urls.py // 项目入口 Urlconf
│ │ └── wsgi.py
│ ├── manage.py
│ ├── static // 项目静态资源目录
│ ├── i18n // 项目多语言目录
│ └── templates // 项目模板目录
\ No newline at end of file
Jumpserver 项目规范(Draft)
============================
语言框架
----------
1. Python 3.6.1 (当前最新)
2. Django 1.11 (当前最新)
3. Flask 0.12 Luna (当前最新)
4. Paramiko 2.12 Coco (当前最新)
Django 规范
--------------
1. 尽量使用 Class Base View 编程,更少代码
2. 使用 Django Form
3. 每个 URL 独立命名,不要硬编码,同理 Static 也是
4. 数据库表名手动指定,不要使用默认
5. 代码优雅简洁
6. 注释明确优美
7. 测试案例尽可能完整
8. 尽可能利用 Django 造好的轮子
代码风格
-----------
Python 方面大致的风格,我们采用 pocoo 的\ `Style
Guidance`_\ ,但是有些细节部分会尽量放开 参考国内翻译
基本的代码布局
~~~~~~~~~~~~~~
缩进
^^^^^^^^
1. Python 严格采用4个空格的缩进,任何 Python 代码都都必须遵守此规定。
2. Web 部分代码(HTML、CSS、JavaScript),Node.js 采用2空格缩进,同样不使用 TAB。
之所以与 Python 不同,是因为 JS 中有大量回调式的写法,2空格可以显著降低视觉上的负担。
最大行长度
^^^^^^^^^^^^^
按 PEP8 规范,Python 一般限制最大79个字符,
但是 Django 的命名,URL 等通常比较长,
而且21世纪都是宽屏了,所以我们限制最大120字符
**补充说明:HTML 代码不受此规范约束。**
长语句缩进
^^^^^^^^^^^^
编写长语句时,可以使用换行符"\"换行。在这种情况下,下一行应该与上一行的最后一个“.”句点或“=”对齐,或者是缩进4个空格符。
::
this_is_a_very_long(function_call, 'with many parameters') \
.that_returns_an_object_with_an_attribute
MyModel.query.filter(MyModel.scalar > 120) \
.order_by(MyModel.name.desc()) \
.limit(10)
如果你使用括号“()”或花括号“{}”为长语句换行,那么下一行应与括号或花括号对齐:
::
this_is_a_very_long(function_call, 'with many parameters',
23, 42, 'and even more')
对于元素众多的列表或元组,在第一个“[”或“(”之后马上换行:
::
items = [
'this is the first', 'set of items', 'with more items',
'to come in this line', 'like this'
]
.. _Style Guidance: http://www.pocoo.org/internal/styleguide/
空行
^^^^^^
顶层函数与类之间空两行,此外都只空一行。不要在代码中使用太多的空行来区分不同的逻辑模块。
::
def hello(name):
print 'Hello %s!' % name
def goodbye(name):
print 'See you %s.' % name
class MyClass(object):
"""This is a simple docstring."""
def __init__(self, name):
self.name = name
def get_annoying_name(self):
return self.name.upper() + '!!!!111'
语句和表达式
~~~~~~~~~~~~
一般空格规则
^^^^^^^^^^^^
1. 单目运算符与运算对象之间不空格(例如,-,~等),即使单目运算符位于括号内部也一样。
2. 双目运算符与运算对象之间要空格。
::
exp = -1.05
value = (item_value / item_count) * offset / exp
value = my_list[index]
value = my_dict['key']
比较
^^^^
1. 任意类型之间的比较,使用“==”和“!=”。
2. 与单例(singletons)进行比较时,使用 is 和 is not。
3. 永远不要与True或False进行比较(例如,不要这样写:foo ==
False,而应该这样写:not foo)。
否定成员关系检查
^^^^^^^^^^^^^^^^
使用 foo not in bar,而不是 not foo in bar。
命名约定
~~~~~~~~
1. 类名称:采用骆驼拼写法(CamelCase),首字母缩略词保持大写不变(HTTPWriter,而不是 HttpWriter)。
2. 变量名:小写_以及_下划线(lowercase_with_underscores)。
3. 方法与函数名:小写_以及_下划线(lowercase_with_underscores)。
4. 常量:大写_以及_下划线(UPPERCASE_WITH_UNDERSCORES)。
5. 预编译的正则表达式:name_re。
6. 受保护的元素以一个下划线为前缀。双下划线前缀只有定义混入类(mixin classes)时才使用。
7. 如果使用关键词(keywords)作为类名称,应在名称后添加后置下划线(trailing underscore)。
允许与内建变量重名,不要在变量名后添加下划线进行区分。如果函数需要访问重名的内建变量,请将内建变量重新绑定为其他名称。
8. 命名要有寓意, 不使用拼音,不使用无意义简单字母命名 (循环中计数例外 for i in)
9. 命名缩写要谨慎, 尽量是大家认可的缩写
函数和方法的参数:
^^^^^^^^^^^^^^^^^^
1. 类方法:cls 为第一个参数。
2. 实例方法:self 为第一个参数。
3. property函数中使用匿名函数(lambdas)时,匿名函数的第一个参数可以用 x 替代,
例如:display_name = property(lambda x: x.real_name or x.username)。
文档注释(Docstring,即各方法,类的说明文档注释)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
所有文档字符串均以 reStructuredText 格式编写,方便 Sphinx 处理。文档字符串的行数不同,布局也不一样。
如果只有一行,代表字符串结束的三个引号与代表字符串开始的三个引号在同一行。
如果为多行,文档字符串中的文本紧接着代表字符串开始的三个引号编写,代表字符串结束的三个引号则自己独立成一行。
(有能力尽可能用英文, 否则请中文优雅注释)
::
def foo():
"""This is a simple docstring."""
def bar():
"""This is a longer docstring with so much information in there
that it spans three lines. In this case, the closing triple quote
is on its own line.
"""
文档字符串应分成简短摘要(尽量一行)和详细介绍。如果必要的话,摘要与详细介绍之间空一行。
模块头部
~~~~~~~~
模块文件的头部包含有 utf-8 编码声明(如果模块中使用了非 ASCII 编码的字符,建议进行声明),以及标准的文档字符串。
::
# -*- coding: utf-8 -*-
"""
package.module
~~~~~~~~~~~~~~
A brief description goes here.
:copyright: (c) YEAR by AUTHOR.
:license: LICENSE_NAME, see LICENSE_FILE for more details.
"""
注释(Comment)
~~~~~~~~~~~~~~~~
注释的规范与文档字符串编写规范类似。二者均以 reStructuredText 格式编写。
如果使用注释来编写类属性的文档,请在#符号后添加一个冒号“:”。
(有能力尽可能用英文, 否则请中文优雅注释)
::
class User(object):
#: the name of the user as unicode string
name = Column(String)
#: the sha1 hash of the password + inline salt
pw_hash = Column(String)
\ No newline at end of file
快速安装
==========================
Jumpserver 封装了一个 All in one Docker,可以快速启动。该镜像集成了所需要的组件(Windows组件未暂未集成),也支持使用外置 Database 和 Redis
Tips: 不建议在生产中使用, 生产中请使用 详细安装 `CentOS <step_by_step.html>`_ `Ubuntu <setup_by_ubuntu.html>`_
Docker 安装见: `Docker官方安装文档 <https://docs.docker.com/install/>`_
快速启动
```````````````
使用 root 命令行输入::
$ docker run -d -p 8080:80 -p 2222:2222 registry.jumpserver.org/public/jumpserver:1.0.0
访问
```````````````
浏览器访问: http://<容器所在服务器IP>:8080
SSH访问: ssh -p 2222 <容器所在服务器IP>
XShell等工具请添加connection连接
额外环境变量
```````````````
- DB_ENGINE = mysql
- DB_HOST = mysql_host
- DB_PORT = 3306
- DB_USER = xxx
- DB_PASSWORD = xxxx
- DB_NAME = jumpserver
- REDIS_HOST = <redis-host>
- REDIS_PORT = <redis-port>
- REDIS_PASSWORD = <
::
docker run -d -p 8080:80 -p 2222:2222 -e DB_ENGINE=mysql -e DB_HOST=192.168.1.1 -e DB_PORT=3306 -e DB_USER=root -e DB_PASSWORD=xxx -e DB_NAME=jumpserver registry.jumpserver.org/public/jumpserver:1.0.0
仓库地址
```````````````
https://github.com/jumpserver/Dockerfile
命令记录
=========
这里介绍命令记录功能。
点击页面左侧“会话管理”菜单下的“命令记录”,进入命令记录列表页面。
.. contents:: Topics
.. _view_command_session:
查看命令记录
`````````````
命令记录页面默认展示一周内命令记录,页面左上角提供起止时间、用户、资产、系统用户等搜索过滤条件。
.. _detial_command_invoke:
查看命令执行详情
````````````````
点击命令记录列表中需要查看命令执行详情的行,即可显示命令执行详情。
.. _detial_command_session:
转到会话详情
`````````````
在命令记录列表中找到需要转到会话详情的记录,点击“会话”标题下的“转到”按钮,完成转到会话详情。
历史会话
=========
这里介绍历史会话功能。
点击页面左侧“会话管理”菜单下的“历史会话”,进入历史会话列表页面。
.. contents:: Topics
.. _view_history_session:
查看历史会话
`````````````
历史会话页面默认展示一周内历史会话,页面左上角提供起止时间、用户、资产、系统用户等搜索过滤条件。
.. _playback_history_session:
历史会话回放
`````````````
在在线会话列表中找到要回放的历史会话,点击动作标签下的“回放”按钮,弹出回放页面,完成回放历史会话。
\ No newline at end of file
在线会话
=========
这里介绍在线会话功能。
点击页面左侧“会话管理”菜单下的“在线会话”,进入在线会话列表页面。
.. contents:: Topics
.. _view_online_session:
查看在线会话
`````````````
在线会话页面默认展示一周内在线会话,页面左上角提供起止时间、用户、资产、系统用户等搜索过滤条件。
.. _stop_online_session:
终断在线会话
`````````````
在在线会话列表中找到要终止的在线会话,点击动作标签下的“终断”按钮,完成终断在线会话。
终端管理
=========
这里介绍终端管理功能。
点击页面左侧“会话管理”菜单下的“终端管理”,进入终端列表页面。
.. contents:: Topics
.. _view_terminal_session:
查看终端列表
`````````````
终端列表页面默认展示全部终端列表。
.. _upate_terminal_session:
更新终端
`````````````
在在线会话列表中找到要更新的终端,点击动作标签下的“更新”按钮,在更新终端页面填写相关信息,点击提交,完成更新终端。
.. _delete_terminal_session:
删除终端
`````````````
在在线会话列表中找到要删除的终端,点击动作标签下的“删除”按钮,在弹出确认删除页面点击确认,完成删除终端。
Web终端
=========
这里介绍Web终端功能。
点击页面左侧“会话管理”菜单下的“Web终端”,进入Web终端页面。
.. contents:: Topics
.. _login:
主机登录
`````````````
点解页面左侧的”Web终端”,进入主机登录页,然后点击页面右侧的主机IP地址,连接主机,页面右侧会展示当前连接的终端信息。
.. _logout:
主机登出
`````````````
在主机登录页面,选择左上角的“服务器”按钮,出现两个选项,一个“断开链接“按钮,断开当前连接的主机;另一个”断开所有链接“,断开当前所有连接的主机。
Snapshot 截图
+++++++++++++++++
仪表盘
~~~~~~~~
.. image:: _static/img/dash_board.png
用户管理
~~~~~~~~~~
.. image:: _static/img/admin_user.png
资产管理
~~~~~~~~~~
.. image:: _static/img/admin_asset.png
Linux 终端
~~~~~~~~~~~~~
.. image:: _static/img/linux_terminal.png
Windows 终端
~~~~~~~~~~~~~~~~
.. image:: _static/img/windows_terminal.png
一步一步安装
--------------------------
环境
~~~~~~~
- 系统: CentOS 7
- IP: 192.168.244.144
- 关闭 selinux 和防火墙
::
# CentOS 7
$ setenforce 0 # 可以设置配置文件永久关闭
$ systemctl stop iptables.service
$ systemctl stop firewalld.service
# CentOS6
$ setenforce 0
$ service iptables stop
一. 准备 Python3 和 Python 虚拟环境
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**1.1 安装依赖包**
::
$ yum -y install wget sqlite-devel xz gcc automake zlib-devel openssl-devel epel-release git
**1.2 编译安装**
::
$ wget https://www.python.org/ftp/python/3.6.1/Python-3.6.1.tar.xz
$ tar xvf Python-3.6.1.tar.xz && cd Python-3.6.1
$ ./configure && make && make install
**1.3 建立 Python 虚拟环境**
因为 CentOS 6/7 自带的是 Python2,而 Yum 等工具依赖原来的 Python,为了不扰乱原来的环境我们来使用 Python 虚拟环境
::
$ cd /opt
$ python3 -m venv py3
$ source /opt/py3/bin/activate
# 看到下面的提示符代表成功,以后运行 Jumpserver 都要先运行以上 source 命令,以下所有命令均在该虚拟环境中运行
(py3) [root@localhost py3]
二. 安装 Jumpserver 1.0.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**2.1 下载或 Clone 项目**
项目提交较多 git clone 时较大,你可以选择去 Github 项目页面直接下载zip包。
::
$ cd /opt/
$ git clone --depth=1 https://github.com/jumpserver/jumpserver.git && cd jumpserver && git checkout master
**2.2 安装依赖 RPM 包**
::
$ cd /opt/jumpserver/requirements
$ yum -y install $(cat rpm_requirements.txt) # 如果没有任何报错请继续
**2.3 安装 Python 库依赖**
::
$ pip install -r requirements.txt # 不要指定-i参数,因为镜像上可能没有最新的包,如果没有任何报错请继续
**2.4 安装 Redis, Jumpserver 使用 Redis 做 cache 和 celery broke**
::
$ yum -y install redis
$ service redis start
**2.5 安装 MySQL**
本教程使用 Mysql 作为数据库,如果不使用 Mysql 可以跳过相关 Mysql 安装和配置
::
# centos7
$ yum -y install mariadb mariadb-devel mariadb-server # centos7下安装的是mariadb
$ service mariadb start
# centos6
$ yum -y install mysql mysql-devel mysql-server
$ service mysqld start
**2.6 创建数据库 Jumpserver 并授权**
::
$ mysql
> create database jumpserver default charset 'utf8';
> grant all on jumpserver.* to 'jumpserver'@'127.0.0.1' identified by 'somepassword';
**2.7 修改 Jumpserver 配置文件**
::
$ cd /opt/jumpserver
$ cp config_example.py config.py
$ vi config.py # 我们计划修改 DevelopmentConfig中的配置,因为默认jumpserver是使用该配置,它继承自Config
**注意: 配置文件是 Python 格式,不要用 TAB,而要用空格**
::
class DevelopmentConfig(Config):
DEBUG = True
DB_ENGINE = 'mysql'
DB_HOST = '127.0.0.1'
DB_PORT = 3306
DB_USER = 'jumpserver'
DB_PASSWORD = 'somepassword'
DB_NAME = 'jumpserver'
...
config = DevelopmentConfig() # 确保使用的是刚才设置的配置文件
**2.8 生成数据库表结构和初始化数据**
::
$ cd /opt/jumpserver/utils
$ bash make_migrations.sh
**2.9 运行 Jumpserver**
::
$ cd /opt/jumpserver
$ python run_server.py all
运行不报错,请浏览器访问 http://192.168.244.144:8080/
(这里只是 Jumpserver, 没有 Web Terminal,所以访问 Web Terminal 会报错)
账号: admin 密码: admin
三. 安装 SSH Server 和 WebSocket Server: Coco
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**3.1 下载或 Clone 项目**
新开一个终端,连接测试机,别忘了 source /opt/py3/bin/activate
::
$ cd /opt
$ git clone https://github.com/jumpserver/coco.git && cd coco && git checkout master
**3.2 安装依赖**
::
$ cd /opt/coco/requirements
$ yum -y install $(cat rpm_requirements.txt)
$ pip install -r requirements.txt
**3.3 查看配置文件并运行**
::
$ cd /opt/coco
$ cp conf_example.py conf.py
$ python run_server.py
这时需要去 Jumpserver 管理后台-会话管理-终端管理(http://192.168.244.144:8080/terminal/terminal/)接受 Coco 的注册
::
Coco version 0.4.0, more see https://www.jumpserver.org
Starting ssh server at 0.0.0.0:2222
Quit the server with CONTROL-C.
**3.4 测试连接**
::
$ ssh -p2222 admin@192.168.244.144
密码: admin
如果是用在 Windows 下,Xshell Terminal 登录语法如下
$ssh admin@192.168.244.144 2222
密码: admin
如果能登陆代表部署成功
四. 安装 Web Terminal 前端: Luna
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Luna 已改为纯前端,需要 Nginx 来运行访问
访问(https://github.com/jumpserver/luna/releases)下载对应版本的 release 包,直接解压,不需要编译
4.1 解压 Luna
::
$ pwd
/opt/
$ tar xvf luna.tar.gz
$ ls /opt/luna
...
五. 安装 Windows 支持组件
~~~~~~~~~~~~~~~~~~~~~~~~~~
因为手动安装 guacamole 组件比较复杂,这里提供打包好的 docker 使用, 启动 guacamole
.. code:: shell
# 注意:这里一定要改写一下本机的IP地址, 否则会出错
docker run --name jms_guacamole -d \
-p 8081:8080 -v /opt/guacamole/key:/config/guacamole/key \
-e JUMPSERVER_KEY_DIR=/config/guacamole/key \
-e JUMPSERVER_SERVER=http://<填写本机的IP地址>:8080 \
registry.jumpserver.org/public/guacamole:latest
这里所需要注意的是 guacamole 暴露出来的端口是 8081,若与主机上其他端口冲突请自定义一下。
再次强调:修改 JUMPSERVER_SERVER 环境变量的配置,填上 Jumpserver 的内网地址, 这时
去 Jumpserver-会话管理-终端管理 接受[Gua]开头的一个注册
六. 配置 Nginx 整合各组件
~~~~~~~~~~~~~~~~~~~~~~~~~
6.1 安装 Nginx 根据喜好选择安装方式和版本
.. code:: shell
yum -y install nginx
6.2 准备配置文件 修改 /etc/nginx/nginx.conf
::
server {
listen 80;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location /luna/ {
try_files $uri / /index.html;
alias /opt/luna/;
}
location /media/ {
add_header Content-Encoding gzip;
root /opt/jumpserver/data/;
}
location /static/ {
root /opt/jumpserver/data/;
}
location /socket.io/ {
proxy_pass http://localhost:5000/socket.io/;
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /guacamole/ {
proxy_pass http://localhost:8081/;
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
access_log off;
}
location / {
proxy_pass http://localhost:8080;
}
}
6.3 运行 Nginx
::
nginx -t
service nginx start
6.4 访问 http://192.168.244.144
更新升级
-------------
1. 升级 Jumpserver
::
$ git pull && pip install -r requirements/requirements.txt && cd utils && sh make_migrations.sh
2. 升级 Coco
::
$ git pull && cd requirements && pip install -r requirements.txt # 不要指定 -i参数
3. 升级 Luna
重新下载 release 包(https://github.com/jumpserver/luna/releases)
4. 升级 guacamole
::
$ docker pull registry.jumpserver.org/public/guacamole:latest
$ docker stop jms_guacamole # 或者写guacamole的容器ID
$ docker run --name jms_guacamole -d \
-p 8081:8080 -v /opt/guacamole/key:/config/guacamole/key \
-e JUMPSERVER_KEY_DIR=/config/guacamole/key \
-e JUMPSERVER_SERVER=http://<填写本机的IP地址>:8080 \
registry.jumpserver.org/public/guacamole:latest
切换分支或离线升级
-------------------------------
**Jumpserver**
说明: 以下操作,都在jumpserver所在目录运行
1. 备份配置文件
::
$ jumpserver_backup=/tmp/jumpserver_backup
$ mkdir -p $jumpserver_backup
$ cp config.py $jumpserver_backup
2. 备份migrations migrations中存的是数据库表结构的变更,切换分支会丢失
::
$ for app in common users assets ops perms terminal;do
mkdir -p $jumpserver_backup/${app}_migrations
cp apps/${app}/migrations/*.py $jumpserver_backup/${app}_migrations
done
3. 备份数据库,已被不时之需
::
$ mysqldump -u你的数据库账号 -h数据库地址 -p 数据库名称 > $jumpserver_backup/db_backup.sql
4. 备份录像文件
::
$ cp -r data/media $jumpserver_backup/
5. 切换分支或下载离线包, 更新代码
::
$ git checkout master # or other branch
6. 还原配置文件
::
$ cp $jumpserver_backup/config.py .
7. 还原数据库表结构记录
::
$ for app in common users assets ops perms terminal;do
cp $jumpserver_backup/${app}_migrations/*.py ${app}/migrations/
done
8. 还原录像文件
::
$ cp -r $jumpserver_backup/media/* data/media/
9. 更新依赖或表结构
::
$ pip install -r requirements/requirements.txt && cd utils && sh make_migrations.sh
**Coco**
说明: 以下操作都在 coco 项目所在目录
coco是无状态的,备份 keys 目录即可
1. 备份keys
::
$ cp -r keys $jumpserver_backup/
2. 离线更新升级coco
3. 还原 keys目录
::
$ mv keys keys_backup
$ cp -r $jumpserver_backup/keys .
4. 升级依赖
::
$ git pull && cd requirements && pip install -r requirements.txt
**Luna**
直接下载最新Release包替换即可
**Guacamole**
直接参考上面的升级即可, 需要注意的是如果更换机器,请备份
用户列表
========
这里介绍用户列表的功能。
点击页面左侧“用户列表”菜单下的“用户列表“,进入用户列表页面。
.. contents:: Topics
.. _create_user:
创建用户
````````
点击页面左上角“创建用户”按钮,进入创建用户页面,填写账户,角色安全,个人等信息,点击“提交”按钮,用户创建完成。
.. _update_user:
更新用户
````````
点击页面右边的“更新”按钮,进入编辑用户页面,编辑用户信息,点击“提交”按钮,更新用户完成。
.. _delete_user:
删除用户
````````
点击页面右边的“删除”按钮,弹出是否删除确认框,点击“确定”按钮,删除用户完成。
.. _export_user:
导出用户
````````
选中用户,点击右上角的“导出”按钮,导出用户完成。
.. _import_user:
导入用户
````````
点击右上角的“导入”按钮,弹出导入对话框,选择要导入的CSV格式文件,点击“确认”按钮,导入用户完成。
.. _batch_user_operation:
批量操作
````````
选中用户,选择页面左下角的批量操作选项,点击”提交“按钮,批量操作完成。
\ No newline at end of file
个人资产
=========
这里介绍用户个人资产相关的功能。
.. contents:: Topics
.. _view_personal_assets:
查看个人资产
````````````
登录个人用户,默认展示个人资产列表。点击主机名,查看资产的详细信息。
.. _host_login:
主机登录
`````````
点解页面左侧的"Web终端",进入主机登录页,然后点击页面右侧的主机IP地址,连接主机,页面右侧会展示当前连接的终端信息。
.. _host_logout:
主机登出
`````````
在主机登录页面,选择左上角的“服务器”按钮,出现两个选项,一个“断开链接“按钮,断开当前连接的主机;另一个”断开所有链接“,断开当前所有连接的主机。
\ No newline at end of file
用户组列表
============
这里介绍用户组列表的功能。
点击页面左侧“用户管理”菜单下的”用户组“,进入用户组列表页面。
.. contents:: Topics
.. _create_user_group:
创建用户组
``````````
点击页面左上角“创建用户组”按钮,进入创建用户组页面,填写用户组信息,点击“提交”按钮,创建用户完成。
.. _update_user_group:
更新用户组
``````````
点击页面右边的“更新”按钮,进入编辑用户组页面,编辑用户组信息,点击“确认”按钮,更新用户组完成。
.. _delete_user_group:
删除用户组
````````````
点击页面右边的“删除”按钮,弹出删除确认框,点击“确认”按钮,删除用户组完成。
\ No newline at end of file
用户使用文档
=============
这部分给您介绍Jumpserver的用户管理模块的使用方法。
.. toctree::
:maxdepth: 1
user_asset
user_info
\ No newline at end of file
个人信息
=========
这里介绍个人信息相关的功能。
.. contents:: Topics
.. _view_personal_info:
查看个人信息
````````````
点击页面左侧的“个人信息”,查看用户的个人信息、SSH密钥。
.. _modify_personal_info:
修改个人信息
````````````
在个人信息页,点击页面右上角的“设置”按钮,进入个人信息修改页面,填写个人信息,点击“提交”按钮,完成个人信息修改。
.. _update_password:
更新密码
`````````
在个人信息页,点击页面右上角的“重置密码“按钮,进入密码更新页面,填写原来密码、新密码等信息,点击“提交”按钮,完成密码更新。
.. _update_ssh_key:
密钥更新
`````````
在个人信息页,点击页面左上角的“重置SSH密钥“按钮,进入密钥更新页面,填写SSH公钥,点击“提交”按钮,完成密钥更新。
\ No newline at end of file
任务列表
=========
这里介绍任务列表的相关的功能。
.. contents:: Topics
.. _view_asset_works:
查看任务列表
````````````
任务列表页面默认展示一周内所有任务。点击标题可根据当前字段进行排序。
.. _invoke_asset_work:
手动执行任务
````````````
在任务列表中找到要手动执行的任务,点击“动作”标题下的“执行”按钮,完成手动执行当前任务。
.. _delete_asset_work:
删除任务
`````````
在任务列表中找到要删除的任务,点击“动作”标题下的“删除”按钮,完成删除当前任务。
.. _detial_asset_work:
查看任务详情
`````````````
在任务列表中找到要查看的任务,点击要查看的任务名称,即可进入任务详情页面。
...@@ -17,7 +17,7 @@ decorator==4.1.2 ...@@ -17,7 +17,7 @@ decorator==4.1.2
Django==2.1.7 Django==2.1.7
django-auth-ldap==1.7.0 django-auth-ldap==1.7.0
django-bootstrap3==9.1.0 django-bootstrap3==9.1.0
django-celery-beat==1.1.1 django-celery-beat==1.4.0
django-filter==2.0.0 django-filter==2.0.0
django-formtools==2.1 django-formtools==2.1
django-ranged-response==0.2.0 django-ranged-response==0.2.0
...@@ -79,3 +79,5 @@ rest_condition==1.0.3 ...@@ -79,3 +79,5 @@ rest_condition==1.0.3
python-ldap==3.1.0 python-ldap==3.1.0
tencentcloud-sdk-python==3.0.40 tencentcloud-sdk-python==3.0.40
django-radius==1.3.3 django-radius==1.3.3
ipip-ipdb==1.2.1
django-redis-sessions==0.6.1
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment