mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-06-29 16:27:11 +00:00
commit
96206384c0
1
.gitignore
vendored
1
.gitignore
vendored
@ -39,3 +39,4 @@ logs/*
|
|||||||
.vagrant/
|
.vagrant/
|
||||||
release/*
|
release/*
|
||||||
releashe
|
releashe
|
||||||
|
/apps/script.py
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from .base import BaseACL, BaseACLQuerySet
|
from .base import BaseACL, BaseACLQuerySet
|
||||||
from ..utils import contains_ip
|
from common.utils.ip import contains_ip
|
||||||
|
|
||||||
|
|
||||||
class ACLManager(models.Manager):
|
class ACLManager(models.Manager):
|
||||||
|
@ -3,7 +3,7 @@ from django.db.models import Q
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from orgs.mixins.models import OrgModelMixin, OrgManager
|
from orgs.mixins.models import OrgModelMixin, OrgManager
|
||||||
from .base import BaseACL, BaseACLQuerySet
|
from .base import BaseACL, BaseACLQuerySet
|
||||||
from ..utils import contains_ip
|
from common.utils.ip import contains_ip
|
||||||
|
|
||||||
|
|
||||||
class ACLManager(OrgManager):
|
class ACLManager(OrgManager):
|
||||||
|
@ -3,7 +3,7 @@ from rest_framework import serializers
|
|||||||
from common.drf.serializers import BulkModelSerializer
|
from common.drf.serializers import BulkModelSerializer
|
||||||
from orgs.utils import current_org
|
from orgs.utils import current_org
|
||||||
from ..models import LoginACL
|
from ..models import LoginACL
|
||||||
from ..utils import is_ip_address, is_ip_network, is_ip_segment
|
from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['LoginACLSerializer', ]
|
__all__ = ['LoginACLSerializer', ]
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
from ipaddress import ip_network, ip_address
|
|
||||||
|
|
||||||
|
|
||||||
def is_ip_address(address):
|
|
||||||
""" 192.168.10.1 """
|
|
||||||
try:
|
|
||||||
ip_address(address)
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def is_ip_network(ip):
|
|
||||||
""" 192.168.1.0/24 """
|
|
||||||
try:
|
|
||||||
ip_network(ip)
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def is_ip_segment(ip):
|
|
||||||
""" 10.1.1.1-10.1.1.20 """
|
|
||||||
if '-' not in ip:
|
|
||||||
return False
|
|
||||||
ip_address1, ip_address2 = ip.split('-')
|
|
||||||
return is_ip_address(ip_address1) and is_ip_address(ip_address2)
|
|
||||||
|
|
||||||
|
|
||||||
def in_ip_segment(ip, ip_segment):
|
|
||||||
ip1, ip2 = ip_segment.split('-')
|
|
||||||
ip1 = int(ip_address(ip1))
|
|
||||||
ip2 = int(ip_address(ip2))
|
|
||||||
ip = int(ip_address(ip))
|
|
||||||
return min(ip1, ip2) <= ip <= max(ip1, ip2)
|
|
||||||
|
|
||||||
|
|
||||||
def contains_ip(ip, ip_group):
|
|
||||||
"""
|
|
||||||
ip_group:
|
|
||||||
[192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64.]
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
if '*' in ip_group:
|
|
||||||
return True
|
|
||||||
|
|
||||||
for _ip in ip_group:
|
|
||||||
if is_ip_address(_ip):
|
|
||||||
# 192.168.10.1
|
|
||||||
if ip == _ip:
|
|
||||||
return True
|
|
||||||
elif is_ip_network(_ip) and is_ip_address(ip):
|
|
||||||
# 192.168.1.0/24
|
|
||||||
if ip_address(ip) in ip_network(_ip):
|
|
||||||
return True
|
|
||||||
elif is_ip_segment(_ip) and is_ip_address(ip):
|
|
||||||
# 10.1.1.1-10.1.1.20
|
|
||||||
if in_ip_segment(ip, _ip):
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
# is domain name
|
|
||||||
if ip == _ip:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
@ -1,4 +1,4 @@
|
|||||||
from .application import *
|
from .application import *
|
||||||
from .application_user import *
|
from .account import *
|
||||||
from .mixin import *
|
from .mixin import *
|
||||||
from .remote_app import *
|
from .remote_app import *
|
||||||
|
70
apps/applications/api/account.py
Normal file
70
apps/applications/api/account.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
#
|
||||||
|
|
||||||
|
from django_filters import rest_framework as filters
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db.models import F, Value, CharField
|
||||||
|
from django.db.models.functions import Concat
|
||||||
|
from django.http import Http404
|
||||||
|
|
||||||
|
from common.drf.filters import BaseFilterSet
|
||||||
|
from common.drf.api import JMSModelViewSet
|
||||||
|
from common.utils import unique
|
||||||
|
from perms.models import ApplicationPermission
|
||||||
|
from ..hands import IsOrgAdminOrAppUser, IsOrgAdmin, NeedMFAVerify
|
||||||
|
from .. import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class AccountFilterSet(BaseFilterSet):
|
||||||
|
username = filters.CharFilter(field_name='username')
|
||||||
|
app = filters.CharFilter(field_name='applications', lookup_expr='exact')
|
||||||
|
app_name = filters.CharFilter(field_name='app_name', lookup_expr='exact')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ApplicationPermission
|
||||||
|
fields = ['type', 'category']
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationAccountViewSet(JMSModelViewSet):
|
||||||
|
permission_classes = (IsOrgAdmin, )
|
||||||
|
search_fields = ['username', 'app_name']
|
||||||
|
filterset_class = AccountFilterSet
|
||||||
|
filterset_fields = ['username', 'app_name', 'type', 'category']
|
||||||
|
serializer_class = serializers.ApplicationAccountSerializer
|
||||||
|
|
||||||
|
http_method_names = ['get', 'put', 'patch', 'options']
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = ApplicationPermission.objects.all() \
|
||||||
|
.annotate(uid=Concat(
|
||||||
|
'applications', Value('_'), 'system_users', output_field=CharField()
|
||||||
|
)) \
|
||||||
|
.annotate(systemuser=F('system_users')) \
|
||||||
|
.annotate(systemuser_display=F('system_users__name')) \
|
||||||
|
.annotate(username=F('system_users__username')) \
|
||||||
|
.annotate(password=F('system_users__password')) \
|
||||||
|
.annotate(app=F('applications')) \
|
||||||
|
.annotate(app_name=F("applications__name")) \
|
||||||
|
.values('username', 'password', 'systemuser', 'systemuser_display',
|
||||||
|
'app', 'app_name', 'category', 'type', 'uid')
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
obj = self.get_queryset().filter(
|
||||||
|
uid=self.kwargs['pk']
|
||||||
|
).first()
|
||||||
|
if not obj:
|
||||||
|
raise Http404()
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def filter_queryset(self, queryset):
|
||||||
|
queryset = super().filter_queryset(queryset)
|
||||||
|
queryset_list = unique(queryset, key=lambda x: (x['app'], x['systemuser']))
|
||||||
|
return queryset_list
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationAccountSecretViewSet(ApplicationAccountViewSet):
|
||||||
|
serializer_class = serializers.ApplicationAccountSecretSerializer
|
||||||
|
permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
|
||||||
|
http_method_names = ['get', 'options']
|
||||||
|
|
@ -2,7 +2,10 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from common.tree import TreeNodeSerializer
|
||||||
from ..hands import IsOrgAdminOrAppUser
|
from ..hands import IsOrgAdminOrAppUser
|
||||||
from .. import serializers
|
from .. import serializers
|
||||||
from ..models import Application
|
from ..models import Application
|
||||||
@ -13,7 +16,22 @@ __all__ = ['ApplicationViewSet']
|
|||||||
|
|
||||||
class ApplicationViewSet(OrgBulkModelViewSet):
|
class ApplicationViewSet(OrgBulkModelViewSet):
|
||||||
model = Application
|
model = Application
|
||||||
filterset_fields = ('name', 'type', 'category')
|
filterset_fields = {
|
||||||
search_fields = filterset_fields
|
'name': ['exact'],
|
||||||
|
'category': ['exact'],
|
||||||
|
'type': ['exact', 'in'],
|
||||||
|
}
|
||||||
|
search_fields = ('name', 'type', 'category')
|
||||||
permission_classes = (IsOrgAdminOrAppUser,)
|
permission_classes = (IsOrgAdminOrAppUser,)
|
||||||
serializer_class = serializers.ApplicationSerializer
|
serializer_classes = {
|
||||||
|
'default': serializers.ApplicationSerializer,
|
||||||
|
'get_tree': TreeNodeSerializer
|
||||||
|
}
|
||||||
|
|
||||||
|
@action(methods=['GET'], detail=False, url_path='tree')
|
||||||
|
def get_tree(self, request, *args, **kwargs):
|
||||||
|
show_count = request.query_params.get('show_count', '1') == '1'
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
tree_nodes = Application.create_tree_nodes(queryset, show_count=show_count)
|
||||||
|
serializer = self.get_serializer(tree_nodes, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
# coding: utf-8
|
|
||||||
#
|
|
||||||
|
|
||||||
from rest_framework import generics
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from ..hands import IsOrgAdminOrAppUser, IsOrgAdmin, NeedMFAVerify
|
|
||||||
from .. import serializers
|
|
||||||
from ..models import Application, ApplicationUser
|
|
||||||
from perms.models import ApplicationPermission
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationUserListApi(generics.ListAPIView):
|
|
||||||
permission_classes = (IsOrgAdmin, )
|
|
||||||
filterset_fields = ('name', 'username')
|
|
||||||
search_fields = filterset_fields
|
|
||||||
serializer_class = serializers.ApplicationUserSerializer
|
|
||||||
_application = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def application(self):
|
|
||||||
if self._application is None:
|
|
||||||
app_id = self.request.query_params.get('application_id')
|
|
||||||
if app_id:
|
|
||||||
self._application = Application.objects.get(id=app_id)
|
|
||||||
return self._application
|
|
||||||
|
|
||||||
def get_serializer_context(self):
|
|
||||||
context = super().get_serializer_context()
|
|
||||||
context.update({
|
|
||||||
'application': self.application
|
|
||||||
})
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
queryset = ApplicationUser.objects.none()
|
|
||||||
if not self.application:
|
|
||||||
return queryset
|
|
||||||
system_user_ids = ApplicationPermission.objects.filter(applications=self.application)\
|
|
||||||
.values_list('system_users', flat=True)
|
|
||||||
if not system_user_ids:
|
|
||||||
return queryset
|
|
||||||
queryset = ApplicationUser.objects.filter(id__in=system_user_ids)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationUserAuthInfoListApi(ApplicationUserListApi):
|
|
||||||
serializer_class = serializers.ApplicationUserWithAuthInfoSerializer
|
|
||||||
http_method_names = ['get']
|
|
||||||
permission_classes = [IsOrgAdminOrAppUser]
|
|
||||||
|
|
||||||
def get_permissions(self):
|
|
||||||
if settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
|
||||||
self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
|
|
||||||
return super().get_permissions()
|
|
@ -1,89 +1,53 @@
|
|||||||
from orgs.models import Organization
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
from common.tree import TreeNode
|
||||||
|
from orgs.models import Organization
|
||||||
|
from ..models import Application
|
||||||
|
|
||||||
__all__ = ['SerializeApplicationToTreeNodeMixin']
|
__all__ = ['SerializeApplicationToTreeNodeMixin']
|
||||||
|
|
||||||
|
|
||||||
class SerializeApplicationToTreeNodeMixin:
|
class SerializeApplicationToTreeNodeMixin:
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _serialize_db(db):
|
|
||||||
return {
|
|
||||||
'id': db.id,
|
|
||||||
'name': db.name,
|
|
||||||
'title': db.name,
|
|
||||||
'pId': '',
|
|
||||||
'open': False,
|
|
||||||
'iconSkin': 'database',
|
|
||||||
'meta': {'type': 'database_app'}
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _serialize_remote_app(remote_app):
|
|
||||||
return {
|
|
||||||
'id': remote_app.id,
|
|
||||||
'name': remote_app.name,
|
|
||||||
'title': remote_app.name,
|
|
||||||
'pId': '',
|
|
||||||
'open': False,
|
|
||||||
'isParent': False,
|
|
||||||
'iconSkin': 'chrome',
|
|
||||||
'meta': {'type': 'remote_app'}
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _serialize_cloud(cloud):
|
|
||||||
return {
|
|
||||||
'id': cloud.id,
|
|
||||||
'name': cloud.name,
|
|
||||||
'title': cloud.name,
|
|
||||||
'pId': '',
|
|
||||||
'open': False,
|
|
||||||
'isParent': False,
|
|
||||||
'iconSkin': 'k8s',
|
|
||||||
'meta': {'type': 'k8s_app'}
|
|
||||||
}
|
|
||||||
|
|
||||||
def _serialize_application(self, application):
|
|
||||||
method_name = f'_serialize_{application.category}'
|
|
||||||
data = getattr(self, method_name)(application)
|
|
||||||
data.update({
|
|
||||||
'pId': application.org.id,
|
|
||||||
'org_name': application.org_name
|
|
||||||
})
|
|
||||||
return data
|
|
||||||
|
|
||||||
def serialize_applications(self, applications):
|
|
||||||
data = [self._serialize_application(application) for application in applications]
|
|
||||||
return data
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _serialize_organization(org):
|
|
||||||
return {
|
|
||||||
'id': org.id,
|
|
||||||
'name': org.name,
|
|
||||||
'title': org.name,
|
|
||||||
'pId': '',
|
|
||||||
'open': True,
|
|
||||||
'isParent': True,
|
|
||||||
'meta': {
|
|
||||||
'type': 'node'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def serialize_organizations(self, organizations):
|
|
||||||
data = [self._serialize_organization(org) for org in organizations]
|
|
||||||
return data
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def filter_organizations(applications):
|
def filter_organizations(applications):
|
||||||
organization_ids = set(applications.values_list('org_id', flat=True))
|
organization_ids = set(applications.values_list('org_id', flat=True))
|
||||||
organizations = [Organization.get_instance(org_id) for org_id in organization_ids]
|
organizations = [Organization.get_instance(org_id) for org_id in organization_ids]
|
||||||
|
organizations.sort(key=lambda x: x.name)
|
||||||
return organizations
|
return organizations
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_root_node():
|
||||||
|
name = _('My applications')
|
||||||
|
node = TreeNode(**{
|
||||||
|
'id': 'applications',
|
||||||
|
'name': name,
|
||||||
|
'title': name,
|
||||||
|
'pId': '',
|
||||||
|
'open': True,
|
||||||
|
'isParent': True,
|
||||||
|
'meta': {
|
||||||
|
'type': 'root'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return node
|
||||||
|
|
||||||
def serialize_applications_with_org(self, applications):
|
def serialize_applications_with_org(self, applications):
|
||||||
|
root_node = self.create_root_node()
|
||||||
|
tree_nodes = [root_node]
|
||||||
organizations = self.filter_organizations(applications)
|
organizations = self.filter_organizations(applications)
|
||||||
data_organizations = self.serialize_organizations(organizations)
|
|
||||||
data_applications = self.serialize_applications(applications)
|
for i, org in enumerate(organizations):
|
||||||
data = data_organizations + data_applications
|
# 组织节点
|
||||||
return data
|
org_node = org.as_tree_node(pid=root_node.id)
|
||||||
|
tree_nodes.append(org_node)
|
||||||
|
org_applications = applications.filter(org_id=org.id)
|
||||||
|
count = org_applications.count()
|
||||||
|
org_node.name += '({})'.format(count)
|
||||||
|
|
||||||
|
# 各应用节点
|
||||||
|
apps_nodes = Application.create_tree_nodes(
|
||||||
|
queryset=org_applications, root_node=org_node,
|
||||||
|
show_empty=False
|
||||||
|
)
|
||||||
|
tree_nodes += apps_nodes
|
||||||
|
return tree_nodes
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
#
|
#
|
||||||
|
|
||||||
from django.db.models import TextChoices
|
from django.db.models import TextChoices
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class ApplicationCategoryChoices(TextChoices):
|
class AppCategory(TextChoices):
|
||||||
db = 'db', _('Database')
|
db = 'db', _('Database')
|
||||||
remote_app = 'remote_app', _('Remote app')
|
remote_app = 'remote_app', _('Remote app')
|
||||||
cloud = 'cloud', 'Cloud'
|
cloud = 'cloud', 'Cloud'
|
||||||
@ -15,7 +14,7 @@ class ApplicationCategoryChoices(TextChoices):
|
|||||||
return dict(cls.choices).get(category, '')
|
return dict(cls.choices).get(category, '')
|
||||||
|
|
||||||
|
|
||||||
class ApplicationTypeChoices(TextChoices):
|
class AppType(TextChoices):
|
||||||
# db category
|
# db category
|
||||||
mysql = 'mysql', 'MySQL'
|
mysql = 'mysql', 'MySQL'
|
||||||
oracle = 'oracle', 'Oracle'
|
oracle = 'oracle', 'Oracle'
|
||||||
@ -31,19 +30,38 @@ class ApplicationTypeChoices(TextChoices):
|
|||||||
# cloud category
|
# cloud category
|
||||||
k8s = 'k8s', 'Kubernetes'
|
k8s = 'k8s', 'Kubernetes'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def category_types_mapper(cls):
|
||||||
|
return {
|
||||||
|
AppCategory.db: [cls.mysql, cls.oracle, cls.pgsql, cls.mariadb],
|
||||||
|
AppCategory.remote_app: [cls.chrome, cls.mysql_workbench, cls.vmware_client, cls.custom],
|
||||||
|
AppCategory.cloud: [cls.k8s]
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def type_category_mapper(cls):
|
||||||
|
mapper = {}
|
||||||
|
for category, tps in cls.category_types_mapper().items():
|
||||||
|
for tp in tps:
|
||||||
|
mapper[tp] = category
|
||||||
|
return mapper
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_label(cls, tp):
|
def get_label(cls, tp):
|
||||||
return dict(cls.choices).get(tp, '')
|
return dict(cls.choices).get(tp, '')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def db_types(cls):
|
def db_types(cls):
|
||||||
return [cls.mysql.value, cls.oracle.value, cls.pgsql.value, cls.mariadb.value]
|
return [tp.value for tp in cls.category_types_mapper()[AppCategory.db]]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def remote_app_types(cls):
|
def remote_app_types(cls):
|
||||||
return [cls.chrome.value, cls.mysql_workbench.value, cls.vmware_client.value, cls.custom.value]
|
return [tp.value for tp in cls.category_types_mapper()[AppCategory.remote_app]]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def cloud_types(cls):
|
def cloud_types(cls):
|
||||||
return [cls.k8s.value]
|
return [tp.value for tp in cls.category_types_mapper()[AppCategory.cloud]]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
0
apps/applications/models/account.py
Normal file
0
apps/applications/models/account.py
Normal file
@ -1,19 +1,174 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orgs.mixins.models import OrgModelMixin
|
from orgs.mixins.models import OrgModelMixin
|
||||||
from common.mixins import CommonModelMixin
|
from common.mixins import CommonModelMixin
|
||||||
|
from common.tree import TreeNode
|
||||||
from assets.models import Asset, SystemUser
|
from assets.models import Asset, SystemUser
|
||||||
from .. import const
|
from .. import const
|
||||||
|
|
||||||
|
|
||||||
class Application(CommonModelMixin, OrgModelMixin):
|
class ApplicationTreeNodeMixin:
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
type: str
|
||||||
|
category: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_choice_node(cls, c, id_, pid, tp, opened=False, counts=None,
|
||||||
|
show_empty=True, show_count=True):
|
||||||
|
count = counts.get(c.value, 0)
|
||||||
|
if count == 0 and not show_empty:
|
||||||
|
return None
|
||||||
|
label = c.label
|
||||||
|
if count is not None and show_count:
|
||||||
|
label = '{} ({})'.format(label, count)
|
||||||
|
data = {
|
||||||
|
'id': id_,
|
||||||
|
'name': label,
|
||||||
|
'title': label,
|
||||||
|
'pId': pid,
|
||||||
|
'isParent': bool(count),
|
||||||
|
'open': opened,
|
||||||
|
'iconSkin': '',
|
||||||
|
'meta': {
|
||||||
|
'type': tp,
|
||||||
|
'data': {
|
||||||
|
'name': c.name,
|
||||||
|
'value': c.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TreeNode(**data)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_root_tree_node(cls, queryset, show_count=True):
|
||||||
|
count = queryset.count() if show_count else None
|
||||||
|
root_id = 'applications'
|
||||||
|
root_name = _('Applications')
|
||||||
|
if count is not None and show_count:
|
||||||
|
root_name = '{} ({})'.format(root_name, count)
|
||||||
|
node = TreeNode(**{
|
||||||
|
'id': root_id,
|
||||||
|
'name': root_name,
|
||||||
|
'title': root_name,
|
||||||
|
'pId': '',
|
||||||
|
'isParent': True,
|
||||||
|
'open': True,
|
||||||
|
'iconSkin': '',
|
||||||
|
'meta': {
|
||||||
|
'type': 'applications_root',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return node
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_category_tree_nodes(cls, root_node, counts=None, show_empty=True, show_count=True):
|
||||||
|
nodes = []
|
||||||
|
categories = const.AppType.category_types_mapper().keys()
|
||||||
|
for category in categories:
|
||||||
|
i = root_node.id + '_' + category.value
|
||||||
|
node = cls.create_choice_node(
|
||||||
|
category, i, pid=root_node.id, tp='category',
|
||||||
|
counts=counts, opened=False, show_empty=show_empty,
|
||||||
|
show_count=show_count
|
||||||
|
)
|
||||||
|
if not node:
|
||||||
|
continue
|
||||||
|
nodes.append(node)
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_types_tree_nodes(cls, root_node, counts, show_empty=True, show_count=True):
|
||||||
|
nodes = []
|
||||||
|
type_category_mapper = const.AppType.type_category_mapper()
|
||||||
|
for tp in const.AppType.type_category_mapper().keys():
|
||||||
|
category = type_category_mapper.get(tp)
|
||||||
|
pid = root_node.id + '_' + category.value
|
||||||
|
i = root_node.id + '_' + tp.value
|
||||||
|
node = cls.create_choice_node(
|
||||||
|
tp, i, pid, tp='type', counts=counts, opened=False,
|
||||||
|
show_empty=show_empty, show_count=show_count
|
||||||
|
)
|
||||||
|
if not node:
|
||||||
|
continue
|
||||||
|
nodes.append(node)
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_tree_node_counts(queryset):
|
||||||
|
counts = defaultdict(int)
|
||||||
|
values = queryset.values_list('type', 'category')
|
||||||
|
for i in values:
|
||||||
|
tp = i[0]
|
||||||
|
category = i[1]
|
||||||
|
counts[tp] += 1
|
||||||
|
counts[category] += 1
|
||||||
|
return counts
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_tree_nodes(cls, queryset, root_node=None, show_empty=True, show_count=True):
|
||||||
|
counts = cls.get_tree_node_counts(queryset)
|
||||||
|
tree_nodes = []
|
||||||
|
|
||||||
|
# 根节点有可能是组织名称
|
||||||
|
if root_node is None:
|
||||||
|
root_node = cls.create_root_tree_node(queryset, show_count=show_count)
|
||||||
|
tree_nodes.append(root_node)
|
||||||
|
|
||||||
|
# 类别的节点
|
||||||
|
tree_nodes += cls.create_category_tree_nodes(
|
||||||
|
root_node, counts, show_empty=show_empty,
|
||||||
|
show_count=show_count
|
||||||
|
)
|
||||||
|
|
||||||
|
# 类型的节点
|
||||||
|
tree_nodes += cls.create_types_tree_nodes(
|
||||||
|
root_node, counts, show_empty=show_empty,
|
||||||
|
show_count=show_count
|
||||||
|
)
|
||||||
|
|
||||||
|
# 应用的节点
|
||||||
|
for app in queryset:
|
||||||
|
pid = root_node.id + '_' + app.type
|
||||||
|
tree_nodes.append(app.as_tree_node(pid))
|
||||||
|
return tree_nodes
|
||||||
|
|
||||||
|
def as_tree_node(self, pid):
|
||||||
|
icon_skin_category_mapper = {
|
||||||
|
'remote_app': 'chrome',
|
||||||
|
'db': 'database',
|
||||||
|
'cloud': 'cloud'
|
||||||
|
}
|
||||||
|
icon_skin = icon_skin_category_mapper.get(self.category, 'file')
|
||||||
|
node = TreeNode(**{
|
||||||
|
'id': str(self.id),
|
||||||
|
'name': self.name,
|
||||||
|
'title': self.name,
|
||||||
|
'pId': pid,
|
||||||
|
'isParent': False,
|
||||||
|
'open': False,
|
||||||
|
'iconSkin': icon_skin,
|
||||||
|
'meta': {
|
||||||
|
'type': 'application',
|
||||||
|
'data': {
|
||||||
|
'category': self.category,
|
||||||
|
'type': self.type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
|
||||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||||
category = models.CharField(
|
category = models.CharField(
|
||||||
max_length=16, choices=const.ApplicationCategoryChoices.choices, verbose_name=_('Category')
|
max_length=16, choices=const.AppCategory.choices, verbose_name=_('Category')
|
||||||
)
|
)
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
max_length=16, choices=const.ApplicationTypeChoices.choices, verbose_name=_('Type')
|
max_length=16, choices=const.AppType.choices, verbose_name=_('Type')
|
||||||
)
|
)
|
||||||
domain = models.ForeignKey(
|
domain = models.ForeignKey(
|
||||||
'assets.Domain', null=True, blank=True, related_name='applications',
|
'assets.Domain', null=True, blank=True, related_name='applications',
|
||||||
@ -35,7 +190,7 @@ class Application(CommonModelMixin, OrgModelMixin):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def category_remote_app(self):
|
def category_remote_app(self):
|
||||||
return self.category == const.ApplicationCategoryChoices.remote_app.value
|
return self.category == const.AppCategory.remote_app.value
|
||||||
|
|
||||||
def get_rdp_remote_app_setting(self):
|
def get_rdp_remote_app_setting(self):
|
||||||
from applications.serializers.attrs import get_serializer_class_by_application_type
|
from applications.serializers.attrs import get_serializer_class_by_application_type
|
||||||
|
@ -6,12 +6,12 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
from common.drf.serializers import MethodSerializer
|
from common.drf.serializers import MethodSerializer
|
||||||
from .attrs import category_serializer_classes_mapping, type_serializer_classes_mapping
|
from .attrs import category_serializer_classes_mapping, type_serializer_classes_mapping
|
||||||
from assets.serializers import SystemUserSerializer
|
|
||||||
from .. import models
|
from .. import models
|
||||||
|
from .. import const
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'ApplicationSerializer', 'ApplicationSerializerMixin',
|
'ApplicationSerializer', 'ApplicationSerializerMixin',
|
||||||
'ApplicationUserSerializer', 'ApplicationUserWithAuthInfoSerializer'
|
'ApplicationAccountSerializer', 'ApplicationAccountSecretSerializer'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -45,16 +45,15 @@ class ApplicationSerializerMixin(serializers.Serializer):
|
|||||||
|
|
||||||
|
|
||||||
class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSerializer):
|
class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSerializer):
|
||||||
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category(Display)'))
|
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category display'))
|
||||||
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type(Dispaly)'))
|
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Application
|
model = models.Application
|
||||||
fields_mini = ['id', 'name']
|
fields_mini = ['id', 'name']
|
||||||
fields_small = fields_mini + [
|
fields_small = fields_mini + [
|
||||||
'category', 'category_display', 'type', 'type_display', 'attrs',
|
'category', 'category_display', 'type', 'type_display',
|
||||||
'date_created', 'date_updated',
|
'attrs', 'date_created', 'date_updated', 'created_by', 'comment'
|
||||||
'created_by', 'comment'
|
|
||||||
]
|
]
|
||||||
fields_fk = ['domain']
|
fields_fk = ['domain']
|
||||||
fields = fields_small + fields_fk
|
fields = fields_small + fields_fk
|
||||||
@ -68,41 +67,34 @@ class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSeri
|
|||||||
return _attrs
|
return _attrs
|
||||||
|
|
||||||
|
|
||||||
class ApplicationUserSerializer(SystemUserSerializer):
|
class ApplicationAccountSerializer(serializers.Serializer):
|
||||||
application_name = serializers.SerializerMethodField(label=_('Application name'))
|
username = serializers.ReadOnlyField(label=_("Username"))
|
||||||
application_category = serializers.SerializerMethodField(label=_('Application category'))
|
password = serializers.CharField(write_only=True, label=_("Password"))
|
||||||
application_type = serializers.SerializerMethodField(label=_('Application type'))
|
systemuser = serializers.ReadOnlyField(label=_('System user'))
|
||||||
|
systemuser_display = serializers.ReadOnlyField(label=_("System user display"))
|
||||||
|
app = serializers.ReadOnlyField(label=_('App'))
|
||||||
|
uid = serializers.ReadOnlyField(label=_("Union id"))
|
||||||
|
app_name = serializers.ReadOnlyField(label=_("Application name"), read_only=True)
|
||||||
|
category = serializers.ChoiceField(label=_('Category'), choices=const.AppCategory.choices, read_only=True)
|
||||||
|
category_display = serializers.SerializerMethodField(label=_('Category display'))
|
||||||
|
type = serializers.ChoiceField(label=_('Type'), choices=const.AppType.choices, read_only=True)
|
||||||
|
type_display = serializers.SerializerMethodField(label=_('Type display'))
|
||||||
|
|
||||||
class Meta(SystemUserSerializer.Meta):
|
category_mapper = dict(const.AppCategory.choices)
|
||||||
model = models.ApplicationUser
|
type_mapper = dict(const.AppType.choices)
|
||||||
fields_mini = [
|
|
||||||
'id', 'application_name', 'application_category', 'application_type', 'name', 'username'
|
|
||||||
]
|
|
||||||
fields_small = fields_mini + [
|
|
||||||
'protocol', 'login_mode', 'login_mode_display', 'priority',
|
|
||||||
"username_same_with_user", 'comment',
|
|
||||||
]
|
|
||||||
fields = fields_small
|
|
||||||
extra_kwargs = {
|
|
||||||
'login_mode_display': {'label': _('Login mode display')},
|
|
||||||
'created_by': {'read_only': True},
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
def create(self, validated_data):
|
||||||
def application(self):
|
pass
|
||||||
return self.context['application']
|
|
||||||
|
|
||||||
def get_application_name(self, obj):
|
def update(self, instance, validated_data):
|
||||||
return self.application.name
|
pass
|
||||||
|
|
||||||
def get_application_category(self, obj):
|
def get_category_display(self, obj):
|
||||||
return self.application.get_category_display()
|
return self.category_mapper.get(obj['category'])
|
||||||
|
|
||||||
def get_application_type(self, obj):
|
def get_type_display(self, obj):
|
||||||
return self.application.get_type_display()
|
return self.type_mapper.get(obj['type'])
|
||||||
|
|
||||||
|
|
||||||
class ApplicationUserWithAuthInfoSerializer(ApplicationUserSerializer):
|
class ApplicationAccountSecretSerializer(ApplicationAccountSerializer):
|
||||||
|
password = serializers.CharField(write_only=False, label=_("Password"))
|
||||||
class Meta(ApplicationUserSerializer.Meta):
|
|
||||||
fields = ApplicationUserSerializer.Meta.fields + ['password', 'token']
|
|
||||||
|
@ -5,7 +5,7 @@ from rest_framework import serializers
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
from common.utils import get_logger, is_uuid
|
from common.utils import get_logger, is_uuid, get_object_or_none
|
||||||
from assets.models import Asset
|
from assets.models import Asset
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
@ -14,22 +14,26 @@ logger = get_logger(__file__)
|
|||||||
__all__ = ['RemoteAppSerializer']
|
__all__ = ['RemoteAppSerializer']
|
||||||
|
|
||||||
|
|
||||||
class CharPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
|
class AssetCharPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
instance = super().to_internal_value(data)
|
instance = super().to_internal_value(data)
|
||||||
return str(instance.id)
|
return str(instance.id)
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, _id):
|
||||||
# value is instance.id
|
# _id 是 instance.id
|
||||||
if self.pk_field is not None:
|
if self.pk_field is not None:
|
||||||
return self.pk_field.to_representation(value)
|
return self.pk_field.to_representation(_id)
|
||||||
return value
|
# 解决删除资产后,远程应用更新页面会显示资产ID的问题
|
||||||
|
asset = get_object_or_none(Asset, id=_id)
|
||||||
|
if asset:
|
||||||
|
return None
|
||||||
|
return _id
|
||||||
|
|
||||||
|
|
||||||
class RemoteAppSerializer(serializers.Serializer):
|
class RemoteAppSerializer(serializers.Serializer):
|
||||||
asset_info = serializers.SerializerMethodField()
|
asset_info = serializers.SerializerMethodField()
|
||||||
asset = CharPrimaryKeyRelatedField(
|
asset = AssetCharPrimaryKeyRelatedField(
|
||||||
queryset=Asset.objects, required=False, label=_("Asset"), allow_null=True
|
queryset=Asset.objects, required=False, label=_("Asset"), allow_null=True
|
||||||
)
|
)
|
||||||
path = serializers.CharField(
|
path = serializers.CharField(
|
||||||
|
@ -14,9 +14,9 @@ __all__ = [
|
|||||||
# ---------------------------------------------------
|
# ---------------------------------------------------
|
||||||
|
|
||||||
category_serializer_classes_mapping = {
|
category_serializer_classes_mapping = {
|
||||||
const.ApplicationCategoryChoices.db.value: application_category.DBSerializer,
|
const.AppCategory.db.value: application_category.DBSerializer,
|
||||||
const.ApplicationCategoryChoices.remote_app.value: application_category.RemoteAppSerializer,
|
const.AppCategory.remote_app.value: application_category.RemoteAppSerializer,
|
||||||
const.ApplicationCategoryChoices.cloud.value: application_category.CloudSerializer,
|
const.AppCategory.cloud.value: application_category.CloudSerializer,
|
||||||
}
|
}
|
||||||
|
|
||||||
# define `attrs` field `type serializers mapping`
|
# define `attrs` field `type serializers mapping`
|
||||||
@ -24,17 +24,17 @@ category_serializer_classes_mapping = {
|
|||||||
|
|
||||||
type_serializer_classes_mapping = {
|
type_serializer_classes_mapping = {
|
||||||
# db
|
# db
|
||||||
const.ApplicationTypeChoices.mysql.value: application_type.MySQLSerializer,
|
const.AppType.mysql.value: application_type.MySQLSerializer,
|
||||||
const.ApplicationTypeChoices.mariadb.value: application_type.MariaDBSerializer,
|
const.AppType.mariadb.value: application_type.MariaDBSerializer,
|
||||||
const.ApplicationTypeChoices.oracle.value: application_type.OracleSerializer,
|
const.AppType.oracle.value: application_type.OracleSerializer,
|
||||||
const.ApplicationTypeChoices.pgsql.value: application_type.PostgreSerializer,
|
const.AppType.pgsql.value: application_type.PostgreSerializer,
|
||||||
# remote-app
|
# remote-app
|
||||||
const.ApplicationTypeChoices.chrome.value: application_type.ChromeSerializer,
|
const.AppType.chrome.value: application_type.ChromeSerializer,
|
||||||
const.ApplicationTypeChoices.mysql_workbench.value: application_type.MySQLWorkbenchSerializer,
|
const.AppType.mysql_workbench.value: application_type.MySQLWorkbenchSerializer,
|
||||||
const.ApplicationTypeChoices.vmware_client.value: application_type.VMwareClientSerializer,
|
const.AppType.vmware_client.value: application_type.VMwareClientSerializer,
|
||||||
const.ApplicationTypeChoices.custom.value: application_type.CustomSerializer,
|
const.AppType.custom.value: application_type.CustomSerializer,
|
||||||
# cloud
|
# cloud
|
||||||
const.ApplicationTypeChoices.k8s.value: application_type.K8SSerializer
|
const.AppType.k8s.value: application_type.K8SSerializer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,12 +10,14 @@ app_name = 'applications'
|
|||||||
|
|
||||||
router = BulkRouter()
|
router = BulkRouter()
|
||||||
router.register(r'applications', api.ApplicationViewSet, 'application')
|
router.register(r'applications', api.ApplicationViewSet, 'application')
|
||||||
|
router.register(r'accounts', api.ApplicationAccountViewSet, 'application-account')
|
||||||
|
router.register(r'account-secrets', api.ApplicationAccountSecretViewSet, 'application-account-secret')
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'),
|
path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'),
|
||||||
path('application-users/', api.ApplicationUserListApi.as_view(), name='application-user'),
|
# path('accounts/', api.ApplicationAccountViewSet.as_view(), name='application-account'),
|
||||||
path('application-user-auth-infos/', api.ApplicationUserAuthInfoListApi.as_view(), name='application-user-auth-info')
|
# path('account-secrets/', api.ApplicationAccountSecretViewSet.as_view(), name='application-account-secret')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
from django.conf import settings
|
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from django_filters import rest_framework as filters
|
from django_filters import rest_framework as filters
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from rest_framework.generics import CreateAPIView
|
from rest_framework.generics import CreateAPIView
|
||||||
|
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, NeedMFAVerify
|
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, NeedMFAVerify
|
||||||
from common.drf.filters import BaseFilterSet
|
from common.drf.filters import BaseFilterSet
|
||||||
from ..tasks.account_connectivity import test_accounts_connectivity_manual
|
from ..tasks.account_connectivity import test_accounts_connectivity_manual
|
||||||
from ..models import AuthBook
|
from ..models import AuthBook, Node
|
||||||
from .. import serializers
|
from .. import serializers
|
||||||
|
|
||||||
__all__ = ['AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI']
|
__all__ = ['AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI']
|
||||||
@ -19,11 +19,13 @@ class AccountFilterSet(BaseFilterSet):
|
|||||||
username = filters.CharFilter(method='do_nothing')
|
username = filters.CharFilter(method='do_nothing')
|
||||||
ip = filters.CharFilter(field_name='ip', lookup_expr='exact')
|
ip = filters.CharFilter(field_name='ip', lookup_expr='exact')
|
||||||
hostname = filters.CharFilter(field_name='hostname', lookup_expr='exact')
|
hostname = filters.CharFilter(field_name='hostname', lookup_expr='exact')
|
||||||
|
node = filters.CharFilter(method='do_nothing')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def qs(self):
|
def qs(self):
|
||||||
qs = super().qs
|
qs = super().qs
|
||||||
qs = self.filter_username(qs)
|
qs = self.filter_username(qs)
|
||||||
|
qs = self.filter_node(qs)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def filter_username(self, qs):
|
def filter_username(self, qs):
|
||||||
@ -33,6 +35,16 @@ class AccountFilterSet(BaseFilterSet):
|
|||||||
qs = qs.filter(Q(username=username) | Q(systemuser__username=username)).distinct()
|
qs = qs.filter(Q(username=username) | Q(systemuser__username=username)).distinct()
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
def filter_node(self, qs):
|
||||||
|
node_id = self.get_query_param('node')
|
||||||
|
if not node_id:
|
||||||
|
return qs
|
||||||
|
node = get_object_or_404(Node, pk=node_id)
|
||||||
|
node_ids = node.get_children(with_self=True).values_list('id', flat=True)
|
||||||
|
node_ids = list(node_ids)
|
||||||
|
qs = qs.filter(asset__nodes__in=node_ids)
|
||||||
|
return qs
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AuthBook
|
model = AuthBook
|
||||||
fields = [
|
fields = [
|
||||||
@ -74,11 +86,6 @@ class AccountSecretsViewSet(AccountViewSet):
|
|||||||
permission_classes = (IsOrgAdmin, NeedMFAVerify)
|
permission_classes = (IsOrgAdmin, NeedMFAVerify)
|
||||||
http_method_names = ['get']
|
http_method_names = ['get']
|
||||||
|
|
||||||
def get_permissions(self):
|
|
||||||
if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
|
||||||
self.permission_classes = [IsOrgAdminOrAppUser]
|
|
||||||
return super().get_permissions()
|
|
||||||
|
|
||||||
|
|
||||||
class AccountTaskCreateAPI(CreateAPIView):
|
class AccountTaskCreateAPI(CreateAPIView):
|
||||||
permission_classes = (IsOrgAdminOrAppUser,)
|
permission_classes = (IsOrgAdminOrAppUser,)
|
||||||
|
@ -9,10 +9,11 @@ from common.utils import get_logger, get_object_or_none
|
|||||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser
|
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
from orgs.mixins import generics
|
from orgs.mixins import generics
|
||||||
from ..models import Asset, Node, Platform
|
from ..models import Asset, Node, Platform, SystemUser
|
||||||
from .. import serializers
|
from .. import serializers
|
||||||
from ..tasks import (
|
from ..tasks import (
|
||||||
update_assets_hardware_info_manual, test_assets_connectivity_manual
|
update_assets_hardware_info_manual, test_assets_connectivity_manual,
|
||||||
|
test_system_users_connectivity_a_asset, push_system_users_a_asset
|
||||||
)
|
)
|
||||||
from ..filters import FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend
|
from ..filters import FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend
|
||||||
|
|
||||||
@ -94,21 +95,27 @@ class AssetPlatformViewSet(ModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class AssetsTaskMixin:
|
class AssetsTaskMixin:
|
||||||
|
|
||||||
def perform_assets_task(self, serializer):
|
def perform_assets_task(self, serializer):
|
||||||
data = serializer.validated_data
|
data = serializer.validated_data
|
||||||
assets = data['assets']
|
|
||||||
action = data['action']
|
action = data['action']
|
||||||
|
assets = data.get('assets', [])
|
||||||
if action == "refresh":
|
if action == "refresh":
|
||||||
task = update_assets_hardware_info_manual.delay(assets)
|
task = update_assets_hardware_info_manual.delay(assets)
|
||||||
else:
|
else:
|
||||||
|
# action == 'test':
|
||||||
task = test_assets_connectivity_manual.delay(assets)
|
task = test_assets_connectivity_manual.delay(assets)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
task = self.perform_assets_task(serializer)
|
||||||
|
self.set_task_to_serializer_data(serializer, task)
|
||||||
|
|
||||||
|
def set_task_to_serializer_data(self, serializer, task):
|
||||||
data = getattr(serializer, '_data', {})
|
data = getattr(serializer, '_data', {})
|
||||||
data["task"] = task.id
|
data["task"] = task.id
|
||||||
setattr(serializer, '_data', data)
|
setattr(serializer, '_data', data)
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
|
||||||
self.perform_assets_task(serializer)
|
|
||||||
|
|
||||||
|
|
||||||
class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
||||||
model = Asset
|
model = Asset
|
||||||
@ -117,13 +124,37 @@ class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
|||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
pk = self.kwargs.get('pk')
|
pk = self.kwargs.get('pk')
|
||||||
|
request.data['asset'] = pk
|
||||||
request.data['assets'] = [pk]
|
request.data['assets'] = [pk]
|
||||||
return super().create(request, *args, **kwargs)
|
return super().create(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def perform_asset_task(self, serializer):
|
||||||
|
data = serializer.validated_data
|
||||||
|
action = data['action']
|
||||||
|
if action not in ['push_system_user', 'test_system_user']:
|
||||||
|
return
|
||||||
|
asset = data['asset']
|
||||||
|
system_users = data.get('system_users')
|
||||||
|
if not system_users:
|
||||||
|
system_users = asset.get_all_systemusers()
|
||||||
|
if action == 'push_system_user':
|
||||||
|
task = push_system_users_a_asset.delay(system_users, asset=asset)
|
||||||
|
elif action == 'test_system_user':
|
||||||
|
task = test_system_users_connectivity_a_asset.delay(system_users, asset=asset)
|
||||||
|
else:
|
||||||
|
task = None
|
||||||
|
return task
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
task = self.perform_asset_task(serializer)
|
||||||
|
if not task:
|
||||||
|
task = self.perform_assets_task(serializer)
|
||||||
|
self.set_task_to_serializer_data(serializer, task)
|
||||||
|
|
||||||
|
|
||||||
class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
||||||
model = Asset
|
model = Asset
|
||||||
serializer_class = serializers.AssetTaskSerializer
|
serializer_class = serializers.AssetsTaskSerializer
|
||||||
permission_classes = (IsOrgAdmin,)
|
permission_classes = (IsOrgAdmin,)
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ class GatewayViewSet(OrgBulkModelViewSet):
|
|||||||
model = Gateway
|
model = Gateway
|
||||||
filterset_fields = ("domain__name", "name", "username", "ip", "domain")
|
filterset_fields = ("domain__name", "name", "username", "ip", "domain")
|
||||||
search_fields = ("domain__name", "name", "username", "ip")
|
search_fields = ("domain__name", "name", "username", "ip")
|
||||||
permission_classes = (IsOrgAdmin,)
|
permission_classes = (IsOrgAdminOrAppUser,)
|
||||||
serializer_class = serializers.GatewaySerializer
|
serializer_class = serializers.GatewaySerializer
|
||||||
|
|
||||||
|
|
||||||
|
@ -333,7 +333,7 @@ class Asset(AbsConnectivity, ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
|||||||
'iconSkin': icon_skin,
|
'iconSkin': icon_skin,
|
||||||
'meta': {
|
'meta': {
|
||||||
'type': 'asset',
|
'type': 'asset',
|
||||||
'asset': {
|
'data': {
|
||||||
'id': self.id,
|
'id': self.id,
|
||||||
'hostname': self.hostname,
|
'hostname': self.hostname,
|
||||||
'ip': self.ip,
|
'ip': self.ip,
|
||||||
@ -345,6 +345,13 @@ class Asset(AbsConnectivity, ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
|||||||
tree_node = TreeNode(**data)
|
tree_node = TreeNode(**data)
|
||||||
return tree_node
|
return tree_node
|
||||||
|
|
||||||
|
def get_all_systemusers(self):
|
||||||
|
from .user import SystemUser
|
||||||
|
system_user_ids = SystemUser.assets.through.objects.filter(asset=self)\
|
||||||
|
.values_list('systemuser_id', flat=True)
|
||||||
|
system_users = SystemUser.objects.filter(id__in=system_user_ids)
|
||||||
|
return system_users
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = [('org_id', 'hostname')]
|
unique_together = [('org_id', 'hostname')]
|
||||||
verbose_name = _("Asset")
|
verbose_name = _("Asset")
|
||||||
|
@ -67,7 +67,10 @@ class AuthMixin:
|
|||||||
if self.public_key:
|
if self.public_key:
|
||||||
public_key = self.public_key
|
public_key = self.public_key
|
||||||
elif self.private_key:
|
elif self.private_key:
|
||||||
public_key = ssh_pubkey_gen(private_key=self.private_key, password=self.password)
|
try:
|
||||||
|
public_key = ssh_pubkey_gen(private_key=self.private_key, password=self.password)
|
||||||
|
except IOError as e:
|
||||||
|
return str(e)
|
||||||
else:
|
else:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
@ -3,17 +3,19 @@
|
|||||||
import socket
|
import socket
|
||||||
import uuid
|
import uuid
|
||||||
import random
|
import random
|
||||||
import re
|
|
||||||
|
|
||||||
|
from django.core.cache import cache
|
||||||
import paramiko
|
import paramiko
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import TextChoices
|
from django.db.models import TextChoices
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from common.utils.strings import no_special_chars
|
from common.utils import get_logger
|
||||||
from orgs.mixins.models import OrgModelMixin
|
from orgs.mixins.models import OrgModelMixin
|
||||||
from .base import BaseUser
|
from .base import BaseUser
|
||||||
|
|
||||||
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
__all__ = ['Domain', 'Gateway']
|
__all__ = ['Domain', 'Gateway']
|
||||||
|
|
||||||
|
|
||||||
@ -40,10 +42,19 @@ class Domain(OrgModelMixin):
|
|||||||
return self.gateway_set.filter(is_active=True)
|
return self.gateway_set.filter(is_active=True)
|
||||||
|
|
||||||
def random_gateway(self):
|
def random_gateway(self):
|
||||||
return random.choice(self.gateways)
|
gateways = [gw for gw in self.gateways if gw.is_connective]
|
||||||
|
if gateways:
|
||||||
|
return random.choice(gateways)
|
||||||
|
else:
|
||||||
|
logger.warn(f'Gateway all bad. domain={self}, gateway_num={len(self.gateways)}.')
|
||||||
|
return random.choice(self.gateways)
|
||||||
|
|
||||||
|
|
||||||
class Gateway(BaseUser):
|
class Gateway(BaseUser):
|
||||||
|
UNCONNECTIVE_KEY_TMPL = 'asset_unconnective_gateway_{}'
|
||||||
|
UNCONNECTIVE_SILENCE_PERIOD_KEY_TMPL = 'asset_unconnective_gateway_silence_period_{}'
|
||||||
|
UNCONNECTIVE_SILENCE_PERIOD_BEGIN_VALUE = 60 * 5
|
||||||
|
|
||||||
class Protocol(TextChoices):
|
class Protocol(TextChoices):
|
||||||
ssh = 'ssh', 'SSH'
|
ssh = 'ssh', 'SSH'
|
||||||
|
|
||||||
@ -61,11 +72,40 @@ class Gateway(BaseUser):
|
|||||||
unique_together = [('name', 'org_id')]
|
unique_together = [('name', 'org_id')]
|
||||||
verbose_name = _("Gateway")
|
verbose_name = _("Gateway")
|
||||||
|
|
||||||
|
def set_unconnective(self):
|
||||||
|
unconnective_key = self.UNCONNECTIVE_KEY_TMPL.format(self.id)
|
||||||
|
unconnective_silence_period_key = self.UNCONNECTIVE_SILENCE_PERIOD_KEY_TMPL.format(self.id)
|
||||||
|
|
||||||
|
unconnective_silence_period = cache.get(unconnective_silence_period_key,
|
||||||
|
self.UNCONNECTIVE_SILENCE_PERIOD_BEGIN_VALUE)
|
||||||
|
cache.set(unconnective_silence_period_key, unconnective_silence_period * 2)
|
||||||
|
cache.set(unconnective_key, unconnective_silence_period, unconnective_silence_period)
|
||||||
|
|
||||||
|
def set_connective(self):
|
||||||
|
unconnective_key = self.UNCONNECTIVE_KEY_TMPL.format(self.id)
|
||||||
|
unconnective_silence_period_key = self.UNCONNECTIVE_SILENCE_PERIOD_KEY_TMPL.format(self.id)
|
||||||
|
|
||||||
|
cache.delete(unconnective_key)
|
||||||
|
cache.delete(unconnective_silence_period_key)
|
||||||
|
|
||||||
|
def get_is_unconnective(self):
|
||||||
|
unconnective_key = self.UNCONNECTIVE_KEY_TMPL.format(self.id)
|
||||||
|
return cache.get(unconnective_key, False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_connective(self):
|
||||||
|
return not self.get_is_unconnective()
|
||||||
|
|
||||||
|
@is_connective.setter
|
||||||
|
def is_connective(self, value):
|
||||||
|
if value:
|
||||||
|
self.set_connective()
|
||||||
|
else:
|
||||||
|
self.set_unconnective()
|
||||||
|
|
||||||
def test_connective(self, local_port=None):
|
def test_connective(self, local_port=None):
|
||||||
if local_port is None:
|
if local_port is None:
|
||||||
local_port = self.port
|
local_port = self.port
|
||||||
if self.password and not no_special_chars(self.password):
|
|
||||||
return False, _("Password should not contains special characters")
|
|
||||||
|
|
||||||
client = paramiko.SSHClient()
|
client = paramiko.SSHClient()
|
||||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
@ -82,7 +122,14 @@ class Gateway(BaseUser):
|
|||||||
paramiko.SSHException,
|
paramiko.SSHException,
|
||||||
paramiko.ssh_exception.NoValidConnectionsError,
|
paramiko.ssh_exception.NoValidConnectionsError,
|
||||||
socket.gaierror) as e:
|
socket.gaierror) as e:
|
||||||
return False, str(e)
|
err = str(e)
|
||||||
|
if err.startswith('[Errno None] Unable to connect to port'):
|
||||||
|
err = _('Unable to connect to port {port} on {ip}')
|
||||||
|
err = err.format(port=self.port, ip=self.ip)
|
||||||
|
elif err == 'Authentication failed.':
|
||||||
|
err = _('Authentication failed')
|
||||||
|
self.is_connective = False
|
||||||
|
return False, err
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sock = proxy.get_transport().open_channel(
|
sock = proxy.get_transport().open_channel(
|
||||||
@ -96,7 +143,9 @@ class Gateway(BaseUser):
|
|||||||
timeout=5)
|
timeout=5)
|
||||||
except (paramiko.SSHException, paramiko.ssh_exception.SSHException,
|
except (paramiko.SSHException, paramiko.ssh_exception.SSHException,
|
||||||
paramiko.AuthenticationException, TimeoutError) as e:
|
paramiko.AuthenticationException, TimeoutError) as e:
|
||||||
|
self.is_connective = False
|
||||||
return False, str(e)
|
return False, str(e)
|
||||||
finally:
|
finally:
|
||||||
client.close()
|
client.close()
|
||||||
|
self.is_connective = True
|
||||||
return True, None
|
return True, None
|
||||||
|
@ -608,7 +608,7 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
|
|||||||
'isParent': True,
|
'isParent': True,
|
||||||
'open': self.is_org_root(),
|
'open': self.is_org_root(),
|
||||||
'meta': {
|
'meta': {
|
||||||
'node': {
|
'data': {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"value": self.value,
|
"value": self.value,
|
||||||
|
@ -60,10 +60,10 @@ class ProtocolMixin:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_protocol_by_application_type(cls, app_type):
|
def get_protocol_by_application_type(cls, app_type):
|
||||||
from applications.const import ApplicationTypeChoices
|
from applications.const import AppType
|
||||||
if app_type in cls.APPLICATION_CATEGORY_PROTOCOLS:
|
if app_type in cls.APPLICATION_CATEGORY_PROTOCOLS:
|
||||||
protocol = app_type
|
protocol = app_type
|
||||||
elif app_type in ApplicationTypeChoices.remote_app_types():
|
elif app_type in AppType.remote_app_types():
|
||||||
protocol = cls.Protocol.rdp
|
protocol = cls.Protocol.rdp
|
||||||
else:
|
else:
|
||||||
protocol = None
|
protocol = None
|
||||||
|
@ -5,6 +5,7 @@ from assets.models import AuthBook
|
|||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
|
|
||||||
from .base import AuthSerializerMixin
|
from .base import AuthSerializerMixin
|
||||||
|
from .utils import validate_password_contains_left_double_curly_bracket
|
||||||
|
|
||||||
|
|
||||||
class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||||
@ -21,10 +22,15 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||||||
fields = fields_small + fields_fk
|
fields = fields_small + fields_fk
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'username': {'required': True},
|
'username': {'required': True},
|
||||||
'password': {'write_only': True},
|
'password': {
|
||||||
|
'write_only': True,
|
||||||
|
"validators": [validate_password_contains_left_double_curly_bracket]
|
||||||
|
},
|
||||||
'private_key': {'write_only': True},
|
'private_key': {'write_only': True},
|
||||||
'public_key': {'write_only': True},
|
'public_key': {'write_only': True},
|
||||||
|
'systemuser_display': {'label': _('System user display')}
|
||||||
}
|
}
|
||||||
|
ref_name = 'AssetAccountSerializer'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_eager_loading(cls, queryset):
|
def setup_eager_loading(cls, queryset):
|
||||||
|
@ -10,7 +10,7 @@ from ..models import Asset, Node, Platform, SystemUser
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
'AssetSerializer', 'AssetSimpleSerializer',
|
'AssetSerializer', 'AssetSimpleSerializer',
|
||||||
'ProtocolsField', 'PlatformSerializer',
|
'ProtocolsField', 'PlatformSerializer',
|
||||||
'AssetTaskSerializer',
|
'AssetTaskSerializer', 'AssetsTaskSerializer', 'ProtocolsField'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -183,7 +183,7 @@ class AssetSimpleSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'hostname', 'ip', 'port', 'connectivity', 'date_verified']
|
fields = ['id', 'hostname', 'ip', 'port', 'connectivity', 'date_verified']
|
||||||
|
|
||||||
|
|
||||||
class AssetTaskSerializer(serializers.Serializer):
|
class AssetsTaskSerializer(serializers.Serializer):
|
||||||
ACTION_CHOICES = (
|
ACTION_CHOICES = (
|
||||||
('refresh', 'refresh'),
|
('refresh', 'refresh'),
|
||||||
('test', 'test'),
|
('test', 'test'),
|
||||||
@ -193,3 +193,16 @@ class AssetTaskSerializer(serializers.Serializer):
|
|||||||
assets = serializers.PrimaryKeyRelatedField(
|
assets = serializers.PrimaryKeyRelatedField(
|
||||||
queryset=Asset.objects, required=False, allow_empty=True, many=True
|
queryset=Asset.objects, required=False, allow_empty=True, many=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AssetTaskSerializer(AssetsTaskSerializer):
|
||||||
|
ACTION_CHOICES = tuple(list(AssetsTaskSerializer.ACTION_CHOICES) + [
|
||||||
|
('push_system_user', 'push_system_user'),
|
||||||
|
('test_system_user', 'test_system_user')
|
||||||
|
])
|
||||||
|
asset = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=Asset.objects, required=False, allow_empty=True, many=False
|
||||||
|
)
|
||||||
|
system_users = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=SystemUser.objects, required=False, allow_empty=True, many=True
|
||||||
|
)
|
||||||
|
@ -4,14 +4,13 @@ from rest_framework import serializers
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
from common.validators import NoSpecialChars
|
|
||||||
from ..models import Domain, Gateway
|
from ..models import Domain, Gateway
|
||||||
from .base import AuthSerializerMixin
|
from .base import AuthSerializerMixin
|
||||||
|
|
||||||
|
|
||||||
class DomainSerializer(BulkOrgResourceModelSerializer):
|
class DomainSerializer(BulkOrgResourceModelSerializer):
|
||||||
asset_count = serializers.SerializerMethodField(label=_('Assets count'))
|
asset_count = serializers.SerializerMethodField(label=_('Assets amount'))
|
||||||
application_count = serializers.SerializerMethodField(label=_('Applications count'))
|
application_count = serializers.SerializerMethodField(label=_('Applications amount'))
|
||||||
gateway_count = serializers.SerializerMethodField(label=_('Gateways count'))
|
gateway_count = serializers.SerializerMethodField(label=_('Gateways count'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -43,6 +42,8 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||||
|
is_connective = serializers.BooleanField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Gateway
|
model = Gateway
|
||||||
fields_mini = ['id', 'name']
|
fields_mini = ['id', 'name']
|
||||||
@ -51,14 +52,14 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||||||
]
|
]
|
||||||
fields_small = fields_mini + fields_write_only + [
|
fields_small = fields_mini + fields_write_only + [
|
||||||
'username', 'ip', 'port', 'protocol',
|
'username', 'ip', 'port', 'protocol',
|
||||||
'is_active',
|
'is_active', 'is_connective',
|
||||||
'date_created', 'date_updated',
|
'date_created', 'date_updated',
|
||||||
'created_by', 'comment',
|
'created_by', 'comment',
|
||||||
]
|
]
|
||||||
fields_fk = ['domain']
|
fields_fk = ['domain']
|
||||||
fields = fields_small + fields_fk
|
fields = fields_small + fields_fk
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'password': {'write_only': True, 'validators': [NoSpecialChars()]},
|
'password': {'write_only': True},
|
||||||
'private_key': {"write_only": True},
|
'private_key': {"write_only": True},
|
||||||
'public_key': {"write_only": True},
|
'public_key': {"write_only": True},
|
||||||
}
|
}
|
||||||
@ -67,7 +68,7 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||||||
class GatewayWithAuthSerializer(GatewaySerializer):
|
class GatewayWithAuthSerializer(GatewaySerializer):
|
||||||
class Meta(GatewaySerializer.Meta):
|
class Meta(GatewaySerializer.Meta):
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'password': {'write_only': False, 'validators': [NoSpecialChars()]},
|
'password': {'write_only': False},
|
||||||
'private_key': {"write_only": False},
|
'private_key': {"write_only": False},
|
||||||
'public_key': {"write_only": False},
|
'public_key': {"write_only": False},
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ from common.mixins.serializers import BulkSerializerMixin
|
|||||||
from common.utils import ssh_pubkey_gen
|
from common.utils import ssh_pubkey_gen
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
from ..models import SystemUser, Asset
|
from ..models import SystemUser, Asset
|
||||||
|
from .utils import validate_password_contains_left_double_curly_bracket
|
||||||
from .base import AuthSerializerMixin
|
from .base import AuthSerializerMixin
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -40,7 +41,10 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||||||
fields_m2m = ['cmd_filters', 'assets_amount']
|
fields_m2m = ['cmd_filters', 'assets_amount']
|
||||||
fields = fields_small + fields_m2m
|
fields = fields_small + fields_m2m
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'password': {"write_only": True},
|
'password': {
|
||||||
|
"write_only": True,
|
||||||
|
"validators": [validate_password_contains_left_double_curly_bracket]
|
||||||
|
},
|
||||||
'public_key': {"write_only": True},
|
'public_key': {"write_only": True},
|
||||||
'private_key': {"write_only": True},
|
'private_key': {"write_only": True},
|
||||||
'token': {"write_only": True},
|
'token': {"write_only": True},
|
||||||
|
9
apps/assets/serializers/utils.py
Normal file
9
apps/assets/serializers/utils.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
def validate_password_contains_left_double_curly_bracket(password):
|
||||||
|
# validate password contains left double curly bracket
|
||||||
|
# check password not contains `{{`
|
||||||
|
if '{{' in password:
|
||||||
|
raise serializers.ValidationError(_('Password can not contains `{{` '))
|
@ -60,9 +60,12 @@ def parse_windows_result_to_users(result):
|
|||||||
task_result.pop()
|
task_result.pop()
|
||||||
|
|
||||||
for line in task_result:
|
for line in task_result:
|
||||||
user = space.split(line)
|
username_list = space.split(line)
|
||||||
if user[0]:
|
# such as: ['Admini', 'appadm', 'DefaultAccount', '']
|
||||||
users[user[0]] = {}
|
for username in username_list:
|
||||||
|
if not username:
|
||||||
|
continue
|
||||||
|
users[username] = {}
|
||||||
return users
|
return users
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ logger = get_logger(__file__)
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
'push_system_user_util', 'push_system_user_to_assets',
|
'push_system_user_util', 'push_system_user_to_assets',
|
||||||
'push_system_user_to_assets_manual', 'push_system_user_a_asset_manual',
|
'push_system_user_to_assets_manual', 'push_system_user_a_asset_manual',
|
||||||
|
'push_system_users_a_asset'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -280,14 +281,21 @@ def push_system_user_a_asset_manual(system_user, asset, username=None):
|
|||||||
"""
|
"""
|
||||||
将系统用户推送到一个资产上
|
将系统用户推送到一个资产上
|
||||||
"""
|
"""
|
||||||
if username is None:
|
# if username is None:
|
||||||
username = system_user.username
|
# username = system_user.username
|
||||||
task_name = _("Push system users to asset: {}({}) => {}").format(
|
task_name = _("Push system users to asset: {}({}) => {}").format(
|
||||||
system_user.name, username, asset
|
system_user.name, username, asset
|
||||||
)
|
)
|
||||||
return push_system_user_util(system_user, [asset], task_name=task_name, username=username)
|
return push_system_user_util(system_user, [asset], task_name=task_name, username=username)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(queue="ansible")
|
||||||
|
@tmp_to_root_org()
|
||||||
|
def push_system_users_a_asset(system_users, asset):
|
||||||
|
for system_user in system_users:
|
||||||
|
push_system_user_a_asset_manual(system_user, asset)
|
||||||
|
|
||||||
|
|
||||||
@shared_task(queue="ansible")
|
@shared_task(queue="ansible")
|
||||||
@tmp_to_root_org()
|
@tmp_to_root_org()
|
||||||
def push_system_user_to_assets(system_user_id, asset_ids, username=None):
|
def push_system_user_to_assets(system_user_id, asset_ids, username=None):
|
||||||
|
@ -18,6 +18,7 @@ logger = get_logger(__name__)
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
'test_system_user_connectivity_util', 'test_system_user_connectivity_manual',
|
'test_system_user_connectivity_util', 'test_system_user_connectivity_manual',
|
||||||
'test_system_user_connectivity_period', 'test_system_user_connectivity_a_asset',
|
'test_system_user_connectivity_period', 'test_system_user_connectivity_a_asset',
|
||||||
|
'test_system_users_connectivity_a_asset'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -131,6 +132,12 @@ def test_system_user_connectivity_a_asset(system_user, asset):
|
|||||||
test_system_user_connectivity_util(system_user, [asset], task_name)
|
test_system_user_connectivity_util(system_user, [asset], task_name)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(queue="ansible")
|
||||||
|
def test_system_users_connectivity_a_asset(system_users, asset):
|
||||||
|
for system_user in system_users:
|
||||||
|
test_system_user_connectivity_a_asset(system_user, asset)
|
||||||
|
|
||||||
|
|
||||||
@shared_task(queue="ansible")
|
@shared_task(queue="ansible")
|
||||||
def test_system_user_connectivity_period():
|
def test_system_user_connectivity_period():
|
||||||
if not const.PERIOD_TASK_ENABLED:
|
if not const.PERIOD_TASK_ENABLED:
|
||||||
|
@ -5,7 +5,7 @@ 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 django.utils import timezone
|
||||||
|
|
||||||
from orgs.mixins.models import OrgModelMixin
|
from orgs.mixins.models import OrgModelMixin, Organization
|
||||||
from orgs.utils import current_org
|
from orgs.utils import current_org
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -63,6 +63,11 @@ class OperateLog(OrgModelMixin):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "<{}> {} <{}>".format(self.user, self.action, self.resource)
|
return "<{}> {} <{}>".format(self.user, self.action, self.resource)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if current_org.is_root() and not self.org_id:
|
||||||
|
self.org_id = Organization.ROOT_ID
|
||||||
|
return super(OperateLog, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class PasswordChangeLog(models.Model):
|
class PasswordChangeLog(models.Model):
|
||||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
from django.db.models.signals import post_save, post_delete
|
from django.db.models.signals import post_save, post_delete, m2m_changed
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
@ -11,6 +11,8 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from rest_framework.renderers import JSONRenderer
|
from rest_framework.renderers import JSONRenderer
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
|
||||||
|
from assets.models import Asset
|
||||||
|
from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR
|
||||||
from jumpserver.utils import current_request
|
from jumpserver.utils import current_request
|
||||||
from common.utils import get_request_ip, get_logger, get_syslogger
|
from common.utils import get_request_ip, get_logger, get_syslogger
|
||||||
from users.models import User
|
from users.models import User
|
||||||
@ -20,6 +22,9 @@ from terminal.models import Session, Command
|
|||||||
from common.utils.encode import model_to_json
|
from common.utils.encode import model_to_json
|
||||||
from .utils import write_login_log
|
from .utils import write_login_log
|
||||||
from . import models
|
from . import models
|
||||||
|
from .models import OperateLog
|
||||||
|
from orgs.utils import current_org
|
||||||
|
from perms.models import AssetPermission, ApplicationPermission
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
sys_logger = get_syslogger(__name__)
|
sys_logger = get_syslogger(__name__)
|
||||||
@ -90,6 +95,119 @@ def create_operate_log(action, sender, resource):
|
|||||||
logger.error("Create operate log error: {}".format(e))
|
logger.error("Create operate log error: {}".format(e))
|
||||||
|
|
||||||
|
|
||||||
|
M2M_NEED_RECORD = {
|
||||||
|
'OrganizationMember': (
|
||||||
|
_('User and Organization'),
|
||||||
|
_('{User} *JOINED* {Organization}'),
|
||||||
|
_('{User} *LEFT* {Organization}')
|
||||||
|
),
|
||||||
|
User.groups.through._meta.object_name: (
|
||||||
|
_('User and Group'),
|
||||||
|
_('{User} *JOINED* {UserGroup}'),
|
||||||
|
_('{User} *LEFT* {UserGroup}')
|
||||||
|
),
|
||||||
|
Asset.nodes.through._meta.object_name: (
|
||||||
|
_('Node and Asset'),
|
||||||
|
_('{Node} *ADD* {Asset}'),
|
||||||
|
_('{Node} *REMOVE* {Asset}')
|
||||||
|
),
|
||||||
|
AssetPermission.users.through._meta.object_name: (
|
||||||
|
_('User asset permissions'),
|
||||||
|
_('{AssetPermission} *ADD* {User}'),
|
||||||
|
_('{AssetPermission} *REMOVE* {User}'),
|
||||||
|
),
|
||||||
|
AssetPermission.user_groups.through._meta.object_name: (
|
||||||
|
_('User group asset permissions'),
|
||||||
|
_('{AssetPermission} *ADD* {UserGroup}'),
|
||||||
|
_('{AssetPermission} *REMOVE* {UserGroup}'),
|
||||||
|
),
|
||||||
|
AssetPermission.assets.through._meta.object_name: (
|
||||||
|
_('Asset permission'),
|
||||||
|
_('{AssetPermission} *ADD* {Asset}'),
|
||||||
|
_('{AssetPermission} *REMOVE* {Asset}'),
|
||||||
|
),
|
||||||
|
AssetPermission.nodes.through._meta.object_name: (
|
||||||
|
_('Node permission'),
|
||||||
|
_('{AssetPermission} *ADD* {Node}'),
|
||||||
|
_('{AssetPermission} *REMOVE* {Node}'),
|
||||||
|
),
|
||||||
|
AssetPermission.system_users.through._meta.object_name: (
|
||||||
|
_('Asset permission and SystemUser'),
|
||||||
|
_('{AssetPermission} *ADD* {SystemUser}'),
|
||||||
|
_('{AssetPermission} *REMOVE* {SystemUser}'),
|
||||||
|
),
|
||||||
|
ApplicationPermission.users.through._meta.object_name: (
|
||||||
|
_('User application permissions'),
|
||||||
|
_('{ApplicationPermission} *ADD* {User}'),
|
||||||
|
_('{ApplicationPermission} *REMOVE* {User}'),
|
||||||
|
),
|
||||||
|
ApplicationPermission.user_groups.through._meta.object_name: (
|
||||||
|
_('User group application permissions'),
|
||||||
|
_('{ApplicationPermission} *ADD* {UserGroup}'),
|
||||||
|
_('{ApplicationPermission} *REMOVE* {UserGroup}'),
|
||||||
|
),
|
||||||
|
ApplicationPermission.applications.through._meta.object_name: (
|
||||||
|
_('Application permission'),
|
||||||
|
_('{ApplicationPermission} *ADD* {Application}'),
|
||||||
|
_('{ApplicationPermission} *REMOVE* {Application}'),
|
||||||
|
),
|
||||||
|
ApplicationPermission.system_users.through._meta.object_name: (
|
||||||
|
_('Application permission and SystemUser'),
|
||||||
|
_('{ApplicationPermission} *ADD* {SystemUser}'),
|
||||||
|
_('{ApplicationPermission} *REMOVE* {SystemUser}'),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
M2M_ACTION = {
|
||||||
|
POST_ADD: 'add',
|
||||||
|
POST_REMOVE: 'remove',
|
||||||
|
POST_CLEAR: 'remove',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(m2m_changed)
|
||||||
|
def on_m2m_changed(sender, action, instance, reverse, model, pk_set, **kwargs):
|
||||||
|
if action not in M2M_ACTION:
|
||||||
|
return
|
||||||
|
|
||||||
|
user = current_request.user if current_request else None
|
||||||
|
if not user or not user.is_authenticated:
|
||||||
|
return
|
||||||
|
|
||||||
|
sender_name = sender._meta.object_name
|
||||||
|
if sender_name in M2M_NEED_RECORD:
|
||||||
|
action = M2M_ACTION[action]
|
||||||
|
org_id = current_org.id
|
||||||
|
remote_addr = get_request_ip(current_request)
|
||||||
|
user = str(user)
|
||||||
|
resource_type, resource_tmpl_add, resource_tmpl_remove = M2M_NEED_RECORD[sender_name]
|
||||||
|
if action == 'add':
|
||||||
|
resource_tmpl = resource_tmpl_add
|
||||||
|
elif action == 'remove':
|
||||||
|
resource_tmpl = resource_tmpl_remove
|
||||||
|
|
||||||
|
to_create = []
|
||||||
|
objs = model.objects.filter(pk__in=pk_set)
|
||||||
|
|
||||||
|
instance_name = instance._meta.object_name
|
||||||
|
instance_value = str(instance)
|
||||||
|
|
||||||
|
model_name = model._meta.object_name
|
||||||
|
|
||||||
|
for obj in objs:
|
||||||
|
resource = resource_tmpl.format(**{
|
||||||
|
instance_name: instance_value,
|
||||||
|
model_name: str(obj)
|
||||||
|
})[:128] # `resource` 字段只有 128 个字符长 😔
|
||||||
|
|
||||||
|
to_create.append(OperateLog(
|
||||||
|
user=user, action=action, resource_type=resource_type,
|
||||||
|
resource=resource, remote_addr=remote_addr, org_id=org_id
|
||||||
|
))
|
||||||
|
OperateLog.objects.bulk_create(to_create)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save)
|
@receiver(post_save)
|
||||||
def on_object_created_or_update(sender, instance=None, created=False, update_fields=None, **kwargs):
|
def on_object_created_or_update(sender, instance=None, created=False, update_fields=None, **kwargs):
|
||||||
# last_login 改变是最后登录日期, 每次登录都会改变
|
# last_login 改变是最后登录日期, 每次登录都会改变
|
||||||
|
@ -9,4 +9,5 @@ from .login_confirm import *
|
|||||||
from .sso import *
|
from .sso import *
|
||||||
from .wecom import *
|
from .wecom import *
|
||||||
from .dingtalk import *
|
from .dingtalk import *
|
||||||
|
from .feishu import *
|
||||||
from .password import *
|
from .password import *
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
@ -8,6 +11,7 @@ from django.shortcuts import get_object_or_404
|
|||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.request import Request
|
||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
@ -17,90 +21,40 @@ from authentication.signals import post_auth_failed, post_auth_success
|
|||||||
from common.utils import get_logger, random_string
|
from common.utils import get_logger, random_string
|
||||||
from common.drf.api import SerializerMixin
|
from common.drf.api import SerializerMixin
|
||||||
from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser
|
from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser
|
||||||
|
|
||||||
from orgs.mixins.api import RootOrgViewMixin
|
from orgs.mixins.api import RootOrgViewMixin
|
||||||
|
from common.http import is_true
|
||||||
|
|
||||||
from ..serializers import (
|
from ..serializers import (
|
||||||
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
|
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
|
||||||
RDPFileSerializer
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
__all__ = ['UserConnectionTokenViewSet']
|
__all__ = ['UserConnectionTokenViewSet']
|
||||||
|
|
||||||
|
|
||||||
class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin, GenericViewSet):
|
class ClientProtocolMixin:
|
||||||
permission_classes = (IsSuperUserOrAppUser,)
|
request: Request
|
||||||
serializer_classes = {
|
get_serializer: Callable
|
||||||
'default': ConnectionTokenSerializer,
|
create_token: Callable
|
||||||
'get_secret_detail': ConnectionTokenSecretSerializer,
|
|
||||||
'get_rdp_file': RDPFileSerializer
|
|
||||||
}
|
|
||||||
CACHE_KEY_PREFIX = 'CONNECTION_TOKEN_{}'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def check_resource_permission(user, asset, application, system_user):
|
|
||||||
from perms.utils.asset import has_asset_system_permission
|
|
||||||
from perms.utils.application import has_application_system_permission
|
|
||||||
if asset and not has_asset_system_permission(user, asset, system_user):
|
|
||||||
error = f'User not has this asset and system user permission: ' \
|
|
||||||
f'user={user.id} system_user={system_user.id} asset={asset.id}'
|
|
||||||
raise PermissionDenied(error)
|
|
||||||
if application and not has_application_system_permission(user, application, system_user):
|
|
||||||
error = f'User not has this application and system user permission: ' \
|
|
||||||
f'user={user.id} system_user={system_user.id} application={application.id}'
|
|
||||||
raise PermissionDenied(error)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def create_token(self, user, asset, application, system_user, ttl=5*60):
|
|
||||||
if not self.request.user.is_superuser and user != self.request.user:
|
|
||||||
raise PermissionDenied('Only super user can create user token')
|
|
||||||
self.check_resource_permission(user, asset, application, system_user)
|
|
||||||
token = random_string(36)
|
|
||||||
value = {
|
|
||||||
'user': str(user.id),
|
|
||||||
'username': user.username,
|
|
||||||
'system_user': str(system_user.id),
|
|
||||||
'system_user_name': system_user.name
|
|
||||||
}
|
|
||||||
|
|
||||||
if asset:
|
|
||||||
value.update({
|
|
||||||
'type': 'asset',
|
|
||||||
'asset': str(asset.id),
|
|
||||||
'hostname': asset.hostname,
|
|
||||||
})
|
|
||||||
elif application:
|
|
||||||
value.update({
|
|
||||||
'type': 'application',
|
|
||||||
'application': application.id,
|
|
||||||
'application_name': str(application)
|
|
||||||
})
|
|
||||||
|
|
||||||
key = self.CACHE_KEY_PREFIX.format(token)
|
|
||||||
cache.set(key, value, timeout=ttl)
|
|
||||||
return token
|
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
|
||||||
serializer = self.get_serializer(data=request.data)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
|
|
||||||
|
def get_request_resource(self, serializer):
|
||||||
asset = serializer.validated_data.get('asset')
|
asset = serializer.validated_data.get('asset')
|
||||||
application = serializer.validated_data.get('application')
|
application = serializer.validated_data.get('application')
|
||||||
system_user = serializer.validated_data['system_user']
|
system_user = serializer.validated_data['system_user']
|
||||||
user = serializer.validated_data.get('user')
|
|
||||||
token = self.create_token(user, asset, application, system_user)
|
|
||||||
return Response({"token": token}, status=201)
|
|
||||||
|
|
||||||
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file', permission_classes=[IsValidUser])
|
user = serializer.validated_data.get('user')
|
||||||
def get_rdp_file(self, request, *args, **kwargs):
|
if not user or not self.request.user.is_superuser:
|
||||||
|
user = self.request.user
|
||||||
|
return asset, application, system_user, user
|
||||||
|
|
||||||
|
def get_rdp_file_content(self, serializer):
|
||||||
options = {
|
options = {
|
||||||
'full address:s': '',
|
'full address:s': '',
|
||||||
'username:s': '',
|
'username:s': '',
|
||||||
'screen mode id:i': '0',
|
# 'screen mode id:i': '1',
|
||||||
# 'desktopwidth:i': '1280',
|
# 'desktopwidth:i': '1280',
|
||||||
# 'desktopheight:i': '800',
|
# 'desktopheight:i': '800',
|
||||||
'use multimon:i': '1',
|
'use multimon:i': '0',
|
||||||
'session bpp:i': '32',
|
'session bpp:i': '32',
|
||||||
'audiomode:i': '0',
|
'audiomode:i': '0',
|
||||||
'disable wallpaper:i': '0',
|
'disable wallpaper:i': '0',
|
||||||
@ -125,24 +79,17 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin, GenericViewS
|
|||||||
# 'remoteapplicationname:s': 'Firefox',
|
# 'remoteapplicationname:s': 'Firefox',
|
||||||
# 'remoteapplicationcmdline:s': '',
|
# 'remoteapplicationcmdline:s': '',
|
||||||
}
|
}
|
||||||
if self.request.method == 'GET':
|
|
||||||
data = self.request.query_params
|
|
||||||
else:
|
|
||||||
data = request.data
|
|
||||||
serializer = self.get_serializer(data=data)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
|
|
||||||
asset = serializer.validated_data.get('asset')
|
asset, application, system_user, user = self.get_request_resource(serializer)
|
||||||
application = serializer.validated_data.get('application')
|
height = self.request.query_params.get('height')
|
||||||
system_user = serializer.validated_data['system_user']
|
width = self.request.query_params.get('width')
|
||||||
height = serializer.validated_data.get('height')
|
full_screen = is_true(self.request.query_params.get('full_screen'))
|
||||||
width = serializer.validated_data.get('width')
|
|
||||||
user = request.user
|
|
||||||
token = self.create_token(user, asset, application, system_user)
|
token = self.create_token(user, asset, application, system_user)
|
||||||
|
|
||||||
|
options['screen mode id:i'] = '2' if full_screen else '1'
|
||||||
address = settings.TERMINAL_RDP_ADDR
|
address = settings.TERMINAL_RDP_ADDR
|
||||||
if not address or address == 'localhost:3389':
|
if not address or address == 'localhost:3389':
|
||||||
address = request.get_host().split(':')[0] + ':3389'
|
address = self.request.get_host().split(':')[0] + ':3389'
|
||||||
options['full address:s'] = address
|
options['full address:s'] = address
|
||||||
options['username:s'] = '{}|{}'.format(user.username, token)
|
options['username:s'] = '{}|{}'.format(user.username, token)
|
||||||
if system_user.ad_domain:
|
if system_user.ad_domain:
|
||||||
@ -152,21 +99,73 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin, GenericViewS
|
|||||||
options['desktopheight:i'] = height
|
options['desktopheight:i'] = height
|
||||||
else:
|
else:
|
||||||
options['smart sizing:i'] = '1'
|
options['smart sizing:i'] = '1'
|
||||||
data = ''
|
content = ''
|
||||||
for k, v in options.items():
|
for k, v in options.items():
|
||||||
data += f'{k}:{v}\n'
|
content += f'{k}:{v}\n'
|
||||||
if asset:
|
if asset:
|
||||||
name = asset.hostname
|
name = asset.hostname
|
||||||
elif application:
|
elif application:
|
||||||
name = application.name
|
name = application.name
|
||||||
else:
|
else:
|
||||||
name = '*'
|
name = '*'
|
||||||
|
return name, content
|
||||||
|
|
||||||
|
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file', permission_classes=[IsValidUser])
|
||||||
|
def get_rdp_file(self, request, *args, **kwargs):
|
||||||
|
if self.request.method == 'GET':
|
||||||
|
data = self.request.query_params
|
||||||
|
else:
|
||||||
|
data = self.request.data
|
||||||
|
serializer = self.get_serializer(data=data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
name, data = self.get_rdp_file_content(serializer)
|
||||||
response = HttpResponse(data, content_type='application/octet-stream')
|
response = HttpResponse(data, content_type='application/octet-stream')
|
||||||
filename = "{}-{}-jumpserver.rdp".format(user.username, name)
|
filename = "{}-{}-jumpserver.rdp".format(self.request.user.username, name)
|
||||||
filename = urllib.parse.quote(filename)
|
filename = urllib.parse.quote(filename)
|
||||||
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
|
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
def get_valid_serializer(self):
|
||||||
|
if self.request.method == 'GET':
|
||||||
|
data = self.request.query_params
|
||||||
|
else:
|
||||||
|
data = self.request.data
|
||||||
|
serializer = self.get_serializer(data=data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
return serializer
|
||||||
|
|
||||||
|
def get_client_protocol_data(self, serializer):
|
||||||
|
asset, application, system_user, user = self.get_request_resource(serializer)
|
||||||
|
protocol = system_user.protocol
|
||||||
|
if protocol == 'rdp':
|
||||||
|
name, config = self.get_rdp_file_content(serializer)
|
||||||
|
elif protocol == 'vnc':
|
||||||
|
raise HttpResponse(status=404, data={"error": "VNC not support"})
|
||||||
|
else:
|
||||||
|
config = 'ssh://system_user@asset@user@jumpserver-ssh'
|
||||||
|
data = {
|
||||||
|
"protocol": system_user.protocol,
|
||||||
|
"username": user.username,
|
||||||
|
"config": config
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
@action(methods=['POST', 'GET'], detail=False, url_path='client-url', permission_classes=[IsValidUser])
|
||||||
|
def get_client_protocol_url(self, request, *args, **kwargs):
|
||||||
|
serializer = self.get_valid_serializer()
|
||||||
|
protocol_data = self.get_client_protocol_data(serializer)
|
||||||
|
protocol_data = base64.b64encode(json.dumps(protocol_data).encode()).decode()
|
||||||
|
data = {
|
||||||
|
'url': 'jms://{}'.format(protocol_data),
|
||||||
|
}
|
||||||
|
return Response(data=data)
|
||||||
|
|
||||||
|
|
||||||
|
class SecretDetailMixin:
|
||||||
|
valid_token: Callable
|
||||||
|
request: Request
|
||||||
|
get_serializer: Callable
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_application_secret_detail(application):
|
def _get_application_secret_detail(application):
|
||||||
from perms.models import Action
|
from perms.models import Action
|
||||||
@ -212,6 +211,100 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin, GenericViewS
|
|||||||
'actions': actions,
|
'actions': actions,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action(methods=['POST'], detail=False, permission_classes=[IsSuperUserOrAppUser], url_path='secret-info/detail')
|
||||||
|
def get_secret_detail(self, request, *args, **kwargs):
|
||||||
|
token = request.data.get('token', '')
|
||||||
|
try:
|
||||||
|
value, user, system_user, asset, app, expired_at = self.valid_token(token)
|
||||||
|
except serializers.ValidationError as e:
|
||||||
|
post_auth_failed.send(
|
||||||
|
sender=self.__class__, username='', request=self.request,
|
||||||
|
reason=_('Invalid token')
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
data = dict(user=user, system_user=system_user, expired_at=expired_at)
|
||||||
|
if asset:
|
||||||
|
asset_detail = self._get_asset_secret_detail(asset, user=user, system_user=system_user)
|
||||||
|
system_user.load_asset_more_auth(asset.id, user.username, user.id)
|
||||||
|
data['type'] = 'asset'
|
||||||
|
data.update(asset_detail)
|
||||||
|
else:
|
||||||
|
app_detail = self._get_application_secret_detail(app)
|
||||||
|
system_user.load_app_more_auth(app.id, user.id)
|
||||||
|
data['type'] = 'application'
|
||||||
|
data.update(app_detail)
|
||||||
|
|
||||||
|
self.request.session['auth_backend'] = settings.AUTH_BACKEND_AUTH_TOKEN
|
||||||
|
post_auth_success.send(sender=self.__class__, user=user, request=self.request, login_type='T')
|
||||||
|
|
||||||
|
serializer = self.get_serializer(data)
|
||||||
|
return Response(data=serializer.data, status=200)
|
||||||
|
|
||||||
|
|
||||||
|
class UserConnectionTokenViewSet(
|
||||||
|
RootOrgViewMixin, SerializerMixin, ClientProtocolMixin,
|
||||||
|
SecretDetailMixin, GenericViewSet
|
||||||
|
):
|
||||||
|
permission_classes = (IsSuperUserOrAppUser,)
|
||||||
|
serializer_classes = {
|
||||||
|
'default': ConnectionTokenSerializer,
|
||||||
|
'get_secret_detail': ConnectionTokenSecretSerializer,
|
||||||
|
}
|
||||||
|
CACHE_KEY_PREFIX = 'CONNECTION_TOKEN_{}'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_resource_permission(user, asset, application, system_user):
|
||||||
|
from perms.utils.asset import has_asset_system_permission
|
||||||
|
from perms.utils.application import has_application_system_permission
|
||||||
|
|
||||||
|
if asset and not has_asset_system_permission(user, asset, system_user):
|
||||||
|
error = f'User not has this asset and system user permission: ' \
|
||||||
|
f'user={user.id} system_user={system_user.id} asset={asset.id}'
|
||||||
|
raise PermissionDenied(error)
|
||||||
|
if application and not has_application_system_permission(user, application, system_user):
|
||||||
|
error = f'User not has this application and system user permission: ' \
|
||||||
|
f'user={user.id} system_user={system_user.id} application={application.id}'
|
||||||
|
raise PermissionDenied(error)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def create_token(self, user, asset, application, system_user, ttl=5 * 60):
|
||||||
|
if not self.request.user.is_superuser and user != self.request.user:
|
||||||
|
raise PermissionDenied('Only super user can create user token')
|
||||||
|
self.check_resource_permission(user, asset, application, system_user)
|
||||||
|
token = random_string(36)
|
||||||
|
value = {
|
||||||
|
'user': str(user.id),
|
||||||
|
'username': user.username,
|
||||||
|
'system_user': str(system_user.id),
|
||||||
|
'system_user_name': system_user.name
|
||||||
|
}
|
||||||
|
|
||||||
|
if asset:
|
||||||
|
value.update({
|
||||||
|
'type': 'asset',
|
||||||
|
'asset': str(asset.id),
|
||||||
|
'hostname': asset.hostname,
|
||||||
|
})
|
||||||
|
elif application:
|
||||||
|
value.update({
|
||||||
|
'type': 'application',
|
||||||
|
'application': application.id,
|
||||||
|
'application_name': str(application)
|
||||||
|
})
|
||||||
|
|
||||||
|
key = self.CACHE_KEY_PREFIX.format(token)
|
||||||
|
cache.set(key, value, timeout=ttl)
|
||||||
|
return token
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
asset, application, system_user, user = self.get_request_resource(serializer)
|
||||||
|
token = self.create_token(user, asset, application, system_user)
|
||||||
|
return Response({"token": token}, status=201)
|
||||||
|
|
||||||
def valid_token(self, token):
|
def valid_token(self, token):
|
||||||
from users.models import User
|
from users.models import User
|
||||||
from assets.models import SystemUser, Asset
|
from assets.models import SystemUser, Asset
|
||||||
@ -244,39 +337,8 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin, GenericViewS
|
|||||||
|
|
||||||
if not has_perm:
|
if not has_perm:
|
||||||
raise serializers.ValidationError('Permission expired or invalid')
|
raise serializers.ValidationError('Permission expired or invalid')
|
||||||
|
|
||||||
return value, user, system_user, asset, app, expired_at
|
return value, user, system_user, asset, app, expired_at
|
||||||
|
|
||||||
@action(methods=['POST'], detail=False, permission_classes=[IsSuperUserOrAppUser], url_path='secret-info/detail')
|
|
||||||
def get_secret_detail(self, request, *args, **kwargs):
|
|
||||||
token = request.data.get('token', '')
|
|
||||||
try:
|
|
||||||
value, user, system_user, asset, app, expired_at = self.valid_token(token)
|
|
||||||
except serializers.ValidationError as e:
|
|
||||||
post_auth_failed.send(
|
|
||||||
sender=self.__class__, username='', request=self.request,
|
|
||||||
reason=_('Invalid token')
|
|
||||||
)
|
|
||||||
raise e
|
|
||||||
|
|
||||||
data = dict(user=user, system_user=system_user, expired_at=expired_at)
|
|
||||||
if asset:
|
|
||||||
asset_detail = self._get_asset_secret_detail(asset, user=user, system_user=system_user)
|
|
||||||
system_user.load_asset_more_auth(asset.id, user.username, user.id)
|
|
||||||
data['type'] = 'asset'
|
|
||||||
data.update(asset_detail)
|
|
||||||
else:
|
|
||||||
app_detail = self._get_application_secret_detail(app)
|
|
||||||
system_user.load_app_more_auth(app.id, user.id)
|
|
||||||
data['type'] = 'application'
|
|
||||||
data.update(app_detail)
|
|
||||||
|
|
||||||
self.request.session['auth_backend'] = settings.AUTH_BACKEND_AUTH_TOKEN
|
|
||||||
post_auth_success.send(sender=self.__class__, user=user, request=self.request, login_type='T')
|
|
||||||
|
|
||||||
serializer = self.get_serializer(data)
|
|
||||||
return Response(data=serializer.data, status=200)
|
|
||||||
|
|
||||||
def get_permissions(self):
|
def get_permissions(self):
|
||||||
if self.action in ["create", "get_rdp_file"]:
|
if self.action in ["create", "get_rdp_file"]:
|
||||||
if self.request.data.get('user', None):
|
if self.request.data.get('user', None):
|
||||||
|
45
apps/authentication/api/feishu.py
Normal file
45
apps/authentication/api/feishu.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from users.permissions import IsAuthPasswdTimeValid
|
||||||
|
from users.models import User
|
||||||
|
from common.utils import get_logger
|
||||||
|
from common.permissions import IsOrgAdmin
|
||||||
|
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||||
|
from authentication import errors
|
||||||
|
|
||||||
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
class FeiShuQRUnBindBase(APIView):
|
||||||
|
user: User
|
||||||
|
|
||||||
|
def post(self, request: Request, **kwargs):
|
||||||
|
user = self.user
|
||||||
|
|
||||||
|
if not user.feishu_id:
|
||||||
|
raise errors.FeiShuNotBound
|
||||||
|
|
||||||
|
user.feishu_id = None
|
||||||
|
user.save()
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
|
||||||
|
class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase):
|
||||||
|
permission_classes = (IsAuthPasswdTimeValid,)
|
||||||
|
|
||||||
|
|
||||||
|
class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase):
|
||||||
|
user_id_url_kwarg = 'user_id'
|
||||||
|
permission_classes = (IsOrgAdmin,)
|
||||||
|
|
||||||
|
|
||||||
|
class FeiShuEventSubscriptionCallback(APIView):
|
||||||
|
"""
|
||||||
|
# https://open.feishu.cn/document/ukTMukTMukTM/uUTNz4SN1MjL1UzM
|
||||||
|
"""
|
||||||
|
permission_classes = ()
|
||||||
|
|
||||||
|
def post(self, request: Request, *args, **kwargs):
|
||||||
|
return Response(data=request.data)
|
@ -11,7 +11,7 @@ from rest_framework.permissions import AllowAny
|
|||||||
|
|
||||||
from common.utils.timezone import utcnow
|
from common.utils.timezone import utcnow
|
||||||
from common.const.http import POST, GET
|
from common.const.http import POST, GET
|
||||||
from common.drf.api import JmsGenericViewSet
|
from common.drf.api import JMSGenericViewSet
|
||||||
from common.drf.serializers import EmptySerializer
|
from common.drf.serializers import EmptySerializer
|
||||||
from common.permissions import IsSuperUser
|
from common.permissions import IsSuperUser
|
||||||
from common.utils import reverse
|
from common.utils import reverse
|
||||||
@ -26,7 +26,7 @@ NEXT_URL = 'next'
|
|||||||
AUTH_KEY = 'authkey'
|
AUTH_KEY = 'authkey'
|
||||||
|
|
||||||
|
|
||||||
class SSOViewSet(AuthMixin, JmsGenericViewSet):
|
class SSOViewSet(AuthMixin, JMSGenericViewSet):
|
||||||
queryset = SSOToken.objects.all()
|
queryset = SSOToken.objects.all()
|
||||||
serializer_classes = {
|
serializer_classes = {
|
||||||
'login_url': SSOTokenSerializer,
|
'login_url': SSOTokenSerializer,
|
||||||
|
@ -240,6 +240,15 @@ class DingTalkAuthentication(JMSModelBackend):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FeiShuAuthentication(JMSModelBackend):
|
||||||
|
"""
|
||||||
|
什么也不做呀😺
|
||||||
|
"""
|
||||||
|
|
||||||
|
def authenticate(self, request, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AuthorizationTokenAuthentication(JMSModelBackend):
|
class AuthorizationTokenAuthentication(JMSModelBackend):
|
||||||
"""
|
"""
|
||||||
什么也不做呀😺
|
什么也不做呀😺
|
||||||
|
@ -315,6 +315,11 @@ class DingTalkNotBound(JMSException):
|
|||||||
default_detail = 'DingTalk is not bound'
|
default_detail = 'DingTalk is not bound'
|
||||||
|
|
||||||
|
|
||||||
|
class FeiShuNotBound(JMSException):
|
||||||
|
default_code = 'feishu_not_bound'
|
||||||
|
default_detail = 'FeiShu is not bound'
|
||||||
|
|
||||||
|
|
||||||
class PasswdInvalid(JMSException):
|
class PasswdInvalid(JMSException):
|
||||||
default_code = 'passwd_invalid'
|
default_code = 'passwd_invalid'
|
||||||
default_detail = _('Your password is invalid')
|
default_detail = _('Your password is invalid')
|
||||||
|
@ -198,7 +198,3 @@ class ConnectionTokenSecretSerializer(serializers.Serializer):
|
|||||||
actions = ActionsField()
|
actions = ActionsField()
|
||||||
expired_at = serializers.IntegerField()
|
expired_at = serializers.IntegerField()
|
||||||
|
|
||||||
|
|
||||||
class RDPFileSerializer(ConnectionTokenSerializer):
|
|
||||||
width = serializers.IntegerField(allow_null=True, max_value=3112, min_value=100, required=False)
|
|
||||||
height = serializers.IntegerField(allow_null=True, max_value=4096, min_value=100, required=False)
|
|
||||||
|
@ -191,7 +191,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{% if AUTH_OPENID or AUTH_CAS or AUTH_WECOM or AUTH_DINGTALK %}
|
{% if AUTH_OPENID or AUTH_CAS or AUTH_WECOM or AUTH_DINGTALK or AUTH_FEISHU %}
|
||||||
<div class="hr-line-dashed"></div>
|
<div class="hr-line-dashed"></div>
|
||||||
<div style="display: inline-block; float: left">
|
<div style="display: inline-block; float: left">
|
||||||
<b class="text-muted text-left" >{% trans "More login options" %}</b>
|
<b class="text-muted text-left" >{% trans "More login options" %}</b>
|
||||||
@ -215,6 +215,11 @@
|
|||||||
<i class="fa"><img src="{{ LOGIN_DINGTALK_LOGO_URL }}" height="13" width="13"></i> {% trans 'DingTalk' %}
|
<i class="fa"><img src="{{ LOGIN_DINGTALK_LOGO_URL }}" height="13" width="13"></i> {% trans 'DingTalk' %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if AUTH_FEISHU %}
|
||||||
|
<a href="{% url 'authentication:feishu-qr-login' %}" class="more-login-item">
|
||||||
|
<i class="fa"><img src="{{ LOGIN_FEISHU_LOGO_URL }}" height="13" width="13"></i> {% trans 'FeiShu' %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -20,6 +20,10 @@ urlpatterns = [
|
|||||||
path('dingtalk/qr/unbind/', api.DingTalkQRUnBindForUserApi.as_view(), name='dingtalk-qr-unbind'),
|
path('dingtalk/qr/unbind/', api.DingTalkQRUnBindForUserApi.as_view(), name='dingtalk-qr-unbind'),
|
||||||
path('dingtalk/qr/unbind/<uuid:user_id>/', api.DingTalkQRUnBindForAdminApi.as_view(), name='dingtalk-qr-unbind-for-admin'),
|
path('dingtalk/qr/unbind/<uuid:user_id>/', api.DingTalkQRUnBindForAdminApi.as_view(), name='dingtalk-qr-unbind-for-admin'),
|
||||||
|
|
||||||
|
path('feishu/qr/unbind/', api.FeiShuQRUnBindForUserApi.as_view(), name='feishu-qr-unbind'),
|
||||||
|
path('feishu/qr/unbind/<uuid:user_id>/', api.FeiShuQRUnBindForAdminApi.as_view(), name='feishu-qr-unbind-for-admin'),
|
||||||
|
path('feishu/event/subscription/callback/', api.FeiShuEventSubscriptionCallback.as_view(), name='feishu-event-subscription-callback'),
|
||||||
|
|
||||||
path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
|
path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
|
||||||
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
|
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
|
||||||
path('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'),
|
path('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'),
|
||||||
|
@ -37,6 +37,14 @@ urlpatterns = [
|
|||||||
path('dingtalk/qr/bind/<uuid:user_id>/callback/', views.DingTalkQRBindCallbackView.as_view(), name='dingtalk-qr-bind-callback'),
|
path('dingtalk/qr/bind/<uuid:user_id>/callback/', views.DingTalkQRBindCallbackView.as_view(), name='dingtalk-qr-bind-callback'),
|
||||||
path('dingtalk/qr/login/callback/', views.DingTalkQRLoginCallbackView.as_view(), name='dingtalk-qr-login-callback'),
|
path('dingtalk/qr/login/callback/', views.DingTalkQRLoginCallbackView.as_view(), name='dingtalk-qr-login-callback'),
|
||||||
|
|
||||||
|
path('feishu/bind/success-flash-msg/', views.FlashDingTalkBindSucceedMsgView.as_view(), name='feishu-bind-success-flash-msg'),
|
||||||
|
path('feishu/bind/failed-flash-msg/', views.FlashDingTalkBindFailedMsgView.as_view(), name='feishu-bind-failed-flash-msg'),
|
||||||
|
path('feishu/bind/start/', views.FeiShuEnableStartView.as_view(), name='feishu-bind-start'),
|
||||||
|
path('feishu/qr/bind/', views.FeiShuQRBindView.as_view(), name='feishu-qr-bind'),
|
||||||
|
path('feishu/qr/login/', views.FeiShuQRLoginView.as_view(), name='feishu-qr-login'),
|
||||||
|
path('feishu/qr/bind/callback/', views.FeiShuQRBindCallbackView.as_view(), name='feishu-qr-bind-callback'),
|
||||||
|
path('feishu/qr/login/callback/', views.FeiShuQRLoginCallbackView.as_view(), name='feishu-qr-login-callback'),
|
||||||
|
|
||||||
# Profile
|
# Profile
|
||||||
path('profile/pubkey/generate/', users_view.UserPublicKeyGenerateView.as_view(), name='user-pubkey-generate'),
|
path('profile/pubkey/generate/', users_view.UserPublicKeyGenerateView.as_view(), name='user-pubkey-generate'),
|
||||||
path('profile/otp/enable/start/', users_view.UserOtpEnableStartView.as_view(), name='user-otp-enable-start'),
|
path('profile/otp/enable/start/', users_view.UserOtpEnableStartView.as_view(), name='user-otp-enable-start'),
|
||||||
|
@ -4,3 +4,4 @@ from .login import *
|
|||||||
from .mfa import *
|
from .mfa import *
|
||||||
from .wecom import *
|
from .wecom import *
|
||||||
from .dingtalk import *
|
from .dingtalk import *
|
||||||
|
from .feishu import *
|
||||||
|
253
apps/authentication/views/feishu.py
Normal file
253
apps/authentication/views/feishu.py
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
import urllib
|
||||||
|
|
||||||
|
from django.http.response import HttpResponseRedirect, HttpResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.views.decorators.cache import never_cache
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
from django.views import View
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http.request import HttpRequest
|
||||||
|
from django.db.utils import IntegrityError
|
||||||
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||||
|
from rest_framework.exceptions import APIException
|
||||||
|
|
||||||
|
from users.utils import is_auth_password_time_valid
|
||||||
|
from users.views import UserVerifyPasswordView
|
||||||
|
from users.models import User
|
||||||
|
from common.utils import get_logger
|
||||||
|
from common.utils.random import random_string
|
||||||
|
from common.utils.django import reverse, get_object_or_none
|
||||||
|
from common.mixins.views import PermissionsMixin
|
||||||
|
from common.message.backends.feishu import FeiShu, URL
|
||||||
|
from authentication import errors
|
||||||
|
from authentication.mixins import AuthMixin
|
||||||
|
|
||||||
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
FEISHU_STATE_SESSION_KEY = '_feishu_state'
|
||||||
|
|
||||||
|
|
||||||
|
class FeiShuQRMixin(PermissionsMixin, View):
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
except APIException as e:
|
||||||
|
msg = str(e.detail)
|
||||||
|
return self.get_failed_reponse(
|
||||||
|
'/',
|
||||||
|
_('FeiShu Error'),
|
||||||
|
msg
|
||||||
|
)
|
||||||
|
|
||||||
|
def verify_state(self):
|
||||||
|
state = self.request.GET.get('state')
|
||||||
|
session_state = self.request.session.get(FEISHU_STATE_SESSION_KEY)
|
||||||
|
if state != session_state:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_verify_state_failed_response(self, redirect_uri):
|
||||||
|
msg = _("You've been hacked")
|
||||||
|
return self.get_failed_reponse(redirect_uri, msg, msg)
|
||||||
|
|
||||||
|
def get_qr_url(self, redirect_uri):
|
||||||
|
state = random_string(16)
|
||||||
|
self.request.session[FEISHU_STATE_SESSION_KEY] = state
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'app_id': settings.FEISHU_APP_ID,
|
||||||
|
'state': state,
|
||||||
|
'redirect_uri': redirect_uri,
|
||||||
|
}
|
||||||
|
url = URL.AUTHEN + '?' + urllib.parse.urlencode(params)
|
||||||
|
return url
|
||||||
|
|
||||||
|
def get_success_reponse(self, redirect_url, title, msg):
|
||||||
|
ok_flash_msg_url = reverse('authentication:feishu-bind-success-flash-msg')
|
||||||
|
ok_flash_msg_url += '?' + urllib.parse.urlencode({
|
||||||
|
'redirect_url': redirect_url,
|
||||||
|
'title': title,
|
||||||
|
'msg': msg
|
||||||
|
})
|
||||||
|
return HttpResponseRedirect(ok_flash_msg_url)
|
||||||
|
|
||||||
|
def get_failed_reponse(self, redirect_url, title, msg):
|
||||||
|
failed_flash_msg_url = reverse('authentication:feishu-bind-failed-flash-msg')
|
||||||
|
failed_flash_msg_url += '?' + urllib.parse.urlencode({
|
||||||
|
'redirect_url': redirect_url,
|
||||||
|
'title': title,
|
||||||
|
'msg': msg
|
||||||
|
})
|
||||||
|
return HttpResponseRedirect(failed_flash_msg_url)
|
||||||
|
|
||||||
|
def get_already_bound_response(self, redirect_url):
|
||||||
|
msg = _('FeiShu is already bound')
|
||||||
|
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class FeiShuQRBindView(FeiShuQRMixin, View):
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
|
||||||
|
def get(self, request: HttpRequest):
|
||||||
|
user = request.user
|
||||||
|
redirect_url = request.GET.get('redirect_url')
|
||||||
|
|
||||||
|
if not is_auth_password_time_valid(request.session):
|
||||||
|
msg = _('Please verify your password first')
|
||||||
|
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
redirect_uri = reverse('authentication:feishu-qr-bind-callback', external=True)
|
||||||
|
redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url})
|
||||||
|
|
||||||
|
url = self.get_qr_url(redirect_uri)
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
|
||||||
|
class FeiShuQRBindCallbackView(FeiShuQRMixin, View):
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
|
||||||
|
def get(self, request: HttpRequest):
|
||||||
|
code = request.GET.get('code')
|
||||||
|
redirect_url = request.GET.get('redirect_url')
|
||||||
|
|
||||||
|
if not self.verify_state():
|
||||||
|
return self.get_verify_state_failed_response(redirect_url)
|
||||||
|
|
||||||
|
user = request.user
|
||||||
|
|
||||||
|
if user.feishu_id:
|
||||||
|
response = self.get_already_bound_response(redirect_url)
|
||||||
|
return response
|
||||||
|
|
||||||
|
feishu = FeiShu(
|
||||||
|
app_id=settings.FEISHU_APP_ID,
|
||||||
|
app_secret=settings.FEISHU_APP_SECRET
|
||||||
|
)
|
||||||
|
user_id = feishu.get_user_id_by_code(code)
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
msg = _('FeiShu query user failed')
|
||||||
|
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
try:
|
||||||
|
user.feishu_id = user_id
|
||||||
|
user.save()
|
||||||
|
except IntegrityError as e:
|
||||||
|
if e.args[0] == 1062:
|
||||||
|
msg = _('The FeiShu is already bound to another user')
|
||||||
|
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||||
|
return response
|
||||||
|
raise e
|
||||||
|
|
||||||
|
msg = _('Binding FeiShu successfully')
|
||||||
|
response = self.get_success_reponse(redirect_url, msg, msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class FeiShuEnableStartView(UserVerifyPasswordView):
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
referer = self.request.META.get('HTTP_REFERER')
|
||||||
|
redirect_url = self.request.GET.get("redirect_url")
|
||||||
|
|
||||||
|
success_url = reverse('authentication:feishu-qr-bind')
|
||||||
|
|
||||||
|
success_url += '?' + urllib.parse.urlencode({
|
||||||
|
'redirect_url': redirect_url or referer
|
||||||
|
})
|
||||||
|
|
||||||
|
return success_url
|
||||||
|
|
||||||
|
|
||||||
|
class FeiShuQRLoginView(FeiShuQRMixin, View):
|
||||||
|
permission_classes = (AllowAny,)
|
||||||
|
|
||||||
|
def get(self, request: HttpRequest):
|
||||||
|
redirect_url = request.GET.get('redirect_url')
|
||||||
|
|
||||||
|
redirect_uri = reverse('authentication:feishu-qr-login-callback', external=True)
|
||||||
|
redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url})
|
||||||
|
|
||||||
|
url = self.get_qr_url(redirect_uri)
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
|
||||||
|
class FeiShuQRLoginCallbackView(AuthMixin, FeiShuQRMixin, View):
|
||||||
|
permission_classes = (AllowAny,)
|
||||||
|
|
||||||
|
def get(self, request: HttpRequest):
|
||||||
|
code = request.GET.get('code')
|
||||||
|
redirect_url = request.GET.get('redirect_url')
|
||||||
|
login_url = reverse('authentication:login')
|
||||||
|
|
||||||
|
if not self.verify_state():
|
||||||
|
return self.get_verify_state_failed_response(redirect_url)
|
||||||
|
|
||||||
|
feishu = FeiShu(
|
||||||
|
app_id=settings.FEISHU_APP_ID,
|
||||||
|
app_secret=settings.FEISHU_APP_SECRET
|
||||||
|
)
|
||||||
|
user_id = feishu.get_user_id_by_code(code)
|
||||||
|
if not user_id:
|
||||||
|
# 正常流程不会出这个错误,hack 行为
|
||||||
|
msg = _('Failed to get user from FeiShu')
|
||||||
|
response = self.get_failed_reponse(login_url, title=msg, msg=msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
user = get_object_or_none(User, feishu_id=user_id)
|
||||||
|
if user is None:
|
||||||
|
title = _('FeiShu is not bound')
|
||||||
|
msg = _('Please login with a password and then bind the WeCom')
|
||||||
|
response = self.get_failed_reponse(login_url, title=title, msg=msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.check_oauth2_auth(user, settings.AUTH_BACKEND_FEISHU)
|
||||||
|
except errors.AuthFailedError as e:
|
||||||
|
self.set_login_failed_mark()
|
||||||
|
msg = e.msg
|
||||||
|
response = self.get_failed_reponse(login_url, title=msg, msg=msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
return self.redirect_to_guard_view()
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(never_cache, name='dispatch')
|
||||||
|
class FlashFeiShuBindSucceedMsgView(TemplateView):
|
||||||
|
template_name = 'flash_message_standalone.html'
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
title = request.GET.get('title')
|
||||||
|
msg = request.GET.get('msg')
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'title': title or _('Binding FeiShu successfully'),
|
||||||
|
'messages': msg or _('Binding FeiShu successfully'),
|
||||||
|
'interval': 5,
|
||||||
|
'redirect_url': request.GET.get('redirect_url'),
|
||||||
|
'auto_redirect': True,
|
||||||
|
}
|
||||||
|
return self.render_to_response(context)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(never_cache, name='dispatch')
|
||||||
|
class FlashFeiShuBindFailedMsgView(TemplateView):
|
||||||
|
template_name = 'flash_message_standalone.html'
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
title = request.GET.get('title')
|
||||||
|
msg = request.GET.get('msg')
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'title': title or _('Binding FeiShu failed'),
|
||||||
|
'messages': msg or _('Binding FeiShu failed'),
|
||||||
|
'interval': 5,
|
||||||
|
'redirect_url': request.GET.get('redirect_url'),
|
||||||
|
'auto_redirect': True,
|
||||||
|
}
|
||||||
|
return self.render_to_response(context)
|
@ -46,24 +46,44 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
|||||||
return None
|
return None
|
||||||
next_url = request.GET.get('next') or '/'
|
next_url = request.GET.get('next') or '/'
|
||||||
auth_type = ''
|
auth_type = ''
|
||||||
auth_url = ''
|
|
||||||
if settings.AUTH_OPENID:
|
if settings.AUTH_OPENID:
|
||||||
auth_type = 'OIDC'
|
auth_type = 'OIDC'
|
||||||
auth_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME) + f'?next={next_url}'
|
openid_auth_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME) + f'?next={next_url}'
|
||||||
elif settings.AUTH_CAS:
|
else:
|
||||||
|
openid_auth_url = None
|
||||||
|
|
||||||
|
if settings.AUTH_CAS:
|
||||||
auth_type = 'CAS'
|
auth_type = 'CAS'
|
||||||
auth_url = reverse(settings.CAS_LOGIN_URL_NAME) + f'?next={next_url}'
|
cas_auth_url = reverse(settings.CAS_LOGIN_URL_NAME) + f'?next={next_url}'
|
||||||
if not auth_url:
|
else:
|
||||||
|
cas_auth_url = None
|
||||||
|
|
||||||
|
if not any([openid_auth_url, cas_auth_url]):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
message_data = {
|
if settings.LOGIN_REDIRECT_TO_BACKEND == 'OPENID' and openid_auth_url:
|
||||||
'title': _('Redirecting'),
|
auth_url = openid_auth_url
|
||||||
'message': _("Redirecting to {} authentication").format(auth_type),
|
|
||||||
'redirect_url': auth_url,
|
elif settings.LOGIN_REDIRECT_TO_BACKEND == 'CAS' and cas_auth_url:
|
||||||
'has_cancel': True,
|
auth_url = cas_auth_url
|
||||||
'cancel_url': reverse('authentication:login') + '?admin=1'
|
|
||||||
}
|
else:
|
||||||
redirect_url = FlashMessageUtil.gen_message_url(message_data)
|
auth_url = openid_auth_url or cas_auth_url
|
||||||
|
|
||||||
|
if settings.LOGIN_REDIRECT_TO_BACKEND:
|
||||||
|
redirect_url = auth_url
|
||||||
|
else:
|
||||||
|
message_data = {
|
||||||
|
'title': _('Redirecting'),
|
||||||
|
'message': _("Redirecting to {} authentication").format(auth_type),
|
||||||
|
'redirect_url': auth_url,
|
||||||
|
'interval': 3,
|
||||||
|
'has_cancel': True,
|
||||||
|
'cancel_url': reverse('authentication:login') + '?admin=1'
|
||||||
|
}
|
||||||
|
redirect_url = FlashMessageUtil.gen_message_url(message_data)
|
||||||
|
|
||||||
query_string = request.GET.urlencode()
|
query_string = request.GET.urlencode()
|
||||||
redirect_url = "{}&{}".format(redirect_url, query_string)
|
redirect_url = "{}&{}".format(redirect_url, query_string)
|
||||||
return redirect_url
|
return redirect_url
|
||||||
@ -134,6 +154,7 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
|||||||
'AUTH_CAS': settings.AUTH_CAS,
|
'AUTH_CAS': settings.AUTH_CAS,
|
||||||
'AUTH_WECOM': settings.AUTH_WECOM,
|
'AUTH_WECOM': settings.AUTH_WECOM,
|
||||||
'AUTH_DINGTALK': settings.AUTH_DINGTALK,
|
'AUTH_DINGTALK': settings.AUTH_DINGTALK,
|
||||||
|
'AUTH_FEISHU': settings.AUTH_FEISHU,
|
||||||
'rsa_public_key': rsa_public_key,
|
'rsa_public_key': rsa_public_key,
|
||||||
'forgot_password_url': forgot_password_url
|
'forgot_password_url': forgot_password_url
|
||||||
}
|
}
|
||||||
|
@ -10,3 +10,15 @@ def on_transaction_commit(func):
|
|||||||
def inner(*args, **kwargs):
|
def inner(*args, **kwargs):
|
||||||
transaction.on_commit(lambda: func(*args, **kwargs))
|
transaction.on_commit(lambda: func(*args, **kwargs))
|
||||||
return inner
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
class Singleton(object):
|
||||||
|
""" 单例类 """
|
||||||
|
def __init__(self, cls):
|
||||||
|
self._cls = cls
|
||||||
|
self._instance = {}
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
if self._cls not in self._instance:
|
||||||
|
self._instance[self._cls] = self._cls()
|
||||||
|
return self._instance[self._cls]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet, ViewSet
|
||||||
from rest_framework_bulk import BulkModelViewSet
|
from rest_framework_bulk import BulkModelViewSet
|
||||||
|
|
||||||
from ..mixins.api import (
|
from ..mixins.api import (
|
||||||
@ -15,19 +15,23 @@ class CommonMixin(SerializerMixin,
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class JmsGenericViewSet(CommonMixin,
|
class JMSGenericViewSet(CommonMixin, GenericViewSet):
|
||||||
GenericViewSet):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class JMSModelViewSet(CommonMixin,
|
class JMSViewSet(CommonMixin, ViewSet):
|
||||||
ModelViewSet):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class JMSBulkModelViewSet(CommonMixin,
|
class JMSModelViewSet(CommonMixin, ModelViewSet):
|
||||||
AllowBulkDestroyMixin,
|
pass
|
||||||
BulkModelViewSet):
|
|
||||||
|
|
||||||
|
class JMSReadOnlyModelViewSet(CommonMixin, ReadOnlyModelViewSet):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class JMSBulkModelViewSet(CommonMixin, AllowBulkDestroyMixin, BulkModelViewSet):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
0
apps/common/management/__init__.py
Normal file
0
apps/common/management/__init__.py
Normal file
0
apps/common/management/commands/__init__.py
Normal file
0
apps/common/management/commands/__init__.py
Normal file
6
apps/common/management/commands/restart.py
Normal file
6
apps/common/management/commands/restart.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from .services.command import BaseActionCommand, Action
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseActionCommand):
|
||||||
|
help = 'Restart services'
|
||||||
|
action = Action.restart.value
|
139
apps/common/management/commands/services/command.py
Normal file
139
apps/common/management/commands/services/command.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.db.models import TextChoices
|
||||||
|
from .utils import ServicesUtil
|
||||||
|
from .hands import *
|
||||||
|
|
||||||
|
|
||||||
|
class Services(TextChoices):
|
||||||
|
gunicorn = 'gunicorn', 'gunicorn'
|
||||||
|
daphne = 'daphne', 'daphne'
|
||||||
|
celery_ansible = 'celery_ansible', 'celery_ansible'
|
||||||
|
celery_default = 'celery_default', 'celery_default'
|
||||||
|
beat = 'beat', 'beat'
|
||||||
|
flower = 'flower', 'flower'
|
||||||
|
ws = 'ws', 'ws'
|
||||||
|
web = 'web', 'web'
|
||||||
|
celery = 'celery', 'celery'
|
||||||
|
task = 'task', 'task'
|
||||||
|
all = 'all', 'all'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_service_object_class(cls, name):
|
||||||
|
from . import services
|
||||||
|
services_map = {
|
||||||
|
cls.gunicorn.value: services.GunicornService,
|
||||||
|
cls.daphne: services.DaphneService,
|
||||||
|
cls.flower: services.FlowerService,
|
||||||
|
cls.celery_default: services.CeleryDefaultService,
|
||||||
|
cls.celery_ansible: services.CeleryAnsibleService,
|
||||||
|
cls.beat: services.BeatService
|
||||||
|
}
|
||||||
|
return services_map.get(name)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def ws_services(cls):
|
||||||
|
return [cls.daphne]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def web_services(cls):
|
||||||
|
return [cls.gunicorn, cls.daphne]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def celery_services(cls):
|
||||||
|
return [cls.celery_ansible, cls.celery_default]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def task_services(cls):
|
||||||
|
return cls.celery_services() + [cls.beat, cls.flower]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def all_services(cls):
|
||||||
|
return cls.web_services() + cls.task_services()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_services_values(cls):
|
||||||
|
return [cls.all.value, cls.web.value, cls.task.value]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_service_objects(cls, service_names, **kwargs):
|
||||||
|
services = set()
|
||||||
|
for name in service_names:
|
||||||
|
method_name = f'{name}_services'
|
||||||
|
if hasattr(cls, method_name):
|
||||||
|
_services = getattr(cls, method_name)()
|
||||||
|
elif hasattr(cls, name):
|
||||||
|
_services = [getattr(cls, name)]
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
services.update(set(_services))
|
||||||
|
|
||||||
|
service_objects = []
|
||||||
|
for s in services:
|
||||||
|
service_class = cls.get_service_object_class(s.value)
|
||||||
|
if not service_class:
|
||||||
|
continue
|
||||||
|
kwargs.update({
|
||||||
|
'name': s.value
|
||||||
|
})
|
||||||
|
service_object = service_class(**kwargs)
|
||||||
|
service_objects.append(service_object)
|
||||||
|
return service_objects
|
||||||
|
|
||||||
|
|
||||||
|
class Action(TextChoices):
|
||||||
|
start = 'start', 'start'
|
||||||
|
status = 'status', 'status'
|
||||||
|
stop = 'stop', 'stop'
|
||||||
|
restart = 'restart', 'restart'
|
||||||
|
|
||||||
|
|
||||||
|
class BaseActionCommand(BaseCommand):
|
||||||
|
help = 'Service Base Command'
|
||||||
|
|
||||||
|
action = None
|
||||||
|
util = None
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'services', nargs='+', choices=Services.export_services_values(), help='Service',
|
||||||
|
)
|
||||||
|
parser.add_argument('-d', '--daemon', nargs="?", const=True)
|
||||||
|
parser.add_argument('-w', '--worker', type=int, nargs="?", default=4)
|
||||||
|
parser.add_argument('-f', '--force', nargs="?", const=True)
|
||||||
|
|
||||||
|
def initial_util(self, *args, **options):
|
||||||
|
service_names = options.get('services')
|
||||||
|
service_kwargs = {
|
||||||
|
'worker_gunicorn': options.get('worker')
|
||||||
|
}
|
||||||
|
services = Services.get_service_objects(service_names=service_names, **service_kwargs)
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
'services': services,
|
||||||
|
'run_daemon': options.get('daemon', False),
|
||||||
|
'stop_daemon': self.action == Action.stop.value and Services.all.value in service_names,
|
||||||
|
'force_stop': options.get('force') or False,
|
||||||
|
}
|
||||||
|
self.util = ServicesUtil(**kwargs)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
self.initial_util(*args, **options)
|
||||||
|
assert self.action in Action.values, f'The action {self.action} is not in the optional list'
|
||||||
|
_handle = getattr(self, f'_handle_{self.action}', lambda: None)
|
||||||
|
_handle()
|
||||||
|
|
||||||
|
def _handle_start(self):
|
||||||
|
self.util.start_and_watch()
|
||||||
|
os._exit(0)
|
||||||
|
|
||||||
|
def _handle_stop(self):
|
||||||
|
self.util.stop()
|
||||||
|
|
||||||
|
def _handle_restart(self):
|
||||||
|
self.util.restart()
|
||||||
|
|
||||||
|
def _handle_status(self):
|
||||||
|
self.util.show_status()
|
26
apps/common/management/commands/services/hands.py
Normal file
26
apps/common/management/commands/services/hands.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from apps.jumpserver.const import CONFIG
|
||||||
|
|
||||||
|
try:
|
||||||
|
from apps.jumpserver import const
|
||||||
|
__version__ = const.VERSION
|
||||||
|
except ImportError as e:
|
||||||
|
print("Not found __version__: {}".format(e))
|
||||||
|
print("Python is: ")
|
||||||
|
logging.info(sys.executable)
|
||||||
|
__version__ = 'Unknown'
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
HTTP_HOST = CONFIG.HTTP_BIND_HOST or '127.0.0.1'
|
||||||
|
HTTP_PORT = CONFIG.HTTP_LISTEN_PORT or 8080
|
||||||
|
WS_PORT = CONFIG.WS_LISTEN_PORT or 8082
|
||||||
|
DEBUG = CONFIG.DEBUG or False
|
||||||
|
BASE_DIR = os.path.dirname(settings.BASE_DIR)
|
||||||
|
LOG_DIR = os.path.join(BASE_DIR, 'logs')
|
||||||
|
APPS_DIR = os.path.join(BASE_DIR, 'apps')
|
||||||
|
TMP_DIR = os.path.join(BASE_DIR, 'tmp')
|
@ -0,0 +1,6 @@
|
|||||||
|
from .beat import *
|
||||||
|
from .celery_ansible import *
|
||||||
|
from .celery_default import *
|
||||||
|
from .daphne import *
|
||||||
|
from .flower import *
|
||||||
|
from .gunicorn import *
|
204
apps/common/management/commands/services/services/base.py
Normal file
204
apps/common/management/commands/services/services/base.py
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import abc
|
||||||
|
import time
|
||||||
|
import shutil
|
||||||
|
import psutil
|
||||||
|
import datetime
|
||||||
|
import threading
|
||||||
|
import subprocess
|
||||||
|
from ..hands import *
|
||||||
|
|
||||||
|
|
||||||
|
class BaseService(object):
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.name = kwargs['name']
|
||||||
|
self._process = None
|
||||||
|
self.STOP_TIMEOUT = 10
|
||||||
|
self.max_retry = 0
|
||||||
|
self.retry = 3
|
||||||
|
self.LOG_KEEP_DAYS = 7
|
||||||
|
self.EXIT_EVENT = threading.Event()
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abc.abstractmethod
|
||||||
|
def cmd(self):
|
||||||
|
return []
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abc.abstractmethod
|
||||||
|
def cwd(self):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_running(self):
|
||||||
|
if self.pid == 0:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
os.kill(self.pid, 0)
|
||||||
|
except (OSError, ProcessLookupError):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def show_status(self):
|
||||||
|
if self.is_running:
|
||||||
|
msg = f'{self.name} is running: {self.pid}.'
|
||||||
|
else:
|
||||||
|
msg = f'{self.name} is stopped.'
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
# -- log --
|
||||||
|
@property
|
||||||
|
def log_filename(self):
|
||||||
|
return f'{self.name}.log'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def log_filepath(self):
|
||||||
|
return os.path.join(LOG_DIR, self.log_filename)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def log_file(self):
|
||||||
|
return open(self.log_filepath, 'a')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def log_dir(self):
|
||||||
|
return os.path.dirname(self.log_filepath)
|
||||||
|
# -- end log --
|
||||||
|
|
||||||
|
# -- pid --
|
||||||
|
@property
|
||||||
|
def pid_filepath(self):
|
||||||
|
return os.path.join(TMP_DIR, f'{self.name}.pid')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pid(self):
|
||||||
|
if not os.path.isfile(self.pid_filepath):
|
||||||
|
return 0
|
||||||
|
with open(self.pid_filepath) as f:
|
||||||
|
try:
|
||||||
|
pid = int(f.read().strip())
|
||||||
|
except ValueError:
|
||||||
|
pid = 0
|
||||||
|
return pid
|
||||||
|
|
||||||
|
def write_pid(self):
|
||||||
|
with open(self.pid_filepath, 'w') as f:
|
||||||
|
f.write(str(self.process.pid))
|
||||||
|
|
||||||
|
def remove_pid(self):
|
||||||
|
if os.path.isfile(self.pid_filepath):
|
||||||
|
os.unlink(self.pid_filepath)
|
||||||
|
# -- end pid --
|
||||||
|
|
||||||
|
# -- process --
|
||||||
|
@property
|
||||||
|
def process(self):
|
||||||
|
if not self._process:
|
||||||
|
try:
|
||||||
|
self._process = psutil.Process(self.pid)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return self._process
|
||||||
|
|
||||||
|
# -- end process --
|
||||||
|
|
||||||
|
# -- action --
|
||||||
|
def open_subprocess(self):
|
||||||
|
kwargs = {'cwd': self.cwd, 'stderr': self.log_file, 'stdout': self.log_file}
|
||||||
|
self._process = subprocess.Popen(self.cmd, **kwargs)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
if self.is_running:
|
||||||
|
self.show_status()
|
||||||
|
return
|
||||||
|
self.remove_pid()
|
||||||
|
self.open_subprocess()
|
||||||
|
self.write_pid()
|
||||||
|
self.start_other()
|
||||||
|
|
||||||
|
def start_other(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stop(self, force=False):
|
||||||
|
if not self.is_running:
|
||||||
|
self.show_status()
|
||||||
|
# self.remove_pid()
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f'Stop service: {self.name}', end='')
|
||||||
|
sig = 9 if force else 15
|
||||||
|
os.kill(self.pid, sig)
|
||||||
|
|
||||||
|
if self.process is None:
|
||||||
|
print("\033[31m No process found\033[0m")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.process.wait(1)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for i in range(self.STOP_TIMEOUT):
|
||||||
|
if i == self.STOP_TIMEOUT - 1:
|
||||||
|
print("\033[31m Error\033[0m")
|
||||||
|
if not self.is_running:
|
||||||
|
print("\033[32m Ok\033[0m")
|
||||||
|
self.remove_pid()
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
def watch(self):
|
||||||
|
self._check()
|
||||||
|
if not self.is_running:
|
||||||
|
self._restart()
|
||||||
|
self._rotate_log()
|
||||||
|
|
||||||
|
def _check(self):
|
||||||
|
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
print(f"{now} Check service status: {self.name} -> ", end='')
|
||||||
|
if self.process:
|
||||||
|
try:
|
||||||
|
self.process.wait(1) # 不wait,子进程可能无法回收
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self.is_running:
|
||||||
|
print(f'running at {self.pid}')
|
||||||
|
else:
|
||||||
|
print(f'stopped at {self.pid}')
|
||||||
|
|
||||||
|
def _restart(self):
|
||||||
|
if self.retry > self.max_retry:
|
||||||
|
logging.info("Service start failed, exit: ", self.name)
|
||||||
|
self.EXIT_EVENT.set()
|
||||||
|
return
|
||||||
|
self.retry += 1
|
||||||
|
logging.info(f'> Find {self.name} stopped, retry {self.retry}, {self.pid}')
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def _rotate_log(self):
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
_time = now.strftime('%H:%M')
|
||||||
|
if _time != '23:59':
|
||||||
|
return
|
||||||
|
|
||||||
|
backup_date = now.strftime('%Y-%m-%d')
|
||||||
|
backup_log_dir = os.path.join(self.log_dir, backup_date)
|
||||||
|
if not os.path.exists(backup_log_dir):
|
||||||
|
os.mkdir(backup_log_dir)
|
||||||
|
|
||||||
|
backup_log_path = os.path.join(backup_log_dir, self.log_filename)
|
||||||
|
if os.path.isfile(self.log_filepath) and not os.path.isfile(backup_log_path):
|
||||||
|
logging.info(f'Rotate log file: {self.log_filepath} => {backup_log_path}')
|
||||||
|
shutil.copy(self.log_filepath, backup_log_path)
|
||||||
|
with open(self.log_filepath, 'w') as f:
|
||||||
|
pass
|
||||||
|
|
||||||
|
to_delete_date = now - datetime.timedelta(days=self.LOG_KEEP_DAYS)
|
||||||
|
to_delete_dir = os.path.join(LOG_DIR, to_delete_date.strftime('%Y-%m-%d'))
|
||||||
|
if os.path.exists(to_delete_dir):
|
||||||
|
logging.info(f'Remove old log: {to_delete_dir}')
|
||||||
|
shutil.rmtree(to_delete_dir, ignore_errors=True)
|
||||||
|
# -- end action --
|
||||||
|
|
25
apps/common/management/commands/services/services/beat.py
Normal file
25
apps/common/management/commands/services/services/beat.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from ..hands import *
|
||||||
|
from .base import BaseService
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['BeatService']
|
||||||
|
|
||||||
|
|
||||||
|
class BeatService(BaseService):
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.lock = cache.lock('beat-distribute-start-lock', expire=60)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cmd(self):
|
||||||
|
print("\n- Start Beat as Periodic Task Scheduler")
|
||||||
|
cmd = [
|
||||||
|
sys.executable, 'start_celery_beat.py',
|
||||||
|
]
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cwd(self):
|
||||||
|
return os.path.join(BASE_DIR, 'utils')
|
@ -0,0 +1,11 @@
|
|||||||
|
from .celery_base import CeleryBaseService
|
||||||
|
|
||||||
|
__all__ = ['CeleryAnsibleService']
|
||||||
|
|
||||||
|
|
||||||
|
class CeleryAnsibleService(CeleryBaseService):
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
kwargs['queue'] = 'ansible'
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
@ -0,0 +1,38 @@
|
|||||||
|
from ..hands import *
|
||||||
|
from .base import BaseService
|
||||||
|
|
||||||
|
|
||||||
|
class CeleryBaseService(BaseService):
|
||||||
|
|
||||||
|
def __init__(self, queue, num=10, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.queue = queue
|
||||||
|
self.num = num
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cmd(self):
|
||||||
|
print('\n- Start Celery as Distributed Task Queue: {}'.format(self.queue.capitalize()))
|
||||||
|
|
||||||
|
os.environ.setdefault('PYTHONOPTIMIZE', '1')
|
||||||
|
os.environ.setdefault('ANSIBLE_FORCE_COLOR', 'True')
|
||||||
|
|
||||||
|
if os.getuid() == 0:
|
||||||
|
os.environ.setdefault('C_FORCE_ROOT', '1')
|
||||||
|
server_hostname = os.environ.get("SERVER_HOSTNAME")
|
||||||
|
if not server_hostname:
|
||||||
|
server_hostname = '%h'
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
'celery', 'worker',
|
||||||
|
'-P', 'threads',
|
||||||
|
'-A', 'ops',
|
||||||
|
'-l', 'INFO',
|
||||||
|
'-c', str(self.num),
|
||||||
|
'-Q', self.queue,
|
||||||
|
'-n', f'{self.queue}@{server_hostname}'
|
||||||
|
]
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cwd(self):
|
||||||
|
return APPS_DIR
|
@ -0,0 +1,16 @@
|
|||||||
|
from .celery_base import CeleryBaseService
|
||||||
|
|
||||||
|
__all__ = ['CeleryDefaultService']
|
||||||
|
|
||||||
|
|
||||||
|
class CeleryDefaultService(CeleryBaseService):
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
kwargs['queue'] = 'celery'
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
def start_other(self):
|
||||||
|
from terminal.startup import CeleryTerminal
|
||||||
|
celery_terminal = CeleryTerminal()
|
||||||
|
celery_terminal.start_heartbeat_thread()
|
||||||
|
|
25
apps/common/management/commands/services/services/daphne.py
Normal file
25
apps/common/management/commands/services/services/daphne.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from ..hands import *
|
||||||
|
from .base import BaseService
|
||||||
|
|
||||||
|
__all__ = ['DaphneService']
|
||||||
|
|
||||||
|
|
||||||
|
class DaphneService(BaseService):
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cmd(self):
|
||||||
|
print("\n- Start Daphne ASGI WS Server")
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
'daphne', 'jumpserver.asgi:application',
|
||||||
|
'-b', HTTP_HOST,
|
||||||
|
'-p', str(WS_PORT),
|
||||||
|
]
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cwd(self):
|
||||||
|
return APPS_DIR
|
31
apps/common/management/commands/services/services/flower.py
Normal file
31
apps/common/management/commands/services/services/flower.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from ..hands import *
|
||||||
|
from .base import BaseService
|
||||||
|
|
||||||
|
__all__ = ['FlowerService']
|
||||||
|
|
||||||
|
|
||||||
|
class FlowerService(BaseService):
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cmd(self):
|
||||||
|
print("\n- Start Flower as Task Monitor")
|
||||||
|
|
||||||
|
if os.getuid() == 0:
|
||||||
|
os.environ.setdefault('C_FORCE_ROOT', '1')
|
||||||
|
cmd = [
|
||||||
|
'celery', 'flower',
|
||||||
|
'-A', 'ops',
|
||||||
|
'-l', 'INFO',
|
||||||
|
'--url_prefix=/core/flower',
|
||||||
|
'--auto_refresh=False',
|
||||||
|
'--max_tasks=1000',
|
||||||
|
'--tasks_columns=uuid,name,args,state,received,started,runtime,worker'
|
||||||
|
]
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cwd(self):
|
||||||
|
return APPS_DIR
|
@ -0,0 +1,40 @@
|
|||||||
|
from ..hands import *
|
||||||
|
from .base import BaseService
|
||||||
|
|
||||||
|
__all__ = ['GunicornService']
|
||||||
|
|
||||||
|
|
||||||
|
class GunicornService(BaseService):
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.worker = kwargs['worker_gunicorn']
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cmd(self):
|
||||||
|
print("\n- Start Gunicorn WSGI HTTP Server")
|
||||||
|
|
||||||
|
log_format = '%(h)s %(t)s %(L)ss "%(r)s" %(s)s %(b)s '
|
||||||
|
bind = f'{HTTP_HOST}:{HTTP_PORT}'
|
||||||
|
cmd = [
|
||||||
|
'gunicorn', 'jumpserver.wsgi',
|
||||||
|
'-b', bind,
|
||||||
|
'-k', 'gthread',
|
||||||
|
'--threads', '10',
|
||||||
|
'-w', str(self.worker),
|
||||||
|
'--max-requests', '4096',
|
||||||
|
'--access-logformat', log_format,
|
||||||
|
'--access-logfile', '-'
|
||||||
|
]
|
||||||
|
if DEBUG:
|
||||||
|
cmd.append('--reload')
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cwd(self):
|
||||||
|
return APPS_DIR
|
||||||
|
|
||||||
|
def start_other(self):
|
||||||
|
from terminal.startup import CoreTerminal
|
||||||
|
core_terminal = CoreTerminal()
|
||||||
|
core_terminal.start_heartbeat_thread()
|
140
apps/common/management/commands/services/utils.py
Normal file
140
apps/common/management/commands/services/utils.py
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import threading
|
||||||
|
import signal
|
||||||
|
import time
|
||||||
|
import daemon
|
||||||
|
from daemon import pidfile
|
||||||
|
from .hands import *
|
||||||
|
from .hands import __version__
|
||||||
|
from .services.base import BaseService
|
||||||
|
|
||||||
|
|
||||||
|
class ServicesUtil(object):
|
||||||
|
|
||||||
|
def __init__(self, services, run_daemon=False, force_stop=False, stop_daemon=False):
|
||||||
|
self._services = services
|
||||||
|
self.run_daemon = run_daemon
|
||||||
|
self.force_stop = force_stop
|
||||||
|
self.stop_daemon = stop_daemon
|
||||||
|
self.EXIT_EVENT = threading.Event()
|
||||||
|
self.check_interval = 30
|
||||||
|
self.files_preserve_map = {}
|
||||||
|
|
||||||
|
def restart(self):
|
||||||
|
self.stop()
|
||||||
|
time.sleep(5)
|
||||||
|
self.start_and_watch()
|
||||||
|
|
||||||
|
def start_and_watch(self):
|
||||||
|
logging.info(time.ctime())
|
||||||
|
logging.info(f'JumpServer version {__version__}, more see https://www.jumpserver.org')
|
||||||
|
self.start()
|
||||||
|
if self.run_daemon:
|
||||||
|
self.show_status()
|
||||||
|
with self.daemon_context:
|
||||||
|
self.watch()
|
||||||
|
else:
|
||||||
|
self.watch()
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
for service in self._services:
|
||||||
|
service: BaseService
|
||||||
|
service.start()
|
||||||
|
self.files_preserve_map[service.name] = service.log_file
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
for service in self._services:
|
||||||
|
service: BaseService
|
||||||
|
service.stop(force=self.force_stop)
|
||||||
|
|
||||||
|
if self.stop_daemon:
|
||||||
|
self._stop_daemon()
|
||||||
|
|
||||||
|
# -- watch --
|
||||||
|
def watch(self):
|
||||||
|
while not self.EXIT_EVENT.is_set():
|
||||||
|
try:
|
||||||
|
_exit = self._watch()
|
||||||
|
if _exit:
|
||||||
|
break
|
||||||
|
time.sleep(self.check_interval)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print('Start stop services')
|
||||||
|
break
|
||||||
|
self.clean_up()
|
||||||
|
|
||||||
|
def _watch(self):
|
||||||
|
for service in self._services:
|
||||||
|
service: BaseService
|
||||||
|
service.watch()
|
||||||
|
if service.EXIT_EVENT.is_set():
|
||||||
|
self.EXIT_EVENT.set()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
# -- end watch --
|
||||||
|
|
||||||
|
def clean_up(self):
|
||||||
|
if not self.EXIT_EVENT.is_set():
|
||||||
|
self.EXIT_EVENT.set()
|
||||||
|
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
def show_status(self):
|
||||||
|
for service in self._services:
|
||||||
|
service: BaseService
|
||||||
|
service.show_status()
|
||||||
|
|
||||||
|
# -- daemon --
|
||||||
|
def _stop_daemon(self):
|
||||||
|
if self.daemon_pid and self.daemon_is_running:
|
||||||
|
os.kill(self.daemon_pid, 15)
|
||||||
|
self.remove_daemon_pid()
|
||||||
|
|
||||||
|
def remove_daemon_pid(self):
|
||||||
|
if os.path.isfile(self.daemon_pid_filepath):
|
||||||
|
os.unlink(self.daemon_pid_filepath)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def daemon_pid(self):
|
||||||
|
if not os.path.isfile(self.daemon_pid_filepath):
|
||||||
|
return 0
|
||||||
|
with open(self.daemon_pid_filepath) as f:
|
||||||
|
try:
|
||||||
|
pid = int(f.read().strip())
|
||||||
|
except ValueError:
|
||||||
|
pid = 0
|
||||||
|
return pid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def daemon_is_running(self):
|
||||||
|
try:
|
||||||
|
os.kill(self.daemon_pid, 0)
|
||||||
|
except (OSError, ProcessLookupError):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def daemon_pid_filepath(self):
|
||||||
|
return os.path.join(TMP_DIR, 'jms.pid')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def daemon_log_filepath(self):
|
||||||
|
return os.path.join(LOG_DIR, 'jms.log')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def daemon_context(self):
|
||||||
|
daemon_log_file = open(self.daemon_log_filepath, 'a')
|
||||||
|
context = daemon.DaemonContext(
|
||||||
|
pidfile=pidfile.TimeoutPIDLockFile(self.daemon_pid_filepath),
|
||||||
|
signal_map={
|
||||||
|
signal.SIGTERM: lambda x, y: self.clean_up(),
|
||||||
|
signal.SIGHUP: 'terminate',
|
||||||
|
},
|
||||||
|
stdout=daemon_log_file,
|
||||||
|
stderr=daemon_log_file,
|
||||||
|
files_preserve=list(self.files_preserve_map.values()),
|
||||||
|
detach_process=True,
|
||||||
|
)
|
||||||
|
return context
|
||||||
|
# -- end daemon --
|
6
apps/common/management/commands/start.py
Normal file
6
apps/common/management/commands/start.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from .services.command import BaseActionCommand, Action
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseActionCommand):
|
||||||
|
help = 'Start services'
|
||||||
|
action = Action.start.value
|
6
apps/common/management/commands/status.py
Normal file
6
apps/common/management/commands/status.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from .services.command import BaseActionCommand, Action
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseActionCommand):
|
||||||
|
help = 'Show services status'
|
||||||
|
action = Action.status.value
|
6
apps/common/management/commands/stop.py
Normal file
6
apps/common/management/commands/stop.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from .services.command import BaseActionCommand, Action
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseActionCommand):
|
||||||
|
help = 'Stop services'
|
||||||
|
action = Action.stop.value
|
@ -2,8 +2,7 @@ import time
|
|||||||
import hmac
|
import hmac
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from common.message.backends.utils import request
|
from common.message.backends.utils import digest, as_request
|
||||||
from common.message.backends.utils import digest
|
|
||||||
from common.message.backends.mixin import BaseRequest
|
from common.message.backends.mixin import BaseRequest
|
||||||
|
|
||||||
|
|
||||||
@ -34,7 +33,7 @@ class URL:
|
|||||||
|
|
||||||
|
|
||||||
class DingTalkRequests(BaseRequest):
|
class DingTalkRequests(BaseRequest):
|
||||||
invalid_token_errcode = ErrorCode.INVALID_TOKEN
|
invalid_token_errcodes = (ErrorCode.INVALID_TOKEN,)
|
||||||
|
|
||||||
def __init__(self, appid, appsecret, agentid, timeout=None):
|
def __init__(self, appid, appsecret, agentid, timeout=None):
|
||||||
self._appid = appid
|
self._appid = appid
|
||||||
@ -55,21 +54,33 @@ class DingTalkRequests(BaseRequest):
|
|||||||
expires_in = data['expires_in']
|
expires_in = data['expires_in']
|
||||||
return access_token, expires_in
|
return access_token, expires_in
|
||||||
|
|
||||||
@request
|
def add_token(self, kwargs: dict):
|
||||||
|
params = kwargs.get('params')
|
||||||
|
if params is None:
|
||||||
|
params = {}
|
||||||
|
kwargs['params'] = params
|
||||||
|
params['access_token'] = self.access_token
|
||||||
|
|
||||||
def get(self, url, params=None,
|
def get(self, url, params=None,
|
||||||
with_token=False, with_sign=False,
|
with_token=False, with_sign=False,
|
||||||
check_errcode_is_0=True,
|
check_errcode_is_0=True,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
pass
|
pass
|
||||||
|
get = as_request(get)
|
||||||
|
|
||||||
@request
|
|
||||||
def post(self, url, json=None, params=None,
|
def post(self, url, json=None, params=None,
|
||||||
with_token=False, with_sign=False,
|
with_token=False, with_sign=False,
|
||||||
check_errcode_is_0=True,
|
check_errcode_is_0=True,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
pass
|
pass
|
||||||
|
post = as_request(post)
|
||||||
|
|
||||||
|
def _add_sign(self, kwargs: dict):
|
||||||
|
params = kwargs.get('params')
|
||||||
|
if params is None:
|
||||||
|
params = {}
|
||||||
|
kwargs['params'] = params
|
||||||
|
|
||||||
def _add_sign(self, params: dict):
|
|
||||||
timestamp = str(int(time.time() * 1000))
|
timestamp = str(int(time.time() * 1000))
|
||||||
signature = sign(self._appsecret, timestamp)
|
signature = sign(self._appsecret, timestamp)
|
||||||
accessKey = self._appid
|
accessKey = self._appid
|
||||||
@ -78,23 +89,17 @@ class DingTalkRequests(BaseRequest):
|
|||||||
params['signature'] = signature
|
params['signature'] = signature
|
||||||
params['accessKey'] = accessKey
|
params['accessKey'] = accessKey
|
||||||
|
|
||||||
def request(self, method, url, params=None,
|
def request(self, method, url,
|
||||||
with_token=False, with_sign=False,
|
with_token=False, with_sign=False,
|
||||||
check_errcode_is_0=True,
|
check_errcode_is_0=True,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
if not isinstance(params, dict):
|
|
||||||
params = {}
|
|
||||||
|
|
||||||
if with_token:
|
|
||||||
params['access_token'] = self.access_token
|
|
||||||
|
|
||||||
if with_sign:
|
if with_sign:
|
||||||
self._add_sign(params)
|
self._add_sign(kwargs)
|
||||||
|
|
||||||
data = self.raw_request(method, url, params=params, **kwargs)
|
|
||||||
if check_errcode_is_0:
|
|
||||||
self.check_errcode_is_0(data)
|
|
||||||
|
|
||||||
|
data = super().request(
|
||||||
|
method, url, with_token=with_token,
|
||||||
|
check_errcode_is_0=check_errcode_is_0, **kwargs)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
114
apps/common/message/backends/feishu/__init__.py
Normal file
114
apps/common/message/backends/feishu/__init__.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from rest_framework.exceptions import APIException
|
||||||
|
|
||||||
|
from common.utils.common import get_logger
|
||||||
|
from common.message.backends.utils import digest
|
||||||
|
from common.message.backends.mixin import RequestMixin, BaseRequest
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class URL:
|
||||||
|
AUTHEN = 'https://open.feishu.cn/open-apis/authen/v1/index'
|
||||||
|
|
||||||
|
GET_TOKEN = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/'
|
||||||
|
|
||||||
|
# https://open.feishu.cn/document/ukTMukTMukTM/uEDO4UjLxgDO14SM4gTN
|
||||||
|
GET_USER_INFO_BY_CODE = 'https://open.feishu.cn/open-apis/authen/v1/access_token'
|
||||||
|
|
||||||
|
SEND_MESSAGE = 'https://open.feishu.cn/open-apis/im/v1/messages'
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorCode:
|
||||||
|
INVALID_APP_ACCESS_TOKEN = 99991664
|
||||||
|
INVALID_USER_ACCESS_TOKEN = 99991668
|
||||||
|
INVALID_TENANT_ACCESS_TOKEN = 99991663
|
||||||
|
|
||||||
|
|
||||||
|
class FeishuRequests(BaseRequest):
|
||||||
|
"""
|
||||||
|
处理系统级错误,抛出 API 异常,直接生成 HTTP 响应,业务代码无需关心这些错误
|
||||||
|
- 确保 status_code == 200
|
||||||
|
- 确保 access_token 无效时重试
|
||||||
|
"""
|
||||||
|
invalid_token_errcodes = (
|
||||||
|
ErrorCode.INVALID_USER_ACCESS_TOKEN, ErrorCode.INVALID_TENANT_ACCESS_TOKEN,
|
||||||
|
ErrorCode.INVALID_APP_ACCESS_TOKEN
|
||||||
|
)
|
||||||
|
code_key = 'code'
|
||||||
|
msg_key = 'msg'
|
||||||
|
|
||||||
|
def __init__(self, app_id, app_secret, timeout=None):
|
||||||
|
self._app_id = app_id
|
||||||
|
self._app_secret = app_secret
|
||||||
|
|
||||||
|
super().__init__(timeout=timeout)
|
||||||
|
|
||||||
|
def get_access_token_cache_key(self):
|
||||||
|
return digest(self._app_id, self._app_secret)
|
||||||
|
|
||||||
|
def request_access_token(self):
|
||||||
|
data = {'app_id': self._app_id, 'app_secret': self._app_secret}
|
||||||
|
response = self.raw_request('post', url=URL.GET_TOKEN, data=data)
|
||||||
|
self.check_errcode_is_0(response)
|
||||||
|
|
||||||
|
access_token = response['tenant_access_token']
|
||||||
|
expires_in = response['expire']
|
||||||
|
return access_token, expires_in
|
||||||
|
|
||||||
|
def add_token(self, kwargs: dict):
|
||||||
|
headers = kwargs.setdefault('headers', {})
|
||||||
|
headers['Authorization'] = f'Bearer {self.access_token}'
|
||||||
|
|
||||||
|
|
||||||
|
class FeiShu(RequestMixin):
|
||||||
|
"""
|
||||||
|
非业务数据导致的错误直接抛异常,说明是系统配置错误,业务代码不用理会
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app_id, app_secret, timeout=None):
|
||||||
|
self._app_id = app_id
|
||||||
|
self._app_secret = app_secret
|
||||||
|
|
||||||
|
self._requests = FeishuRequests(
|
||||||
|
app_id=app_id,
|
||||||
|
app_secret=app_secret,
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_user_id_by_code(self, code):
|
||||||
|
# https://open.feishu.cn/document/ukTMukTMukTM/uEDO4UjLxgDO14SM4gTN
|
||||||
|
|
||||||
|
body = {
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'code': code
|
||||||
|
}
|
||||||
|
|
||||||
|
data = self._requests.post(URL.GET_USER_INFO_BY_CODE, json=body, check_errcode_is_0=False)
|
||||||
|
|
||||||
|
self._requests.check_errcode_is_0(data)
|
||||||
|
return data['data']['user_id']
|
||||||
|
|
||||||
|
def send_text(self, user_ids, msg):
|
||||||
|
params = {
|
||||||
|
'receive_id_type': 'user_id'
|
||||||
|
}
|
||||||
|
|
||||||
|
body = {
|
||||||
|
'msg_type': 'text',
|
||||||
|
'content': json.dumps({'text': msg})
|
||||||
|
}
|
||||||
|
|
||||||
|
invalid_users = []
|
||||||
|
for user_id in user_ids:
|
||||||
|
body['receive_id'] = user_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._requests.post(URL.SEND_MESSAGE, params=params, json=body)
|
||||||
|
except APIException as e:
|
||||||
|
# 只处理可预知的错误
|
||||||
|
logger.exception(e)
|
||||||
|
invalid_users.append(user_id)
|
||||||
|
return invalid_users
|
@ -6,7 +6,7 @@ from django.core.cache import cache
|
|||||||
from .utils import DictWrapper
|
from .utils import DictWrapper
|
||||||
from common.utils.common import get_logger
|
from common.utils.common import get_logger
|
||||||
from common.utils import lazyproperty
|
from common.utils import lazyproperty
|
||||||
from common.message.backends.utils import set_default
|
from common.message.backends.utils import set_default, as_request
|
||||||
|
|
||||||
from . import exceptions as exce
|
from . import exceptions as exce
|
||||||
|
|
||||||
@ -14,17 +14,37 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class RequestMixin:
|
class RequestMixin:
|
||||||
def check_errcode_is_0(self, data: DictWrapper):
|
code_key: str
|
||||||
errcode = data['errcode']
|
msg_key: str
|
||||||
|
|
||||||
|
|
||||||
|
class BaseRequest(RequestMixin):
|
||||||
|
"""
|
||||||
|
定义了 `access_token` 的过期刷新框架
|
||||||
|
"""
|
||||||
|
invalid_token_errcodes = ()
|
||||||
|
code_key = 'errcode'
|
||||||
|
msg_key = 'err_msg'
|
||||||
|
|
||||||
|
def __init__(self, timeout=None):
|
||||||
|
self._request_kwargs = {
|
||||||
|
'timeout': timeout
|
||||||
|
}
|
||||||
|
self.init_access_token()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check_errcode_is_0(cls, data: DictWrapper):
|
||||||
|
errcode = data[cls.code_key]
|
||||||
if errcode != 0:
|
if errcode != 0:
|
||||||
# 如果代码写的对,配置没问题,这里不该出错,系统性错误,直接抛异常
|
# 如果代码写的对,配置没问题,这里不该出错,系统性错误,直接抛异常
|
||||||
errmsg = data['errmsg']
|
errmsg = data[cls.msg_key]
|
||||||
logger.error(f'Response 200 but errcode is not 0: '
|
logger.error(f'Response 200 but errcode is not 0: '
|
||||||
f'errcode={errcode} '
|
f'errcode={errcode} '
|
||||||
f'errmsg={errmsg} ')
|
f'errmsg={errmsg} ')
|
||||||
raise exce.ErrCodeNot0(detail=data.raw_data)
|
raise exce.ErrCodeNot0(detail=data.raw_data)
|
||||||
|
|
||||||
def check_http_is_200(self, response):
|
@staticmethod
|
||||||
|
def check_http_is_200(response):
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
# 正常情况下不会返回非 200 响应码
|
# 正常情况下不会返回非 200 响应码
|
||||||
logger.error(f'Response error: '
|
logger.error(f'Response error: '
|
||||||
@ -33,25 +53,28 @@ class RequestMixin:
|
|||||||
f'\ncontent={response.content}')
|
f'\ncontent={response.content}')
|
||||||
raise exce.HTTPNot200(detail=response.json())
|
raise exce.HTTPNot200(detail=response.json())
|
||||||
|
|
||||||
|
|
||||||
class BaseRequest(RequestMixin):
|
|
||||||
invalid_token_errcode = -1
|
|
||||||
|
|
||||||
def __init__(self, timeout=None):
|
|
||||||
self._request_kwargs = {
|
|
||||||
'timeout': timeout
|
|
||||||
}
|
|
||||||
self.init_access_token()
|
|
||||||
|
|
||||||
def request_access_token(self):
|
def request_access_token(self):
|
||||||
|
"""
|
||||||
|
获取新的 `access_token` 的方法,子类需要实现
|
||||||
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def get_access_token_cache_key(self):
|
def get_access_token_cache_key(self):
|
||||||
|
"""
|
||||||
|
获取 `access_token` 的缓存 key, 子类需要实现
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def add_token(self, kwargs: dict):
|
||||||
|
"""
|
||||||
|
添加 token ,子类需要实现
|
||||||
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def is_token_invalid(self, data):
|
def is_token_invalid(self, data):
|
||||||
errcode = data['errcode']
|
code = data[self.code_key]
|
||||||
if errcode == self.invalid_token_errcode:
|
if code in self.invalid_token_errcodes:
|
||||||
|
logger.error(f'OAuth token invalid: {data}')
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -69,26 +92,58 @@ class BaseRequest(RequestMixin):
|
|||||||
def refresh_access_token(self):
|
def refresh_access_token(self):
|
||||||
access_token, expires_in = self.request_access_token()
|
access_token, expires_in = self.request_access_token()
|
||||||
self.access_token = access_token
|
self.access_token = access_token
|
||||||
cache.set(self.access_token_cache_key, access_token, expires_in)
|
cache.set(self.access_token_cache_key, access_token, expires_in - 10)
|
||||||
|
|
||||||
def raw_request(self, method, url, **kwargs):
|
def raw_request(self, method, url, **kwargs):
|
||||||
set_default(kwargs, self._request_kwargs)
|
set_default(kwargs, self._request_kwargs)
|
||||||
raw_data = ''
|
try:
|
||||||
|
response = getattr(requests, method)(url, **kwargs)
|
||||||
|
self.check_http_is_200(response)
|
||||||
|
raw_data = response.json()
|
||||||
|
data = DictWrapper(raw_data)
|
||||||
|
|
||||||
|
return data
|
||||||
|
except req_exce.ReadTimeout as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise exce.NetError
|
||||||
|
|
||||||
|
def token_request(self, method, url, **kwargs):
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
# 循环为了防止 access_token 失效
|
# 循环为了防止 access_token 失效
|
||||||
try:
|
self.add_token(kwargs)
|
||||||
response = getattr(requests, method)(url, **kwargs)
|
data = self.raw_request(method, url, **kwargs)
|
||||||
self.check_http_is_200(response)
|
|
||||||
raw_data = response.json()
|
|
||||||
data = DictWrapper(raw_data)
|
|
||||||
|
|
||||||
if self.is_token_invalid(data):
|
if self.is_token_invalid(data):
|
||||||
self.refresh_access_token()
|
self.refresh_access_token()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return data
|
return data
|
||||||
except req_exce.ReadTimeout as e:
|
logger.error(f'Get access_token error, check config: url={url} data={data.raw_data}')
|
||||||
logger.exception(e)
|
raise PermissionDenied(data.raw_data)
|
||||||
raise exce.NetError
|
|
||||||
logger.error(f'Get access_token error, check config: url={url} data={raw_data}')
|
def get(self, url, params=None, with_token=True,
|
||||||
raise PermissionDenied(raw_data)
|
check_errcode_is_0=True, **kwargs):
|
||||||
|
# self.request ...
|
||||||
|
pass
|
||||||
|
get = as_request(get)
|
||||||
|
|
||||||
|
def post(self, url, params=None, json=None,
|
||||||
|
with_token=True, check_errcode_is_0=True,
|
||||||
|
**kwargs):
|
||||||
|
# self.request ...
|
||||||
|
pass
|
||||||
|
post = as_request(post)
|
||||||
|
|
||||||
|
def request(self, method, url,
|
||||||
|
with_token=True,
|
||||||
|
check_errcode_is_0=True,
|
||||||
|
**kwargs):
|
||||||
|
|
||||||
|
if with_token:
|
||||||
|
data = self.token_request(method, url, **kwargs)
|
||||||
|
else:
|
||||||
|
data = self.raw_request(method, url, **kwargs)
|
||||||
|
|
||||||
|
if check_errcode_is_0:
|
||||||
|
self.check_errcode_is_0(data)
|
||||||
|
return data
|
||||||
|
@ -54,7 +54,7 @@ class DictWrapper:
|
|||||||
return str(self.raw_data)
|
return str(self.raw_data)
|
||||||
|
|
||||||
|
|
||||||
def request(func):
|
def as_request(func):
|
||||||
def inner(*args, **kwargs):
|
def inner(*args, **kwargs):
|
||||||
signature = inspect.signature(func)
|
signature = inspect.signature(func)
|
||||||
bound_args = signature.bind(*args, **kwargs)
|
bound_args = signature.bind(*args, **kwargs)
|
||||||
|
@ -2,13 +2,9 @@ from typing import Iterable, AnyStr
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework.exceptions import APIException
|
from rest_framework.exceptions import APIException
|
||||||
from requests.exceptions import ReadTimeout
|
|
||||||
import requests
|
|
||||||
from django.core.cache import cache
|
|
||||||
|
|
||||||
from common.utils.common import get_logger
|
from common.utils.common import get_logger
|
||||||
from common.message.backends.utils import digest, DictWrapper, update_values, set_default
|
from common.message.backends.utils import digest, DictWrapper, update_values, set_default
|
||||||
from common.message.backends.utils import request
|
|
||||||
from common.message.backends.mixin import RequestMixin, BaseRequest
|
from common.message.backends.mixin import RequestMixin, BaseRequest
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@ -48,7 +44,7 @@ class WeComRequests(BaseRequest):
|
|||||||
- 确保 status_code == 200
|
- 确保 status_code == 200
|
||||||
- 确保 access_token 无效时重试
|
- 确保 access_token 无效时重试
|
||||||
"""
|
"""
|
||||||
invalid_token_errcode = ErrorCode.INVALID_TOKEN
|
invalid_token_errcodes = (ErrorCode.INVALID_TOKEN,)
|
||||||
|
|
||||||
def __init__(self, corpid, corpsecret, agentid, timeout=None):
|
def __init__(self, corpid, corpsecret, agentid, timeout=None):
|
||||||
self._corpid = corpid
|
self._corpid = corpid
|
||||||
@ -68,35 +64,13 @@ class WeComRequests(BaseRequest):
|
|||||||
expires_in = data['expires_in']
|
expires_in = data['expires_in']
|
||||||
return access_token, expires_in
|
return access_token, expires_in
|
||||||
|
|
||||||
@request
|
def add_token(self, kwargs: dict):
|
||||||
def get(self, url, params=None, with_token=True,
|
params = kwargs.get('params')
|
||||||
check_errcode_is_0=True, **kwargs):
|
if params is None:
|
||||||
# self.request ...
|
|
||||||
pass
|
|
||||||
|
|
||||||
@request
|
|
||||||
def post(self, url, params=None, json=None,
|
|
||||||
with_token=True, check_errcode_is_0=True,
|
|
||||||
**kwargs):
|
|
||||||
# self.request ...
|
|
||||||
pass
|
|
||||||
|
|
||||||
def request(self, method, url,
|
|
||||||
params=None,
|
|
||||||
with_token=True,
|
|
||||||
check_errcode_is_0=True,
|
|
||||||
**kwargs):
|
|
||||||
|
|
||||||
if not isinstance(params, dict):
|
|
||||||
params = {}
|
params = {}
|
||||||
|
kwargs['params'] = params
|
||||||
|
|
||||||
if with_token:
|
params['access_token'] = self.access_token
|
||||||
params['access_token'] = self.access_token
|
|
||||||
|
|
||||||
data = self.raw_request(method, url, params=params, **kwargs)
|
|
||||||
if check_errcode_is_0:
|
|
||||||
self.check_errcode_is_0(data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class WeCom(RequestMixin):
|
class WeCom(RequestMixin):
|
||||||
@ -147,7 +121,7 @@ class WeCom(RequestMixin):
|
|||||||
if errcode in (ErrorCode.RECIPIENTS_INVALID, ErrorCode.RECIPIENTS_EMPTY):
|
if errcode in (ErrorCode.RECIPIENTS_INVALID, ErrorCode.RECIPIENTS_EMPTY):
|
||||||
# 全部接收人无权限或不存在
|
# 全部接收人无权限或不存在
|
||||||
return users
|
return users
|
||||||
self.check_errcode_is_0(data)
|
self._requests.check_errcode_is_0(data)
|
||||||
|
|
||||||
invaliduser = data['invaliduser']
|
invaliduser = data['invaliduser']
|
||||||
if not invaliduser:
|
if not invaliduser:
|
||||||
@ -173,7 +147,7 @@ class WeCom(RequestMixin):
|
|||||||
logger.warn(f'WeCom get_user_id_by_code invalid code: code={code}')
|
logger.warn(f'WeCom get_user_id_by_code invalid code: code={code}')
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
self.check_errcode_is_0(data)
|
self._requests.check_errcode_is_0(data)
|
||||||
|
|
||||||
USER_ID = 'UserId'
|
USER_ID = 'UserId'
|
||||||
OPEN_ID = 'OpenId'
|
OPEN_ID = 'OpenId'
|
||||||
|
@ -24,6 +24,7 @@ from ..utils import lazyproperty
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
'JSONResponseMixin', 'CommonApiMixin', 'AsyncApiMixin', 'RelationMixin',
|
'JSONResponseMixin', 'CommonApiMixin', 'AsyncApiMixin', 'RelationMixin',
|
||||||
'QuerySetMixin', 'ExtraFilterFieldsMixin', 'RenderToJsonMixin',
|
'QuerySetMixin', 'ExtraFilterFieldsMixin', 'RenderToJsonMixin',
|
||||||
|
'SerializerMixin', 'AllowBulkDestroyMixin', 'PaginatedResponseMixin'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -112,6 +112,9 @@ class UserCanUpdateSSHKey(permissions.BasePermission):
|
|||||||
|
|
||||||
class NeedMFAVerify(permissions.BasePermission):
|
class NeedMFAVerify(permissions.BasePermission):
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
|
if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
||||||
|
return True
|
||||||
|
|
||||||
mfa_verify_time = request.session.get('MFA_VERIFY_TIME', 0)
|
mfa_verify_time = request.session.get('MFA_VERIFY_TIME', 0)
|
||||||
if time.time() - mfa_verify_time < settings.SECURITY_MFA_VERIFY_TTL:
|
if time.time() - mfa_verify_time < settings.SECURITY_MFA_VERIFY_TTL:
|
||||||
return True
|
return True
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
from django.http.request import HttpRequest
|
|
||||||
from django.http.response import HttpResponse
|
|
||||||
|
|
||||||
from orgs.utils import current_org
|
|
||||||
|
|
||||||
|
|
||||||
class RequestLogMiddleware:
|
|
||||||
def __init__(self, get_response):
|
|
||||||
self.get_response = get_response
|
|
||||||
|
|
||||||
def __call__(self, request: HttpRequest):
|
|
||||||
print(f'Request {request.method} --> ', request.get_raw_uri())
|
|
||||||
response: HttpResponse = self.get_response(request)
|
|
||||||
print(f'Response {current_org.name} {request.method} {response.status_code} --> ', request.get_raw_uri())
|
|
||||||
return response
|
|
@ -8,4 +8,5 @@ from .http import *
|
|||||||
from .ipip import *
|
from .ipip import *
|
||||||
from .crypto import *
|
from .crypto import *
|
||||||
from .random import *
|
from .random import *
|
||||||
from .jumpserver import *
|
from .jumpserver import *
|
||||||
|
from .ip import *
|
||||||
|
@ -7,8 +7,6 @@ import logging
|
|||||||
import datetime
|
import datetime
|
||||||
import uuid
|
import uuid
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import string
|
|
||||||
import random
|
|
||||||
import time
|
import time
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import psutil
|
import psutil
|
||||||
@ -242,11 +240,20 @@ class lazyproperty:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def get_disk_usage():
|
def get_disk_usage(path):
|
||||||
partitions = psutil.disk_partitions()
|
return psutil.disk_usage(path=path).percent
|
||||||
mount_points = [p.mountpoint for p in partitions]
|
|
||||||
usages = {p: psutil.disk_usage(p) for p in mount_points}
|
|
||||||
return usages
|
def get_cpu_load():
|
||||||
|
cpu_load_1, cpu_load_5, cpu_load_15 = psutil.getloadavg()
|
||||||
|
cpu_count = psutil.cpu_count()
|
||||||
|
single_cpu_load_1 = cpu_load_1 / cpu_count
|
||||||
|
single_cpu_load_1 = '%.2f' % single_cpu_load_1
|
||||||
|
return float(single_cpu_load_1)
|
||||||
|
|
||||||
|
|
||||||
|
def get_memory_usage():
|
||||||
|
return psutil.virtual_memory().percent
|
||||||
|
|
||||||
|
|
||||||
class Time:
|
class Time:
|
||||||
@ -273,3 +280,17 @@ def bulk_get(d, *keys, default=None):
|
|||||||
for key in keys:
|
for key in keys:
|
||||||
values.append(d.get(key, default))
|
values.append(d.get(key, default))
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def unique(objects, key=None):
|
||||||
|
seen = OrderedDict()
|
||||||
|
|
||||||
|
if key is None:
|
||||||
|
key = lambda item: item
|
||||||
|
|
||||||
|
for obj in objects:
|
||||||
|
v = key(obj)
|
||||||
|
if v not in seen:
|
||||||
|
seen[v] = obj
|
||||||
|
return list(seen.values())
|
||||||
|
|
||||||
|
68
apps/common/utils/ip.py
Normal file
68
apps/common/utils/ip.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
from ipaddress import ip_network, ip_address
|
||||||
|
|
||||||
|
|
||||||
|
def is_ip_address(address):
|
||||||
|
""" 192.168.10.1 """
|
||||||
|
try:
|
||||||
|
ip_address(address)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def is_ip_network(ip):
|
||||||
|
""" 192.168.1.0/24 """
|
||||||
|
try:
|
||||||
|
ip_network(ip)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def is_ip_segment(ip):
|
||||||
|
""" 10.1.1.1-10.1.1.20 """
|
||||||
|
if '-' not in ip:
|
||||||
|
return False
|
||||||
|
ip_address1, ip_address2 = ip.split('-')
|
||||||
|
return is_ip_address(ip_address1) and is_ip_address(ip_address2)
|
||||||
|
|
||||||
|
|
||||||
|
def in_ip_segment(ip, ip_segment):
|
||||||
|
ip1, ip2 = ip_segment.split('-')
|
||||||
|
ip1 = int(ip_address(ip1))
|
||||||
|
ip2 = int(ip_address(ip2))
|
||||||
|
ip = int(ip_address(ip))
|
||||||
|
return min(ip1, ip2) <= ip <= max(ip1, ip2)
|
||||||
|
|
||||||
|
|
||||||
|
def contains_ip(ip, ip_group):
|
||||||
|
"""
|
||||||
|
ip_group:
|
||||||
|
[192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64.]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if '*' in ip_group:
|
||||||
|
return True
|
||||||
|
|
||||||
|
for _ip in ip_group:
|
||||||
|
if is_ip_address(_ip):
|
||||||
|
# 192.168.10.1
|
||||||
|
if ip == _ip:
|
||||||
|
return True
|
||||||
|
elif is_ip_network(_ip) and is_ip_address(ip):
|
||||||
|
# 192.168.1.0/24
|
||||||
|
if ip_address(ip) in ip_network(_ip):
|
||||||
|
return True
|
||||||
|
elif is_ip_segment(_ip) and is_ip_address(ip):
|
||||||
|
# 10.1.1.1-10.1.1.20
|
||||||
|
if in_ip_segment(ip, _ip):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# is domain name
|
||||||
|
if ip == _ip:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
@ -16,9 +16,7 @@ import json
|
|||||||
import yaml
|
import yaml
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.templatetags.static import static
|
|
||||||
from urllib.parse import urljoin, urlparse
|
from urllib.parse import urljoin, urlparse
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
PROJECT_DIR = os.path.dirname(BASE_DIR)
|
PROJECT_DIR = os.path.dirname(BASE_DIR)
|
||||||
@ -230,6 +228,10 @@ class Config(dict):
|
|||||||
'DINGTALK_APPKEY': '',
|
'DINGTALK_APPKEY': '',
|
||||||
'DINGTALK_APPSECRET': '',
|
'DINGTALK_APPSECRET': '',
|
||||||
|
|
||||||
|
'AUTH_FEISHU': False,
|
||||||
|
'FEISHU_APP_ID': '',
|
||||||
|
'FEISHU_APP_SECRET': '',
|
||||||
|
|
||||||
'OTP_VALID_WINDOW': 2,
|
'OTP_VALID_WINDOW': 2,
|
||||||
'OTP_ISSUER_NAME': 'JumpServer',
|
'OTP_ISSUER_NAME': 'JumpServer',
|
||||||
'EMAIL_SUFFIX': 'jumpserver.org',
|
'EMAIL_SUFFIX': 'jumpserver.org',
|
||||||
@ -244,7 +246,7 @@ class Config(dict):
|
|||||||
'TERMINAL_TELNET_REGEX': '',
|
'TERMINAL_TELNET_REGEX': '',
|
||||||
'TERMINAL_COMMAND_STORAGE': {},
|
'TERMINAL_COMMAND_STORAGE': {},
|
||||||
|
|
||||||
'SECURITY_MFA_AUTH': False,
|
'SECURITY_MFA_AUTH': 0, # 0 不开启 1 全局开启 2 管理员开启
|
||||||
'SECURITY_COMMAND_EXECUTION': True,
|
'SECURITY_COMMAND_EXECUTION': True,
|
||||||
'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True,
|
'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True,
|
||||||
'SECURITY_VIEW_AUTH_NEED_MFA': True,
|
'SECURITY_VIEW_AUTH_NEED_MFA': True,
|
||||||
@ -253,6 +255,7 @@ class Config(dict):
|
|||||||
'SECURITY_MAX_IDLE_TIME': 30,
|
'SECURITY_MAX_IDLE_TIME': 30,
|
||||||
'SECURITY_PASSWORD_EXPIRATION_TIME': 9999,
|
'SECURITY_PASSWORD_EXPIRATION_TIME': 9999,
|
||||||
'SECURITY_PASSWORD_MIN_LENGTH': 6,
|
'SECURITY_PASSWORD_MIN_LENGTH': 6,
|
||||||
|
'SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH': 6,
|
||||||
'SECURITY_PASSWORD_UPPER_CASE': False,
|
'SECURITY_PASSWORD_UPPER_CASE': False,
|
||||||
'SECURITY_PASSWORD_LOWER_CASE': False,
|
'SECURITY_PASSWORD_LOWER_CASE': False,
|
||||||
'SECURITY_PASSWORD_NUMBER': False,
|
'SECURITY_PASSWORD_NUMBER': False,
|
||||||
@ -264,6 +267,7 @@ class Config(dict):
|
|||||||
'SECURITY_INSECURE_COMMAND_LEVEL': 5,
|
'SECURITY_INSECURE_COMMAND_LEVEL': 5,
|
||||||
'SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER': '',
|
'SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER': '',
|
||||||
'SECURITY_LUNA_REMEMBER_AUTH': True,
|
'SECURITY_LUNA_REMEMBER_AUTH': True,
|
||||||
|
'SECURITY_WATERMARK_ENABLED': False,
|
||||||
|
|
||||||
'HTTP_BIND_HOST': '0.0.0.0',
|
'HTTP_BIND_HOST': '0.0.0.0',
|
||||||
'HTTP_LISTEN_PORT': 8080,
|
'HTTP_LISTEN_PORT': 8080,
|
||||||
@ -301,11 +305,11 @@ class Config(dict):
|
|||||||
'CONNECTION_TOKEN_ENABLED': False,
|
'CONNECTION_TOKEN_ENABLED': False,
|
||||||
'ONLY_ALLOW_EXIST_USER_AUTH': False,
|
'ONLY_ALLOW_EXIST_USER_AUTH': False,
|
||||||
'ONLY_ALLOW_AUTH_FROM_SOURCE': False,
|
'ONLY_ALLOW_AUTH_FROM_SOURCE': False,
|
||||||
'DISK_CHECK_ENABLED': True,
|
|
||||||
'SESSION_SAVE_EVERY_REQUEST': True,
|
'SESSION_SAVE_EVERY_REQUEST': True,
|
||||||
'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False,
|
'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False,
|
||||||
'FORGOT_PASSWORD_URL': '',
|
'FORGOT_PASSWORD_URL': '',
|
||||||
'HEALTH_CHECK_TOKEN': '',
|
'HEALTH_CHECK_TOKEN': '',
|
||||||
|
'LOGIN_REDIRECT_TO_BACKEND': None, # 'OPENID / CAS
|
||||||
|
|
||||||
'TERMINAL_RDP_ADDR': ''
|
'TERMINAL_RDP_ADDR': ''
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ def jumpserver_processor(request):
|
|||||||
'LOGIN_CAS_LOGO_URL': static('img/login_cas_logo.png'),
|
'LOGIN_CAS_LOGO_URL': static('img/login_cas_logo.png'),
|
||||||
'LOGIN_WECOM_LOGO_URL': static('img/login_wecom_logo.png'),
|
'LOGIN_WECOM_LOGO_URL': static('img/login_wecom_logo.png'),
|
||||||
'LOGIN_DINGTALK_LOGO_URL': static('img/login_dingtalk_logo.png'),
|
'LOGIN_DINGTALK_LOGO_URL': static('img/login_dingtalk_logo.png'),
|
||||||
|
'LOGIN_FEISHU_LOGO_URL': static('img/login_feishu_logo.png'),
|
||||||
'JMS_TITLE': _('JumpServer Open Source Bastion Host'),
|
'JMS_TITLE': _('JumpServer Open Source Bastion Host'),
|
||||||
'VERSION': settings.VERSION,
|
'VERSION': settings.VERSION,
|
||||||
'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2021',
|
'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2021',
|
||||||
|
@ -117,6 +117,10 @@ DINGTALK_AGENTID = CONFIG.DINGTALK_AGENTID
|
|||||||
DINGTALK_APPKEY = CONFIG.DINGTALK_APPKEY
|
DINGTALK_APPKEY = CONFIG.DINGTALK_APPKEY
|
||||||
DINGTALK_APPSECRET = CONFIG.DINGTALK_APPSECRET
|
DINGTALK_APPSECRET = CONFIG.DINGTALK_APPSECRET
|
||||||
|
|
||||||
|
# FeiShu auth
|
||||||
|
AUTH_FEISHU = CONFIG.AUTH_FEISHU
|
||||||
|
FEISHU_APP_ID = CONFIG.FEISHU_APP_ID
|
||||||
|
FEISHU_APP_SECRET = CONFIG.FEISHU_APP_SECRET
|
||||||
|
|
||||||
# Other setting
|
# Other setting
|
||||||
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
|
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
|
||||||
@ -134,12 +138,13 @@ AUTH_BACKEND_CAS = 'authentication.backends.cas.CASBackend'
|
|||||||
AUTH_BACKEND_SSO = 'authentication.backends.api.SSOAuthentication'
|
AUTH_BACKEND_SSO = 'authentication.backends.api.SSOAuthentication'
|
||||||
AUTH_BACKEND_WECOM = 'authentication.backends.api.WeComAuthentication'
|
AUTH_BACKEND_WECOM = 'authentication.backends.api.WeComAuthentication'
|
||||||
AUTH_BACKEND_DINGTALK = 'authentication.backends.api.DingTalkAuthentication'
|
AUTH_BACKEND_DINGTALK = 'authentication.backends.api.DingTalkAuthentication'
|
||||||
|
AUTH_BACKEND_FEISHU = 'authentication.backends.api.FeiShuAuthentication'
|
||||||
AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.api.AuthorizationTokenAuthentication'
|
AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.api.AuthorizationTokenAuthentication'
|
||||||
|
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = [
|
AUTHENTICATION_BACKENDS = [
|
||||||
AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY, AUTH_BACKEND_WECOM,
|
AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY, AUTH_BACKEND_WECOM,
|
||||||
AUTH_BACKEND_DINGTALK, AUTH_BACKEND_AUTH_TOKEN
|
AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU, AUTH_BACKEND_AUTH_TOKEN,
|
||||||
]
|
]
|
||||||
|
|
||||||
if AUTH_CAS:
|
if AUTH_CAS:
|
||||||
|
@ -38,6 +38,7 @@ SECURITY_LOGIN_LIMIT_TIME = CONFIG.SECURITY_LOGIN_LIMIT_TIME # Unit: minute
|
|||||||
SECURITY_MAX_IDLE_TIME = CONFIG.SECURITY_MAX_IDLE_TIME # Unit: minute
|
SECURITY_MAX_IDLE_TIME = CONFIG.SECURITY_MAX_IDLE_TIME # Unit: minute
|
||||||
SECURITY_PASSWORD_EXPIRATION_TIME = CONFIG.SECURITY_PASSWORD_EXPIRATION_TIME # Unit: day
|
SECURITY_PASSWORD_EXPIRATION_TIME = CONFIG.SECURITY_PASSWORD_EXPIRATION_TIME # Unit: day
|
||||||
SECURITY_PASSWORD_MIN_LENGTH = CONFIG.SECURITY_PASSWORD_MIN_LENGTH # Unit: bit
|
SECURITY_PASSWORD_MIN_LENGTH = CONFIG.SECURITY_PASSWORD_MIN_LENGTH # Unit: bit
|
||||||
|
SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH = CONFIG.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH # Unit: bit
|
||||||
OLD_PASSWORD_HISTORY_LIMIT_COUNT = CONFIG.OLD_PASSWORD_HISTORY_LIMIT_COUNT
|
OLD_PASSWORD_HISTORY_LIMIT_COUNT = CONFIG.OLD_PASSWORD_HISTORY_LIMIT_COUNT
|
||||||
SECURITY_PASSWORD_UPPER_CASE = CONFIG.SECURITY_PASSWORD_UPPER_CASE
|
SECURITY_PASSWORD_UPPER_CASE = CONFIG.SECURITY_PASSWORD_UPPER_CASE
|
||||||
SECURITY_PASSWORD_LOWER_CASE = CONFIG.SECURITY_PASSWORD_LOWER_CASE
|
SECURITY_PASSWORD_LOWER_CASE = CONFIG.SECURITY_PASSWORD_LOWER_CASE
|
||||||
@ -118,7 +119,6 @@ TICKETS_ENABLED = CONFIG.TICKETS_ENABLED
|
|||||||
REFERER_CHECK_ENABLED = CONFIG.REFERER_CHECK_ENABLED
|
REFERER_CHECK_ENABLED = CONFIG.REFERER_CHECK_ENABLED
|
||||||
|
|
||||||
CONNECTION_TOKEN_ENABLED = CONFIG.CONNECTION_TOKEN_ENABLED
|
CONNECTION_TOKEN_ENABLED = CONFIG.CONNECTION_TOKEN_ENABLED
|
||||||
DISK_CHECK_ENABLED = CONFIG.DISK_CHECK_ENABLED
|
|
||||||
FORGOT_PASSWORD_URL = CONFIG.FORGOT_PASSWORD_URL
|
FORGOT_PASSWORD_URL = CONFIG.FORGOT_PASSWORD_URL
|
||||||
|
|
||||||
|
|
||||||
@ -128,3 +128,6 @@ HEALTH_CHECK_TOKEN = CONFIG.HEALTH_CHECK_TOKEN
|
|||||||
|
|
||||||
TERMINAL_RDP_ADDR = CONFIG.TERMINAL_RDP_ADDR
|
TERMINAL_RDP_ADDR = CONFIG.TERMINAL_RDP_ADDR
|
||||||
SECURITY_LUNA_REMEMBER_AUTH = CONFIG.SECURITY_LUNA_REMEMBER_AUTH
|
SECURITY_LUNA_REMEMBER_AUTH = CONFIG.SECURITY_LUNA_REMEMBER_AUTH
|
||||||
|
SECURITY_WATERMARK_ENABLED = CONFIG.SECURITY_WATERMARK_ENABLED
|
||||||
|
|
||||||
|
LOGIN_REDIRECT_TO_BACKEND = CONFIG.LOGIN_REDIRECT_TO_BACKEND
|
||||||
|
@ -40,8 +40,8 @@ REST_FRAMEWORK = {
|
|||||||
'DEFAULT_METADATA_CLASS': 'common.drf.metadata.SimpleMetadataWithFilters',
|
'DEFAULT_METADATA_CLASS': 'common.drf.metadata.SimpleMetadataWithFilters',
|
||||||
'ORDERING_PARAM': "order",
|
'ORDERING_PARAM': "order",
|
||||||
'SEARCH_PARAM': "search",
|
'SEARCH_PARAM': "search",
|
||||||
'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S %z',
|
'DATETIME_FORMAT': '%Y/%m/%d %H:%M:%S %z',
|
||||||
'DATETIME_INPUT_FORMATS': ['iso-8601', '%Y-%m-%d %H:%M:%S %z'],
|
'DATETIME_INPUT_FORMATS': ['%Y/%m/%d %H:%M:%S %z', 'iso-8601', '%Y-%m-%d %H:%M:%S %z'],
|
||||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
||||||
'EXCEPTION_HANDLER': 'common.drf.exc_handlers.common_exception_handler',
|
'EXCEPTION_HANDLER': 'common.drf.exc_handlers.common_exception_handler',
|
||||||
# 'PAGE_SIZE': 100,
|
# 'PAGE_SIZE': 100,
|
||||||
|
@ -20,7 +20,7 @@ def celery_flower_view(request, path):
|
|||||||
try:
|
try:
|
||||||
response = proxy_view(request, remote_url)
|
response = proxy_view(request, remote_url)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
msg = _("<h1>Flow service unavailable, check it</h1>") + \
|
msg = _("<h1>Flower service unavailable, check it</h1>") + \
|
||||||
'<br><br> <div>{}</div>'.format(e)
|
'<br><br> <div>{}</div>'.format(e)
|
||||||
response = HttpResponse(msg)
|
response = HttpResponse(msg)
|
||||||
return response
|
return response
|
||||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -73,7 +73,7 @@ msgid ""
|
|||||||
"User list、User group、Asset list、Domain list、Admin user、System user、"
|
"User list、User group、Asset list、Domain list、Admin user、System user、"
|
||||||
"Labels、Asset permission"
|
"Labels、Asset permission"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"用户列表、用户组、资产列表、网域列表、管理用户、系统用户、标签管理、资产授权"
|
"用户列表、用户组、资产列表、网域列表、特权用户、系统用户、标签管理、资产授权"
|
||||||
"规则"
|
"规则"
|
||||||
|
|
||||||
#: static/js/jumpserver.js:416
|
#: static/js/jumpserver.js:416
|
||||||
|
@ -4,7 +4,7 @@ from rest_framework.views import APIView
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from common.drf.api import JmsGenericViewSet
|
from common.drf.api import JMSGenericViewSet
|
||||||
from notifications.notifications import system_msgs
|
from notifications.notifications import system_msgs
|
||||||
from notifications.models import SystemMsgSubscription
|
from notifications.models import SystemMsgSubscription
|
||||||
from notifications.backends import BACKEND
|
from notifications.backends import BACKEND
|
||||||
@ -30,7 +30,7 @@ class BackendListView(APIView):
|
|||||||
|
|
||||||
class SystemMsgSubscriptionViewSet(ListModelMixin,
|
class SystemMsgSubscriptionViewSet(ListModelMixin,
|
||||||
UpdateModelMixin,
|
UpdateModelMixin,
|
||||||
JmsGenericViewSet):
|
JMSGenericViewSet):
|
||||||
lookup_field = 'message_type'
|
lookup_field = 'message_type'
|
||||||
queryset = SystemMsgSubscription.objects.all()
|
queryset = SystemMsgSubscription.objects.all()
|
||||||
serializer_classes = {
|
serializer_classes = {
|
||||||
|
@ -5,7 +5,7 @@ from rest_framework.decorators import action
|
|||||||
from common.http import is_true
|
from common.http import is_true
|
||||||
from common.permissions import IsValidUser
|
from common.permissions import IsValidUser
|
||||||
from common.const.http import GET, PATCH, POST
|
from common.const.http import GET, PATCH, POST
|
||||||
from common.drf.api import JmsGenericViewSet
|
from common.drf.api import JMSGenericViewSet
|
||||||
from ..serializers import (
|
from ..serializers import (
|
||||||
SiteMessageDetailSerializer, SiteMessageIdsSerializer,
|
SiteMessageDetailSerializer, SiteMessageIdsSerializer,
|
||||||
SiteMessageSendSerializer,
|
SiteMessageSendSerializer,
|
||||||
@ -16,7 +16,7 @@ from ..filters import SiteMsgFilter
|
|||||||
__all__ = ('SiteMessageViewSet', )
|
__all__ = ('SiteMessageViewSet', )
|
||||||
|
|
||||||
|
|
||||||
class SiteMessageViewSet(ListModelMixin, RetrieveModelMixin, JmsGenericViewSet):
|
class SiteMessageViewSet(ListModelMixin, RetrieveModelMixin, JMSGenericViewSet):
|
||||||
permission_classes = (IsValidUser,)
|
permission_classes = (IsValidUser,)
|
||||||
serializer_classes = {
|
serializer_classes = {
|
||||||
'default': SiteMessageDetailSerializer,
|
'default': SiteMessageDetailSerializer,
|
||||||
|
@ -5,6 +5,7 @@ from .dingtalk import DingTalk
|
|||||||
from .email import Email
|
from .email import Email
|
||||||
from .site_msg import SiteMessage
|
from .site_msg import SiteMessage
|
||||||
from .wecom import WeCom
|
from .wecom import WeCom
|
||||||
|
from .feishu import FeiShu
|
||||||
|
|
||||||
|
|
||||||
class BACKEND(models.TextChoices):
|
class BACKEND(models.TextChoices):
|
||||||
@ -12,6 +13,7 @@ class BACKEND(models.TextChoices):
|
|||||||
WECOM = 'wecom', _('WeCom')
|
WECOM = 'wecom', _('WeCom')
|
||||||
DINGTALK = 'dingtalk', _('DingTalk')
|
DINGTALK = 'dingtalk', _('DingTalk')
|
||||||
SITE_MSG = 'site_msg', _('Site message')
|
SITE_MSG = 'site_msg', _('Site message')
|
||||||
|
FEISHU = 'feishu', _('FeiShu')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def client(self):
|
def client(self):
|
||||||
@ -19,7 +21,8 @@ class BACKEND(models.TextChoices):
|
|||||||
self.EMAIL: Email,
|
self.EMAIL: Email,
|
||||||
self.WECOM: WeCom,
|
self.WECOM: WeCom,
|
||||||
self.DINGTALK: DingTalk,
|
self.DINGTALK: DingTalk,
|
||||||
self.SITE_MSG: SiteMessage
|
self.SITE_MSG: SiteMessage,
|
||||||
|
self.FEISHU: FeiShu,
|
||||||
}[self]
|
}[self]
|
||||||
return client
|
return client
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from common.message.backends.dingtalk import DingTalk as Client
|
from common.message.backends.dingtalk import DingTalk as Client
|
||||||
from .base import BackendBase
|
from .base import BackendBase
|
||||||
|
|
||||||
|
19
apps/notifications/backends/feishu.py
Normal file
19
apps/notifications/backends/feishu.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from common.message.backends.feishu import FeiShu as Client
|
||||||
|
from .base import BackendBase
|
||||||
|
|
||||||
|
|
||||||
|
class FeiShu(BackendBase):
|
||||||
|
account_field = 'feishu_id'
|
||||||
|
is_enable_field_in_settings = 'AUTH_FEISHU'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.client = Client(
|
||||||
|
app_id=settings.FEISHU_APP_ID,
|
||||||
|
app_secret=settings.FEISHU_APP_SECRET
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_msg(self, users, msg):
|
||||||
|
accounts, __, __ = self.get_accounts(users)
|
||||||
|
return self.client.send_text(accounts, msg)
|
@ -92,8 +92,9 @@ class Message(metaclass=MessageType):
|
|||||||
|
|
||||||
def get_email_msg(self) -> dict:
|
def get_email_msg(self) -> dict:
|
||||||
msg = self.get_common_msg()
|
msg = self.get_common_msg()
|
||||||
|
subject = f'{msg[:20]} ...' if len(msg) >= 20 else msg
|
||||||
return {
|
return {
|
||||||
'subject': msg,
|
'subject': subject,
|
||||||
'message': msg
|
'message': msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,8 +4,9 @@ from notifications.notifications import SystemMessage
|
|||||||
from notifications.models import SystemMsgSubscription
|
from notifications.models import SystemMsgSubscription
|
||||||
from users.models import User
|
from users.models import User
|
||||||
from notifications.backends import BACKEND
|
from notifications.backends import BACKEND
|
||||||
|
from terminal.models import Status, Terminal
|
||||||
|
|
||||||
__all__ = ('ServerPerformanceMessage',)
|
__all__ = ('ServerPerformanceMessage', 'ServerPerformanceCheckUtil')
|
||||||
|
|
||||||
|
|
||||||
class ServerPerformanceMessage(SystemMessage):
|
class ServerPerformanceMessage(SystemMessage):
|
||||||
@ -13,13 +14,11 @@ class ServerPerformanceMessage(SystemMessage):
|
|||||||
category_label = _('Operations')
|
category_label = _('Operations')
|
||||||
message_type_label = _('Server performance')
|
message_type_label = _('Server performance')
|
||||||
|
|
||||||
def __init__(self, path, usage):
|
def __init__(self, msg):
|
||||||
self.path = path
|
self._msg = msg
|
||||||
self.usage = usage
|
|
||||||
|
|
||||||
def get_common_msg(self):
|
def get_common_msg(self):
|
||||||
msg = _("Disk used more than 80%: {} => {}").format(self.path, self.usage.percent)
|
return self._msg
|
||||||
return msg
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def post_insert_to_db(cls, subscription: SystemMsgSubscription):
|
def post_insert_to_db(cls, subscription: SystemMsgSubscription):
|
||||||
@ -27,3 +26,78 @@ class ServerPerformanceMessage(SystemMessage):
|
|||||||
subscription.users.add(*admins)
|
subscription.users.add(*admins)
|
||||||
subscription.receive_backends = [BACKEND.EMAIL]
|
subscription.receive_backends = [BACKEND.EMAIL]
|
||||||
subscription.save()
|
subscription.save()
|
||||||
|
|
||||||
|
|
||||||
|
class ServerPerformanceCheckUtil(object):
|
||||||
|
items_mapper = {
|
||||||
|
'is_alive': {
|
||||||
|
'default': False,
|
||||||
|
'max_threshold': False,
|
||||||
|
'alarm_msg_format': _('[Alive] The terminal is offline: {name}')
|
||||||
|
},
|
||||||
|
'disk_usage': {
|
||||||
|
'default': 0,
|
||||||
|
'max_threshold': 80,
|
||||||
|
'alarm_msg_format': _(
|
||||||
|
'[Disk] Disk used more than {max_threshold}%: => {value} ({name})'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
'memory_usage': {
|
||||||
|
'default': 0,
|
||||||
|
'max_threshold': 85,
|
||||||
|
'alarm_msg_format': _(
|
||||||
|
'[Memory] Memory used more than {max_threshold}%: => {value} ({name})'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
'cpu_load': {
|
||||||
|
'default': 0,
|
||||||
|
'max_threshold': 5,
|
||||||
|
'alarm_msg_format': _(
|
||||||
|
'[CPU] CPU load more than {max_threshold}: => {value} ({name})'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.alarm_messages = []
|
||||||
|
self._terminals = []
|
||||||
|
self._terminal = None
|
||||||
|
|
||||||
|
def check_and_publish(self):
|
||||||
|
self.check()
|
||||||
|
self.publish()
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
self.alarm_messages = []
|
||||||
|
self.initial_terminals()
|
||||||
|
for item, data in self.items_mapper.items():
|
||||||
|
for self._terminal in self._terminals:
|
||||||
|
self.check_item(item, data)
|
||||||
|
|
||||||
|
def check_item(self, item, data):
|
||||||
|
default = data['default']
|
||||||
|
max_threshold = data['max_threshold']
|
||||||
|
value = getattr(self._terminal.stat, item, default)
|
||||||
|
print(value, max_threshold, self._terminal.name, self._terminal.id)
|
||||||
|
if isinstance(value, bool) and value != max_threshold:
|
||||||
|
return
|
||||||
|
elif isinstance(value, (int, float)) and value < max_threshold:
|
||||||
|
return
|
||||||
|
msg = data['alarm_msg_format']
|
||||||
|
msg = msg.format(max_threshold=max_threshold, value=value, name=self._terminal.name)
|
||||||
|
self.alarm_messages.append(msg)
|
||||||
|
|
||||||
|
def publish(self):
|
||||||
|
if not self.alarm_messages:
|
||||||
|
return
|
||||||
|
msg = '<br>'.join(self.alarm_messages)
|
||||||
|
ServerPerformanceMessage(msg).publish()
|
||||||
|
|
||||||
|
def initial_terminals(self):
|
||||||
|
terminals = []
|
||||||
|
for terminal in Terminal.objects.filter(is_deleted=False):
|
||||||
|
if not terminal.is_active:
|
||||||
|
continue
|
||||||
|
terminal.stat = Status.get_terminal_latest_stat(terminal)
|
||||||
|
terminals.append(terminal)
|
||||||
|
self._terminals = terminals
|
||||||
|
@ -9,7 +9,7 @@ from celery.exceptions import SoftTimeLimitExceeded
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from common.utils import get_logger, get_object_or_none, get_disk_usage, get_log_keep_day
|
from common.utils import get_logger, get_object_or_none, get_log_keep_day
|
||||||
from orgs.utils import tmp_to_root_org, tmp_to_org
|
from orgs.utils import tmp_to_root_org, tmp_to_org
|
||||||
from .celery.decorator import (
|
from .celery.decorator import (
|
||||||
register_as_period_task, after_app_shutdown_clean_periodic,
|
register_as_period_task, after_app_shutdown_clean_periodic,
|
||||||
@ -20,7 +20,7 @@ from .celery.utils import (
|
|||||||
disable_celery_periodic_task, delete_celery_periodic_task
|
disable_celery_periodic_task, delete_celery_periodic_task
|
||||||
)
|
)
|
||||||
from .models import Task, CommandExecution, CeleryTask
|
from .models import Task, CommandExecution, CeleryTask
|
||||||
from .notifications import ServerPerformanceMessage
|
from .notifications import ServerPerformanceCheckUtil
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
@ -132,18 +132,7 @@ def create_or_update_registered_periodic_tasks():
|
|||||||
@shared_task
|
@shared_task
|
||||||
@register_as_period_task(interval=3600)
|
@register_as_period_task(interval=3600)
|
||||||
def check_server_performance_period():
|
def check_server_performance_period():
|
||||||
if not settings.DISK_CHECK_ENABLED:
|
ServerPerformanceCheckUtil().check_and_publish()
|
||||||
return
|
|
||||||
usages = get_disk_usage()
|
|
||||||
uncheck_paths = ['/etc', '/boot']
|
|
||||||
|
|
||||||
for path, usage in usages.items():
|
|
||||||
need_check = True
|
|
||||||
for uncheck_path in uncheck_paths:
|
|
||||||
if path.startswith(uncheck_path):
|
|
||||||
need_check = False
|
|
||||||
if need_check and usage.percent > 80:
|
|
||||||
ServerPerformanceMessage(path=path, usage=usage).publish()
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task(queue="ansible")
|
@shared_task(queue="ansible")
|
||||||
|
@ -49,6 +49,9 @@ class OrgModelMixin(models.Model):
|
|||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
org = get_current_org()
|
org = get_current_org()
|
||||||
|
# 这里不可以优化成, 因为 root 组织下可以设置组织 id 来保存
|
||||||
|
# if org.is_root() and not self.org_id:
|
||||||
|
# raise ...
|
||||||
if org.is_root():
|
if org.is_root():
|
||||||
if not self.org_id:
|
if not self.org_id:
|
||||||
raise ValidationError('Please save in a organization')
|
raise ValidationError('Please save in a organization')
|
||||||
|
@ -9,6 +9,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
|
|
||||||
from common.utils import lazyproperty, settings
|
from common.utils import lazyproperty, settings
|
||||||
from common.const import choices
|
from common.const import choices
|
||||||
|
from common.tree import TreeNode
|
||||||
from common.db.models import TextChoices
|
from common.db.models import TextChoices
|
||||||
|
|
||||||
|
|
||||||
@ -233,6 +234,20 @@ class Organization(models.Model):
|
|||||||
with tmp_to_org(self):
|
with tmp_to_org(self):
|
||||||
return resource_model.objects.all().count()
|
return resource_model.objects.all().count()
|
||||||
|
|
||||||
|
def as_tree_node(self, pid, opened=True):
|
||||||
|
node = TreeNode(**{
|
||||||
|
'id': str(self.id),
|
||||||
|
'name': self.name,
|
||||||
|
'title': self.name,
|
||||||
|
'pId': pid,
|
||||||
|
'open': opened,
|
||||||
|
'isParent': True,
|
||||||
|
'meta': {
|
||||||
|
'type': 'org'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
def _convert_to_uuid_set(users):
|
def _convert_to_uuid_set(users):
|
||||||
rst = set()
|
rst = set()
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user