mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-07-15 23:46:30 +00:00
commit
7d51d8c570
@ -6,9 +6,10 @@
|
|||||||
|
|
||||||
- [ENGLISH](https://github.com/jumpserver/jumpserver/blob/master/README_EN.md)
|
- [ENGLISH](https://github.com/jumpserver/jumpserver/blob/master/README_EN.md)
|
||||||
|
|
||||||
|安全通知|
|
|
||||||
|
|《新一代堡垒机建设指南》开放下载|
|
||||||
|------------------|
|
|------------------|
|
||||||
|2021年1月15日 JumpServer 发现远程执行漏洞,请速度修复 [详见](https://github.com/jumpserver/jumpserver/issues/5533), 非常感谢 **reactivity of Alibaba Hackerone bug bounty program**(瑞典) 向我们报告了此 BUG|
|
|本白皮书由JumpServer开源项目组编著而成。编写团队从企业实践和技术演进的双重视角出发,结合自身在身份与访问安全领域长期研发及落地经验组织撰写,同时积极听取行业内专家的意见和建议,在此基础上完成了本白皮书的编写任务。下载链接:https://jinshuju.net/f/E0qAl8|
|
||||||
|
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ from rest_framework.generics import CreateAPIView, RetrieveDestroyAPIView
|
|||||||
from common.permissions import IsAppUser
|
from common.permissions import IsAppUser
|
||||||
from common.utils import reverse, lazyproperty
|
from common.utils import reverse, lazyproperty
|
||||||
from orgs.utils import tmp_to_org, tmp_to_root_org
|
from orgs.utils import tmp_to_org, tmp_to_root_org
|
||||||
from tickets.models import Ticket
|
from tickets.api import GenericTicketStatusRetrieveCloseAPI
|
||||||
from ..models import LoginAssetACL
|
from ..models import LoginAssetACL
|
||||||
from .. import serializers
|
from .. import serializers
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ class LoginAssetCheckAPI(CreateAPIView):
|
|||||||
org_id=self.serializer.org.id
|
org_id=self.serializer.org.id
|
||||||
)
|
)
|
||||||
confirm_status_url = reverse(
|
confirm_status_url = reverse(
|
||||||
view_name='acls:login-asset-confirm-status',
|
view_name='api-acls:login-asset-confirm-status',
|
||||||
kwargs={'pk': str(ticket.id)}
|
kwargs={'pk': str(ticket.id)}
|
||||||
)
|
)
|
||||||
ticket_detail_url = reverse(
|
ticket_detail_url = reverse(
|
||||||
@ -72,34 +72,6 @@ class LoginAssetCheckAPI(CreateAPIView):
|
|||||||
return serializer
|
return serializer
|
||||||
|
|
||||||
|
|
||||||
class LoginAssetConfirmStatusAPI(RetrieveDestroyAPIView):
|
class LoginAssetConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
|
||||||
permission_classes = (IsAppUser, )
|
pass
|
||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
|
||||||
if self.ticket.action_open:
|
|
||||||
status = 'await'
|
|
||||||
elif self.ticket.action_approve:
|
|
||||||
status = 'approve'
|
|
||||||
else:
|
|
||||||
status = 'reject'
|
|
||||||
data = {
|
|
||||||
'status': status,
|
|
||||||
'action': self.ticket.action,
|
|
||||||
'processor': self.ticket.processor_display
|
|
||||||
}
|
|
||||||
return Response(data=data, status=200)
|
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
|
||||||
if self.ticket.status_open:
|
|
||||||
self.ticket.close(processor=self.ticket.applicant)
|
|
||||||
data = {
|
|
||||||
'action': self.ticket.action,
|
|
||||||
'status': self.ticket.status,
|
|
||||||
'processor': self.ticket.processor_display
|
|
||||||
}
|
|
||||||
return Response(data=data, status=200)
|
|
||||||
|
|
||||||
@lazyproperty
|
|
||||||
def ticket(self):
|
|
||||||
with tmp_to_root_org():
|
|
||||||
return get_object_or_404(Ticket, pk=self.kwargs['pk'])
|
|
||||||
|
@ -33,6 +33,9 @@ class LoginACL(BaseACL):
|
|||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('priority', '-date_updated', 'name')
|
ordering = ('priority', '-date_updated', 'name')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def action_reject(self):
|
def action_reject(self):
|
||||||
return self.action == self.ActionChoices.reject
|
return self.action == self.ActionChoices.reject
|
||||||
|
@ -38,6 +38,9 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
|
|||||||
unique_together = ('name', 'org_id')
|
unique_together = ('name', 'org_id')
|
||||||
ordering = ('priority', '-date_updated', 'name')
|
ordering = ('priority', '-date_updated', 'name')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def filter(cls, user, asset, system_user, action):
|
def filter(cls, user, asset, system_user, action):
|
||||||
queryset = cls.objects.filter(action=action)
|
queryset = cls.objects.filter(action=action)
|
||||||
|
@ -35,10 +35,15 @@ class LoginACLSerializer(BulkModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = LoginACL
|
model = LoginACL
|
||||||
fields = [
|
fields_mini = ['id', 'name']
|
||||||
'id', 'name', 'priority', 'ip_group', 'user', 'user_display', 'action',
|
fields_small = fields_mini + [
|
||||||
'action_display', 'is_active', 'comment', 'created_by', 'date_created', 'date_updated'
|
'priority', 'ip_group', 'action', 'action_display',
|
||||||
|
'is_active',
|
||||||
|
'date_created', 'date_updated',
|
||||||
|
'comment', 'created_by',
|
||||||
]
|
]
|
||||||
|
fields_fk = ['user', 'user_display',]
|
||||||
|
fields = fields_small + fields_fk
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'priority': {'default': 50},
|
'priority': {'default': 50},
|
||||||
'is_active': {'default': True},
|
'is_active': {'default': True},
|
||||||
|
@ -54,7 +54,7 @@ class LoginAssetACLSystemUsersSerializer(serializers.Serializer):
|
|||||||
protocol_group = serializers.ListField(
|
protocol_group = serializers.ListField(
|
||||||
default=['*'], child=serializers.CharField(max_length=16), label=_('Protocol'),
|
default=['*'], child=serializers.CharField(max_length=16), label=_('Protocol'),
|
||||||
help_text=protocol_group_help_text.format(
|
help_text=protocol_group_help_text.format(
|
||||||
', '.join(SystemUser.ASSET_CATEGORY_PROTOCOLS)
|
', '.join([SystemUser.PROTOCOL_SSH, SystemUser.PROTOCOL_TELNET])
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -76,11 +76,15 @@ class LoginAssetACLSerializer(BulkOrgResourceModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.LoginAssetACL
|
model = models.LoginAssetACL
|
||||||
fields = [
|
fields_mini = ['id', 'name']
|
||||||
'id', 'name', 'priority', 'users', 'system_users', 'assets', 'action', 'action_display',
|
fields_small = fields_mini + [
|
||||||
'is_active', 'comment', 'reviewers', 'reviewers_amount', 'created_by', 'date_created',
|
'users', 'system_users', 'assets',
|
||||||
'date_updated', 'org_id'
|
'is_active',
|
||||||
|
'date_created', 'date_updated',
|
||||||
|
'priority', 'action', 'action_display', 'comment', 'created_by', 'org_id'
|
||||||
]
|
]
|
||||||
|
fields_m2m = ['reviewers', 'reviewers_amount']
|
||||||
|
fields = fields_small + fields_m2m
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"reviewers": {'allow_null': False, 'required': True},
|
"reviewers": {'allow_null': False, 'required': True},
|
||||||
'priority': {'default': 50},
|
'priority': {'default': 50},
|
||||||
|
@ -49,10 +49,14 @@ class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSeri
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Application
|
model = models.Application
|
||||||
fields = [
|
fields_mini = ['id', 'name']
|
||||||
'id', 'name', 'category', 'category_display', 'type', 'type_display', 'attrs',
|
fields_small = fields_mini + [
|
||||||
'domain', 'created_by', 'date_created', 'date_updated', 'comment'
|
'category', 'category_display', 'type', 'type_display', 'attrs',
|
||||||
|
'date_created', 'date_updated',
|
||||||
|
'created_by', 'comment'
|
||||||
]
|
]
|
||||||
|
fields_fk = ['domain']
|
||||||
|
fields = fields_small + fields_fk
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
'created_by', 'date_created', 'date_updated', 'get_type_display',
|
'created_by', 'date_created', 'date_updated', 'get_type_display',
|
||||||
]
|
]
|
||||||
|
@ -10,10 +10,10 @@ from common.permissions import IsOrgAdminOrAppUser, NeedMFAVerify
|
|||||||
from common.utils import get_object_or_none, get_logger
|
from common.utils import get_object_or_none, get_logger
|
||||||
from common.mixins import CommonApiMixin
|
from common.mixins import CommonApiMixin
|
||||||
from ..backends import AssetUserManager
|
from ..backends import AssetUserManager
|
||||||
from ..models import Asset, Node, SystemUser
|
from ..models import Node
|
||||||
from .. import serializers
|
from .. import serializers
|
||||||
from ..tasks import (
|
from ..tasks import (
|
||||||
test_asset_users_connectivity_manual, push_system_user_a_asset_manual
|
test_asset_users_connectivity_manual
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -100,12 +100,6 @@ class AssetUserViewSet(CommonApiMixin, BulkModelViewSet):
|
|||||||
obj = queryset.get(id=pk)
|
obj = queryset.get(id=pk)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def get_exception_handler(self):
|
|
||||||
def handler(e, context):
|
|
||||||
logger.error(e, exc_info=True)
|
|
||||||
return Response({"error": str(e)}, status=400)
|
|
||||||
return handler
|
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
manager = AssetUserManager()
|
manager = AssetUserManager()
|
||||||
manager.delete(instance)
|
manager.delete(instance)
|
||||||
|
@ -1,15 +1,25 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.generics import CreateAPIView, RetrieveDestroyAPIView
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
|
from common.utils import reverse
|
||||||
|
from common.utils import lazyproperty
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
from ..hands import IsOrgAdmin
|
from orgs.utils import tmp_to_root_org
|
||||||
|
from tickets.models import Ticket
|
||||||
|
from tickets.api import GenericTicketStatusRetrieveCloseAPI
|
||||||
|
from ..hands import IsOrgAdmin, IsAppUser
|
||||||
from ..models import CommandFilter, CommandFilterRule
|
from ..models import CommandFilter, CommandFilterRule
|
||||||
from .. import serializers
|
from .. import serializers
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['CommandFilterViewSet', 'CommandFilterRuleViewSet']
|
__all__ = [
|
||||||
|
'CommandFilterViewSet', 'CommandFilterRuleViewSet', 'CommandConfirmAPI',
|
||||||
|
'CommandConfirmStatusAPI'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class CommandFilterViewSet(OrgBulkModelViewSet):
|
class CommandFilterViewSet(OrgBulkModelViewSet):
|
||||||
@ -35,3 +45,50 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet):
|
|||||||
return cmd_filter.rules.all()
|
return cmd_filter.rules.all()
|
||||||
|
|
||||||
|
|
||||||
|
class CommandConfirmAPI(CreateAPIView):
|
||||||
|
permission_classes = (IsAppUser, )
|
||||||
|
serializer_class = serializers.CommandConfirmSerializer
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
ticket = self.create_command_confirm_ticket()
|
||||||
|
response_data = self.get_response_data(ticket)
|
||||||
|
return Response(data=response_data, status=200)
|
||||||
|
|
||||||
|
def create_command_confirm_ticket(self):
|
||||||
|
ticket = self.serializer.cmd_filter_rule.create_command_confirm_ticket(
|
||||||
|
run_command=self.serializer.data.get('run_command'),
|
||||||
|
session=self.serializer.session,
|
||||||
|
cmd_filter_rule=self.serializer.cmd_filter_rule,
|
||||||
|
org_id=self.serializer.org.id
|
||||||
|
)
|
||||||
|
return ticket
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_response_data(ticket):
|
||||||
|
confirm_status_url = reverse(
|
||||||
|
view_name='api-assets:command-confirm-status',
|
||||||
|
kwargs={'pk': str(ticket.id)}
|
||||||
|
)
|
||||||
|
ticket_detail_url = reverse(
|
||||||
|
view_name='api-tickets:ticket-detail',
|
||||||
|
kwargs={'pk': str(ticket.id)},
|
||||||
|
external=True, api_to_ui=True
|
||||||
|
)
|
||||||
|
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type)
|
||||||
|
return {
|
||||||
|
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
|
||||||
|
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},
|
||||||
|
'ticket_detail_url': ticket_detail_url,
|
||||||
|
'reviewers': [str(user) for user in ticket.assignees.all()]
|
||||||
|
}
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def serializer(self):
|
||||||
|
serializer = self.get_serializer(data=self.request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
return serializer
|
||||||
|
|
||||||
|
|
||||||
|
class CommandConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@ -71,8 +71,8 @@ class NodeViewSet(OrgModelViewSet):
|
|||||||
if node.is_org_root():
|
if node.is_org_root():
|
||||||
error = _("You can't delete the root node ({})".format(node.value))
|
error = _("You can't delete the root node ({})".format(node.value))
|
||||||
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
|
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
|
||||||
if node.has_children_or_has_assets():
|
if node.has_offspring_assets():
|
||||||
error = _("Deletion failed and the node contains children or assets")
|
error = _("Deletion failed and the node contains assets")
|
||||||
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
|
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
|
||||||
return super().destroy(request, *args, **kwargs)
|
return super().destroy(request, *args, **kwargs)
|
||||||
|
|
||||||
|
25
apps/assets/migrations/0070_auto_20210426_1515.py
Normal file
25
apps/assets/migrations/0070_auto_20210426_1515.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 3.1 on 2021-04-26 07:15
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('assets', '0069_change_node_key0_to_key1'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='commandfilterrule',
|
||||||
|
name='reviewers',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='review_cmd_filter_rules', to=settings.AUTH_USER_MODEL, verbose_name='Reviewers'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='commandfilterrule',
|
||||||
|
name='action',
|
||||||
|
field=models.IntegerField(choices=[(0, 'Deny'), (1, 'Allow'), (2, 'Reconfirm')], default=0, verbose_name='Action'),
|
||||||
|
),
|
||||||
|
]
|
@ -35,7 +35,7 @@ def default_node():
|
|||||||
try:
|
try:
|
||||||
from .node import Node
|
from .node import Node
|
||||||
root = Node.org_root()
|
root = Node.org_root()
|
||||||
return root
|
return Node.objects.filter(id=root.id)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -41,11 +41,12 @@ class CommandFilterRule(OrgModelMixin):
|
|||||||
(TYPE_COMMAND, _('Command')),
|
(TYPE_COMMAND, _('Command')),
|
||||||
)
|
)
|
||||||
|
|
||||||
ACTION_DENY, ACTION_ALLOW, ACTION_UNKNOWN = range(3)
|
ACTION_UNKNOWN = 10
|
||||||
ACTION_CHOICES = (
|
|
||||||
(ACTION_DENY, _('Deny')),
|
class ActionChoices(models.IntegerChoices):
|
||||||
(ACTION_ALLOW, _('Allow')),
|
deny = 0, _('Deny')
|
||||||
)
|
allow = 1, _('Allow')
|
||||||
|
confirm = 2, _('Reconfirm')
|
||||||
|
|
||||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||||
filter = models.ForeignKey('CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules')
|
filter = models.ForeignKey('CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules')
|
||||||
@ -53,7 +54,13 @@ class CommandFilterRule(OrgModelMixin):
|
|||||||
priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"),
|
priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"),
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(100)])
|
validators=[MinValueValidator(1), MaxValueValidator(100)])
|
||||||
content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command"))
|
content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command"))
|
||||||
action = models.IntegerField(default=ACTION_DENY, choices=ACTION_CHOICES, verbose_name=_("Action"))
|
action = models.IntegerField(default=ActionChoices.deny, choices=ActionChoices.choices, verbose_name=_("Action"))
|
||||||
|
# 动作: 附加字段
|
||||||
|
# - confirm: 命令复核人
|
||||||
|
reviewers = models.ManyToManyField(
|
||||||
|
'users.User', related_name='review_cmd_filter_rules', blank=True,
|
||||||
|
verbose_name=_("Reviewers")
|
||||||
|
)
|
||||||
comment = models.CharField(max_length=64, blank=True, default='', verbose_name=_("Comment"))
|
comment = models.CharField(max_length=64, blank=True, default='', verbose_name=_("Comment"))
|
||||||
date_created = models.DateTimeField(auto_now_add=True)
|
date_created = models.DateTimeField(auto_now_add=True)
|
||||||
date_updated = models.DateTimeField(auto_now=True)
|
date_updated = models.DateTimeField(auto_now=True)
|
||||||
@ -89,10 +96,32 @@ class CommandFilterRule(OrgModelMixin):
|
|||||||
if not found:
|
if not found:
|
||||||
return self.ACTION_UNKNOWN, ''
|
return self.ACTION_UNKNOWN, ''
|
||||||
|
|
||||||
if self.action == self.ACTION_ALLOW:
|
if self.action == self.ActionChoices.allow:
|
||||||
return self.ACTION_ALLOW, found.group()
|
return self.ActionChoices.allow, found.group()
|
||||||
else:
|
else:
|
||||||
return self.ACTION_DENY, found.group()
|
return self.ActionChoices.deny, found.group()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{} % {}'.format(self.type, self.content)
|
return '{} % {}'.format(self.type, self.content)
|
||||||
|
|
||||||
|
def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id):
|
||||||
|
from tickets.const import TicketTypeChoices
|
||||||
|
from tickets.models import Ticket
|
||||||
|
data = {
|
||||||
|
'title': _('Command confirm') + ' ({})'.format(session.user),
|
||||||
|
'type': TicketTypeChoices.command_confirm,
|
||||||
|
'meta': {
|
||||||
|
'apply_run_user': session.user,
|
||||||
|
'apply_run_asset': session.asset,
|
||||||
|
'apply_run_system_user': session.system_user,
|
||||||
|
'apply_run_command': run_command,
|
||||||
|
'apply_from_session_id': str(session.id),
|
||||||
|
'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id),
|
||||||
|
'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id)
|
||||||
|
},
|
||||||
|
'org_id': org_id,
|
||||||
|
}
|
||||||
|
ticket = Ticket.objects.create(**data)
|
||||||
|
ticket.assignees.set(self.reviewers.all())
|
||||||
|
ticket.open(applicant=session.user_obj)
|
||||||
|
return ticket
|
||||||
|
@ -38,8 +38,7 @@ def compute_parent_key(key):
|
|||||||
|
|
||||||
|
|
||||||
class NodeQuerySet(models.QuerySet):
|
class NodeQuerySet(models.QuerySet):
|
||||||
def delete(self):
|
pass
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class FamilyMixin:
|
class FamilyMixin:
|
||||||
@ -622,14 +621,14 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
|
|||||||
tree_node = TreeNode(**data)
|
tree_node = TreeNode(**data)
|
||||||
return tree_node
|
return tree_node
|
||||||
|
|
||||||
def has_children_or_has_assets(self):
|
def has_offspring_assets(self):
|
||||||
if self.children or self.get_assets().exists():
|
# 拥有后代资产
|
||||||
return True
|
return self.get_all_assets().exists()
|
||||||
return False
|
|
||||||
|
|
||||||
def delete(self, using=None, keep_parents=False):
|
def delete(self, using=None, keep_parents=False):
|
||||||
if self.has_children_or_has_assets():
|
if self.has_offspring_assets():
|
||||||
return
|
return
|
||||||
|
self.all_children.delete()
|
||||||
return super().delete(using=using, keep_parents=keep_parents)
|
return super().delete(using=using, keep_parents=keep_parents)
|
||||||
|
|
||||||
def update_child_full_value(self):
|
def update_child_full_value(self):
|
||||||
|
@ -196,9 +196,9 @@ class SystemUser(BaseUser):
|
|||||||
def is_command_can_run(self, command):
|
def is_command_can_run(self, command):
|
||||||
for rule in self.cmd_filter_rules:
|
for rule in self.cmd_filter_rules:
|
||||||
action, matched_cmd = rule.match(command)
|
action, matched_cmd = rule.match(command)
|
||||||
if action == rule.ACTION_ALLOW:
|
if action == rule.ActionChoices.allow:
|
||||||
return True, None
|
return True, None
|
||||||
elif action == rule.ACTION_DENY:
|
elif action == rule.ActionChoices.deny:
|
||||||
return False, matched_cmd
|
return False, matched_cmd
|
||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
|
@ -16,10 +16,14 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AdminUser
|
model = AdminUser
|
||||||
fields = [
|
fields_mini = ['id', 'name', 'username']
|
||||||
'id', 'name', 'username', 'password', 'private_key', 'public_key',
|
fields_write_only = ['password', 'private_key', 'public_key']
|
||||||
'comment', 'assets_amount', 'date_created', 'date_updated', 'created_by',
|
fields_small = fields_mini + fields_write_only + [
|
||||||
|
'date_created', 'date_updated',
|
||||||
|
'comment', 'created_by'
|
||||||
]
|
]
|
||||||
|
fields_fk = ['assets_amount']
|
||||||
|
fields = fields_small + fields_fk
|
||||||
read_only_fields = ['date_created', 'date_updated', 'created_by', 'assets_amount']
|
read_only_fields = ['date_created', 'date_updated', 'created_by', 'assets_amount']
|
||||||
|
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
|
@ -65,7 +65,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
|
|||||||
platform = serializers.SlugRelatedField(
|
platform = serializers.SlugRelatedField(
|
||||||
slug_field='name', queryset=Platform.objects.all(), label=_("Platform")
|
slug_field='name', queryset=Platform.objects.all(), label=_("Platform")
|
||||||
)
|
)
|
||||||
protocols = ProtocolsField(label=_('Protocols'), required=False)
|
protocols = ProtocolsField(label=_('Protocols'), required=False, default=['ssh/22'])
|
||||||
domain_display = serializers.ReadOnlyField(source='domain.name', label=_('Domain name'))
|
domain_display = serializers.ReadOnlyField(source='domain.name', label=_('Domain name'))
|
||||||
admin_user_display = serializers.ReadOnlyField(source='admin_user.name', label=_('Admin user name'))
|
admin_user_display = serializers.ReadOnlyField(source='admin_user.name', label=_('Admin user name'))
|
||||||
nodes_display = serializers.ListField(child=serializers.CharField(), label=_('Nodes name'), required=False)
|
nodes_display = serializers.ListField(child=serializers.CharField(), label=_('Nodes name'), required=False)
|
||||||
|
@ -22,10 +22,11 @@ class AssetUserWriteSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializ
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = AuthBook
|
model = AuthBook
|
||||||
list_serializer_class = AdaptedBulkListSerializer
|
list_serializer_class = AdaptedBulkListSerializer
|
||||||
fields = [
|
fields_mini = ['id', 'username']
|
||||||
'id', 'username', 'password', 'private_key', "public_key",
|
fields_write_only = ['password', 'private_key', "public_key"]
|
||||||
'asset', 'comment',
|
fields_small = fields_mini + fields_write_only + ['comment']
|
||||||
]
|
fields_fk = ['asset']
|
||||||
|
fields = fields_small + fields_fk
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'username': {'required': True},
|
'username': {'required': True},
|
||||||
'password': {'write_only': True},
|
'password': {'write_only': True},
|
||||||
@ -52,11 +53,15 @@ class AssetUserReadSerializer(AssetUserWriteSerializer):
|
|||||||
'date_created', 'date_updated',
|
'date_created', 'date_updated',
|
||||||
'created_by', 'version',
|
'created_by', 'version',
|
||||||
)
|
)
|
||||||
fields = [
|
fields_mini = ['id', 'username']
|
||||||
'id', 'username', 'password', 'private_key', "public_key",
|
fields_write_only = ['password', 'private_key', "public_key"]
|
||||||
'asset', 'hostname', 'ip', 'backend', 'version',
|
fields_small = fields_mini + fields_write_only + [
|
||||||
'date_created', "date_updated", 'comment',
|
'backend', 'version',
|
||||||
|
'date_created', "date_updated",
|
||||||
|
'comment'
|
||||||
]
|
]
|
||||||
|
fields_fk = ['asset', 'hostname', 'ip']
|
||||||
|
fields = fields_small + fields_fk
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'username': {'required': True},
|
'username': {'required': True},
|
||||||
'password': {'write_only': True},
|
'password': {'write_only': True},
|
||||||
|
@ -6,6 +6,9 @@ from rest_framework import serializers
|
|||||||
from common.drf.serializers import AdaptedBulkListSerializer
|
from common.drf.serializers import AdaptedBulkListSerializer
|
||||||
from ..models import CommandFilter, CommandFilterRule, SystemUser
|
from ..models import CommandFilter, CommandFilterRule, SystemUser
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
|
from orgs.utils import tmp_to_root_org
|
||||||
|
from common.utils import get_object_or_none, lazyproperty
|
||||||
|
from terminal.models import Session
|
||||||
|
|
||||||
|
|
||||||
class CommandFilterSerializer(BulkOrgResourceModelSerializer):
|
class CommandFilterSerializer(BulkOrgResourceModelSerializer):
|
||||||
@ -13,11 +16,16 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CommandFilter
|
model = CommandFilter
|
||||||
list_serializer_class = AdaptedBulkListSerializer
|
list_serializer_class = AdaptedBulkListSerializer
|
||||||
fields = [
|
fields_mini = ['id', 'name']
|
||||||
'id', 'name', 'org_id', 'org_name', 'is_active', 'comment',
|
fields_small = fields_mini + [
|
||||||
'created_by', 'date_created', 'date_updated', 'rules', 'system_users'
|
'org_id', 'org_name',
|
||||||
|
'is_active',
|
||||||
|
'date_created', 'date_updated',
|
||||||
|
'comment', 'created_by',
|
||||||
]
|
]
|
||||||
|
fields_fk = ['rules']
|
||||||
|
fields_m2m = ['system_users']
|
||||||
|
fields = fields_small + fields_fk + fields_m2m
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'rules': {'read_only': True},
|
'rules': {'read_only': True},
|
||||||
'system_users': {'required': False},
|
'system_users': {'required': False},
|
||||||
@ -34,8 +42,9 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
|
|||||||
fields_mini = ['id']
|
fields_mini = ['id']
|
||||||
fields_small = fields_mini + [
|
fields_small = fields_mini + [
|
||||||
'type', 'type_display', 'content', 'priority',
|
'type', 'type_display', 'content', 'priority',
|
||||||
'action', 'action_display',
|
'action', 'action_display', 'reviewers',
|
||||||
'comment', 'created_by', 'date_created', 'date_updated'
|
'date_created', 'date_updated',
|
||||||
|
'comment', 'created_by',
|
||||||
]
|
]
|
||||||
fields_fk = ['filter']
|
fields_fk = ['filter']
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
@ -50,3 +59,35 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
|
|||||||
# msg = _("Content should not be contain: {}").format(invalid_char)
|
# msg = _("Content should not be contain: {}").format(invalid_char)
|
||||||
# raise serializers.ValidationError(msg)
|
# raise serializers.ValidationError(msg)
|
||||||
# return content
|
# return content
|
||||||
|
|
||||||
|
|
||||||
|
class CommandConfirmSerializer(serializers.Serializer):
|
||||||
|
session_id = serializers.UUIDField(required=True, allow_null=False)
|
||||||
|
cmd_filter_rule_id = serializers.UUIDField(required=True, allow_null=False)
|
||||||
|
run_command = serializers.CharField(required=True, allow_null=False)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.session = None
|
||||||
|
self.cmd_filter_rule = None
|
||||||
|
|
||||||
|
def validate_session_id(self, session_id):
|
||||||
|
self.session = self.validate_object_exist(Session, session_id)
|
||||||
|
return session_id
|
||||||
|
|
||||||
|
def validate_cmd_filter_rule_id(self, cmd_filter_rule_id):
|
||||||
|
self.cmd_filter_rule = self.validate_object_exist(CommandFilterRule, cmd_filter_rule_id)
|
||||||
|
return cmd_filter_rule_id
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_object_exist(model, field_id):
|
||||||
|
with tmp_to_root_org():
|
||||||
|
obj = get_object_or_none(model, id=field_id)
|
||||||
|
if not obj:
|
||||||
|
error = '{} Model object does not exist'.format(model.__name__)
|
||||||
|
raise serializers.ValidationError(error)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def org(self):
|
||||||
|
return self.session.org
|
||||||
|
@ -48,11 +48,18 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Gateway
|
model = Gateway
|
||||||
list_serializer_class = AdaptedBulkListSerializer
|
list_serializer_class = AdaptedBulkListSerializer
|
||||||
fields = [
|
fields_mini = ['id', 'name']
|
||||||
'id', 'name', 'ip', 'port', 'protocol', 'username', 'password',
|
fields_write_only = [
|
||||||
'private_key', 'public_key', 'domain', 'is_active', 'date_created',
|
'password', 'private_key', 'public_key',
|
||||||
'date_updated', 'created_by', 'comment',
|
|
||||||
]
|
]
|
||||||
|
fields_small = fields_mini + fields_write_only + [
|
||||||
|
'username', 'ip', 'port', 'protocol',
|
||||||
|
'is_active',
|
||||||
|
'date_created', 'date_updated',
|
||||||
|
'created_by', 'comment',
|
||||||
|
]
|
||||||
|
fields_fk = ['domain']
|
||||||
|
fields = fields_small + fields_fk
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'password': {'validators': [NoSpecialChars()]}
|
'password': {'validators': [NoSpecialChars()]}
|
||||||
}
|
}
|
||||||
|
@ -10,11 +10,14 @@ from ..models import GatheredUser
|
|||||||
class GatheredUserSerializer(OrgResourceModelSerializerMixin):
|
class GatheredUserSerializer(OrgResourceModelSerializerMixin):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = GatheredUser
|
model = GatheredUser
|
||||||
fields = [
|
fields_mini = ['id']
|
||||||
'id', 'asset', 'hostname', 'ip', 'username',
|
fields_small = fields_mini + [
|
||||||
'date_last_login', 'ip_last_login',
|
'username', 'ip_last_login',
|
||||||
'present', 'date_created', 'date_updated'
|
'present',
|
||||||
|
'date_last_login', 'date_created', 'date_updated'
|
||||||
]
|
]
|
||||||
|
fields_fk = ['asset', 'hostname', 'ip']
|
||||||
|
fields = fields_small + fields_fk
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'hostname': {'label': _("Hostname")},
|
'hostname': {'label': _("Hostname")},
|
||||||
|
@ -15,10 +15,15 @@ class LabelSerializer(BulkOrgResourceModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Label
|
model = Label
|
||||||
fields = [
|
fields_mini = ['id', 'name']
|
||||||
'id', 'name', 'value', 'category', 'is_active', 'comment',
|
fields_small = fields_mini + [
|
||||||
'date_created', 'asset_count', 'assets', 'category_display'
|
'value', 'category', 'category_display',
|
||||||
|
'is_active',
|
||||||
|
'date_created',
|
||||||
|
'comment',
|
||||||
]
|
]
|
||||||
|
fields_m2m = ['asset_count', 'assets']
|
||||||
|
fields = fields_small + fields_m2m
|
||||||
read_only_fields = (
|
read_only_fields = (
|
||||||
'category', 'date_created', 'asset_count',
|
'category', 'date_created', 'asset_count',
|
||||||
)
|
)
|
||||||
|
@ -26,16 +26,18 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = SystemUser
|
model = SystemUser
|
||||||
list_serializer_class = AdaptedBulkListSerializer
|
list_serializer_class = AdaptedBulkListSerializer
|
||||||
fields = [
|
fields_mini = ['id', 'name', 'username']
|
||||||
'id', 'name', 'username', 'protocol',
|
fields_write_only = ['password', 'public_key', 'private_key']
|
||||||
'password', 'public_key', 'private_key',
|
fields_small = fields_mini + fields_write_only + [
|
||||||
'login_mode', 'login_mode_display',
|
'protocol', 'login_mode', 'login_mode_display', 'priority',
|
||||||
'priority', 'username_same_with_user',
|
'sudo', 'shell', 'sftp_root', 'token',
|
||||||
'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment',
|
'home', 'system_groups', 'ad_domain',
|
||||||
'auto_generate_key', 'sftp_root', 'token',
|
'username_same_with_user', 'auto_push', 'auto_generate_key',
|
||||||
'assets_amount', 'date_created', 'date_updated', 'created_by',
|
'date_created', 'date_updated',
|
||||||
'home', 'system_groups', 'ad_domain'
|
'comment', 'created_by',
|
||||||
]
|
]
|
||||||
|
fields_m2m = [ 'cmd_filters', 'assets_amount']
|
||||||
|
fields = fields_small + fields_m2m
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'password': {"write_only": True},
|
'password': {"write_only": True},
|
||||||
'public_key': {"write_only": True},
|
'public_key': {"write_only": True},
|
||||||
@ -147,17 +149,18 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||||||
class SystemUserListSerializer(SystemUserSerializer):
|
class SystemUserListSerializer(SystemUserSerializer):
|
||||||
|
|
||||||
class Meta(SystemUserSerializer.Meta):
|
class Meta(SystemUserSerializer.Meta):
|
||||||
fields = [
|
fields_mini = ['id', 'name', 'username']
|
||||||
'id', 'name', 'username', 'protocol',
|
fields_write_only = ['password', 'public_key', 'private_key']
|
||||||
'password', 'public_key', 'private_key',
|
fields_small = fields_mini + fields_write_only + [
|
||||||
'login_mode', 'login_mode_display',
|
'protocol', 'login_mode', 'login_mode_display', 'priority',
|
||||||
'priority', "username_same_with_user",
|
'sudo', 'shell', 'home', 'system_groups',
|
||||||
'auto_push', 'sudo', 'shell', 'comment',
|
'ad_domain', 'sftp_root',
|
||||||
"assets_amount", 'home', 'system_groups',
|
"username_same_with_user", 'auto_push', 'auto_generate_key',
|
||||||
'auto_generate_key', 'ad_domain',
|
'date_created', 'date_updated',
|
||||||
'sftp_root', 'created_by', 'date_created',
|
'comment', 'created_by',
|
||||||
'date_updated',
|
|
||||||
]
|
]
|
||||||
|
fields_m2m = ["assets_amount",]
|
||||||
|
fields = fields_small + fields_m2m
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'password': {"write_only": True},
|
'password': {"write_only": True},
|
||||||
'public_key': {"write_only": True},
|
'public_key': {"write_only": True},
|
||||||
@ -178,15 +181,15 @@ class SystemUserListSerializer(SystemUserSerializer):
|
|||||||
|
|
||||||
class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
|
class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
|
||||||
class Meta(SystemUserSerializer.Meta):
|
class Meta(SystemUserSerializer.Meta):
|
||||||
fields = [
|
fields_mini = ['id', 'name', 'username']
|
||||||
'id', 'name', 'username', 'protocol',
|
fields_write_only = ['password', 'public_key', 'private_key']
|
||||||
'password', 'public_key', 'private_key',
|
fields_small = fields_mini + fields_write_only + [
|
||||||
'login_mode', 'login_mode_display',
|
'protocol', 'login_mode', 'login_mode_display', 'priority',
|
||||||
'priority', 'username_same_with_user',
|
'sudo', 'shell', 'ad_domain', 'sftp_root', 'token',
|
||||||
'auto_push', 'sudo', 'shell', 'comment',
|
"username_same_with_user", 'auto_push', 'auto_generate_key',
|
||||||
'auto_generate_key', 'sftp_root', 'token',
|
'comment',
|
||||||
'ad_domain',
|
|
||||||
]
|
]
|
||||||
|
fields = fields_small
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'nodes_amount': {'label': _('Node')},
|
'nodes_amount': {'label': _('Node')},
|
||||||
'assets_amount': {'label': _('Asset')},
|
'assets_amount': {'label': _('Asset')},
|
||||||
|
@ -56,8 +56,8 @@ def get_push_unixlike_system_user_tasks(system_user, username=None):
|
|||||||
'shell': system_user.shell or Empty,
|
'shell': system_user.shell or Empty,
|
||||||
'state': 'present',
|
'state': 'present',
|
||||||
'home': system_user.home or Empty,
|
'home': system_user.home or Empty,
|
||||||
|
'expires': -1,
|
||||||
'groups': groups or Empty,
|
'groups': groups or Empty,
|
||||||
'expires': 99999,
|
|
||||||
'comment': comment
|
'comment': comment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,6 +63,9 @@ urlpatterns = [
|
|||||||
|
|
||||||
path('gateways/<uuid:pk>/test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
|
path('gateways/<uuid:pk>/test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
|
||||||
|
|
||||||
|
path('cmd-filters/command-confirm/', api.CommandConfirmAPI.as_view(), name='command-confirm'),
|
||||||
|
path('cmd-filters/command-confirm/<uuid:pk>/status/', api.CommandConfirmStatusAPI.as_view(), name='command-confirm-status')
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
old_version_urlpatterns = [
|
old_version_urlpatterns = [
|
||||||
|
@ -16,10 +16,14 @@ class FTPLogSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.FTPLog
|
model = models.FTPLog
|
||||||
fields = (
|
fields_mini = ['id']
|
||||||
'id', 'user', 'remote_addr', 'asset', 'system_user', 'org_id',
|
fields_small = fields_mini + [
|
||||||
'operate', 'filename', 'is_success', 'date_start', 'operate_display'
|
'user', 'remote_addr', 'asset', 'system_user', 'org_id',
|
||||||
)
|
'operate', 'filename', 'operate_display',
|
||||||
|
'is_success',
|
||||||
|
'date_start',
|
||||||
|
]
|
||||||
|
fields = fields_small
|
||||||
|
|
||||||
|
|
||||||
class UserLoginLogSerializer(serializers.ModelSerializer):
|
class UserLoginLogSerializer(serializers.ModelSerializer):
|
||||||
@ -29,11 +33,14 @@ class UserLoginLogSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.UserLoginLog
|
model = models.UserLoginLog
|
||||||
fields = (
|
fields_mini = ['id']
|
||||||
'id', 'username', 'type', 'type_display', 'ip', 'city', 'user_agent',
|
fields_small = fields_mini + [
|
||||||
'mfa', 'reason', 'status', 'status_display', 'datetime', 'mfa_display',
|
'username', 'type', 'type_display', 'ip', 'city', 'user_agent',
|
||||||
'backend'
|
'mfa', 'mfa_display', 'reason', 'backend',
|
||||||
)
|
'status', 'status_display',
|
||||||
|
'datetime',
|
||||||
|
]
|
||||||
|
fields = fields_small
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"user_agent": {'label': _('User agent')}
|
"user_agent": {'label': _('User agent')}
|
||||||
}
|
}
|
||||||
@ -42,10 +49,13 @@ class UserLoginLogSerializer(serializers.ModelSerializer):
|
|||||||
class OperateLogSerializer(serializers.ModelSerializer):
|
class OperateLogSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.OperateLog
|
model = models.OperateLog
|
||||||
fields = (
|
fields_mini = ['id']
|
||||||
'id', 'user', 'action', 'resource_type', 'resource',
|
fields_small = fields_mini + [
|
||||||
'remote_addr', 'datetime', 'org_id'
|
'user', 'action', 'resource_type', 'resource', 'remote_addr',
|
||||||
)
|
'datetime',
|
||||||
|
'org_id'
|
||||||
|
]
|
||||||
|
fields = fields_small
|
||||||
|
|
||||||
|
|
||||||
class PasswordChangeLogSerializer(serializers.ModelSerializer):
|
class PasswordChangeLogSerializer(serializers.ModelSerializer):
|
||||||
|
@ -27,11 +27,23 @@ json_render = JSONRenderer()
|
|||||||
|
|
||||||
|
|
||||||
MODELS_NEED_RECORD = (
|
MODELS_NEED_RECORD = (
|
||||||
'User', 'UserGroup', 'Asset', 'Node', 'AdminUser', 'SystemUser',
|
# users
|
||||||
'Domain', 'Gateway', 'Organization', 'AssetPermission', 'CommandFilter',
|
'User', 'UserGroup',
|
||||||
'CommandFilterRule', 'License', 'Setting', 'Account', 'SyncInstanceTask',
|
# acls
|
||||||
'Platform', 'ChangeAuthPlan', 'GatherUserTask',
|
'LoginACL', 'LoginAssetACL',
|
||||||
'RemoteApp', 'RemoteAppPermission', 'DatabaseApp', 'DatabaseAppPermission',
|
# assets
|
||||||
|
'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule',
|
||||||
|
'CommandFilter', 'Platform',
|
||||||
|
# applications
|
||||||
|
'Application',
|
||||||
|
# orgs
|
||||||
|
'Organization',
|
||||||
|
# settings
|
||||||
|
'Setting',
|
||||||
|
# perms
|
||||||
|
'AssetPermission', 'ApplicationPermission',
|
||||||
|
# xpack
|
||||||
|
'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'GatherUserTask',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -45,6 +57,8 @@ class AuthBackendLabelMapping(LazyObject):
|
|||||||
backend_label_mapping[settings.AUTH_BACKEND_PUBKEY] = _('SSH Key')
|
backend_label_mapping[settings.AUTH_BACKEND_PUBKEY] = _('SSH Key')
|
||||||
backend_label_mapping[settings.AUTH_BACKEND_MODEL] = _('Password')
|
backend_label_mapping[settings.AUTH_BACKEND_MODEL] = _('Password')
|
||||||
backend_label_mapping[settings.AUTH_BACKEND_SSO] = _('SSO')
|
backend_label_mapping[settings.AUTH_BACKEND_SSO] = _('SSO')
|
||||||
|
backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom')
|
||||||
|
backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk')
|
||||||
return backend_label_mapping
|
return backend_label_mapping
|
||||||
|
|
||||||
def _setup(self):
|
def _setup(self):
|
||||||
|
@ -7,3 +7,6 @@ from .mfa import *
|
|||||||
from .access_key import *
|
from .access_key import *
|
||||||
from .login_confirm import *
|
from .login_confirm import *
|
||||||
from .sso import *
|
from .sso import *
|
||||||
|
from .wecom import *
|
||||||
|
from .dingtalk import *
|
||||||
|
from .password import *
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
@ -77,7 +79,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
|||||||
})
|
})
|
||||||
|
|
||||||
key = self.CACHE_KEY_PREFIX.format(token)
|
key = self.CACHE_KEY_PREFIX.format(token)
|
||||||
cache.set(key, value, timeout=20)
|
cache.set(key, value, timeout=30*60)
|
||||||
return token
|
return token
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
@ -100,7 +102,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
|||||||
'desktopwidth:i': '1280',
|
'desktopwidth:i': '1280',
|
||||||
'desktopheight:i': '800',
|
'desktopheight:i': '800',
|
||||||
'use multimon:i': '1',
|
'use multimon:i': '1',
|
||||||
'session bpp:i': '24',
|
'session bpp:i': '32',
|
||||||
'audiomode:i': '0',
|
'audiomode:i': '0',
|
||||||
'disable wallpaper:i': '0',
|
'disable wallpaper:i': '0',
|
||||||
'disable full window drag:i': '0',
|
'disable full window drag:i': '0',
|
||||||
@ -140,15 +142,16 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
|||||||
# Todo: 上线后地址是 JumpServerAddr:3389
|
# Todo: 上线后地址是 JumpServerAddr:3389
|
||||||
address = self.request.query_params.get('address') or '1.1.1.1'
|
address = self.request.query_params.get('address') or '1.1.1.1'
|
||||||
options['full address:s'] = address
|
options['full address:s'] = address
|
||||||
options['username:s'] = '{}@{}'.format(user.username, token)
|
options['username:s'] = '{}|{}'.format(user.username, token)
|
||||||
options['desktopwidth:i'] = width
|
options['desktopwidth:i'] = width
|
||||||
options['desktopheight:i'] = height
|
options['desktopheight:i'] = height
|
||||||
data = ''
|
data = ''
|
||||||
for k, v in options.items():
|
for k, v in options.items():
|
||||||
data += f'{k}:{v}\n'
|
data += f'{k}:{v}\n'
|
||||||
response = HttpResponse(data, content_type='text/plain')
|
response = HttpResponse(data, content_type='application/octet-stream')
|
||||||
filename = "{}-{}-jumpserver.rdp".format(user.username, asset.hostname)
|
filename = "{}-{}-jumpserver.rdp".format(user.username, asset.hostname)
|
||||||
response['Content-Disposition'] = 'attachment; filename={}'.format(filename)
|
filename = urllib.parse.quote(filename)
|
||||||
|
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
35
apps/authentication/api/dingtalk.py
Normal file
35
apps/authentication/api/dingtalk.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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 DingTalkQRUnBindBase(APIView):
|
||||||
|
user: User
|
||||||
|
|
||||||
|
def post(self, request: Request, **kwargs):
|
||||||
|
user = self.user
|
||||||
|
|
||||||
|
if not user.dingtalk_id:
|
||||||
|
raise errors.DingTalkNotBound
|
||||||
|
|
||||||
|
user.dingtalk_id = ''
|
||||||
|
user.save()
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
|
||||||
|
class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase):
|
||||||
|
permission_classes = (IsAuthPasswdTimeValid,)
|
||||||
|
|
||||||
|
|
||||||
|
class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase):
|
||||||
|
user_id_url_kwarg = 'user_id'
|
||||||
|
permission_classes = (IsOrgAdmin,)
|
26
apps/authentication/api/password.py
Normal file
26
apps/authentication/api/password.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from rest_framework.generics import CreateAPIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from authentication.serializers import PasswordVerifySerializer
|
||||||
|
from common.permissions import IsValidUser
|
||||||
|
from authentication.mixins import authenticate
|
||||||
|
from authentication.errors import PasswdInvalid
|
||||||
|
from authentication.mixins import AuthMixin
|
||||||
|
|
||||||
|
|
||||||
|
class UserPasswordVerifyApi(AuthMixin, CreateAPIView):
|
||||||
|
permission_classes = (IsValidUser,)
|
||||||
|
serializer_class = PasswordVerifySerializer
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
password = serializer.validated_data['password']
|
||||||
|
user = self.request.user
|
||||||
|
|
||||||
|
user = authenticate(request=request, username=user.username, password=password)
|
||||||
|
if not user:
|
||||||
|
raise PasswdInvalid
|
||||||
|
|
||||||
|
self.set_passwd_verify_on_session(user)
|
||||||
|
return Response()
|
35
apps/authentication/api/wecom.py
Normal file
35
apps/authentication/api/wecom.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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 WeComQRUnBindBase(APIView):
|
||||||
|
user: User
|
||||||
|
|
||||||
|
def post(self, request: Request, **kwargs):
|
||||||
|
user = self.user
|
||||||
|
|
||||||
|
if not user.wecom_id:
|
||||||
|
raise errors.WeComNotBound
|
||||||
|
|
||||||
|
user.wecom_id = ''
|
||||||
|
user.save()
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
|
||||||
|
class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase):
|
||||||
|
permission_classes = (IsAuthPasswdTimeValid,)
|
||||||
|
|
||||||
|
|
||||||
|
class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase):
|
||||||
|
user_id_url_kwarg = 'user_id'
|
||||||
|
permission_classes = (IsOrgAdmin,)
|
@ -205,3 +205,21 @@ class SSOAuthentication(ModelBackend):
|
|||||||
|
|
||||||
def authenticate(self, request, sso_token=None, **kwargs):
|
def authenticate(self, request, sso_token=None, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WeComAuthentication(ModelBackend):
|
||||||
|
"""
|
||||||
|
什么也不做呀😺
|
||||||
|
"""
|
||||||
|
|
||||||
|
def authenticate(self, request, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DingTalkAuthentication(ModelBackend):
|
||||||
|
"""
|
||||||
|
什么也不做呀😺
|
||||||
|
"""
|
||||||
|
|
||||||
|
def authenticate(self, request, **kwargs):
|
||||||
|
pass
|
||||||
|
@ -19,6 +19,7 @@ reason_user_inactive = 'user_inactive'
|
|||||||
reason_user_expired = 'user_expired'
|
reason_user_expired = 'user_expired'
|
||||||
reason_backend_not_match = 'backend_not_match'
|
reason_backend_not_match = 'backend_not_match'
|
||||||
reason_acl_not_allow = 'acl_not_allow'
|
reason_acl_not_allow = 'acl_not_allow'
|
||||||
|
only_local_users_are_allowed = 'only_local_users_are_allowed'
|
||||||
|
|
||||||
reason_choices = {
|
reason_choices = {
|
||||||
reason_password_failed: _('Username/password check failed'),
|
reason_password_failed: _('Username/password check failed'),
|
||||||
@ -32,6 +33,7 @@ reason_choices = {
|
|||||||
reason_user_expired: _("This account is expired"),
|
reason_user_expired: _("This account is expired"),
|
||||||
reason_backend_not_match: _("Auth backend not match"),
|
reason_backend_not_match: _("Auth backend not match"),
|
||||||
reason_acl_not_allow: _("ACL is not allowed"),
|
reason_acl_not_allow: _("ACL is not allowed"),
|
||||||
|
only_local_users_are_allowed: _("Only local users are allowed")
|
||||||
}
|
}
|
||||||
old_reason_choices = {
|
old_reason_choices = {
|
||||||
'0': '-',
|
'0': '-',
|
||||||
@ -275,6 +277,15 @@ class PasswdTooSimple(JMSException):
|
|||||||
self.url = url
|
self.url = url
|
||||||
|
|
||||||
|
|
||||||
|
class PasswdNeedUpdate(JMSException):
|
||||||
|
default_code = 'passwd_need_update'
|
||||||
|
default_detail = _('You should to change your password before login')
|
||||||
|
|
||||||
|
def __init__(self, url, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.url = url
|
||||||
|
|
||||||
|
|
||||||
class PasswordRequireResetError(JMSException):
|
class PasswordRequireResetError(JMSException):
|
||||||
default_code = 'passwd_has_expired'
|
default_code = 'passwd_has_expired'
|
||||||
default_detail = _('Your password has expired, please reset before logging in')
|
default_detail = _('Your password has expired, please reset before logging in')
|
||||||
@ -282,3 +293,28 @@ class PasswordRequireResetError(JMSException):
|
|||||||
def __init__(self, url, *args, **kwargs):
|
def __init__(self, url, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.url = url
|
self.url = url
|
||||||
|
|
||||||
|
|
||||||
|
class WeComCodeInvalid(JMSException):
|
||||||
|
default_code = 'wecom_code_invalid'
|
||||||
|
default_detail = 'Code invalid, can not get user info'
|
||||||
|
|
||||||
|
|
||||||
|
class WeComBindAlready(JMSException):
|
||||||
|
default_code = 'wecom_bind_already'
|
||||||
|
default_detail = 'WeCom already binded'
|
||||||
|
|
||||||
|
|
||||||
|
class WeComNotBound(JMSException):
|
||||||
|
default_code = 'wecom_not_bound'
|
||||||
|
default_detail = 'WeCom is not bound'
|
||||||
|
|
||||||
|
|
||||||
|
class DingTalkNotBound(JMSException):
|
||||||
|
default_code = 'dingtalk_not_bound'
|
||||||
|
default_detail = 'DingTalk is not bound'
|
||||||
|
|
||||||
|
|
||||||
|
class PasswdInvalid(JMSException):
|
||||||
|
default_code = 'passwd_invalid'
|
||||||
|
default_detail = _('Your password is invalid')
|
||||||
|
@ -5,15 +5,17 @@ from urllib.parse import urlencode
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from django.core.cache import cache
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
from django.contrib.auth import (
|
from django.contrib.auth import (
|
||||||
BACKEND_SESSION_KEY, _get_backends,
|
BACKEND_SESSION_KEY, _get_backends,
|
||||||
PermissionDenied, user_login_failed, _clean_credentials
|
PermissionDenied, user_login_failed, _clean_credentials
|
||||||
)
|
)
|
||||||
from django.shortcuts import reverse
|
from django.shortcuts import reverse, redirect
|
||||||
|
|
||||||
from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get
|
from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get, FlashMessageUtil
|
||||||
from users.models import User
|
from users.models import User
|
||||||
from users.utils import LoginBlockUtil, MFABlockUtils
|
from users.utils import LoginBlockUtil, MFABlockUtils
|
||||||
from . import errors
|
from . import errors
|
||||||
@ -81,6 +83,8 @@ class AuthMixin:
|
|||||||
request = None
|
request = None
|
||||||
partial_credential_error = None
|
partial_credential_error = None
|
||||||
|
|
||||||
|
key_prefix_captcha = "_LOGIN_INVALID_{}"
|
||||||
|
|
||||||
def get_user_from_session(self):
|
def get_user_from_session(self):
|
||||||
if self.request.session.is_empty():
|
if self.request.session.is_empty():
|
||||||
raise errors.SessionEmptyError()
|
raise errors.SessionEmptyError()
|
||||||
@ -109,11 +113,7 @@ class AuthMixin:
|
|||||||
ip = ip or get_request_ip(self.request)
|
ip = ip or get_request_ip(self.request)
|
||||||
return ip
|
return ip
|
||||||
|
|
||||||
def check_is_block(self, raise_exception=True):
|
def _check_is_block(self, username, raise_exception=True):
|
||||||
if hasattr(self.request, 'data'):
|
|
||||||
username = self.request.data.get("username")
|
|
||||||
else:
|
|
||||||
username = self.request.POST.get("username")
|
|
||||||
ip = self.get_request_ip()
|
ip = self.get_request_ip()
|
||||||
if LoginBlockUtil(username, ip).is_block():
|
if LoginBlockUtil(username, ip).is_block():
|
||||||
logger.warn('Ip was blocked' + ': ' + username + ':' + ip)
|
logger.warn('Ip was blocked' + ': ' + username + ':' + ip)
|
||||||
@ -123,6 +123,13 @@ class AuthMixin:
|
|||||||
else:
|
else:
|
||||||
return exception
|
return exception
|
||||||
|
|
||||||
|
def check_is_block(self, raise_exception=True):
|
||||||
|
if hasattr(self.request, 'data'):
|
||||||
|
username = self.request.data.get("username")
|
||||||
|
else:
|
||||||
|
username = self.request.POST.get("username")
|
||||||
|
self._check_is_block(username, raise_exception)
|
||||||
|
|
||||||
def decrypt_passwd(self, raw_passwd):
|
def decrypt_passwd(self, raw_passwd):
|
||||||
# 获取解密密钥,对密码进行解密
|
# 获取解密密钥,对密码进行解密
|
||||||
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
|
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
|
||||||
@ -139,6 +146,9 @@ class AuthMixin:
|
|||||||
def raise_credential_error(self, error):
|
def raise_credential_error(self, error):
|
||||||
raise self.partial_credential_error(error=error)
|
raise self.partial_credential_error(error=error)
|
||||||
|
|
||||||
|
def _set_partial_credential_error(self, username, ip, request):
|
||||||
|
self.partial_credential_error = partial(errors.CredentialError, username=username, ip=ip, request=request)
|
||||||
|
|
||||||
def get_auth_data(self, decrypt_passwd=False):
|
def get_auth_data(self, decrypt_passwd=False):
|
||||||
request = self.request
|
request = self.request
|
||||||
if hasattr(request, 'data'):
|
if hasattr(request, 'data'):
|
||||||
@ -150,7 +160,7 @@ class AuthMixin:
|
|||||||
username, password, challenge, public_key, auto_login = bulk_get(data, *items, default='')
|
username, password, challenge, public_key, auto_login = bulk_get(data, *items, default='')
|
||||||
password = password + challenge.strip()
|
password = password + challenge.strip()
|
||||||
ip = self.get_request_ip()
|
ip = self.get_request_ip()
|
||||||
self.partial_credential_error = partial(errors.CredentialError, username=username, ip=ip, request=request)
|
self._set_partial_credential_error(username=username, ip=ip, request=request)
|
||||||
|
|
||||||
if decrypt_passwd:
|
if decrypt_passwd:
|
||||||
password = self.decrypt_passwd(password)
|
password = self.decrypt_passwd(password)
|
||||||
@ -183,6 +193,21 @@ class AuthMixin:
|
|||||||
if not is_allowed:
|
if not is_allowed:
|
||||||
raise errors.LoginIPNotAllowed(username=user.username, request=self.request)
|
raise errors.LoginIPNotAllowed(username=user.username, request=self.request)
|
||||||
|
|
||||||
|
def set_login_failed_mark(self):
|
||||||
|
ip = self.get_request_ip()
|
||||||
|
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
|
||||||
|
|
||||||
|
def set_passwd_verify_on_session(self, user: User):
|
||||||
|
self.request.session['user_id'] = str(user.id)
|
||||||
|
self.request.session['auth_password'] = 1
|
||||||
|
self.request.session['auth_password_expired_at'] = time.time() + settings.AUTH_EXPIRED_SECONDS
|
||||||
|
|
||||||
|
def check_is_need_captcha(self):
|
||||||
|
# 最近有登录失败时需要填写验证码
|
||||||
|
ip = get_request_ip(self.request)
|
||||||
|
need = cache.get(self.key_prefix_captcha.format(ip))
|
||||||
|
return need
|
||||||
|
|
||||||
def check_user_auth(self, decrypt_passwd=False):
|
def check_user_auth(self, decrypt_passwd=False):
|
||||||
self.check_is_block()
|
self.check_is_block()
|
||||||
request = self.request
|
request = self.request
|
||||||
@ -194,6 +219,7 @@ class AuthMixin:
|
|||||||
self._check_login_acl(user, ip)
|
self._check_login_acl(user, ip)
|
||||||
self._check_password_require_reset_or_not(user)
|
self._check_password_require_reset_or_not(user)
|
||||||
self._check_passwd_is_too_simple(user, password)
|
self._check_passwd_is_too_simple(user, password)
|
||||||
|
self._check_passwd_need_update(user)
|
||||||
|
|
||||||
LoginBlockUtil(username, ip).clean_failed_count()
|
LoginBlockUtil(username, ip).clean_failed_count()
|
||||||
request.session['auth_password'] = 1
|
request.session['auth_password'] = 1
|
||||||
@ -202,34 +228,62 @@ class AuthMixin:
|
|||||||
request.session['auth_backend'] = getattr(user, 'backend', settings.AUTH_BACKEND_MODEL)
|
request.session['auth_backend'] = getattr(user, 'backend', settings.AUTH_BACKEND_MODEL)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
def _check_is_local_user(self, user: User):
|
||||||
|
if user.source != User.Source.local:
|
||||||
|
raise self.raise_credential_error(error=errors.only_local_users_are_allowed)
|
||||||
|
|
||||||
|
def check_oauth2_auth(self, user: User, auth_backend):
|
||||||
|
ip = self.get_request_ip()
|
||||||
|
request = self.request
|
||||||
|
|
||||||
|
self._set_partial_credential_error(user.username, ip, request)
|
||||||
|
self._check_is_local_user(user)
|
||||||
|
self._check_is_block(user.username)
|
||||||
|
self._check_login_acl(user, ip)
|
||||||
|
|
||||||
|
LoginBlockUtil(user.username, ip).clean_failed_count()
|
||||||
|
MFABlockUtils(user.username, ip).clean_failed_count()
|
||||||
|
|
||||||
|
request.session['auth_password'] = 1
|
||||||
|
request.session['user_id'] = str(user.id)
|
||||||
|
request.session['auth_backend'] = auth_backend
|
||||||
|
return user
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate_reset_password_url_with_flash_msg(cls, user: User, flash_view_name):
|
def generate_reset_password_url_with_flash_msg(cls, user, message):
|
||||||
reset_passwd_url = reverse('authentication:reset-password')
|
reset_passwd_url = reverse('authentication:reset-password')
|
||||||
query_str = urlencode({
|
query_str = urlencode({
|
||||||
'token': user.generate_reset_token()
|
'token': user.generate_reset_token()
|
||||||
})
|
})
|
||||||
reset_passwd_url = f'{reset_passwd_url}?{query_str}'
|
reset_passwd_url = f'{reset_passwd_url}?{query_str}'
|
||||||
|
|
||||||
flash_page_url = reverse(flash_view_name)
|
message_data = {
|
||||||
query_str = urlencode({
|
'title': _('Please change your password'),
|
||||||
'redirect_url': reset_passwd_url
|
'message': message,
|
||||||
})
|
'interval': 3,
|
||||||
return f'{flash_page_url}?{query_str}'
|
'redirect_url': reset_passwd_url,
|
||||||
|
}
|
||||||
|
return FlashMessageUtil.gen_message_url(message_data)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _check_passwd_is_too_simple(cls, user: User, password):
|
def _check_passwd_is_too_simple(cls, user: User, password):
|
||||||
if user.is_superuser and password == 'admin':
|
if user.is_superuser and password == 'admin':
|
||||||
url = cls.generate_reset_password_url_with_flash_msg(
|
message = _('Your password is too simple, please change it for security')
|
||||||
user, 'authentication:passwd-too-simple-flash-msg'
|
url = cls.generate_reset_password_url_with_flash_msg(user, message=message)
|
||||||
)
|
|
||||||
raise errors.PasswdTooSimple(url)
|
raise errors.PasswdTooSimple(url)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _check_passwd_need_update(cls, user: User):
|
||||||
|
if user.need_update_password:
|
||||||
|
message = _('You should to change your password before login')
|
||||||
|
url = cls.generate_reset_password_url_with_flash_msg(user, message)
|
||||||
|
raise errors.PasswdNeedUpdate(url)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _check_password_require_reset_or_not(cls, user: User):
|
def _check_password_require_reset_or_not(cls, user: User):
|
||||||
if user.password_has_expired:
|
if user.password_has_expired:
|
||||||
url = cls.generate_reset_password_url_with_flash_msg(
|
message = _('Your password has expired, please reset before logging in')
|
||||||
user, 'authentication:passwd-has-expired-flash-msg'
|
url = cls.generate_reset_password_url_with_flash_msg(user, message)
|
||||||
)
|
|
||||||
raise errors.PasswordRequireResetError(url)
|
raise errors.PasswordRequireResetError(url)
|
||||||
|
|
||||||
def check_user_auth_if_need(self, decrypt_passwd=False):
|
def check_user_auth_if_need(self, decrypt_passwd=False):
|
||||||
@ -345,3 +399,10 @@ class AuthMixin:
|
|||||||
sender=self.__class__, username=username,
|
sender=self.__class__, username=username,
|
||||||
request=self.request, reason=reason
|
request=self.request, reason=reason
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def redirect_to_guard_view(self):
|
||||||
|
guard_url = reverse('authentication:login-guard')
|
||||||
|
args = self.request.META.get('QUERY_STRING', '')
|
||||||
|
if args:
|
||||||
|
guard_url = "%s?%s" % (guard_url, args)
|
||||||
|
return redirect(guard_url)
|
||||||
|
@ -8,14 +8,16 @@ from users.models import User
|
|||||||
from assets.models import Asset, SystemUser, Gateway
|
from assets.models import Asset, SystemUser, Gateway
|
||||||
from applications.models import Application
|
from applications.models import Application
|
||||||
from users.serializers import UserProfileSerializer
|
from users.serializers import UserProfileSerializer
|
||||||
|
from assets.serializers import ProtocolsField
|
||||||
from perms.serializers.asset.permission import ActionsField
|
from perms.serializers.asset.permission import ActionsField
|
||||||
from .models import AccessKey, LoginConfirmSetting, SSOToken
|
from .models import AccessKey, LoginConfirmSetting
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer',
|
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer',
|
||||||
'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', 'SSOTokenSerializer',
|
'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', 'SSOTokenSerializer',
|
||||||
'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer', 'RDPFileSerializer'
|
'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer', 'RDPFileSerializer',
|
||||||
|
'PasswordVerifySerializer',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -30,6 +32,10 @@ class OtpVerifySerializer(serializers.Serializer):
|
|||||||
code = serializers.CharField(max_length=6, min_length=6)
|
code = serializers.CharField(max_length=6, min_length=6)
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordVerifySerializer(serializers.Serializer):
|
||||||
|
password = serializers.CharField()
|
||||||
|
|
||||||
|
|
||||||
class BearerTokenSerializer(serializers.Serializer):
|
class BearerTokenSerializer(serializers.Serializer):
|
||||||
username = serializers.CharField(allow_null=True, required=False, write_only=True)
|
username = serializers.CharField(allow_null=True, required=False, write_only=True)
|
||||||
password = serializers.CharField(write_only=True, allow_null=True,
|
password = serializers.CharField(write_only=True, allow_null=True,
|
||||||
@ -150,9 +156,11 @@ class ConnectionTokenUserSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
|
class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
|
||||||
|
protocols = ProtocolsField(label='Protocols', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Asset
|
model = Asset
|
||||||
fields = ['id', 'hostname', 'ip', 'port', 'org_id']
|
fields = ['id', 'hostname', 'ip', 'protocols', 'org_id']
|
||||||
|
|
||||||
|
|
||||||
class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer):
|
class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer):
|
||||||
|
@ -117,6 +117,15 @@
|
|||||||
float: right;
|
float: right;
|
||||||
margin: 10px 10px 0 0;
|
margin: 10px 10px 0 0;
|
||||||
}
|
}
|
||||||
|
.more-login-item {
|
||||||
|
border-right: 1px dashed #dedede;
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-login-item:last-child {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@ -182,10 +191,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{% if AUTH_OPENID or AUTH_CAS %}
|
{% if AUTH_OPENID or AUTH_CAS or AUTH_WECOM or AUTH_DINGTALK %}
|
||||||
<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" style="margin-right: 10px">{% trans "More login options" %}</b>
|
<b class="text-muted text-left" >{% trans "More login options" %}</b>
|
||||||
{% if AUTH_OPENID %}
|
{% if AUTH_OPENID %}
|
||||||
<a href="{% url 'authentication:openid:login' %}" class="more-login-item">
|
<a href="{% url 'authentication:openid:login' %}" class="more-login-item">
|
||||||
<i class="fa fa-openid"></i> {% trans 'OpenID' %}
|
<i class="fa fa-openid"></i> {% trans 'OpenID' %}
|
||||||
@ -196,6 +205,17 @@
|
|||||||
<i class="fa"><img src="{{ LOGIN_CAS_LOGO_URL }}" height="13" width="13"></i> {% trans 'CAS' %}
|
<i class="fa"><img src="{{ LOGIN_CAS_LOGO_URL }}" height="13" width="13"></i> {% trans 'CAS' %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if AUTH_WECOM %}
|
||||||
|
<a href="{% url 'authentication:wecom-qr-login' %}" class="more-login-item">
|
||||||
|
<i class="fa"><img src="{{ LOGIN_WECOM_LOGO_URL }}" height="13" width="13"></i> {% trans 'WeCom' %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if AUTH_DINGTALK %}
|
||||||
|
<a href="{% url 'authentication:dingtalk-qr-login' %}" class="more-login-item">
|
||||||
|
<i class="fa"><img src="{{ LOGIN_DINGTALK_LOGO_URL }}" height="13" width="13"></i> {% trans 'DingTalk' %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center" style="display: inline-block;">
|
<div class="text-center" style="display: inline-block;">
|
||||||
|
@ -14,10 +14,17 @@ router.register('connection-token', api.UserConnectionTokenViewSet, 'connection-
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# path('token/', api.UserToken.as_view(), name='user-token'),
|
# path('token/', api.UserToken.as_view(), name='user-token'),
|
||||||
|
path('wecom/qr/unbind/', api.WeComQRUnBindForUserApi.as_view(), name='wecom-qr-unbind'),
|
||||||
|
path('wecom/qr/unbind/<uuid:user_id>/', api.WeComQRUnBindForAdminApi.as_view(), name='wecom-qr-unbind-for-admin'),
|
||||||
|
|
||||||
|
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('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'),
|
||||||
path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'),
|
path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'),
|
||||||
|
path('password/verify/', api.UserPasswordVerifyApi.as_view(), name='user-password-verify'),
|
||||||
path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'),
|
path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'),
|
||||||
path('login-confirm-settings/<uuid:user_id>/', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update')
|
path('login-confirm-settings/<uuid:user_id>/', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update')
|
||||||
]
|
]
|
||||||
|
@ -18,14 +18,25 @@ urlpatterns = [
|
|||||||
|
|
||||||
# 原来在users中的
|
# 原来在users中的
|
||||||
path('password/forgot/', users_view.UserForgotPasswordView.as_view(), name='forgot-password'),
|
path('password/forgot/', users_view.UserForgotPasswordView.as_view(), name='forgot-password'),
|
||||||
path('password/forgot/sendmail-success/', users_view.UserForgotPasswordSendmailSuccessView.as_view(),
|
|
||||||
name='forgot-password-sendmail-success'),
|
|
||||||
path('password/reset/', users_view.UserResetPasswordView.as_view(), name='reset-password'),
|
path('password/reset/', users_view.UserResetPasswordView.as_view(), name='reset-password'),
|
||||||
path('password/too-simple-flash-msg/', views.FlashPasswdTooSimpleMsgView.as_view(), name='passwd-too-simple-flash-msg'),
|
|
||||||
path('password/has-expired-msg/', views.FlashPasswdHasExpiredMsgView.as_view(), name='passwd-has-expired-flash-msg'),
|
|
||||||
path('password/reset/success/', users_view.UserResetPasswordSuccessView.as_view(), name='reset-password-success'),
|
|
||||||
path('password/verify/', users_view.UserVerifyPasswordView.as_view(), name='user-verify-password'),
|
path('password/verify/', users_view.UserVerifyPasswordView.as_view(), name='user-verify-password'),
|
||||||
|
|
||||||
|
path('wecom/bind/success-flash-msg/', views.FlashWeComBindSucceedMsgView.as_view(), name='wecom-bind-success-flash-msg'),
|
||||||
|
path('wecom/bind/failed-flash-msg/', views.FlashWeComBindFailedMsgView.as_view(), name='wecom-bind-failed-flash-msg'),
|
||||||
|
path('wecom/bind/start/', views.WeComEnableStartView.as_view(), name='wecom-bind-start'),
|
||||||
|
path('wecom/qr/bind/', views.WeComQRBindView.as_view(), name='wecom-qr-bind'),
|
||||||
|
path('wecom/qr/login/', views.WeComQRLoginView.as_view(), name='wecom-qr-login'),
|
||||||
|
path('wecom/qr/bind/<uuid:user_id>/callback/', views.WeComQRBindCallbackView.as_view(), name='wecom-qr-bind-callback'),
|
||||||
|
path('wecom/qr/login/callback/', views.WeComQRLoginCallbackView.as_view(), name='wecom-qr-login-callback'),
|
||||||
|
|
||||||
|
path('dingtalk/bind/success-flash-msg/', views.FlashDingTalkBindSucceedMsgView.as_view(), name='dingtalk-bind-success-flash-msg'),
|
||||||
|
path('dingtalk/bind/failed-flash-msg/', views.FlashDingTalkBindFailedMsgView.as_view(), name='dingtalk-bind-failed-flash-msg'),
|
||||||
|
path('dingtalk/bind/start/', views.DingTalkEnableStartView.as_view(), name='dingtalk-bind-start'),
|
||||||
|
path('dingtalk/qr/bind/', views.DingTalkQRBindView.as_view(), name='dingtalk-qr-bind'),
|
||||||
|
path('dingtalk/qr/login/', views.DingTalkQRLoginView.as_view(), name='dingtalk-qr-login'),
|
||||||
|
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'),
|
||||||
|
|
||||||
# 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'),
|
||||||
|
@ -2,3 +2,5 @@
|
|||||||
#
|
#
|
||||||
from .login import *
|
from .login import *
|
||||||
from .mfa import *
|
from .mfa import *
|
||||||
|
from .wecom import *
|
||||||
|
from .dingtalk import *
|
||||||
|
243
apps/authentication/views/dingtalk.py
Normal file
243
apps/authentication/views/dingtalk.py
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
import urllib
|
||||||
|
|
||||||
|
from django.http.response import HttpResponseRedirect
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
from django.views.decorators.cache import never_cache
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
from django.views import View
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http.request import HttpRequest
|
||||||
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||||
|
|
||||||
|
from users.views import UserVerifyPasswordView
|
||||||
|
from users.utils import is_auth_password_time_valid
|
||||||
|
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.message.backends.dingtalk import URL
|
||||||
|
from common.mixins.views import PermissionsMixin
|
||||||
|
from authentication import errors
|
||||||
|
from authentication.mixins import AuthMixin
|
||||||
|
from common.message.backends.dingtalk import DingTalk
|
||||||
|
|
||||||
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
DINGTALK_STATE_SESSION_KEY = '_dingtalk_state'
|
||||||
|
|
||||||
|
|
||||||
|
class DingTalkQRMixin(PermissionsMixin, View):
|
||||||
|
def verify_state(self):
|
||||||
|
state = self.request.GET.get('state')
|
||||||
|
session_state = self.request.session.get(DINGTALK_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[DINGTALK_STATE_SESSION_KEY] = state
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'appid': settings.DINGTALK_APPKEY,
|
||||||
|
'response_type': 'code',
|
||||||
|
'scope': 'snsapi_login',
|
||||||
|
'state': state,
|
||||||
|
'redirect_uri': redirect_uri,
|
||||||
|
}
|
||||||
|
url = URL.QR_CONNECT + '?' + urllib.parse.urlencode(params)
|
||||||
|
return url
|
||||||
|
|
||||||
|
def get_success_reponse(self, redirect_url, title, msg):
|
||||||
|
ok_flash_msg_url = reverse('authentication:dingtalk-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:dingtalk-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 = _('DingTalk is already bound')
|
||||||
|
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class DingTalkQRBindView(DingTalkQRMixin, 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:dingtalk-qr-bind-callback', kwargs={'user_id': user.id}, external=True)
|
||||||
|
redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url})
|
||||||
|
|
||||||
|
url = self.get_qr_url(redirect_uri)
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
|
||||||
|
class DingTalkQRBindCallbackView(DingTalkQRMixin, View):
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
|
||||||
|
def get(self, request: HttpRequest, user_id):
|
||||||
|
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 = get_object_or_none(User, id=user_id)
|
||||||
|
if user is None:
|
||||||
|
logger.error(f'DingTalkQR bind callback error, user_id invalid: user_id={user_id}')
|
||||||
|
msg = _('Invalid user_id')
|
||||||
|
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
if user.dingtalk_id:
|
||||||
|
response = self.get_already_bound_response(redirect_url)
|
||||||
|
return response
|
||||||
|
|
||||||
|
dingtalk = DingTalk(
|
||||||
|
appid=settings.DINGTALK_APPKEY,
|
||||||
|
appsecret=settings.DINGTALK_APPSECRET,
|
||||||
|
agentid=settings.DINGTALK_AGENTID
|
||||||
|
)
|
||||||
|
userid = dingtalk.get_userid_by_code(code)
|
||||||
|
|
||||||
|
if not userid:
|
||||||
|
msg = _('DingTalk query user failed')
|
||||||
|
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
user.dingtalk_id = userid
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
msg = _('Binding DingTalk successfully')
|
||||||
|
response = self.get_success_reponse(redirect_url, msg, msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class DingTalkEnableStartView(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:dingtalk-qr-bind')
|
||||||
|
|
||||||
|
success_url += '?' + urllib.parse.urlencode({
|
||||||
|
'redirect_url': redirect_url or referer
|
||||||
|
})
|
||||||
|
|
||||||
|
return success_url
|
||||||
|
|
||||||
|
|
||||||
|
class DingTalkQRLoginView(DingTalkQRMixin, View):
|
||||||
|
permission_classes = (AllowAny,)
|
||||||
|
|
||||||
|
def get(self, request: HttpRequest):
|
||||||
|
redirect_url = request.GET.get('redirect_url')
|
||||||
|
|
||||||
|
redirect_uri = reverse('authentication:dingtalk-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 DingTalkQRLoginCallbackView(AuthMixin, DingTalkQRMixin, 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)
|
||||||
|
|
||||||
|
dingtalk = DingTalk(
|
||||||
|
appid=settings.DINGTALK_APPKEY,
|
||||||
|
appsecret=settings.DINGTALK_APPSECRET,
|
||||||
|
agentid=settings.DINGTALK_AGENTID
|
||||||
|
)
|
||||||
|
userid = dingtalk.get_userid_by_code(code)
|
||||||
|
if not userid:
|
||||||
|
# 正常流程不会出这个错误,hack 行为
|
||||||
|
msg = _('Failed to get user from DingTalk')
|
||||||
|
response = self.get_failed_reponse(login_url, title=msg, msg=msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
user = get_object_or_none(User, dingtalk_id=userid)
|
||||||
|
if user is None:
|
||||||
|
title = _('DingTalk is not bound')
|
||||||
|
msg = _('Please login with a password and then bind the WoCom')
|
||||||
|
response = self.get_failed_reponse(login_url, title=title, msg=msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.check_oauth2_auth(user, settings.AUTH_BACKEND_DINGTALK)
|
||||||
|
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 FlashDingTalkBindSucceedMsgView(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 DingTalk successfully'),
|
||||||
|
'messages': msg or _('Binding DingTalk 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 FlashDingTalkBindFailedMsgView(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 DingTalk failed'),
|
||||||
|
'messages': msg or _('Binding DingTalk failed'),
|
||||||
|
'interval': 5,
|
||||||
|
'redirect_url': request.GET.get('redirect_url'),
|
||||||
|
'auto_redirect': True,
|
||||||
|
}
|
||||||
|
return self.render_to_response(context)
|
@ -4,7 +4,6 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import os
|
import os
|
||||||
import datetime
|
import datetime
|
||||||
from django.core.cache import cache
|
|
||||||
from django.contrib.auth import login as auth_login, logout as auth_logout
|
from django.contrib.auth import login as auth_login, logout as auth_logout
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import reverse, redirect
|
from django.shortcuts import reverse, redirect
|
||||||
@ -19,7 +18,7 @@ from django.conf import settings
|
|||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.contrib.auth import BACKEND_SESSION_KEY
|
from django.contrib.auth import BACKEND_SESSION_KEY
|
||||||
|
|
||||||
from common.utils import get_request_ip, get_object_or_none
|
from common.utils import get_request_ip, FlashMessageUtil
|
||||||
from users.utils import (
|
from users.utils import (
|
||||||
redirect_user_first_login_or_index
|
redirect_user_first_login_or_index
|
||||||
)
|
)
|
||||||
@ -31,7 +30,6 @@ from ..forms import get_user_login_form_cls
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
'UserLoginView', 'UserLogoutView',
|
'UserLoginView', 'UserLogoutView',
|
||||||
'UserLoginGuardView', 'UserLoginWaitConfirmView',
|
'UserLoginGuardView', 'UserLoginWaitConfirmView',
|
||||||
'FlashPasswdTooSimpleMsgView', 'FlashPasswdHasExpiredMsgView'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -39,16 +37,45 @@ __all__ = [
|
|||||||
@method_decorator(csrf_protect, name='dispatch')
|
@method_decorator(csrf_protect, name='dispatch')
|
||||||
@method_decorator(never_cache, name='dispatch')
|
@method_decorator(never_cache, name='dispatch')
|
||||||
class UserLoginView(mixins.AuthMixin, FormView):
|
class UserLoginView(mixins.AuthMixin, FormView):
|
||||||
key_prefix_captcha = "_LOGIN_INVALID_{}"
|
|
||||||
redirect_field_name = 'next'
|
redirect_field_name = 'next'
|
||||||
template_name = 'authentication/login.html'
|
template_name = 'authentication/login.html'
|
||||||
|
|
||||||
|
def redirect_third_party_auth_if_need(self, request):
|
||||||
|
# show jumpserver login page if request http://{JUMP-SERVER}/?admin=1
|
||||||
|
if self.request.GET.get("admin", 0):
|
||||||
|
return None
|
||||||
|
auth_type = ''
|
||||||
|
auth_url = ''
|
||||||
|
if settings.AUTH_OPENID:
|
||||||
|
auth_type = 'OIDC'
|
||||||
|
auth_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME)
|
||||||
|
elif settings.AUTH_CAS:
|
||||||
|
auth_type = 'CAS'
|
||||||
|
auth_url = reverse(settings.CAS_LOGIN_URL_NAME)
|
||||||
|
if not auth_url:
|
||||||
|
return None
|
||||||
|
|
||||||
|
message_data = {
|
||||||
|
'title': _('Redirecting'),
|
||||||
|
'message': _("Redirecting to {} authentication").format(auth_type),
|
||||||
|
'redirect_url': auth_url,
|
||||||
|
'has_cancel': True,
|
||||||
|
'cancel_url': reverse('authentication:login') + '?admin=1'
|
||||||
|
}
|
||||||
|
redirect_url = FlashMessageUtil.gen_message_url(message_data)
|
||||||
|
query_string = request.GET.urlencode()
|
||||||
|
redirect_url = "{}&{}".format(redirect_url, query_string)
|
||||||
|
return redirect_url
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if request.user.is_staff:
|
if request.user.is_staff:
|
||||||
first_login_url = redirect_user_first_login_or_index(
|
first_login_url = redirect_user_first_login_or_index(
|
||||||
request, self.redirect_field_name
|
request, self.redirect_field_name
|
||||||
)
|
)
|
||||||
return redirect(first_login_url)
|
return redirect(first_login_url)
|
||||||
|
redirect_url = self.redirect_third_party_auth_if_need(request)
|
||||||
|
if redirect_url:
|
||||||
|
return redirect(redirect_url)
|
||||||
request.session.set_test_cookie()
|
request.session.set_test_cookie()
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
@ -61,31 +88,22 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
|||||||
try:
|
try:
|
||||||
self.check_user_auth(decrypt_passwd=True)
|
self.check_user_auth(decrypt_passwd=True)
|
||||||
except errors.AuthFailedError as e:
|
except errors.AuthFailedError as e:
|
||||||
e = self.check_is_block(raise_exception=False) or e
|
|
||||||
form.add_error(None, e.msg)
|
form.add_error(None, e.msg)
|
||||||
ip = self.get_request_ip()
|
self.set_login_failed_mark()
|
||||||
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
|
|
||||||
form_cls = get_user_login_form_cls(captcha=True)
|
form_cls = get_user_login_form_cls(captcha=True)
|
||||||
new_form = form_cls(data=form.data)
|
new_form = form_cls(data=form.data)
|
||||||
new_form._errors = form.errors
|
new_form._errors = form.errors
|
||||||
context = self.get_context_data(form=new_form)
|
context = self.get_context_data(form=new_form)
|
||||||
self.request.session.set_test_cookie()
|
self.request.session.set_test_cookie()
|
||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
except (errors.PasswdTooSimple, errors.PasswordRequireResetError) as e:
|
except (errors.PasswdTooSimple, errors.PasswordRequireResetError, errors.PasswdNeedUpdate) as e:
|
||||||
return redirect(e.url)
|
return redirect(e.url)
|
||||||
self.clear_rsa_key()
|
self.clear_rsa_key()
|
||||||
return self.redirect_to_guard_view()
|
return self.redirect_to_guard_view()
|
||||||
|
|
||||||
def redirect_to_guard_view(self):
|
|
||||||
guard_url = reverse('authentication:login-guard')
|
|
||||||
args = self.request.META.get('QUERY_STRING', '')
|
|
||||||
if args:
|
|
||||||
guard_url = "%s?%s" % (guard_url, args)
|
|
||||||
return redirect(guard_url)
|
|
||||||
|
|
||||||
def get_form_class(self):
|
def get_form_class(self):
|
||||||
ip = get_request_ip(self.request)
|
if self.check_is_need_captcha():
|
||||||
if cache.get(self.key_prefix_captcha.format(ip)):
|
|
||||||
return get_user_login_form_cls(captcha=True)
|
return get_user_login_form_cls(captcha=True)
|
||||||
else:
|
else:
|
||||||
return get_user_login_form_cls()
|
return get_user_login_form_cls()
|
||||||
@ -113,6 +131,8 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
|||||||
'demo_mode': os.environ.get("DEMO_MODE"),
|
'demo_mode': os.environ.get("DEMO_MODE"),
|
||||||
'AUTH_OPENID': settings.AUTH_OPENID,
|
'AUTH_OPENID': settings.AUTH_OPENID,
|
||||||
'AUTH_CAS': settings.AUTH_CAS,
|
'AUTH_CAS': settings.AUTH_CAS,
|
||||||
|
'AUTH_WECOM': settings.AUTH_WECOM,
|
||||||
|
'AUTH_DINGTALK': settings.AUTH_DINGTALK,
|
||||||
'rsa_public_key': rsa_public_key,
|
'rsa_public_key': rsa_public_key,
|
||||||
'forgot_password_url': forgot_password_url
|
'forgot_password_url': forgot_password_url
|
||||||
}
|
}
|
||||||
@ -218,39 +238,9 @@ class UserLogoutView(TemplateView):
|
|||||||
context = {
|
context = {
|
||||||
'title': _('Logout success'),
|
'title': _('Logout success'),
|
||||||
'messages': _('Logout success, return login page'),
|
'messages': _('Logout success, return login page'),
|
||||||
'interval': 1,
|
'interval': 3,
|
||||||
'redirect_url': reverse('authentication:login'),
|
'redirect_url': reverse('authentication:login'),
|
||||||
'auto_redirect': True,
|
'auto_redirect': True,
|
||||||
}
|
}
|
||||||
kwargs.update(context)
|
kwargs.update(context)
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(never_cache, name='dispatch')
|
|
||||||
class FlashPasswdTooSimpleMsgView(TemplateView):
|
|
||||||
template_name = 'flash_message_standalone.html'
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
context = {
|
|
||||||
'title': _('Please change your password'),
|
|
||||||
'messages': _('Your password is too simple, please change it for security'),
|
|
||||||
'interval': 5,
|
|
||||||
'redirect_url': request.GET.get('redirect_url'),
|
|
||||||
'auto_redirect': True,
|
|
||||||
}
|
|
||||||
return self.render_to_response(context)
|
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(never_cache, name='dispatch')
|
|
||||||
class FlashPasswdHasExpiredMsgView(TemplateView):
|
|
||||||
template_name = 'flash_message_standalone.html'
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
context = {
|
|
||||||
'title': _('Please change your password'),
|
|
||||||
'messages': _('Your password has expired, please reset before logging in'),
|
|
||||||
'interval': 5,
|
|
||||||
'redirect_url': request.GET.get('redirect_url'),
|
|
||||||
'auto_redirect': True,
|
|
||||||
}
|
|
||||||
return self.render_to_response(context)
|
|
||||||
|
241
apps/authentication/views/wecom.py
Normal file
241
apps/authentication/views/wecom.py
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
import urllib
|
||||||
|
|
||||||
|
from django.http.response import HttpResponseRedirect
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
from django.views.decorators.cache import never_cache
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
from django.views import View
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http.request import HttpRequest
|
||||||
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||||
|
|
||||||
|
from users.views import UserVerifyPasswordView
|
||||||
|
from users.utils import is_auth_password_time_valid
|
||||||
|
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.message.backends.wecom import URL
|
||||||
|
from common.message.backends.wecom import WeCom
|
||||||
|
from common.mixins.views import PermissionsMixin
|
||||||
|
from authentication import errors
|
||||||
|
from authentication.mixins import AuthMixin
|
||||||
|
|
||||||
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
WECOM_STATE_SESSION_KEY = '_wecom_state'
|
||||||
|
|
||||||
|
|
||||||
|
class WeComQRMixin(PermissionsMixin, View):
|
||||||
|
def verify_state(self):
|
||||||
|
state = self.request.GET.get('state')
|
||||||
|
session_state = self.request.session.get(WECOM_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[WECOM_STATE_SESSION_KEY] = state
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'appid': settings.WECOM_CORPID,
|
||||||
|
'agentid': settings.WECOM_AGENTID,
|
||||||
|
'state': state,
|
||||||
|
'redirect_uri': redirect_uri,
|
||||||
|
}
|
||||||
|
url = URL.QR_CONNECT + '?' + urllib.parse.urlencode(params)
|
||||||
|
return url
|
||||||
|
|
||||||
|
def get_success_reponse(self, redirect_url, title, msg):
|
||||||
|
ok_flash_msg_url = reverse('authentication:wecom-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:wecom-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 = _('WeCom is already bound')
|
||||||
|
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class WeComQRBindView(WeComQRMixin, 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:wecom-qr-bind-callback', kwargs={'user_id': user.id}, external=True)
|
||||||
|
redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url})
|
||||||
|
|
||||||
|
url = self.get_qr_url(redirect_uri)
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
|
||||||
|
class WeComQRBindCallbackView(WeComQRMixin, View):
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
|
||||||
|
def get(self, request: HttpRequest, user_id):
|
||||||
|
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 = get_object_or_none(User, id=user_id)
|
||||||
|
if user is None:
|
||||||
|
logger.error(f'WeComQR bind callback error, user_id invalid: user_id={user_id}')
|
||||||
|
msg = _('Invalid user_id')
|
||||||
|
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
if user.wecom_id:
|
||||||
|
response = self.get_already_bound_response(redirect_url)
|
||||||
|
return response
|
||||||
|
|
||||||
|
wecom = WeCom(
|
||||||
|
corpid=settings.WECOM_CORPID,
|
||||||
|
corpsecret=settings.WECOM_SECRET,
|
||||||
|
agentid=settings.WECOM_AGENTID
|
||||||
|
)
|
||||||
|
wecom_userid, __ = wecom.get_user_id_by_code(code)
|
||||||
|
if not wecom_userid:
|
||||||
|
msg = _('WeCom query user failed')
|
||||||
|
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
user.wecom_id = wecom_userid
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
msg = _('Binding WeCom successfully')
|
||||||
|
response = self.get_success_reponse(redirect_url, msg, msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class WeComEnableStartView(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:wecom-qr-bind')
|
||||||
|
|
||||||
|
success_url += '?' + urllib.parse.urlencode({
|
||||||
|
'redirect_url': redirect_url or referer
|
||||||
|
})
|
||||||
|
|
||||||
|
return success_url
|
||||||
|
|
||||||
|
|
||||||
|
class WeComQRLoginView(WeComQRMixin, View):
|
||||||
|
permission_classes = (AllowAny,)
|
||||||
|
|
||||||
|
def get(self, request: HttpRequest):
|
||||||
|
redirect_url = request.GET.get('redirect_url')
|
||||||
|
|
||||||
|
redirect_uri = reverse('authentication:wecom-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 WeComQRLoginCallbackView(AuthMixin, WeComQRMixin, 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)
|
||||||
|
|
||||||
|
wecom = WeCom(
|
||||||
|
corpid=settings.WECOM_CORPID,
|
||||||
|
corpsecret=settings.WECOM_SECRET,
|
||||||
|
agentid=settings.WECOM_AGENTID
|
||||||
|
)
|
||||||
|
wecom_userid, __ = wecom.get_user_id_by_code(code)
|
||||||
|
if not wecom_userid:
|
||||||
|
# 正常流程不会出这个错误,hack 行为
|
||||||
|
msg = _('Failed to get user from WeCom')
|
||||||
|
response = self.get_failed_reponse(login_url, title=msg, msg=msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
user = get_object_or_none(User, wecom_id=wecom_userid)
|
||||||
|
if user is None:
|
||||||
|
title = _('WeCom is not bound')
|
||||||
|
msg = _('Please login with a password and then bind the WoCom')
|
||||||
|
response = self.get_failed_reponse(login_url, title=title, msg=msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.check_oauth2_auth(user, settings.AUTH_BACKEND_WECOM)
|
||||||
|
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 FlashWeComBindSucceedMsgView(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 WeCom successfully'),
|
||||||
|
'messages': msg or _('Binding WeCom 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 FlashWeComBindFailedMsgView(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 WeCom failed'),
|
||||||
|
'messages': msg or _('Binding WeCom failed'),
|
||||||
|
'interval': 5,
|
||||||
|
'redirect_url': request.GET.get('redirect_url'),
|
||||||
|
'auto_redirect': True,
|
||||||
|
}
|
||||||
|
return self.render_to_response(context)
|
@ -39,3 +39,9 @@ class ReferencedByOthers(JMSException):
|
|||||||
status_code = status.HTTP_400_BAD_REQUEST
|
status_code = status.HTTP_400_BAD_REQUEST
|
||||||
default_code = 'referenced_by_others'
|
default_code = 'referenced_by_others'
|
||||||
default_detail = _('Is referenced by other objects and cannot be deleted')
|
default_detail = _('Is referenced by other objects and cannot be deleted')
|
||||||
|
|
||||||
|
|
||||||
|
class MFAVerifyRequired(JMSException):
|
||||||
|
status_code = status.HTTP_400_BAD_REQUEST
|
||||||
|
default_code = 'mfa_verify_required'
|
||||||
|
default_detail = _('This action require verify your MFA')
|
||||||
|
19
apps/common/management/commands/expire_caches.py
Normal file
19
apps/common/management/commands/expire_caches.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from assets.signals_handler.node_assets_mapping import expire_node_assets_mapping_for_memory
|
||||||
|
from orgs.models import Organization
|
||||||
|
|
||||||
|
|
||||||
|
def expire_node_assets_mapping():
|
||||||
|
org_ids = Organization.objects.all().values_list('id', flat=True)
|
||||||
|
org_ids = [*org_ids, '00000000-0000-0000-0000-000000000000']
|
||||||
|
|
||||||
|
for org_id in org_ids:
|
||||||
|
expire_node_assets_mapping_for_memory(org_id)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Expire caches'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
expire_node_assets_mapping()
|
0
apps/common/message/__init__.py
Normal file
0
apps/common/message/__init__.py
Normal file
0
apps/common/message/backends/__init__.py
Normal file
0
apps/common/message/backends/__init__.py
Normal file
168
apps/common/message/backends/dingtalk/__init__.py
Normal file
168
apps/common/message/backends/dingtalk/__init__.py
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import time
|
||||||
|
import hmac
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from common.message.backends.utils import request
|
||||||
|
from common.message.backends.utils import digest
|
||||||
|
from common.message.backends.mixin import BaseRequest
|
||||||
|
|
||||||
|
|
||||||
|
def sign(secret, data):
|
||||||
|
|
||||||
|
digest = hmac.HMAC(
|
||||||
|
key=secret.encode('utf8'),
|
||||||
|
msg=data.encode('utf8'),
|
||||||
|
digestmod=hmac._hashlib.sha256).digest()
|
||||||
|
signature = base64.standard_b64encode(digest).decode('utf8')
|
||||||
|
# signature = urllib.parse.quote(signature, safe='')
|
||||||
|
# signature = signature.replace('+', '%20').replace('*', '%2A').replace('~', '%7E').replace('/', '%2F')
|
||||||
|
return signature
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorCode:
|
||||||
|
INVALID_TOKEN = 88
|
||||||
|
|
||||||
|
|
||||||
|
class URL:
|
||||||
|
QR_CONNECT = 'https://oapi.dingtalk.com/connect/qrconnect'
|
||||||
|
GET_USER_INFO_BY_CODE = 'https://oapi.dingtalk.com/sns/getuserinfo_bycode'
|
||||||
|
GET_TOKEN = 'https://oapi.dingtalk.com/gettoken'
|
||||||
|
SEND_MESSAGE_BY_TEMPLATE = 'https://oapi.dingtalk.com/topapi/message/corpconversation/sendbytemplate'
|
||||||
|
SEND_MESSAGE = 'https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2'
|
||||||
|
GET_SEND_MSG_PROGRESS = 'https://oapi.dingtalk.com/topapi/message/corpconversation/getsendprogress'
|
||||||
|
GET_USERID_BY_UNIONID = 'https://oapi.dingtalk.com/topapi/user/getbyunionid'
|
||||||
|
|
||||||
|
|
||||||
|
class DingTalkRequests(BaseRequest):
|
||||||
|
invalid_token_errcode = ErrorCode.INVALID_TOKEN
|
||||||
|
|
||||||
|
def __init__(self, appid, appsecret, agentid, timeout=None):
|
||||||
|
self._appid = appid
|
||||||
|
self._appsecret = appsecret
|
||||||
|
self._agentid = agentid
|
||||||
|
|
||||||
|
super().__init__(timeout=timeout)
|
||||||
|
|
||||||
|
def get_access_token_cache_key(self):
|
||||||
|
return digest(self._appid, self._appsecret)
|
||||||
|
|
||||||
|
def request_access_token(self):
|
||||||
|
# https://developers.dingtalk.com/document/app/obtain-orgapp-token?spm=ding_open_doc.document.0.0.3a256573JEWqIL#topic-1936350
|
||||||
|
params = {'appkey': self._appid, 'appsecret': self._appsecret}
|
||||||
|
data = self.raw_request('get', url=URL.GET_TOKEN, params=params)
|
||||||
|
|
||||||
|
access_token = data['access_token']
|
||||||
|
expires_in = data['expires_in']
|
||||||
|
return access_token, expires_in
|
||||||
|
|
||||||
|
@request
|
||||||
|
def get(self, url, params=None,
|
||||||
|
with_token=False, with_sign=False,
|
||||||
|
check_errcode_is_0=True,
|
||||||
|
**kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@request
|
||||||
|
def post(self, url, json=None, params=None,
|
||||||
|
with_token=False, with_sign=False,
|
||||||
|
check_errcode_is_0=True,
|
||||||
|
**kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _add_sign(self, params: dict):
|
||||||
|
timestamp = str(int(time.time() * 1000))
|
||||||
|
signature = sign(self._appsecret, timestamp)
|
||||||
|
accessKey = self._appid
|
||||||
|
|
||||||
|
params['timestamp'] = timestamp
|
||||||
|
params['signature'] = signature
|
||||||
|
params['accessKey'] = accessKey
|
||||||
|
|
||||||
|
def request(self, method, url, params=None,
|
||||||
|
with_token=False, with_sign=False,
|
||||||
|
check_errcode_is_0=True,
|
||||||
|
**kwargs):
|
||||||
|
if not isinstance(params, dict):
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
if with_token:
|
||||||
|
params['access_token'] = self.access_token
|
||||||
|
|
||||||
|
if with_sign:
|
||||||
|
self._add_sign(params)
|
||||||
|
|
||||||
|
data = self.raw_request(method, url, params=params, **kwargs)
|
||||||
|
if check_errcode_is_0:
|
||||||
|
self.check_errcode_is_0(data)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class DingTalk:
|
||||||
|
def __init__(self, appid, appsecret, agentid, timeout=None):
|
||||||
|
self._appid = appid
|
||||||
|
self._appsecret = appsecret
|
||||||
|
self._agentid = agentid
|
||||||
|
|
||||||
|
self._request = DingTalkRequests(
|
||||||
|
appid=appid, appsecret=appsecret, agentid=agentid,
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_userinfo_bycode(self, code):
|
||||||
|
# https://developers.dingtalk.com/document/app/obtain-the-user-information-based-on-the-sns-temporary-authorization?spm=ding_open_doc.document.0.0.3a256573y8Y7yg#topic-1995619
|
||||||
|
body = {
|
||||||
|
"tmp_auth_code": code
|
||||||
|
}
|
||||||
|
|
||||||
|
data = self._request.post(URL.GET_USER_INFO_BY_CODE, json=body, with_sign=True)
|
||||||
|
return data['user_info']
|
||||||
|
|
||||||
|
def get_userid_by_code(self, code):
|
||||||
|
user_info = self.get_userinfo_bycode(code)
|
||||||
|
unionid = user_info['unionid']
|
||||||
|
userid = self.get_userid_by_unionid(unionid)
|
||||||
|
return userid
|
||||||
|
|
||||||
|
def get_userid_by_unionid(self, unionid):
|
||||||
|
body = {
|
||||||
|
'unionid': unionid
|
||||||
|
}
|
||||||
|
data = self._request.post(URL.GET_USERID_BY_UNIONID, json=body, with_token=True)
|
||||||
|
userid = data['result']['userid']
|
||||||
|
return userid
|
||||||
|
|
||||||
|
def send_by_template(self, template_id, user_ids, dept_ids, data):
|
||||||
|
body = {
|
||||||
|
'agent_id': self._agentid,
|
||||||
|
'template_id': template_id,
|
||||||
|
'userid_list': ','.join(user_ids),
|
||||||
|
'dept_id_list': ','.join(dept_ids),
|
||||||
|
'data': data
|
||||||
|
}
|
||||||
|
data = self._request.post(URL.SEND_MESSAGE_BY_TEMPLATE, json=body, with_token=True)
|
||||||
|
|
||||||
|
def send_text(self, user_ids, msg):
|
||||||
|
body = {
|
||||||
|
'agent_id': self._agentid,
|
||||||
|
'userid_list': ','.join(user_ids),
|
||||||
|
# 'dept_id_list': '',
|
||||||
|
'to_all_user': False,
|
||||||
|
'msg': {
|
||||||
|
'msgtype': 'text',
|
||||||
|
'text': {
|
||||||
|
'content': msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data = self._request.post(URL.SEND_MESSAGE, json=body, with_token=True)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_send_msg_progress(self, task_id):
|
||||||
|
body = {
|
||||||
|
'agent_id': self._agentid,
|
||||||
|
'task_id': task_id
|
||||||
|
}
|
||||||
|
|
||||||
|
data = self._request.post(URL.GET_SEND_MSG_PROGRESS, json=body, with_token=True)
|
||||||
|
return data
|
28
apps/common/message/backends/exceptions.py
Normal file
28
apps/common/message/backends/exceptions.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from rest_framework.exceptions import APIException
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPNot200(APIException):
|
||||||
|
default_code = 'http_not_200'
|
||||||
|
default_detail = 'HTTP status is not 200'
|
||||||
|
|
||||||
|
|
||||||
|
class ErrCodeNot0(APIException):
|
||||||
|
default_code = 'errcode_not_0'
|
||||||
|
default_detail = 'Error code is not 0'
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseDataKeyError(APIException):
|
||||||
|
default_code = 'response_data_key_error'
|
||||||
|
default_detail = 'Response data key error'
|
||||||
|
|
||||||
|
|
||||||
|
class NetError(APIException):
|
||||||
|
default_code = 'net_error'
|
||||||
|
default_detail = _('Network error, please contact system administrator')
|
||||||
|
|
||||||
|
|
||||||
|
class AccessTokenError(APIException):
|
||||||
|
default_code = 'access_token_error'
|
||||||
|
default_detail = 'Access token error, check config'
|
94
apps/common/message/backends/mixin.py
Normal file
94
apps/common/message/backends/mixin.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import requests
|
||||||
|
from requests import exceptions as req_exce
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
from .utils import DictWrapper
|
||||||
|
from common.utils.common import get_logger
|
||||||
|
from common.utils import lazyproperty
|
||||||
|
from common.message.backends.utils import set_default
|
||||||
|
|
||||||
|
from . import exceptions as exce
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestMixin:
|
||||||
|
def check_errcode_is_0(self, data: DictWrapper):
|
||||||
|
errcode = data['errcode']
|
||||||
|
if errcode != 0:
|
||||||
|
# 如果代码写的对,配置没问题,这里不该出错,系统性错误,直接抛异常
|
||||||
|
errmsg = data['errmsg']
|
||||||
|
logger.error(f'Response 200 but errcode is not 0: '
|
||||||
|
f'errcode={errcode} '
|
||||||
|
f'errmsg={errmsg} ')
|
||||||
|
raise exce.ErrCodeNot0(detail=str(data.raw_data))
|
||||||
|
|
||||||
|
def check_http_is_200(self, response):
|
||||||
|
if response.status_code != 200:
|
||||||
|
# 正常情况下不会返回非 200 响应码
|
||||||
|
logger.error(f'Response error: '
|
||||||
|
f'status_code={response.status_code} '
|
||||||
|
f'url={response.url}'
|
||||||
|
f'\ncontent={response.content}')
|
||||||
|
raise exce.HTTPNot200
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_access_token_cache_key(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def is_token_invalid(self, data):
|
||||||
|
errcode = data['errcode']
|
||||||
|
if errcode == self.invalid_token_errcode:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def access_token_cache_key(self):
|
||||||
|
return self.get_access_token_cache_key()
|
||||||
|
|
||||||
|
def init_access_token(self):
|
||||||
|
access_token = cache.get(self.access_token_cache_key)
|
||||||
|
if access_token:
|
||||||
|
self.access_token = access_token
|
||||||
|
return
|
||||||
|
self.refresh_access_token()
|
||||||
|
|
||||||
|
def refresh_access_token(self):
|
||||||
|
access_token, expires_in = self.request_access_token()
|
||||||
|
self.access_token = access_token
|
||||||
|
cache.set(self.access_token_cache_key, access_token, expires_in)
|
||||||
|
|
||||||
|
def raw_request(self, method, url, **kwargs):
|
||||||
|
set_default(kwargs, self._request_kwargs)
|
||||||
|
raw_data = ''
|
||||||
|
for i in range(3):
|
||||||
|
# 循环为了防止 access_token 失效
|
||||||
|
try:
|
||||||
|
response = getattr(requests, method)(url, **kwargs)
|
||||||
|
self.check_http_is_200(response)
|
||||||
|
raw_data = response.json()
|
||||||
|
data = DictWrapper(raw_data)
|
||||||
|
|
||||||
|
if self.is_token_invalid(data):
|
||||||
|
self.refresh_access_token()
|
||||||
|
continue
|
||||||
|
|
||||||
|
return data
|
||||||
|
except req_exce.ReadTimeout as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise exce.NetError
|
||||||
|
logger.error(f'Get access_token error, check config: url={url} data={raw_data}')
|
||||||
|
raise PermissionDenied(raw_data)
|
78
apps/common/message/backends/utils.py
Normal file
78
apps/common/message/backends/utils.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import hashlib
|
||||||
|
import inspect
|
||||||
|
from inspect import Parameter
|
||||||
|
|
||||||
|
from common.utils.common import get_logger
|
||||||
|
from common.message.backends import exceptions as exce
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def digest(corpid, corpsecret):
|
||||||
|
md5 = hashlib.md5()
|
||||||
|
md5.update(corpid.encode())
|
||||||
|
md5.update(corpsecret.encode())
|
||||||
|
digest = md5.hexdigest()
|
||||||
|
return digest
|
||||||
|
|
||||||
|
|
||||||
|
def update_values(default: dict, others: dict):
|
||||||
|
for key in default.keys():
|
||||||
|
if key in others:
|
||||||
|
default[key] = others[key]
|
||||||
|
|
||||||
|
|
||||||
|
def set_default(data: dict, default: dict):
|
||||||
|
for key in default.keys():
|
||||||
|
if key not in data:
|
||||||
|
data[key] = default[key]
|
||||||
|
|
||||||
|
|
||||||
|
class DictWrapper:
|
||||||
|
def __init__(self, data:dict):
|
||||||
|
self.raw_data = data
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
# 网络请求返回的数据,不能完全信任,所以字典操作包在异常里
|
||||||
|
try:
|
||||||
|
return self.raw_data[item]
|
||||||
|
except KeyError as e:
|
||||||
|
msg = f'Response 200 but get field from json error: error={e} data={self.raw_data}'
|
||||||
|
logger.error(msg)
|
||||||
|
raise exce.ResponseDataKeyError(detail=msg)
|
||||||
|
|
||||||
|
def __getattr__(self, item):
|
||||||
|
return getattr(self.raw_data, item)
|
||||||
|
|
||||||
|
def __contains__(self, item):
|
||||||
|
return item in self.raw_data
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.raw_data)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self.raw_data)
|
||||||
|
|
||||||
|
|
||||||
|
def request(func):
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
signature = inspect.signature(func)
|
||||||
|
bound_args = signature.bind(*args, **kwargs)
|
||||||
|
bound_args.apply_defaults()
|
||||||
|
|
||||||
|
arguments = bound_args.arguments
|
||||||
|
self = arguments['self']
|
||||||
|
request_method = func.__name__
|
||||||
|
|
||||||
|
parameters = {}
|
||||||
|
for k, v in signature.parameters.items():
|
||||||
|
if k == 'self':
|
||||||
|
continue
|
||||||
|
if v.kind is Parameter.VAR_KEYWORD:
|
||||||
|
parameters.update(arguments[k])
|
||||||
|
continue
|
||||||
|
parameters[k] = arguments[k]
|
||||||
|
|
||||||
|
response = self.request(request_method, **parameters)
|
||||||
|
return response
|
||||||
|
return inner
|
194
apps/common/message/backends/wecom/__init__.py
Normal file
194
apps/common/message/backends/wecom/__init__.py
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
from typing import Iterable, AnyStr
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
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.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
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class WeComError(APIException):
|
||||||
|
default_code = 'wecom_error'
|
||||||
|
default_detail = _('WeCom error, please contact system administrator')
|
||||||
|
|
||||||
|
|
||||||
|
class URL:
|
||||||
|
GET_TOKEN = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken'
|
||||||
|
SEND_MESSAGE = 'https://qyapi.weixin.qq.com/cgi-bin/message/send'
|
||||||
|
QR_CONNECT = 'https://open.work.weixin.qq.com/wwopen/sso/qrConnect'
|
||||||
|
|
||||||
|
# https://open.work.weixin.qq.com/api/doc/90000/90135/91437
|
||||||
|
GET_USER_ID_BY_CODE = 'https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo'
|
||||||
|
GET_USER_DETAIL = 'https://qyapi.weixin.qq.com/cgi-bin/user/get'
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorCode:
|
||||||
|
# https://open.work.weixin.qq.com/api/doc/90000/90139/90313#%E9%94%99%E8%AF%AF%E7%A0%81%EF%BC%9A81013
|
||||||
|
RECIPIENTS_INVALID = 81013 # UserID、部门ID、标签ID全部非法或无权限。
|
||||||
|
|
||||||
|
# https://open.work.weixin.qq.com/api/doc/90000/90135/91437
|
||||||
|
INVALID_CODE = 40029
|
||||||
|
|
||||||
|
INVALID_TOKEN = 40014 # 无效的 access_token
|
||||||
|
|
||||||
|
|
||||||
|
class WeComRequests(BaseRequest):
|
||||||
|
"""
|
||||||
|
处理系统级错误,抛出 API 异常,直接生成 HTTP 响应,业务代码无需关心这些错误
|
||||||
|
- 确保 status_code == 200
|
||||||
|
- 确保 access_token 无效时重试
|
||||||
|
"""
|
||||||
|
invalid_token_errcode = ErrorCode.INVALID_TOKEN
|
||||||
|
|
||||||
|
def __init__(self, corpid, corpsecret, agentid, timeout=None):
|
||||||
|
self._corpid = corpid
|
||||||
|
self._corpsecret = corpsecret
|
||||||
|
self._agentid = agentid
|
||||||
|
|
||||||
|
super().__init__(timeout=timeout)
|
||||||
|
|
||||||
|
def get_access_token_cache_key(self):
|
||||||
|
return digest(self._corpid, self._corpsecret)
|
||||||
|
|
||||||
|
def request_access_token(self):
|
||||||
|
params = {'corpid': self._corpid, 'corpsecret': self._corpsecret}
|
||||||
|
data = self.raw_request('get', url=URL.GET_TOKEN, params=params)
|
||||||
|
|
||||||
|
access_token = data['access_token']
|
||||||
|
expires_in = data['expires_in']
|
||||||
|
return access_token, expires_in
|
||||||
|
|
||||||
|
@request
|
||||||
|
def get(self, url, params=None, with_token=True,
|
||||||
|
check_errcode_is_0=True, **kwargs):
|
||||||
|
# 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 = {}
|
||||||
|
|
||||||
|
if with_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):
|
||||||
|
"""
|
||||||
|
非业务数据导致的错误直接抛异常,说明是系统配置错误,业务代码不用理会
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, corpid, corpsecret, agentid, timeout=None):
|
||||||
|
self._corpid = corpid
|
||||||
|
self._corpsecret = corpsecret
|
||||||
|
self._agentid = agentid
|
||||||
|
|
||||||
|
self._requests = WeComRequests(
|
||||||
|
corpid=corpid,
|
||||||
|
corpsecret=corpsecret,
|
||||||
|
agentid=agentid,
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_text(self, users: Iterable, msg: AnyStr, **kwargs):
|
||||||
|
"""
|
||||||
|
https://open.work.weixin.qq.com/api/doc/90000/90135/90236
|
||||||
|
|
||||||
|
对于业务代码,只需要关心由 用户id 或 消息不对 导致的错误,其他错误不予理会
|
||||||
|
"""
|
||||||
|
users = tuple(users)
|
||||||
|
|
||||||
|
extra_params = {
|
||||||
|
"safe": 0,
|
||||||
|
"enable_id_trans": 0,
|
||||||
|
"enable_duplicate_check": 0,
|
||||||
|
"duplicate_check_interval": 1800
|
||||||
|
}
|
||||||
|
update_values(extra_params, kwargs)
|
||||||
|
|
||||||
|
body = {
|
||||||
|
"touser": '|'.join(users),
|
||||||
|
"msgtype": "text",
|
||||||
|
"agentid": self._agentid,
|
||||||
|
"text": {
|
||||||
|
"content": msg
|
||||||
|
},
|
||||||
|
**extra_params
|
||||||
|
}
|
||||||
|
data = self._requests.post(URL.SEND_MESSAGE, json=body, check_errcode_is_0=False)
|
||||||
|
|
||||||
|
errcode = data['errcode']
|
||||||
|
if errcode == ErrorCode.RECIPIENTS_INVALID:
|
||||||
|
# 全部接收人无权限或不存在
|
||||||
|
return users
|
||||||
|
self.check_errcode_is_0(data)
|
||||||
|
|
||||||
|
invaliduser = data['invaliduser']
|
||||||
|
if not invaliduser:
|
||||||
|
return ()
|
||||||
|
|
||||||
|
if isinstance(invaliduser, str):
|
||||||
|
logger.error(f'WeCom send text 200, but invaliduser is not str: invaliduser={invaliduser}')
|
||||||
|
raise WeComError
|
||||||
|
|
||||||
|
invalid_users = invaliduser.split('|')
|
||||||
|
return invalid_users
|
||||||
|
|
||||||
|
def get_user_id_by_code(self, code):
|
||||||
|
# # https://open.work.weixin.qq.com/api/doc/90000/90135/91437
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'code': code,
|
||||||
|
}
|
||||||
|
data = self._requests.get(URL.GET_USER_ID_BY_CODE, params=params, check_errcode_is_0=False)
|
||||||
|
|
||||||
|
errcode = data['errcode']
|
||||||
|
if errcode == ErrorCode.INVALID_CODE:
|
||||||
|
logger.warn(f'WeCom get_user_id_by_code invalid code: code={code}')
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
self.check_errcode_is_0(data)
|
||||||
|
|
||||||
|
USER_ID = 'UserId'
|
||||||
|
OPEN_ID = 'OpenId'
|
||||||
|
|
||||||
|
if USER_ID in data:
|
||||||
|
return data[USER_ID], USER_ID
|
||||||
|
elif OPEN_ID in data:
|
||||||
|
return data[OPEN_ID], OPEN_ID
|
||||||
|
else:
|
||||||
|
logger.error(f'WeCom response 200 but get field from json error: fields=UserId|OpenId')
|
||||||
|
raise WeComError
|
||||||
|
|
||||||
|
def get_user_detail(self, id):
|
||||||
|
# https://open.work.weixin.qq.com/api/doc/90000/90135/90196
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'userid': id,
|
||||||
|
}
|
||||||
|
|
||||||
|
data = self._requests.get(URL.GET_USER_DETAIL, params)
|
||||||
|
return data
|
@ -6,10 +6,12 @@ from threading import Thread
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.db.models.signals import m2m_changed
|
from django.db.models.signals import m2m_changed
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
@ -25,6 +27,9 @@ __all__ = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
UserModel = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class JSONResponseMixin(object):
|
class JSONResponseMixin(object):
|
||||||
"""JSON mixin"""
|
"""JSON mixin"""
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -332,3 +337,21 @@ class AllowBulkDestoryMixin:
|
|||||||
"""
|
"""
|
||||||
query = str(filtered.query)
|
query = str(filtered.query)
|
||||||
return '`id` IN (' in query or '`id` =' in query
|
return '`id` IN (' in query or '`id` =' in query
|
||||||
|
|
||||||
|
|
||||||
|
class RoleAdminMixin:
|
||||||
|
kwargs: dict
|
||||||
|
user_id_url_kwarg = 'pk'
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def user(self):
|
||||||
|
user_id = self.kwargs.get(self.user_id_url_kwarg)
|
||||||
|
return UserModel.objects.get(id=user_id)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleUserMixin:
|
||||||
|
request: Request
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def user(self):
|
||||||
|
return self.request.user
|
||||||
|
@ -5,7 +5,7 @@ from django.contrib.auth.mixins import UserPassesTestMixin
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["DatetimeSearchMixin"]
|
__all__ = ["DatetimeSearchMixin", "PermissionsMixin"]
|
||||||
|
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
import time
|
import time
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from common.exceptions import MFAVerifyRequired
|
||||||
|
|
||||||
from orgs.utils import current_org
|
from orgs.utils import current_org
|
||||||
|
|
||||||
@ -114,7 +115,7 @@ class NeedMFAVerify(permissions.BasePermission):
|
|||||||
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
|
||||||
return False
|
raise MFAVerifyRequired()
|
||||||
|
|
||||||
|
|
||||||
class CanUpdateDeleteUser(permissions.BasePermission):
|
class CanUpdateDeleteUser(permissions.BasePermission):
|
||||||
|
15
apps/common/request_log.py
Normal file
15
apps/common/request_log.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
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
|
10
apps/common/urls/view_urls.py
Normal file
10
apps/common/urls/view_urls.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from .. import views
|
||||||
|
|
||||||
|
app_name = 'common'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# login
|
||||||
|
path('flash-message/', views.FlashMessageMsgView.as_view(), name='flash-message'),
|
||||||
|
]
|
@ -8,3 +8,4 @@ from .http import *
|
|||||||
from .ipip import *
|
from .ipip import *
|
||||||
from .crypto import *
|
from .crypto import *
|
||||||
from .random import *
|
from .random import *
|
||||||
|
from .jumpserver import *
|
@ -75,11 +75,16 @@ def ssh_key_string_to_obj(text, password=None):
|
|||||||
key = paramiko.RSAKey.from_private_key(StringIO(text), password=password)
|
key = paramiko.RSAKey.from_private_key(StringIO(text), password=password)
|
||||||
except paramiko.SSHException:
|
except paramiko.SSHException:
|
||||||
pass
|
pass
|
||||||
|
else:
|
||||||
|
return key
|
||||||
|
|
||||||
try:
|
try:
|
||||||
key = paramiko.DSSKey.from_private_key(StringIO(text), password=password)
|
key = paramiko.DSSKey.from_private_key(StringIO(text), password=password)
|
||||||
except paramiko.SSHException:
|
except paramiko.SSHException:
|
||||||
pass
|
pass
|
||||||
|
else:
|
||||||
|
return key
|
||||||
|
|
||||||
return key
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
31
apps/common/utils/jumpserver.py
Normal file
31
apps/common/utils/jumpserver.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from django.core.cache import cache
|
||||||
|
from django.shortcuts import reverse
|
||||||
|
|
||||||
|
from .random import random_string
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['FlashMessageUtil']
|
||||||
|
|
||||||
|
|
||||||
|
class FlashMessageUtil:
|
||||||
|
@staticmethod
|
||||||
|
def get_key(code):
|
||||||
|
key = 'MESSAGE_{}'.format(code)
|
||||||
|
return key
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_message_code(cls, message_data):
|
||||||
|
code = random_string(12)
|
||||||
|
key = cls.get_key(code)
|
||||||
|
cache.set(key, message_data, 60)
|
||||||
|
return code
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_message_by_code(cls, code):
|
||||||
|
key = cls.get_key(code)
|
||||||
|
return cache.get(key)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def gen_message_url(cls, message_data):
|
||||||
|
code = cls.get_message_code(message_data)
|
||||||
|
return reverse('common:flash-message') + f'?code={code}'
|
@ -4,7 +4,6 @@ import struct
|
|||||||
import random
|
import random
|
||||||
import socket
|
import socket
|
||||||
import string
|
import string
|
||||||
import secrets
|
|
||||||
|
|
||||||
|
|
||||||
string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_{}~'
|
string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_{}~'
|
||||||
|
40
apps/common/views.py
Normal file
40
apps/common/views.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.cache import never_cache
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.views.generic.base import TemplateView
|
||||||
|
|
||||||
|
from common.utils import bulk_get, FlashMessageUtil
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(never_cache, name='dispatch')
|
||||||
|
class FlashMessageMsgView(TemplateView):
|
||||||
|
template_name = 'flash_message_standalone.html'
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
code = request.GET.get('code')
|
||||||
|
if not code:
|
||||||
|
return HttpResponse('Not found the code')
|
||||||
|
|
||||||
|
message_data = FlashMessageUtil.get_message_by_code(code)
|
||||||
|
if not message_data:
|
||||||
|
return HttpResponse('Message code error')
|
||||||
|
|
||||||
|
title, message, redirect_url, confirm_button, cancel_url = bulk_get(
|
||||||
|
message_data, 'title', 'message', 'redirect_url', 'confirm_button', 'cancel_url'
|
||||||
|
)
|
||||||
|
|
||||||
|
interval = message_data.get('interval', 3)
|
||||||
|
auto_redirect = message_data.get('auto_redirect', True)
|
||||||
|
has_cancel = message_data.get('has_cancel', False)
|
||||||
|
context = {
|
||||||
|
'title': title,
|
||||||
|
'messages': message,
|
||||||
|
'interval': interval,
|
||||||
|
'redirect_url': redirect_url,
|
||||||
|
'auto_redirect': auto_redirect,
|
||||||
|
'confirm_button': confirm_button,
|
||||||
|
'has_cancel': has_cancel,
|
||||||
|
'cancel_url': cancel_url,
|
||||||
|
}
|
||||||
|
return self.render_to_response(context)
|
@ -18,6 +18,7 @@ from terminal.utils import ComponentsPrometheusMetricsUtil
|
|||||||
from orgs.utils import current_org
|
from orgs.utils import current_org
|
||||||
from common.permissions import IsOrgAdmin, IsOrgAuditor
|
from common.permissions import IsOrgAdmin, IsOrgAuditor
|
||||||
from common.utils import lazyproperty
|
from common.utils import lazyproperty
|
||||||
|
from orgs.caches import OrgResourceStatisticsCache
|
||||||
|
|
||||||
__all__ = ['IndexApi']
|
__all__ = ['IndexApi']
|
||||||
|
|
||||||
@ -99,7 +100,7 @@ class DatesLoginMetricMixin:
|
|||||||
if count is not None:
|
if count is not None:
|
||||||
return count
|
return count
|
||||||
ds, de = self.get_date_start_2_end(date)
|
ds, de = self.get_date_start_2_end(date)
|
||||||
count = len(set(Session.objects.filter(date_start__range=(ds, de)).values_list('user', flat=True)))
|
count = len(set(Session.objects.filter(date_start__range=(ds, de)).values_list('user_id', flat=True)))
|
||||||
self.__set_data_to_cache(date, tp, count)
|
self.__set_data_to_cache(date, tp, count)
|
||||||
return count
|
return count
|
||||||
|
|
||||||
@ -129,7 +130,7 @@ class DatesLoginMetricMixin:
|
|||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def dates_total_count_active_users(self):
|
def dates_total_count_active_users(self):
|
||||||
count = len(set(self.sessions_queryset.values_list('user', flat=True)))
|
count = len(set(self.sessions_queryset.values_list('user_id', flat=True)))
|
||||||
return count
|
return count
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
@ -164,7 +165,7 @@ class DatesLoginMetricMixin:
|
|||||||
|
|
||||||
# 以下是从week中而来
|
# 以下是从week中而来
|
||||||
def get_dates_login_times_top5_users(self):
|
def get_dates_login_times_top5_users(self):
|
||||||
users = self.sessions_queryset.values_list('user', flat=True)
|
users = self.sessions_queryset.values_list('user_id', flat=True)
|
||||||
users = [
|
users = [
|
||||||
{'user': user, 'total': total}
|
{'user': user, 'total': total}
|
||||||
for user, total in Counter(users).most_common(5)
|
for user, total in Counter(users).most_common(5)
|
||||||
@ -172,7 +173,7 @@ class DatesLoginMetricMixin:
|
|||||||
return users
|
return users
|
||||||
|
|
||||||
def get_dates_total_count_login_users(self):
|
def get_dates_total_count_login_users(self):
|
||||||
return len(set(self.sessions_queryset.values_list('user', flat=True)))
|
return len(set(self.sessions_queryset.values_list('user_id', flat=True)))
|
||||||
|
|
||||||
def get_dates_total_count_login_times(self):
|
def get_dates_total_count_login_times(self):
|
||||||
return self.sessions_queryset.count()
|
return self.sessions_queryset.count()
|
||||||
@ -186,8 +187,8 @@ class DatesLoginMetricMixin:
|
|||||||
return list(assets)
|
return list(assets)
|
||||||
|
|
||||||
def get_dates_login_times_top10_users(self):
|
def get_dates_login_times_top10_users(self):
|
||||||
users = self.sessions_queryset.values("user") \
|
users = self.sessions_queryset.values("user_id") \
|
||||||
.annotate(total=Count("user")) \
|
.annotate(total=Count("user_id")) \
|
||||||
.annotate(last=Max("date_start")).order_by("-total")[:10]
|
.annotate(last=Max("date_start")).order_by("-total")[:10]
|
||||||
for user in users:
|
for user in users:
|
||||||
user['last'] = str(user['last'])
|
user['last'] = str(user['last'])
|
||||||
@ -210,26 +211,7 @@ class DatesLoginMetricMixin:
|
|||||||
return sessions
|
return sessions
|
||||||
|
|
||||||
|
|
||||||
class TotalCountMixin:
|
class IndexApi(DatesLoginMetricMixin, APIView):
|
||||||
@staticmethod
|
|
||||||
def get_total_count_users():
|
|
||||||
return current_org.get_members().count()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_total_count_assets():
|
|
||||||
return Asset.objects.all().count()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_total_count_online_users():
|
|
||||||
count = len(set(Session.objects.filter(is_finished=False).values_list('user', flat=True)))
|
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_total_count_online_sessions():
|
|
||||||
return Session.objects.filter(is_finished=False).count()
|
|
||||||
|
|
||||||
|
|
||||||
class IndexApi(TotalCountMixin, DatesLoginMetricMixin, APIView):
|
|
||||||
permission_classes = (IsOrgAdmin | IsOrgAuditor,)
|
permission_classes = (IsOrgAdmin | IsOrgAuditor,)
|
||||||
http_method_names = ['get']
|
http_method_names = ['get']
|
||||||
|
|
||||||
@ -238,26 +220,28 @@ class IndexApi(TotalCountMixin, DatesLoginMetricMixin, APIView):
|
|||||||
|
|
||||||
query_params = self.request.query_params
|
query_params = self.request.query_params
|
||||||
|
|
||||||
|
caches = OrgResourceStatisticsCache(self.request.user.user_orgs[0])
|
||||||
|
|
||||||
_all = query_params.get('all')
|
_all = query_params.get('all')
|
||||||
|
|
||||||
if _all or query_params.get('total_count') or query_params.get('total_count_users'):
|
if _all or query_params.get('total_count') or query_params.get('total_count_users'):
|
||||||
data.update({
|
data.update({
|
||||||
'total_count_users': self.get_total_count_users(),
|
'total_count_users': caches.users_amount,
|
||||||
})
|
})
|
||||||
|
|
||||||
if _all or query_params.get('total_count') or query_params.get('total_count_assets'):
|
if _all or query_params.get('total_count') or query_params.get('total_count_assets'):
|
||||||
data.update({
|
data.update({
|
||||||
'total_count_assets': self.get_total_count_assets(),
|
'total_count_assets': caches.assets_amount,
|
||||||
})
|
})
|
||||||
|
|
||||||
if _all or query_params.get('total_count') or query_params.get('total_count_online_users'):
|
if _all or query_params.get('total_count') or query_params.get('total_count_online_users'):
|
||||||
data.update({
|
data.update({
|
||||||
'total_count_online_users': self.get_total_count_online_users(),
|
'total_count_online_users': caches.total_count_online_users,
|
||||||
})
|
})
|
||||||
|
|
||||||
if _all or query_params.get('total_count') or query_params.get('total_count_online_sessions'):
|
if _all or query_params.get('total_count') or query_params.get('total_count_online_sessions'):
|
||||||
data.update({
|
data.update({
|
||||||
'total_count_online_sessions': self.get_total_count_online_sessions(),
|
'total_count_online_sessions': caches.total_count_online_sessions,
|
||||||
})
|
})
|
||||||
|
|
||||||
if _all or query_params.get('dates_metrics'):
|
if _all or query_params.get('dates_metrics'):
|
||||||
|
@ -216,6 +216,16 @@ class Config(dict):
|
|||||||
'AUTH_SSO': False,
|
'AUTH_SSO': False,
|
||||||
'AUTH_SSO_AUTHKEY_TTL': 60 * 15,
|
'AUTH_SSO_AUTHKEY_TTL': 60 * 15,
|
||||||
|
|
||||||
|
'AUTH_WECOM': False,
|
||||||
|
'WECOM_CORPID': '',
|
||||||
|
'WECOM_AGENTID': '',
|
||||||
|
'WECOM_SECRET': '',
|
||||||
|
|
||||||
|
'AUTH_DINGTALK': False,
|
||||||
|
'DINGTALK_AGENTID': '',
|
||||||
|
'DINGTALK_APPKEY': '',
|
||||||
|
'DINGTALK_APPSECRET': '',
|
||||||
|
|
||||||
'OTP_VALID_WINDOW': 2,
|
'OTP_VALID_WINDOW': 2,
|
||||||
'OTP_ISSUER_NAME': 'JumpServer',
|
'OTP_ISSUER_NAME': 'JumpServer',
|
||||||
'EMAIL_SUFFIX': 'jumpserver.org',
|
'EMAIL_SUFFIX': 'jumpserver.org',
|
||||||
@ -259,6 +269,7 @@ class Config(dict):
|
|||||||
'FTP_LOG_KEEP_DAYS': 200,
|
'FTP_LOG_KEEP_DAYS': 200,
|
||||||
'ASSETS_PERM_CACHE_TIME': 3600 * 24,
|
'ASSETS_PERM_CACHE_TIME': 3600 * 24,
|
||||||
'SECURITY_MFA_VERIFY_TTL': 3600,
|
'SECURITY_MFA_VERIFY_TTL': 3600,
|
||||||
|
'OLD_PASSWORD_HISTORY_LIMIT_COUNT': 5,
|
||||||
'ASSETS_PERM_CACHE_ENABLE': HAS_XPACK,
|
'ASSETS_PERM_CACHE_ENABLE': HAS_XPACK,
|
||||||
'SYSLOG_ADDR': '', # '192.168.0.1:514'
|
'SYSLOG_ADDR': '', # '192.168.0.1:514'
|
||||||
'SYSLOG_FACILITY': 'user',
|
'SYSLOG_FACILITY': 'user',
|
||||||
|
@ -14,6 +14,8 @@ def jumpserver_processor(request):
|
|||||||
'LOGIN_IMAGE_URL': static('img/login_image.png'),
|
'LOGIN_IMAGE_URL': static('img/login_image.png'),
|
||||||
'FAVICON_URL': static('img/facio.ico'),
|
'FAVICON_URL': static('img/facio.ico'),
|
||||||
'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_log.png'),
|
||||||
|
'LOGIN_DINGTALK_LOGO_URL': static('img/login_dingtalk_log.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',
|
||||||
|
@ -101,6 +101,19 @@ CAS_CHECK_NEXT = lambda _next_page: True
|
|||||||
AUTH_SSO = CONFIG.AUTH_SSO
|
AUTH_SSO = CONFIG.AUTH_SSO
|
||||||
AUTH_SSO_AUTHKEY_TTL = CONFIG.AUTH_SSO_AUTHKEY_TTL
|
AUTH_SSO_AUTHKEY_TTL = CONFIG.AUTH_SSO_AUTHKEY_TTL
|
||||||
|
|
||||||
|
# WECOM Auth
|
||||||
|
AUTH_WECOM = CONFIG.AUTH_WECOM
|
||||||
|
WECOM_CORPID = CONFIG.WECOM_CORPID
|
||||||
|
WECOM_AGENTID = CONFIG.WECOM_AGENTID
|
||||||
|
WECOM_SECRET = CONFIG.WECOM_SECRET
|
||||||
|
|
||||||
|
# DingDing auth
|
||||||
|
AUTH_DINGTALK = CONFIG.AUTH_DINGTALK
|
||||||
|
DINGTALK_AGENTID = CONFIG.DINGTALK_AGENTID
|
||||||
|
DINGTALK_APPKEY = CONFIG.DINGTALK_APPKEY
|
||||||
|
DINGTALK_APPSECRET = CONFIG.DINGTALK_APPSECRET
|
||||||
|
|
||||||
|
|
||||||
# Other setting
|
# Other setting
|
||||||
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
|
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
|
||||||
LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE
|
LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE
|
||||||
@ -115,6 +128,8 @@ AUTH_BACKEND_OIDC_CODE = 'jms_oidc_rp.backends.OIDCAuthCodeBackend'
|
|||||||
AUTH_BACKEND_RADIUS = 'authentication.backends.radius.RadiusBackend'
|
AUTH_BACKEND_RADIUS = 'authentication.backends.radius.RadiusBackend'
|
||||||
AUTH_BACKEND_CAS = 'authentication.backends.cas.CASBackend'
|
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_DINGTALK = 'authentication.backends.api.DingTalkAuthentication'
|
||||||
|
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = [AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY]
|
AUTHENTICATION_BACKENDS = [AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY]
|
||||||
@ -128,6 +143,10 @@ if AUTH_RADIUS:
|
|||||||
AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_RADIUS)
|
AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_RADIUS)
|
||||||
if AUTH_SSO:
|
if AUTH_SSO:
|
||||||
AUTHENTICATION_BACKENDS.append(AUTH_BACKEND_SSO)
|
AUTHENTICATION_BACKENDS.append(AUTH_BACKEND_SSO)
|
||||||
|
if AUTH_WECOM:
|
||||||
|
AUTHENTICATION_BACKENDS.append(AUTH_BACKEND_WECOM)
|
||||||
|
if AUTH_DINGTALK:
|
||||||
|
AUTHENTICATION_BACKENDS.append(AUTH_BACKEND_DINGTALK)
|
||||||
|
|
||||||
|
|
||||||
ONLY_ALLOW_EXIST_USER_AUTH = CONFIG.ONLY_ALLOW_EXIST_USER_AUTH
|
ONLY_ALLOW_EXIST_USER_AUTH = CONFIG.ONLY_ALLOW_EXIST_USER_AUTH
|
||||||
|
@ -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
|
||||||
|
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
|
||||||
SECURITY_PASSWORD_NUMBER = CONFIG.SECURITY_PASSWORD_NUMBER
|
SECURITY_PASSWORD_NUMBER = CONFIG.SECURITY_PASSWORD_NUMBER
|
||||||
|
@ -23,12 +23,13 @@ api_v1 = [
|
|||||||
path('applications/', include('applications.urls.api_urls', namespace='api-applications')),
|
path('applications/', include('applications.urls.api_urls', namespace='api-applications')),
|
||||||
path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')),
|
path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')),
|
||||||
path('acls/', include('acls.urls.api_urls', namespace='api-acls')),
|
path('acls/', include('acls.urls.api_urls', namespace='api-acls')),
|
||||||
path('prometheus/metrics/', api.PrometheusMetricsApi.as_view())
|
path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()),
|
||||||
]
|
]
|
||||||
|
|
||||||
app_view_patterns = [
|
app_view_patterns = [
|
||||||
path('auth/', include('authentication.urls.view_urls'), name='auth'),
|
path('auth/', include('authentication.urls.view_urls'), name='auth'),
|
||||||
path('ops/', include('ops.urls.view_urls'), name='ops'),
|
path('ops/', include('ops.urls.view_urls'), name='ops'),
|
||||||
|
path('common/', include('common.urls.view_urls'), name='common'),
|
||||||
re_path(r'flower/(?P<path>.*)', views.celery_flower_view, name='flower-view'),
|
re_path(r'flower/(?P<path>.*)', views.celery_flower_view, name='flower-view'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
import re
|
import re
|
||||||
import time
|
|
||||||
|
|
||||||
from django.http import HttpResponseRedirect, JsonResponse, Http404
|
from django.http import HttpResponseRedirect, JsonResponse, Http404
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework.views import APIView
|
|
||||||
from rest_framework.permissions import AllowAny
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from common.http import HttpResponseTemporaryRedirect
|
from common.http import HttpResponseTemporaryRedirect
|
||||||
|
|
||||||
@ -85,3 +83,4 @@ class KokoView(View):
|
|||||||
"<div>Koko is a separately deployed program, you need to deploy Koko, configure nginx for url distribution,</div> "
|
"<div>Koko is a separately deployed program, you need to deploy Koko, configure nginx for url distribution,</div> "
|
||||||
"</div>If you see this page, prove that you are not accessing the nginx listening port. Good luck.</div>")
|
"</div>If you see this page, prove that you are not accessing the nginx listening port. Good luck.</div>")
|
||||||
return HttpResponse(msg)
|
return HttpResponse(msg)
|
||||||
|
|
||||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,12 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class OpsConfig(AppConfig):
|
class OpsConfig(AppConfig):
|
||||||
name = 'ops'
|
name = 'ops'
|
||||||
|
verbose_name = _('Operations')
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from orgs.models import Organization
|
from orgs.models import Organization
|
||||||
|
@ -14,11 +14,15 @@ class AdHocExecutionSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AdHocExecution
|
model = AdHocExecution
|
||||||
fields = [
|
fields_mini = ['id']
|
||||||
'id', 'task', 'task_display', 'hosts_amount', 'adhoc', 'date_start', 'stat',
|
fields_small = fields_mini + [
|
||||||
'date_finished', 'timedelta', 'is_finished', 'is_success', 'result', 'summary',
|
'hosts_amount', 'timedelta', 'result', 'summary', 'short_id',
|
||||||
'short_id', 'adhoc_short_id', 'last_success', 'last_failure'
|
'is_finished', 'is_success',
|
||||||
|
'date_start', 'date_finished',
|
||||||
]
|
]
|
||||||
|
fields_fk = ['task', 'task_display', 'adhoc', 'adhoc_short_id',]
|
||||||
|
fields_custom = ['stat', 'last_success', 'last_failure']
|
||||||
|
fields = fields_small + fields_fk + fields_custom
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_task(obj):
|
def get_task(obj):
|
||||||
@ -52,11 +56,16 @@ class TaskSerializer(BulkOrgResourceModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Task
|
model = Task
|
||||||
fields = [
|
fields_mini = ['id', 'name']
|
||||||
'id', 'name', 'interval', 'crontab', 'is_periodic',
|
fields_small = fields_mini + [
|
||||||
'is_deleted', 'comment', 'date_created',
|
'interval', 'crontab',
|
||||||
'date_updated', 'latest_execution', 'summary',
|
'is_periodic', 'is_deleted',
|
||||||
|
'date_created', 'date_updated',
|
||||||
|
'comment',
|
||||||
]
|
]
|
||||||
|
fields_fk = ['latest_execution']
|
||||||
|
fields_custom = ['summary']
|
||||||
|
fields = fields_small + fields_fk + fields_custom
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
'is_deleted', 'date_created', 'date_updated',
|
'is_deleted', 'date_created', 'date_updated',
|
||||||
'latest_adhoc', 'latest_execution', 'total_run_amount',
|
'latest_adhoc', 'latest_execution', 'total_run_amount',
|
||||||
@ -77,12 +86,16 @@ class AdHocSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AdHoc
|
model = AdHoc
|
||||||
fields = [
|
fields_mini = ['id']
|
||||||
"id", "task", 'tasks', "pattern", "options",
|
fields_small = fields_mini + [
|
||||||
"hosts", "run_as_admin", "run_as", "become",
|
'tasks', "pattern", "options", "run_as",
|
||||||
"date_created", "short_id",
|
"become", "become_display", "short_id",
|
||||||
"become_display",
|
"run_as_admin",
|
||||||
|
"date_created",
|
||||||
]
|
]
|
||||||
|
fields_fk = ["task"]
|
||||||
|
fields_m2m = ["hosts"]
|
||||||
|
fields = fields_small + fields_fk + fields_m2m
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
'date_created'
|
'date_created'
|
||||||
]
|
]
|
||||||
@ -99,8 +112,8 @@ class AdHocExecutionNestSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = AdHocExecution
|
model = AdHocExecution
|
||||||
fields = (
|
fields = (
|
||||||
'last_success', 'last_failure', 'last_run', 'timedelta', 'is_finished',
|
'last_success', 'last_failure', 'last_run', 'timedelta',
|
||||||
'is_success'
|
'is_finished', 'is_success'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -120,10 +133,15 @@ class CommandExecutionSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CommandExecution
|
model = CommandExecution
|
||||||
fields = [
|
fields_mini = ['id']
|
||||||
'id', 'hosts', 'run_as', 'command', 'result', 'log_url',
|
fields_small = fields_mini + [
|
||||||
'is_finished', 'date_created', 'date_finished'
|
'command', 'result', 'log_url',
|
||||||
|
'is_finished',
|
||||||
|
'date_created', 'date_finished'
|
||||||
]
|
]
|
||||||
|
fields_fk = ['run_as']
|
||||||
|
fields_m2m = ['hosts']
|
||||||
|
fields = fields_small + fields_fk + fields_m2m
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
'result', 'is_finished', 'log_url', 'date_created',
|
'result', 'is_finished', 'log_url', 'date_created',
|
||||||
'date_finished'
|
'date_finished'
|
||||||
|
@ -6,12 +6,12 @@ from orgs.utils import current_org, tmp_to_org
|
|||||||
from common.cache import Cache, IntegerField
|
from common.cache import Cache, IntegerField
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from users.models import UserGroup, User
|
from users.models import UserGroup, User
|
||||||
from assets.models import Node, AdminUser, SystemUser, Domain, Gateway
|
from assets.models import Node, AdminUser, SystemUser, Domain, Gateway, Asset
|
||||||
|
from terminal.models import Session
|
||||||
from applications.models import Application
|
from applications.models import Application
|
||||||
from perms.models import AssetPermission, ApplicationPermission
|
from perms.models import AssetPermission, ApplicationPermission
|
||||||
from .models import OrganizationMember
|
from .models import OrganizationMember
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
@ -64,6 +64,9 @@ class OrgResourceStatisticsCache(OrgRelatedCache):
|
|||||||
asset_perms_amount = IntegerField(queryset=AssetPermission.objects)
|
asset_perms_amount = IntegerField(queryset=AssetPermission.objects)
|
||||||
app_perms_amount = IntegerField(queryset=ApplicationPermission.objects)
|
app_perms_amount = IntegerField(queryset=ApplicationPermission.objects)
|
||||||
|
|
||||||
|
total_count_online_users = IntegerField()
|
||||||
|
total_count_online_sessions = IntegerField()
|
||||||
|
|
||||||
def __init__(self, org):
|
def __init__(self, org):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.org = org
|
self.org = org
|
||||||
@ -86,3 +89,9 @@ class OrgResourceStatisticsCache(OrgRelatedCache):
|
|||||||
def compute_assets_amount(self):
|
def compute_assets_amount(self):
|
||||||
node = Node.org_root()
|
node = Node.org_root()
|
||||||
return node.assets_amount
|
return node.assets_amount
|
||||||
|
|
||||||
|
def compute_total_count_online_users(self):
|
||||||
|
return len(set(Session.objects.filter(is_finished=False).values_list('user_id', flat=True)))
|
||||||
|
|
||||||
|
def compute_total_count_online_sessions(self):
|
||||||
|
return Session.objects.filter(is_finished=False).count()
|
||||||
|
@ -38,8 +38,10 @@ class OrgSerializer(ModelSerializer):
|
|||||||
list_serializer_class = AdaptedBulkListSerializer
|
list_serializer_class = AdaptedBulkListSerializer
|
||||||
fields_mini = ['id', 'name']
|
fields_mini = ['id', 'name']
|
||||||
fields_small = fields_mini + [
|
fields_small = fields_mini + [
|
||||||
'is_default', 'is_root', 'comment',
|
'resource_statistics',
|
||||||
'created_by', 'date_created', 'resource_statistics'
|
'is_default', 'is_root',
|
||||||
|
'date_created',
|
||||||
|
'comment', 'created_by',
|
||||||
]
|
]
|
||||||
|
|
||||||
fields_m2m = ['users', 'admins', 'auditors']
|
fields_m2m = ['users', 'admins', 'auditors']
|
||||||
@ -79,7 +81,12 @@ class OrgMemberSerializer(BulkModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrganizationMember
|
model = OrganizationMember
|
||||||
fields = ('id', 'org', 'user', 'role', 'org_display', 'user_display', 'role_display')
|
fields_mini = ['id']
|
||||||
|
fields_small = fields_mini + [
|
||||||
|
'role', 'role_display'
|
||||||
|
]
|
||||||
|
fields_fk = ['org', 'user', 'org_display', 'user_display',]
|
||||||
|
fields = fields_small + fields_fk
|
||||||
use_model_bulk_create = True
|
use_model_bulk_create = True
|
||||||
model_bulk_create_kwargs = {'ignore_conflicts': True}
|
model_bulk_create_kwargs = {'ignore_conflicts': True}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django.db.models.signals import m2m_changed
|
from django.db.models.signals import m2m_changed
|
||||||
from django.db.models.signals import post_save, pre_delete
|
from django.db.models.signals import post_save, pre_delete, pre_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from orgs.models import Organization, OrganizationMember
|
from orgs.models import Organization, OrganizationMember
|
||||||
@ -7,6 +7,7 @@ from assets.models import Node
|
|||||||
from perms.models import (AssetPermission, ApplicationPermission)
|
from perms.models import (AssetPermission, ApplicationPermission)
|
||||||
from users.models import UserGroup, User
|
from users.models import UserGroup, User
|
||||||
from applications.models import Application
|
from applications.models import Application
|
||||||
|
from terminal.models import Session
|
||||||
from assets.models import Asset, AdminUser, SystemUser, Domain, Gateway
|
from assets.models import Asset, AdminUser, SystemUser, Domain, Gateway
|
||||||
from common.const.signals import POST_PREFIX
|
from common.const.signals import POST_PREFIX
|
||||||
from orgs.caches import OrgResourceStatisticsCache
|
from orgs.caches import OrgResourceStatisticsCache
|
||||||
@ -47,16 +48,17 @@ def on_org_user_changed_refresh_cache(sender, action, instance, reverse, pk_set,
|
|||||||
|
|
||||||
class OrgResourceStatisticsRefreshUtil:
|
class OrgResourceStatisticsRefreshUtil:
|
||||||
model_cache_field_mapper = {
|
model_cache_field_mapper = {
|
||||||
ApplicationPermission: 'app_perms_amount',
|
ApplicationPermission: ['app_perms_amount'],
|
||||||
AssetPermission: 'asset_perms_amount',
|
AssetPermission: ['asset_perms_amount'],
|
||||||
Application: 'applications_amount',
|
Application: ['applications_amount'],
|
||||||
Gateway: 'gateways_amount',
|
Gateway: ['gateways_amount'],
|
||||||
Domain: 'domains_amount',
|
Domain: ['domains_amount'],
|
||||||
SystemUser: 'system_users_amount',
|
SystemUser: ['system_users_amount'],
|
||||||
AdminUser: 'admin_users_amount',
|
AdminUser: ['admin_users_amount'],
|
||||||
Node: 'nodes_amount',
|
Node: ['nodes_amount'],
|
||||||
Asset: 'assets_amount',
|
Asset: ['assets_amount'],
|
||||||
UserGroup: 'groups_amount',
|
UserGroup: ['groups_amount'],
|
||||||
|
Session: ['total_count_online_users', 'total_count_online_sessions']
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -64,13 +66,12 @@ class OrgResourceStatisticsRefreshUtil:
|
|||||||
cache_field_name = cls.model_cache_field_mapper.get(type(instance))
|
cache_field_name = cls.model_cache_field_mapper.get(type(instance))
|
||||||
if cache_field_name:
|
if cache_field_name:
|
||||||
org_cache = OrgResourceStatisticsCache(instance.org)
|
org_cache = OrgResourceStatisticsCache(instance.org)
|
||||||
org_cache.expire(cache_field_name)
|
org_cache.expire(*cache_field_name)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save)
|
@receiver(pre_save)
|
||||||
def on_post_save_refresh_org_resource_statistics_cache(sender, instance, created, **kwargs):
|
def on_post_save_refresh_org_resource_statistics_cache(sender, instance, **kwargs):
|
||||||
if created:
|
OrgResourceStatisticsRefreshUtil.refresh_if_need(instance)
|
||||||
OrgResourceStatisticsRefreshUtil.refresh_if_need(instance)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete)
|
@receiver(pre_delete)
|
||||||
|
@ -8,7 +8,6 @@ from django.dispatch import receiver
|
|||||||
from django.utils.functional import LazyObject
|
from django.utils.functional import LazyObject
|
||||||
from django.db.models.signals import m2m_changed
|
from django.db.models.signals import m2m_changed
|
||||||
from django.db.models.signals import post_save, post_delete, pre_delete
|
from django.db.models.signals import post_save, post_delete, pre_delete
|
||||||
from django.utils.translation import ugettext as _
|
|
||||||
|
|
||||||
from orgs.utils import tmp_to_org
|
from orgs.utils import tmp_to_org
|
||||||
from orgs.models import Organization, OrganizationMember
|
from orgs.models import Organization, OrganizationMember
|
||||||
@ -19,7 +18,6 @@ from common.const.signals import PRE_REMOVE, POST_REMOVE
|
|||||||
from common.signals import django_ready
|
from common.signals import django_ready
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from common.utils.connection import RedisPubSub
|
from common.utils.connection import RedisPubSub
|
||||||
from common.exceptions import JMSException
|
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
import uuid
|
import time
|
||||||
|
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from rest_framework.views import APIView, Response
|
from rest_framework.views import APIView, Response
|
||||||
|
from rest_framework import status
|
||||||
from rest_framework.generics import (
|
from rest_framework.generics import (
|
||||||
ListAPIView, get_object_or_404
|
ListAPIView, get_object_or_404
|
||||||
)
|
)
|
||||||
@ -12,7 +14,8 @@ from orgs.utils import tmp_to_root_org
|
|||||||
from applications.models import Application
|
from applications.models import Application
|
||||||
from perms.utils.application.permission import (
|
from perms.utils.application.permission import (
|
||||||
has_application_system_permission,
|
has_application_system_permission,
|
||||||
get_application_system_user_ids
|
get_application_system_user_ids,
|
||||||
|
validate_permission,
|
||||||
)
|
)
|
||||||
from perms.api.asset.user_permission.mixin import RoleAdminMixin, RoleUserMixin
|
from perms.api.asset.user_permission.mixin import RoleAdminMixin, RoleUserMixin
|
||||||
from common.permissions import IsOrgAdminOrAppUser
|
from common.permissions import IsOrgAdminOrAppUser
|
||||||
@ -61,18 +64,13 @@ class ValidateUserApplicationPermissionApi(APIView):
|
|||||||
application_id = request.query_params.get('application_id', '')
|
application_id = request.query_params.get('application_id', '')
|
||||||
system_user_id = request.query_params.get('system_user_id', '')
|
system_user_id = request.query_params.get('system_user_id', '')
|
||||||
|
|
||||||
try:
|
if not all((user_id, application_id, system_user_id)):
|
||||||
user_id = uuid.UUID(user_id)
|
return Response({'has_permission': False, 'expire_at': int(time.time())})
|
||||||
application_id = uuid.UUID(application_id)
|
|
||||||
system_user_id = uuid.UUID(system_user_id)
|
|
||||||
except ValueError:
|
|
||||||
return Response({'msg': False}, status=403)
|
|
||||||
|
|
||||||
user = get_object_or_404(User, id=user_id)
|
user = User.objects.get(id=user_id)
|
||||||
application = get_object_or_404(Application, id=application_id)
|
application = Application.objects.get(id=application_id)
|
||||||
system_user = get_object_or_404(SystemUser, id=system_user_id)
|
system_user = SystemUser.objects.get(id=system_user_id)
|
||||||
|
|
||||||
if has_application_system_permission(user, application, system_user):
|
has_permission, expire_at = validate_permission(user, application, system_user)
|
||||||
return Response({'msg': True}, status=200)
|
status_code = status.HTTP_200_OK if has_permission else status.HTTP_403_FORBIDDEN
|
||||||
|
return Response({'has_permission': has_permission, 'expire_at': int(expire_at)}, status=status_code)
|
||||||
return Response({'msg': False}, status=403)
|
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
import uuid
|
import uuid
|
||||||
|
import time
|
||||||
|
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from rest_framework.views import APIView, Response
|
from rest_framework.views import APIView, Response
|
||||||
|
from rest_framework import status
|
||||||
from rest_framework.generics import (
|
from rest_framework.generics import (
|
||||||
ListAPIView, get_object_or_404, RetrieveAPIView, DestroyAPIView
|
ListAPIView, get_object_or_404, RetrieveAPIView, DestroyAPIView
|
||||||
)
|
)
|
||||||
|
|
||||||
from orgs.utils import tmp_to_root_org
|
from orgs.utils import tmp_to_root_org
|
||||||
from perms.utils.asset.permission import get_asset_system_user_ids_with_actions_by_user
|
from perms.utils.asset.permission import get_asset_system_user_ids_with_actions_by_user, validate_permission
|
||||||
from common.permissions import IsOrgAdminOrAppUser, IsOrgAdmin, IsValidUser
|
from common.permissions import IsOrgAdminOrAppUser, IsOrgAdmin, IsValidUser
|
||||||
from common.utils import get_logger, lazyproperty
|
from common.utils import get_logger, lazyproperty
|
||||||
|
|
||||||
from perms.hands import User, Asset, SystemUser
|
from perms.hands import User, Asset, SystemUser
|
||||||
from perms import serializers
|
from perms import serializers
|
||||||
from perms.models import Action
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@ -65,32 +66,22 @@ class ValidateUserAssetPermissionApi(APIView):
|
|||||||
def get_cache_policy(self):
|
def get_cache_policy(self):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def get_user(self):
|
|
||||||
user_id = self.request.query_params.get('user_id', '')
|
|
||||||
user = get_object_or_404(User, id=user_id)
|
|
||||||
return user
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
user_id = self.request.query_params.get('user_id', '')
|
||||||
asset_id = request.query_params.get('asset_id', '')
|
asset_id = request.query_params.get('asset_id', '')
|
||||||
system_id = request.query_params.get('system_user_id', '')
|
system_id = request.query_params.get('system_user_id', '')
|
||||||
action_name = request.query_params.get('action_name', '')
|
action_name = request.query_params.get('action_name', '')
|
||||||
|
|
||||||
try:
|
if not all((user_id, asset_id, system_id, action_name)):
|
||||||
asset_id = uuid.UUID(asset_id)
|
return Response({'has_permission': False, 'expire_at': int(time.time())})
|
||||||
system_id = uuid.UUID(system_id)
|
|
||||||
except ValueError:
|
|
||||||
return Response({'msg': False}, status=403)
|
|
||||||
|
|
||||||
asset = get_object_or_404(Asset, id=asset_id, is_active=True)
|
user = User.objects.get(id=user_id)
|
||||||
system_user = get_object_or_404(SystemUser, id=system_id)
|
asset = Asset.objects.valid().get(id=asset_id)
|
||||||
|
system_user = SystemUser.objects.get(id=system_id)
|
||||||
|
|
||||||
system_users_actions = get_asset_system_user_ids_with_actions_by_user(self.get_user(), asset)
|
has_permission, expire_at = validate_permission(user, asset, system_user, action_name)
|
||||||
actions = system_users_actions.get(system_user.id)
|
status_code = status.HTTP_200_OK if has_permission else status.HTTP_403_FORBIDDEN
|
||||||
if actions is None:
|
return Response({'has_permission': has_permission, 'expire_at': int(expire_at)}, status=status_code)
|
||||||
return Response({'msg': False}, status=403)
|
|
||||||
if action_name in Action.value_to_choices(actions):
|
|
||||||
return Response({'msg': True}, status=200)
|
|
||||||
return Response({'msg': False}, status=403)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO 删除
|
# TODO 删除
|
||||||
|
@ -3,8 +3,9 @@
|
|||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
|
||||||
from common.permissions import IsOrgAdminOrAppUser, IsValidUser
|
from common.permissions import IsOrgAdminOrAppUser, IsValidUser
|
||||||
from common.utils import lazyproperty
|
|
||||||
from common.http import is_true
|
from common.http import is_true
|
||||||
|
from common.mixins.api import RoleAdminMixin as _RoleAdminMixin
|
||||||
|
from common.mixins.api import RoleUserMixin as _RoleUserMixin
|
||||||
from orgs.utils import tmp_to_root_org
|
from orgs.utils import tmp_to_root_org
|
||||||
from users.models import User
|
from users.models import User
|
||||||
from perms.utils.asset.user_permission import UserGrantedTreeRefreshController
|
from perms.utils.asset.user_permission import UserGrantedTreeRefreshController
|
||||||
@ -20,24 +21,13 @@ class PermBaseMixin:
|
|||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class RoleAdminMixin(PermBaseMixin):
|
class RoleAdminMixin(PermBaseMixin, _RoleAdminMixin):
|
||||||
permission_classes = (IsOrgAdminOrAppUser,)
|
permission_classes = (IsOrgAdminOrAppUser,)
|
||||||
kwargs: dict
|
|
||||||
|
|
||||||
@lazyproperty
|
|
||||||
def user(self):
|
|
||||||
user_id = self.kwargs.get('pk')
|
|
||||||
return User.objects.get(id=user_id)
|
|
||||||
|
|
||||||
|
|
||||||
class RoleUserMixin(PermBaseMixin):
|
class RoleUserMixin(PermBaseMixin, _RoleUserMixin):
|
||||||
permission_classes = (IsValidUser,)
|
permission_classes = (IsValidUser,)
|
||||||
request: Request
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
with tmp_to_root_org():
|
with tmp_to_root_org():
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
@lazyproperty
|
|
||||||
def user(self):
|
|
||||||
return self.request.user
|
|
||||||
|
@ -143,6 +143,26 @@ class AssetPermission(BasePermission):
|
|||||||
assets = Asset.objects.filter(id__in=asset_ids)
|
assets = Asset.objects.filter(id__in=asset_ids)
|
||||||
return assets
|
return assets
|
||||||
|
|
||||||
|
def users_display(self):
|
||||||
|
names = [user.username for user in self.users.all()]
|
||||||
|
return names
|
||||||
|
|
||||||
|
def user_groups_display(self):
|
||||||
|
names = [group.name for group in self.user_groups.all()]
|
||||||
|
return names
|
||||||
|
|
||||||
|
def assets_display(self):
|
||||||
|
names = [asset.hostname for asset in self.assets.all()]
|
||||||
|
return names
|
||||||
|
|
||||||
|
def system_users_display(self):
|
||||||
|
names = [system_user.name for system_user in self.system_users.all()]
|
||||||
|
return names
|
||||||
|
|
||||||
|
def nodes_display(self):
|
||||||
|
names = [node.full_value for node in self.nodes.all()]
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
class UserAssetGrantedTreeNodeRelation(OrgModelMixin, FamilyMixin, models.JMSBaseModel):
|
class UserAssetGrantedTreeNodeRelation(OrgModelMixin, FamilyMixin, models.JMSBaseModel):
|
||||||
class NodeFrom(ChoiceSet):
|
class NodeFrom(ChoiceSet):
|
||||||
|
@ -20,16 +20,16 @@ class ApplicationPermissionSerializer(BulkOrgResourceModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ApplicationPermission
|
model = ApplicationPermission
|
||||||
mini_fields = ['id', 'name']
|
fields_mini = ['id', 'name']
|
||||||
small_fields = mini_fields + [
|
fields_small = fields_mini + [
|
||||||
'category', 'category_display', 'type', 'type_display', 'is_active', 'is_expired',
|
'category', 'category_display', 'type', 'type_display', 'is_active', 'is_expired',
|
||||||
'is_valid', 'created_by', 'date_created', 'date_expired', 'date_start', 'comment'
|
'is_valid', 'created_by', 'date_created', 'date_expired', 'date_start', 'comment'
|
||||||
]
|
]
|
||||||
m2m_fields = [
|
fields_m2m = [
|
||||||
'users', 'user_groups', 'applications', 'system_users',
|
'users', 'user_groups', 'applications', 'system_users',
|
||||||
'users_amount', 'user_groups_amount', 'applications_amount', 'system_users_amount',
|
'users_amount', 'user_groups_amount', 'applications_amount', 'system_users_amount',
|
||||||
]
|
]
|
||||||
fields = small_fields + m2m_fields
|
fields = fields_small + fields_m2m
|
||||||
read_only_fields = ['created_by', 'date_created']
|
read_only_fields = ['created_by', 'date_created']
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -3,7 +3,9 @@
|
|||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.db.models import Prefetch
|
|
||||||
|
from django.db.models import Prefetch, Q
|
||||||
|
|
||||||
|
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
from perms.models import AssetPermission, Action
|
from perms.models import AssetPermission, Action
|
||||||
@ -41,21 +43,27 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
|
|||||||
actions = ActionsField(required=False, allow_null=True)
|
actions = ActionsField(required=False, allow_null=True)
|
||||||
is_valid = serializers.BooleanField(read_only=True)
|
is_valid = serializers.BooleanField(read_only=True)
|
||||||
is_expired = serializers.BooleanField(read_only=True, label=_('Is expired'))
|
is_expired = serializers.BooleanField(read_only=True, label=_('Is expired'))
|
||||||
|
users_display = serializers.ListField(child=serializers.CharField(), label=_('Users name'), required=False)
|
||||||
|
user_groups_display = serializers.ListField(child=serializers.CharField(), label=_('User groups name'), required=False)
|
||||||
|
assets_display = serializers.ListField(child=serializers.CharField(), label=_('Assets name'), required=False)
|
||||||
|
nodes_display = serializers.ListField(child=serializers.CharField(), label=_('Nodes name'), required=False)
|
||||||
|
system_users_display = serializers.ListField(child=serializers.CharField(), label=_('System users name'), required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AssetPermission
|
model = AssetPermission
|
||||||
mini_fields = ['id', 'name']
|
fields_mini = ['id', 'name']
|
||||||
small_fields = mini_fields + [
|
fields_small = fields_mini + [
|
||||||
'is_active', 'is_expired', 'is_valid', 'actions',
|
'is_active', 'is_expired', 'is_valid', 'actions',
|
||||||
'created_by', 'date_created', 'date_expired',
|
'created_by', 'date_created', 'date_expired',
|
||||||
'date_start', 'comment'
|
'date_start', 'comment'
|
||||||
]
|
]
|
||||||
m2m_fields = [
|
fields_m2m = [
|
||||||
'users', 'user_groups', 'assets', 'nodes', 'system_users',
|
'users', 'users_display', 'user_groups', 'user_groups_display', 'assets', 'assets_display',
|
||||||
|
'nodes', 'nodes_display', 'system_users', 'system_users_display',
|
||||||
'users_amount', 'user_groups_amount', 'assets_amount',
|
'users_amount', 'user_groups_amount', 'assets_amount',
|
||||||
'nodes_amount', 'system_users_amount',
|
'nodes_amount', 'system_users_amount',
|
||||||
]
|
]
|
||||||
fields = small_fields + m2m_fields
|
fields = fields_small + fields_m2m
|
||||||
read_only_fields = ['created_by', 'date_created']
|
read_only_fields = ['created_by', 'date_created']
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'is_expired': {'label': _('Is expired')},
|
'is_expired': {'label': _('Is expired')},
|
||||||
@ -71,11 +79,44 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setup_eager_loading(cls, queryset):
|
def setup_eager_loading(cls, queryset):
|
||||||
""" Perform necessary eager loading of data. """
|
""" Perform necessary eager loading of data. """
|
||||||
queryset = queryset.prefetch_related(
|
queryset = queryset.prefetch_related('users', 'user_groups', 'assets', 'nodes', 'system_users')
|
||||||
Prefetch('system_users', queryset=SystemUser.objects.only('id')),
|
|
||||||
Prefetch('user_groups', queryset=UserGroup.objects.only('id')),
|
|
||||||
Prefetch('users', queryset=User.objects.only('id')),
|
|
||||||
Prefetch('assets', queryset=Asset.objects.only('id')),
|
|
||||||
Prefetch('nodes', queryset=Node.objects.only('id'))
|
|
||||||
)
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
# 系统用户是必填项
|
||||||
|
system_users_display = data.pop('system_users_display', '')
|
||||||
|
for i in range(len(system_users_display)):
|
||||||
|
system_user = SystemUser.objects.filter(name=system_users_display[i]).first()
|
||||||
|
if system_user and system_user.id not in data['system_users']:
|
||||||
|
data['system_users'].append(system_user.id)
|
||||||
|
return super().to_internal_value(data)
|
||||||
|
|
||||||
|
def perform_display_create(self, instance, **kwargs):
|
||||||
|
# 用户
|
||||||
|
users_to_set = User.objects.filter(
|
||||||
|
Q(name__in=kwargs.get('users_display')) | Q(username__in=kwargs.get('users_display'))
|
||||||
|
).distinct()
|
||||||
|
instance.users.add(*users_to_set)
|
||||||
|
# 用户组
|
||||||
|
user_groups_to_set = UserGroup.objects.filter(name__in=kwargs.get('user_groups_display')).distinct()
|
||||||
|
instance.user_groups.add(*user_groups_to_set)
|
||||||
|
# 资产
|
||||||
|
assets_to_set = Asset.objects.filter(
|
||||||
|
Q(ip__in=kwargs.get('assets_display')) | Q(hostname__in=kwargs.get('assets_display'))
|
||||||
|
).distinct()
|
||||||
|
instance.assets.add(*assets_to_set)
|
||||||
|
# 节点
|
||||||
|
nodes_to_set = Node.objects.filter(full_value__in=kwargs.get('nodes_display')).distinct()
|
||||||
|
instance.nodes.add(*nodes_to_set)
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
display = {
|
||||||
|
'users_display' : validated_data.pop('users_display', ''),
|
||||||
|
'user_groups_display' : validated_data.pop('user_groups_display', ''),
|
||||||
|
'assets_display' : validated_data.pop('assets_display', ''),
|
||||||
|
'nodes_display' : validated_data.pop('nodes_display', '')
|
||||||
|
}
|
||||||
|
instance = super().create(validated_data)
|
||||||
|
self.perform_display_create(instance, **display)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
@ -6,6 +8,58 @@ from perms.models import ApplicationPermission
|
|||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_all_app_perm_ids(user) -> set:
|
||||||
|
app_perm_ids = set()
|
||||||
|
user_perm_id = ApplicationPermission.users.through.objects \
|
||||||
|
.filter(user_id=user.id) \
|
||||||
|
.values_list('applicationpermission_id', flat=True) \
|
||||||
|
.distinct()
|
||||||
|
app_perm_ids.update(user_perm_id)
|
||||||
|
|
||||||
|
group_ids = user.groups.through.objects \
|
||||||
|
.filter(user_id=user.id) \
|
||||||
|
.values_list('usergroup_id', flat=True) \
|
||||||
|
.distinct()
|
||||||
|
group_ids = list(group_ids)
|
||||||
|
groups_perm_id = ApplicationPermission.user_groups.through.objects \
|
||||||
|
.filter(usergroup_id__in=group_ids) \
|
||||||
|
.values_list('applicationpermission_id', flat=True) \
|
||||||
|
.distinct()
|
||||||
|
app_perm_ids.update(groups_perm_id)
|
||||||
|
|
||||||
|
app_perm_ids = ApplicationPermission.objects.filter(
|
||||||
|
id__in=app_perm_ids).valid().values_list('id', flat=True)
|
||||||
|
app_perm_ids = set(app_perm_ids)
|
||||||
|
return app_perm_ids
|
||||||
|
|
||||||
|
|
||||||
|
def validate_permission(user, application, system_user):
|
||||||
|
app_perm_ids = get_user_all_app_perm_ids(user)
|
||||||
|
app_perm_ids = ApplicationPermission.applications.through.objects.filter(
|
||||||
|
applicationpermission_id__in=app_perm_ids,
|
||||||
|
application_id=application.id
|
||||||
|
).values_list('applicationpermission_id', flat=True)
|
||||||
|
|
||||||
|
app_perm_ids = set(app_perm_ids)
|
||||||
|
|
||||||
|
app_perm_ids = ApplicationPermission.system_users.through.objects.filter(
|
||||||
|
applicationpermission_id__in=app_perm_ids,
|
||||||
|
systemuser_id=system_user.id
|
||||||
|
).values_list('applicationpermission_id', flat=True)
|
||||||
|
|
||||||
|
app_perm_ids = set(app_perm_ids)
|
||||||
|
|
||||||
|
app_perm = ApplicationPermission.objects.filter(
|
||||||
|
id__in=app_perm_ids
|
||||||
|
).order_by('-date_expired').first()
|
||||||
|
|
||||||
|
app_perm: ApplicationPermission
|
||||||
|
if app_perm:
|
||||||
|
return True, app_perm.date_expired.timestamp()
|
||||||
|
else:
|
||||||
|
return False, time.time()
|
||||||
|
|
||||||
|
|
||||||
def get_application_system_user_ids(user, application):
|
def get_application_system_user_ids(user, application):
|
||||||
queryset = ApplicationPermission.objects.valid()\
|
queryset = ApplicationPermission.objects.valid()\
|
||||||
.filter(
|
.filter(
|
||||||
|
@ -1,15 +1,61 @@
|
|||||||
|
import time
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from perms.models import AssetPermission
|
from perms.models import AssetPermission, Action
|
||||||
from perms.hands import Asset, User, UserGroup, SystemUser
|
from perms.hands import Asset, User, UserGroup, SystemUser, Node
|
||||||
from perms.utils.asset.user_permission import get_user_all_asset_perm_ids
|
from perms.utils.asset.user_permission import get_user_all_asset_perm_ids
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_permission(user, asset, system_user, action_name):
|
||||||
|
|
||||||
|
if not system_user.protocol in asset.protocols_as_dict.keys():
|
||||||
|
return False, time.time()
|
||||||
|
|
||||||
|
asset_perm_ids = get_user_all_asset_perm_ids(user)
|
||||||
|
|
||||||
|
asset_perm_ids_from_asset = AssetPermission.assets.through.objects.filter(
|
||||||
|
assetpermission_id__in=asset_perm_ids,
|
||||||
|
asset_id=asset.id
|
||||||
|
).values_list('assetpermission_id', flat=True)
|
||||||
|
|
||||||
|
nodes = asset.get_nodes()
|
||||||
|
node_keys = set()
|
||||||
|
for node in nodes:
|
||||||
|
ancestor_keys = node.get_ancestor_keys(with_self=True)
|
||||||
|
node_keys.update(ancestor_keys)
|
||||||
|
node_ids = Node.objects.filter(key__in=node_keys).values_list('id', flat=True)
|
||||||
|
|
||||||
|
node_ids = set(node_ids)
|
||||||
|
|
||||||
|
asset_perm_ids_from_node = AssetPermission.nodes.through.objects.filter(
|
||||||
|
assetpermission_id__in=asset_perm_ids,
|
||||||
|
node_id__in=node_ids
|
||||||
|
).values_list('assetpermission_id', flat=True)
|
||||||
|
|
||||||
|
asset_perm_ids = {*asset_perm_ids_from_asset, *asset_perm_ids_from_node}
|
||||||
|
|
||||||
|
asset_perm_ids = AssetPermission.system_users.through.objects.filter(
|
||||||
|
assetpermission_id__in=asset_perm_ids,
|
||||||
|
systemuser_id=system_user.id
|
||||||
|
).values_list('assetpermission_id', flat=True)
|
||||||
|
|
||||||
|
asset_perm_ids = set(asset_perm_ids)
|
||||||
|
|
||||||
|
asset_perms = AssetPermission.objects.filter(
|
||||||
|
id__in=asset_perm_ids
|
||||||
|
).order_by('-date_expired')
|
||||||
|
|
||||||
|
for asset_perm in asset_perms:
|
||||||
|
if action_name in Action.value_to_choices(asset_perm.actions):
|
||||||
|
return True, asset_perm.date_expired.timestamp()
|
||||||
|
return False, time.time()
|
||||||
|
|
||||||
|
|
||||||
def get_asset_system_user_ids_with_actions(asset_perm_ids, asset: Asset):
|
def get_asset_system_user_ids_with_actions(asset_perm_ids, asset: Asset):
|
||||||
nodes = asset.get_nodes()
|
nodes = asset.get_nodes()
|
||||||
node_keys = set()
|
node_keys = set()
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
from .common import *
|
from .common import *
|
||||||
from .ldap import *
|
from .ldap import *
|
||||||
|
from .wecom import *
|
||||||
|
from .dingtalk import *
|
||||||
|
@ -112,6 +112,7 @@ class PublicSettingApi(generics.RetrieveAPIView):
|
|||||||
"LOGIN_CONFIRM_ENABLE": settings.LOGIN_CONFIRM_ENABLE,
|
"LOGIN_CONFIRM_ENABLE": settings.LOGIN_CONFIRM_ENABLE,
|
||||||
"SECURITY_VIEW_AUTH_NEED_MFA": settings.SECURITY_VIEW_AUTH_NEED_MFA,
|
"SECURITY_VIEW_AUTH_NEED_MFA": settings.SECURITY_VIEW_AUTH_NEED_MFA,
|
||||||
"SECURITY_MFA_VERIFY_TTL": settings.SECURITY_MFA_VERIFY_TTL,
|
"SECURITY_MFA_VERIFY_TTL": settings.SECURITY_MFA_VERIFY_TTL,
|
||||||
|
"OLD_PASSWORD_HISTORY_LIMIT_COUNT": settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT,
|
||||||
"SECURITY_COMMAND_EXECUTION": settings.SECURITY_COMMAND_EXECUTION,
|
"SECURITY_COMMAND_EXECUTION": settings.SECURITY_COMMAND_EXECUTION,
|
||||||
"SECURITY_PASSWORD_EXPIRATION_TIME": settings.SECURITY_PASSWORD_EXPIRATION_TIME,
|
"SECURITY_PASSWORD_EXPIRATION_TIME": settings.SECURITY_PASSWORD_EXPIRATION_TIME,
|
||||||
"XPACK_LICENSE_IS_VALID": has_valid_xpack_license(),
|
"XPACK_LICENSE_IS_VALID": has_valid_xpack_license(),
|
||||||
@ -124,7 +125,9 @@ class PublicSettingApi(generics.RetrieveAPIView):
|
|||||||
'SECURITY_PASSWORD_LOWER_CASE': settings.SECURITY_PASSWORD_LOWER_CASE,
|
'SECURITY_PASSWORD_LOWER_CASE': settings.SECURITY_PASSWORD_LOWER_CASE,
|
||||||
'SECURITY_PASSWORD_NUMBER': settings.SECURITY_PASSWORD_NUMBER,
|
'SECURITY_PASSWORD_NUMBER': settings.SECURITY_PASSWORD_NUMBER,
|
||||||
'SECURITY_PASSWORD_SPECIAL_CHAR': settings.SECURITY_PASSWORD_SPECIAL_CHAR,
|
'SECURITY_PASSWORD_SPECIAL_CHAR': settings.SECURITY_PASSWORD_SPECIAL_CHAR,
|
||||||
}
|
},
|
||||||
|
"AUTH_WECOM": settings.AUTH_WECOM,
|
||||||
|
"AUTH_DINGTALK": settings.AUTH_DINGTALK,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return instance
|
return instance
|
||||||
@ -140,6 +143,8 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
|
|||||||
'ldap': serializers.LDAPSettingSerializer,
|
'ldap': serializers.LDAPSettingSerializer,
|
||||||
'email': serializers.EmailSettingSerializer,
|
'email': serializers.EmailSettingSerializer,
|
||||||
'email_content': serializers.EmailContentSettingSerializer,
|
'email_content': serializers.EmailContentSettingSerializer,
|
||||||
|
'wecom': serializers.WeComSettingSerializer,
|
||||||
|
'dingtalk': serializers.DingTalkSettingSerializer,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
@ -162,6 +167,8 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
|
|||||||
category = self.request.query_params.get('category', '')
|
category = self.request.query_params.get('category', '')
|
||||||
for name, value in serializer.validated_data.items():
|
for name, value in serializer.validated_data.items():
|
||||||
encrypted = name in encrypted_items
|
encrypted = name in encrypted_items
|
||||||
|
if encrypted and value in ['', None]:
|
||||||
|
continue
|
||||||
data.append({
|
data.append({
|
||||||
'name': name, 'value': value,
|
'name': name, 'value': value,
|
||||||
'encrypted': encrypted, 'category': category
|
'encrypted': encrypted, 'category': category
|
||||||
|
38
apps/settings/api/dingtalk.py
Normal file
38
apps/settings/api/dingtalk.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import requests
|
||||||
|
|
||||||
|
from rest_framework.views import Response
|
||||||
|
from rest_framework.generics import GenericAPIView
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from common.permissions import IsSuperUser
|
||||||
|
from common.message.backends.dingtalk import URL
|
||||||
|
|
||||||
|
from .. import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class DingTalkTestingAPI(GenericAPIView):
|
||||||
|
permission_classes = (IsSuperUser,)
|
||||||
|
serializer_class = serializers.DingTalkSettingSerializer
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
serializer = self.serializer_class(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
dingtalk_appkey = serializer.validated_data['DINGTALK_APPKEY']
|
||||||
|
dingtalk_agentid = serializer.validated_data['DINGTALK_AGENTID']
|
||||||
|
dingtalk_appsecret = serializer.validated_data['DINGTALK_APPSECRET']
|
||||||
|
|
||||||
|
try:
|
||||||
|
params = {'appkey': dingtalk_appkey, 'appsecret': dingtalk_appsecret}
|
||||||
|
resp = requests.get(url=URL.GET_TOKEN, params=params)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
return Response(status=400, data={'error': resp.json()})
|
||||||
|
|
||||||
|
data = resp.json()
|
||||||
|
errcode = data['errcode']
|
||||||
|
if errcode != 0:
|
||||||
|
return Response(status=400, data={'error': data['errmsg']})
|
||||||
|
|
||||||
|
return Response(status=200, data={'msg': _('OK')})
|
||||||
|
except Exception as e:
|
||||||
|
return Response(status=400, data={'error': str(e)})
|
38
apps/settings/api/wecom.py
Normal file
38
apps/settings/api/wecom.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import requests
|
||||||
|
|
||||||
|
from rest_framework.views import Response
|
||||||
|
from rest_framework.generics import GenericAPIView
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from common.permissions import IsSuperUser
|
||||||
|
from common.message.backends.wecom import URL
|
||||||
|
|
||||||
|
from .. import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class WeComTestingAPI(GenericAPIView):
|
||||||
|
permission_classes = (IsSuperUser,)
|
||||||
|
serializer_class = serializers.WeComSettingSerializer
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
serializer = self.serializer_class(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
wecom_corpid = serializer.validated_data['WECOM_CORPID']
|
||||||
|
wecom_agentid = serializer.validated_data['WECOM_AGENTID']
|
||||||
|
wecom_corpsecret = serializer.validated_data['WECOM_SECRET']
|
||||||
|
|
||||||
|
try:
|
||||||
|
params = {'corpid': wecom_corpid, 'corpsecret': wecom_corpsecret}
|
||||||
|
resp = requests.get(url=URL.GET_TOKEN, params=params)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
return Response(status=400, data={'error': resp.json()})
|
||||||
|
|
||||||
|
data = resp.json()
|
||||||
|
errcode = data['errcode']
|
||||||
|
if errcode != 0:
|
||||||
|
return Response(status=400, data={'error': data['errmsg']})
|
||||||
|
|
||||||
|
return Response(status=200, data={'msg': _('OK')})
|
||||||
|
except Exception as e:
|
||||||
|
return Response(status=400, data={'error': str(e)})
|
@ -6,7 +6,7 @@ from rest_framework import serializers
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
'BasicSettingSerializer', 'EmailSettingSerializer', 'EmailContentSettingSerializer',
|
'BasicSettingSerializer', 'EmailSettingSerializer', 'EmailContentSettingSerializer',
|
||||||
'LDAPSettingSerializer', 'TerminalSettingSerializer', 'SecuritySettingSerializer',
|
'LDAPSettingSerializer', 'TerminalSettingSerializer', 'SecuritySettingSerializer',
|
||||||
'SettingsSerializer'
|
'SettingsSerializer', 'WeComSettingSerializer', 'DingTalkSettingSerializer',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -121,7 +121,11 @@ class TerminalSettingSerializer(serializers.Serializer):
|
|||||||
('50', '50'),
|
('50', '50'),
|
||||||
)
|
)
|
||||||
TERMINAL_PASSWORD_AUTH = serializers.BooleanField(required=False, label=_('Password auth'))
|
TERMINAL_PASSWORD_AUTH = serializers.BooleanField(required=False, label=_('Password auth'))
|
||||||
TERMINAL_PUBLIC_KEY_AUTH = serializers.BooleanField(required=False, label=_('Public key auth'))
|
TERMINAL_PUBLIC_KEY_AUTH = serializers.BooleanField(
|
||||||
|
required=False, label=_('Public key auth'),
|
||||||
|
help_text=_('Tips: If use other auth method, like AD/LDAP, you should disable this to '
|
||||||
|
'avoid being able to log in after deleting')
|
||||||
|
)
|
||||||
TERMINAL_ASSET_LIST_SORT_BY = serializers.ChoiceField(SORT_BY_CHOICES, required=False, label=_('List sort by'))
|
TERMINAL_ASSET_LIST_SORT_BY = serializers.ChoiceField(SORT_BY_CHOICES, required=False, label=_('List sort by'))
|
||||||
TERMINAL_ASSET_LIST_PAGE_SIZE = serializers.ChoiceField(PAGE_SIZE_CHOICES, required=False, label=_('List page size'))
|
TERMINAL_ASSET_LIST_PAGE_SIZE = serializers.ChoiceField(PAGE_SIZE_CHOICES, required=False, label=_('List page size'))
|
||||||
TERMINAL_SESSION_KEEP_DURATION = serializers.IntegerField(
|
TERMINAL_SESSION_KEEP_DURATION = serializers.IntegerField(
|
||||||
@ -163,6 +167,11 @@ class SecuritySettingSerializer(serializers.Serializer):
|
|||||||
label=_('User password expiration'),
|
label=_('User password expiration'),
|
||||||
help_text=_('Tip: (unit: day) If the user does not update the password during the time, the user password will expire failure;The password expiration reminder mail will be automatic sent to the user by system within 5 days (daily) before the password expires')
|
help_text=_('Tip: (unit: day) If the user does not update the password during the time, the user password will expire failure;The password expiration reminder mail will be automatic sent to the user by system within 5 days (daily) before the password expires')
|
||||||
)
|
)
|
||||||
|
OLD_PASSWORD_HISTORY_LIMIT_COUNT = serializers.IntegerField(
|
||||||
|
min_value=0, max_value=99999, required=True,
|
||||||
|
label=_('Number of repeated historical passwords'),
|
||||||
|
help_text=_('Tip: When the user resets the password, it cannot be the previous n historical passwords of the user (the value of n here is the value filled in the input box)')
|
||||||
|
)
|
||||||
SECURITY_PASSWORD_MIN_LENGTH = serializers.IntegerField(
|
SECURITY_PASSWORD_MIN_LENGTH = serializers.IntegerField(
|
||||||
min_value=6, max_value=30, required=True,
|
min_value=6, max_value=30, required=True,
|
||||||
label=_('Password minimum length')
|
label=_('Password minimum length')
|
||||||
@ -180,13 +189,29 @@ class SecuritySettingSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WeComSettingSerializer(serializers.Serializer):
|
||||||
|
WECOM_CORPID = serializers.CharField(max_length=256, required=True, label=_('Corporation ID(corpid)'))
|
||||||
|
WECOM_AGENTID = serializers.CharField(max_length=256, required=True, label=_("Agent ID(agentid)"))
|
||||||
|
WECOM_SECRET = serializers.CharField(max_length=256, required=True, label=_("Secret(secret)"), write_only=True)
|
||||||
|
AUTH_WECOM = serializers.BooleanField(default=False, label=_('Enable WeCom Auth'))
|
||||||
|
|
||||||
|
|
||||||
|
class DingTalkSettingSerializer(serializers.Serializer):
|
||||||
|
DINGTALK_AGENTID = serializers.CharField(max_length=256, required=True, label=_("AgentId"))
|
||||||
|
DINGTALK_APPKEY = serializers.CharField(max_length=256, required=True, label=_("AppKey"))
|
||||||
|
DINGTALK_APPSECRET = serializers.CharField(max_length=256, required=False, label=_("AppSecret"), write_only=True)
|
||||||
|
AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('Enable DingTalk Auth'))
|
||||||
|
|
||||||
|
|
||||||
class SettingsSerializer(
|
class SettingsSerializer(
|
||||||
BasicSettingSerializer,
|
BasicSettingSerializer,
|
||||||
EmailSettingSerializer,
|
EmailSettingSerializer,
|
||||||
EmailContentSettingSerializer,
|
EmailContentSettingSerializer,
|
||||||
LDAPSettingSerializer,
|
LDAPSettingSerializer,
|
||||||
TerminalSettingSerializer,
|
TerminalSettingSerializer,
|
||||||
SecuritySettingSerializer
|
SecuritySettingSerializer,
|
||||||
|
WeComSettingSerializer,
|
||||||
|
DingTalkSettingSerializer,
|
||||||
):
|
):
|
||||||
|
|
||||||
# encrypt_fields 现在使用 write_only 来判断了
|
# encrypt_fields 现在使用 write_only 来判断了
|
||||||
|
@ -13,6 +13,8 @@ urlpatterns = [
|
|||||||
path('ldap/users/', api.LDAPUserListApi.as_view(), name='ldap-user-list'),
|
path('ldap/users/', api.LDAPUserListApi.as_view(), name='ldap-user-list'),
|
||||||
path('ldap/users/import/', api.LDAPUserImportAPI.as_view(), name='ldap-user-import'),
|
path('ldap/users/import/', api.LDAPUserImportAPI.as_view(), name='ldap-user-import'),
|
||||||
path('ldap/cache/refresh/', api.LDAPCacheRefreshAPI.as_view(), name='ldap-cache-refresh'),
|
path('ldap/cache/refresh/', api.LDAPCacheRefreshAPI.as_view(), name='ldap-cache-refresh'),
|
||||||
|
path('wecom/testing/', api.WeComTestingAPI.as_view(), name='wecom-testing'),
|
||||||
|
path('dingtalk/testing/', api.DingTalkTestingAPI.as_view(), name='dingtalk-testing'),
|
||||||
|
|
||||||
path('setting/', api.SettingsApi.as_view(), name='settings-setting'),
|
path('setting/', api.SettingsApi.as_view(), name='settings-setting'),
|
||||||
path('public/', api.PublicSettingApi.as_view(), name='public-setting'),
|
path('public/', api.PublicSettingApi.as_view(), name='public-setting'),
|
||||||
|
BIN
apps/static/img/login_dingtalk_log.png
Normal file
BIN
apps/static/img/login_dingtalk_log.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
BIN
apps/static/img/login_wecom_log.png
Normal file
BIN
apps/static/img/login_wecom_log.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 134 KiB |
@ -4,14 +4,6 @@
|
|||||||
{% block html_title %} {{ title }} {% endblock %}
|
{% block html_title %} {{ title }} {% endblock %}
|
||||||
{% block title %} {{ title }}{% endblock %}
|
{% block title %} {{ title }}{% endblock %}
|
||||||
|
|
||||||
{% block custom_head_css_js %}
|
|
||||||
<style>
|
|
||||||
.passwordBox {
|
|
||||||
max-width: 660px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div>
|
<div>
|
||||||
{% if errors %}
|
{% if errors %}
|
||||||
@ -30,8 +22,21 @@
|
|||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-3">
|
{% if has_cancel %}
|
||||||
<a href="{{ redirect_url }}" class="btn btn-primary block full-width m-b">{% trans 'Return' %}</a>
|
<div class="col-lg-2">
|
||||||
|
<a href="{{ cancel_url }}" class="btn btn-default block full-width m-b">
|
||||||
|
{% trans 'Cancel' %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="col-lg-2">
|
||||||
|
<a href="{{ redirect_url }}" class="btn btn-primary block full-width m-b">
|
||||||
|
{% if confirm_button %}
|
||||||
|
{{ confirm_button }}
|
||||||
|
{% else %}
|
||||||
|
{% trans 'Go' %}
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,6 +17,7 @@ from terminal.filters import CommandFilter
|
|||||||
from orgs.utils import current_org
|
from orgs.utils import current_org
|
||||||
from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsAppUser
|
from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsAppUser
|
||||||
from common.const.http import GET
|
from common.const.http import GET
|
||||||
|
from common.drf.api import JMSBulkModelViewSet
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from terminal.utils import send_command_alert_mail
|
from terminal.utils import send_command_alert_mail
|
||||||
from terminal.serializers import InsecureCommandAlertSerializer
|
from terminal.serializers import InsecureCommandAlertSerializer
|
||||||
@ -94,7 +95,7 @@ class CommandQueryMixin:
|
|||||||
return date_from_st, date_to_st
|
return date_from_st, date_to_st
|
||||||
|
|
||||||
|
|
||||||
class CommandViewSet(viewsets.ModelViewSet):
|
class CommandViewSet(JMSBulkModelViewSet):
|
||||||
"""接受app发送来的command log, 格式如下
|
"""接受app发送来的command log, 格式如下
|
||||||
{
|
{
|
||||||
"user": "admin",
|
"user": "admin",
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class TerminalConfig(AppConfig):
|
class TerminalConfig(AppConfig):
|
||||||
name = 'terminal'
|
name = 'terminal'
|
||||||
|
verbose_name = _('Terminal')
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from . import signals_handler
|
from . import signals_handler
|
||||||
|
@ -7,6 +7,7 @@ import pytz
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.db.models import QuerySet as DJQuerySet
|
from django.db.models import QuerySet as DJQuerySet
|
||||||
from elasticsearch import Elasticsearch
|
from elasticsearch import Elasticsearch
|
||||||
from elasticsearch.helpers import bulk
|
from elasticsearch.helpers import bulk
|
||||||
@ -14,12 +15,18 @@ from elasticsearch.exceptions import RequestError
|
|||||||
|
|
||||||
from common.utils.common import lazyproperty
|
from common.utils.common import lazyproperty
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
|
from common.exceptions import JMSException
|
||||||
from .models import AbstractSessionCommand
|
from .models import AbstractSessionCommand
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidElasticsearch(JMSException):
|
||||||
|
default_code = 'invalid_elasticsearch'
|
||||||
|
default_detail = _('Invalid elasticsearch config')
|
||||||
|
|
||||||
|
|
||||||
class CommandStore():
|
class CommandStore():
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
hosts = config.get("HOSTS")
|
hosts = config.get("HOSTS")
|
||||||
@ -27,12 +34,14 @@ class CommandStore():
|
|||||||
self.index = config.get("INDEX") or 'jumpserver'
|
self.index = config.get("INDEX") or 'jumpserver'
|
||||||
self.doc_type = config.get("DOC_TYPE") or 'command_store'
|
self.doc_type = config.get("DOC_TYPE") or 'command_store'
|
||||||
|
|
||||||
ignore_verify_certs = kwargs.pop('ignore_verify_certs', False)
|
ignore_verify_certs = kwargs.pop('IGNORE_VERIFY_CERTS', False)
|
||||||
if ignore_verify_certs:
|
if ignore_verify_certs:
|
||||||
kwargs['verify_certs'] = None
|
kwargs['verify_certs'] = None
|
||||||
self.es = Elasticsearch(hosts=hosts, max_retries=0, **kwargs)
|
self.es = Elasticsearch(hosts=hosts, max_retries=0, **kwargs)
|
||||||
|
|
||||||
def pre_use_check(self):
|
def pre_use_check(self):
|
||||||
|
if not self.ping(timeout=3):
|
||||||
|
raise InvalidElasticsearch
|
||||||
self._ensure_index_exists()
|
self._ensure_index_exists()
|
||||||
|
|
||||||
def _ensure_index_exists(self):
|
def _ensure_index_exists(self):
|
||||||
|
@ -11,6 +11,7 @@ from django.core.files.storage import default_storage
|
|||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
|
||||||
from assets.models import Asset
|
from assets.models import Asset
|
||||||
|
from users.models import User
|
||||||
from orgs.mixins.models import OrgModelMixin
|
from orgs.mixins.models import OrgModelMixin
|
||||||
from common.db.models import ChoiceSet
|
from common.db.models import ChoiceSet
|
||||||
from ..backends import get_multi_command_storage
|
from ..backends import get_multi_command_storage
|
||||||
@ -79,6 +80,10 @@ class Session(OrgModelMixin):
|
|||||||
def asset_obj(self):
|
def asset_obj(self):
|
||||||
return Asset.objects.get(id=self.asset_id)
|
return Asset.objects.get(id=self.asset_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_obj(self):
|
||||||
|
return User.objects.get(id=self.user_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _date_start_first_has_replay_rdp_session(self):
|
def _date_start_first_has_replay_rdp_session(self):
|
||||||
if self.__class__._DATE_START_FIRST_HAS_REPLAY_RDP_SESSION is None:
|
if self.__class__._DATE_START_FIRST_HAS_REPLAY_RDP_SESSION is None:
|
||||||
|
@ -17,14 +17,17 @@ class SessionSerializer(BulkOrgResourceModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Session
|
model = Session
|
||||||
list_serializer_class = AdaptedBulkListSerializer
|
list_serializer_class = AdaptedBulkListSerializer
|
||||||
fields = [
|
fields_mini = ["id"]
|
||||||
"id", "user", "asset", "system_user",
|
fields_small = fields_mini + [
|
||||||
|
"user", "asset", "system_user",
|
||||||
"user_id", "asset_id", "system_user_id",
|
"user_id", "asset_id", "system_user_id",
|
||||||
"login_from", "login_from_display", "remote_addr",
|
"login_from", "login_from_display", "remote_addr", "protocol",
|
||||||
"is_success", "is_finished", "has_replay", "can_replay",
|
"is_success", "is_finished", "has_replay",
|
||||||
"can_join", "can_terminate", "protocol", "date_start", "date_end",
|
"date_start", "date_end",
|
||||||
"terminal",
|
|
||||||
]
|
]
|
||||||
|
fields_fk = ["terminal",]
|
||||||
|
fields_custom = ["can_replay", "can_join", "can_terminate",]
|
||||||
|
fields = fields_small + fields_fk + fields_custom
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"protocol": {'label': _('Protocol')},
|
"protocol": {'label': _('Protocol')},
|
||||||
'user_id': {'label': _('User ID')},
|
'user_id': {'label': _('User ID')},
|
||||||
|
@ -181,9 +181,9 @@ class CommandStorageTypeESSerializer(serializers.Serializer):
|
|||||||
max_length=1024, default='jumpserver', label=_('Index'), allow_null=True
|
max_length=1024, default='jumpserver', label=_('Index'), allow_null=True
|
||||||
)
|
)
|
||||||
DOC_TYPE = ReadableHiddenField(default='command', label=_('Doc type'), allow_null=True)
|
DOC_TYPE = ReadableHiddenField(default='command', label=_('Doc type'), allow_null=True)
|
||||||
ignore_verify_certs = serializers.BooleanField(
|
IGNORE_VERIFY_CERTS = serializers.BooleanField(
|
||||||
default=False, label=_('Ignore Certificate Verification'),
|
default=False, label=_('Ignore Certificate Verification'),
|
||||||
source='OTHER.ignore_verify_certs', allow_null=True,
|
source='OTHER.IGNORE_VERIFY_CERTS', allow_null=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# mapping
|
# mapping
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user