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-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 %}
This diff is collapsed.
...@@ -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)
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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