mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-07-14 23:31:09 +00:00
commit
7d51d8c570
@ -6,9 +6,10 @@
|
||||
|
||||
- [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.utils import reverse, lazyproperty
|
||||
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 .. import serializers
|
||||
|
||||
@ -48,7 +48,7 @@ class LoginAssetCheckAPI(CreateAPIView):
|
||||
org_id=self.serializer.org.id
|
||||
)
|
||||
confirm_status_url = reverse(
|
||||
view_name='acls:login-asset-confirm-status',
|
||||
view_name='api-acls:login-asset-confirm-status',
|
||||
kwargs={'pk': str(ticket.id)}
|
||||
)
|
||||
ticket_detail_url = reverse(
|
||||
@ -72,34 +72,6 @@ class LoginAssetCheckAPI(CreateAPIView):
|
||||
return serializer
|
||||
|
||||
|
||||
class LoginAssetConfirmStatusAPI(RetrieveDestroyAPIView):
|
||||
permission_classes = (IsAppUser, )
|
||||
class LoginAssetConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
|
||||
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:
|
||||
ordering = ('priority', '-date_updated', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def action_reject(self):
|
||||
return self.action == self.ActionChoices.reject
|
||||
|
@ -38,6 +38,9 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
|
||||
unique_together = ('name', 'org_id')
|
||||
ordering = ('priority', '-date_updated', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def filter(cls, user, asset, system_user, action):
|
||||
queryset = cls.objects.filter(action=action)
|
||||
|
@ -35,10 +35,15 @@ class LoginACLSerializer(BulkModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = LoginACL
|
||||
fields = [
|
||||
'id', 'name', 'priority', 'ip_group', 'user', 'user_display', 'action',
|
||||
'action_display', 'is_active', 'comment', 'created_by', 'date_created', 'date_updated'
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'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 = {
|
||||
'priority': {'default': 50},
|
||||
'is_active': {'default': True},
|
||||
|
@ -54,7 +54,7 @@ class LoginAssetACLSystemUsersSerializer(serializers.Serializer):
|
||||
protocol_group = serializers.ListField(
|
||||
default=['*'], child=serializers.CharField(max_length=16), label=_('Protocol'),
|
||||
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:
|
||||
model = models.LoginAssetACL
|
||||
fields = [
|
||||
'id', 'name', 'priority', 'users', 'system_users', 'assets', 'action', 'action_display',
|
||||
'is_active', 'comment', 'reviewers', 'reviewers_amount', 'created_by', 'date_created',
|
||||
'date_updated', 'org_id'
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'users', 'system_users', 'assets',
|
||||
'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 = {
|
||||
"reviewers": {'allow_null': False, 'required': True},
|
||||
'priority': {'default': 50},
|
||||
|
@ -49,10 +49,14 @@ class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSeri
|
||||
|
||||
class Meta:
|
||||
model = models.Application
|
||||
fields = [
|
||||
'id', 'name', 'category', 'category_display', 'type', 'type_display', 'attrs',
|
||||
'domain', 'created_by', 'date_created', 'date_updated', 'comment'
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'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 = [
|
||||
'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.mixins import CommonApiMixin
|
||||
from ..backends import AssetUserManager
|
||||
from ..models import Asset, Node, SystemUser
|
||||
from ..models import Node
|
||||
from .. import serializers
|
||||
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)
|
||||
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):
|
||||
manager = AssetUserManager()
|
||||
manager.delete(instance)
|
||||
|
@ -1,15 +1,25 @@
|
||||
# -*- 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 common.utils import reverse
|
||||
from common.utils import lazyproperty
|
||||
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 .. import serializers
|
||||
|
||||
|
||||
__all__ = ['CommandFilterViewSet', 'CommandFilterRuleViewSet']
|
||||
__all__ = [
|
||||
'CommandFilterViewSet', 'CommandFilterRuleViewSet', 'CommandConfirmAPI',
|
||||
'CommandConfirmStatusAPI'
|
||||
]
|
||||
|
||||
|
||||
class CommandFilterViewSet(OrgBulkModelViewSet):
|
||||
@ -35,3 +45,50 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet):
|
||||
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():
|
||||
error = _("You can't delete the root node ({})".format(node.value))
|
||||
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
|
||||
if node.has_children_or_has_assets():
|
||||
error = _("Deletion failed and the node contains children or assets")
|
||||
if node.has_offspring_assets():
|
||||
error = _("Deletion failed and the node contains assets")
|
||||
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
|
||||
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:
|
||||
from .node import Node
|
||||
root = Node.org_root()
|
||||
return root
|
||||
return Node.objects.filter(id=root.id)
|
||||
except:
|
||||
return None
|
||||
|
||||
|
@ -41,11 +41,12 @@ class CommandFilterRule(OrgModelMixin):
|
||||
(TYPE_COMMAND, _('Command')),
|
||||
)
|
||||
|
||||
ACTION_DENY, ACTION_ALLOW, ACTION_UNKNOWN = range(3)
|
||||
ACTION_CHOICES = (
|
||||
(ACTION_DENY, _('Deny')),
|
||||
(ACTION_ALLOW, _('Allow')),
|
||||
)
|
||||
ACTION_UNKNOWN = 10
|
||||
|
||||
class ActionChoices(models.IntegerChoices):
|
||||
deny = 0, _('Deny')
|
||||
allow = 1, _('Allow')
|
||||
confirm = 2, _('Reconfirm')
|
||||
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
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"),
|
||||
validators=[MinValueValidator(1), MaxValueValidator(100)])
|
||||
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"))
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
date_updated = models.DateTimeField(auto_now=True)
|
||||
@ -89,10 +96,32 @@ class CommandFilterRule(OrgModelMixin):
|
||||
if not found:
|
||||
return self.ACTION_UNKNOWN, ''
|
||||
|
||||
if self.action == self.ACTION_ALLOW:
|
||||
return self.ACTION_ALLOW, found.group()
|
||||
if self.action == self.ActionChoices.allow:
|
||||
return self.ActionChoices.allow, found.group()
|
||||
else:
|
||||
return self.ACTION_DENY, found.group()
|
||||
return self.ActionChoices.deny, found.group()
|
||||
|
||||
def __str__(self):
|
||||
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):
|
||||
def delete(self):
|
||||
raise NotImplementedError
|
||||
pass
|
||||
|
||||
|
||||
class FamilyMixin:
|
||||
@ -622,14 +621,14 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
|
||||
tree_node = TreeNode(**data)
|
||||
return tree_node
|
||||
|
||||
def has_children_or_has_assets(self):
|
||||
if self.children or self.get_assets().exists():
|
||||
return True
|
||||
return False
|
||||
def has_offspring_assets(self):
|
||||
# 拥有后代资产
|
||||
return self.get_all_assets().exists()
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
if self.has_children_or_has_assets():
|
||||
if self.has_offspring_assets():
|
||||
return
|
||||
self.all_children.delete()
|
||||
return super().delete(using=using, keep_parents=keep_parents)
|
||||
|
||||
def update_child_full_value(self):
|
||||
|
@ -196,9 +196,9 @@ class SystemUser(BaseUser):
|
||||
def is_command_can_run(self, command):
|
||||
for rule in self.cmd_filter_rules:
|
||||
action, matched_cmd = rule.match(command)
|
||||
if action == rule.ACTION_ALLOW:
|
||||
if action == rule.ActionChoices.allow:
|
||||
return True, None
|
||||
elif action == rule.ACTION_DENY:
|
||||
elif action == rule.ActionChoices.deny:
|
||||
return False, matched_cmd
|
||||
return True, None
|
||||
|
||||
|
@ -16,10 +16,14 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = AdminUser
|
||||
fields = [
|
||||
'id', 'name', 'username', 'password', 'private_key', 'public_key',
|
||||
'comment', 'assets_amount', 'date_created', 'date_updated', 'created_by',
|
||||
fields_mini = ['id', 'name', 'username']
|
||||
fields_write_only = ['password', 'private_key', 'public_key']
|
||||
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']
|
||||
|
||||
extra_kwargs = {
|
||||
|
@ -65,7 +65,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
|
||||
platform = serializers.SlugRelatedField(
|
||||
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'))
|
||||
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)
|
||||
|
@ -22,10 +22,11 @@ class AssetUserWriteSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializ
|
||||
class Meta:
|
||||
model = AuthBook
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = [
|
||||
'id', 'username', 'password', 'private_key', "public_key",
|
||||
'asset', 'comment',
|
||||
]
|
||||
fields_mini = ['id', 'username']
|
||||
fields_write_only = ['password', 'private_key', "public_key"]
|
||||
fields_small = fields_mini + fields_write_only + ['comment']
|
||||
fields_fk = ['asset']
|
||||
fields = fields_small + fields_fk
|
||||
extra_kwargs = {
|
||||
'username': {'required': True},
|
||||
'password': {'write_only': True},
|
||||
@ -52,11 +53,15 @@ class AssetUserReadSerializer(AssetUserWriteSerializer):
|
||||
'date_created', 'date_updated',
|
||||
'created_by', 'version',
|
||||
)
|
||||
fields = [
|
||||
'id', 'username', 'password', 'private_key', "public_key",
|
||||
'asset', 'hostname', 'ip', 'backend', 'version',
|
||||
'date_created', "date_updated", 'comment',
|
||||
fields_mini = ['id', 'username']
|
||||
fields_write_only = ['password', 'private_key', "public_key"]
|
||||
fields_small = fields_mini + fields_write_only + [
|
||||
'backend', 'version',
|
||||
'date_created', "date_updated",
|
||||
'comment'
|
||||
]
|
||||
fields_fk = ['asset', 'hostname', 'ip']
|
||||
fields = fields_small + fields_fk
|
||||
extra_kwargs = {
|
||||
'username': {'required': True},
|
||||
'password': {'write_only': True},
|
||||
|
@ -6,6 +6,9 @@ from rest_framework import serializers
|
||||
from common.drf.serializers import AdaptedBulkListSerializer
|
||||
from ..models import CommandFilter, CommandFilterRule, SystemUser
|
||||
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):
|
||||
@ -13,11 +16,16 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer):
|
||||
class Meta:
|
||||
model = CommandFilter
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = [
|
||||
'id', 'name', 'org_id', 'org_name', 'is_active', 'comment',
|
||||
'created_by', 'date_created', 'date_updated', 'rules', 'system_users'
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'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 = {
|
||||
'rules': {'read_only': True},
|
||||
'system_users': {'required': False},
|
||||
@ -34,8 +42,9 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
|
||||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'type', 'type_display', 'content', 'priority',
|
||||
'action', 'action_display',
|
||||
'comment', 'created_by', 'date_created', 'date_updated'
|
||||
'action', 'action_display', 'reviewers',
|
||||
'date_created', 'date_updated',
|
||||
'comment', 'created_by',
|
||||
]
|
||||
fields_fk = ['filter']
|
||||
fields = '__all__'
|
||||
@ -50,3 +59,35 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
|
||||
# msg = _("Content should not be contain: {}").format(invalid_char)
|
||||
# raise serializers.ValidationError(msg)
|
||||
# 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:
|
||||
model = Gateway
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = [
|
||||
'id', 'name', 'ip', 'port', 'protocol', 'username', 'password',
|
||||
'private_key', 'public_key', 'domain', 'is_active', 'date_created',
|
||||
'date_updated', 'created_by', 'comment',
|
||||
fields_mini = ['id', 'name']
|
||||
fields_write_only = [
|
||||
'password', 'private_key', 'public_key',
|
||||
]
|
||||
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 = {
|
||||
'password': {'validators': [NoSpecialChars()]}
|
||||
}
|
||||
|
@ -10,11 +10,14 @@ from ..models import GatheredUser
|
||||
class GatheredUserSerializer(OrgResourceModelSerializerMixin):
|
||||
class Meta:
|
||||
model = GatheredUser
|
||||
fields = [
|
||||
'id', 'asset', 'hostname', 'ip', 'username',
|
||||
'date_last_login', 'ip_last_login',
|
||||
'present', 'date_created', 'date_updated'
|
||||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'username', 'ip_last_login',
|
||||
'present',
|
||||
'date_last_login', 'date_created', 'date_updated'
|
||||
]
|
||||
fields_fk = ['asset', 'hostname', 'ip']
|
||||
fields = fields_small + fields_fk
|
||||
read_only_fields = fields
|
||||
extra_kwargs = {
|
||||
'hostname': {'label': _("Hostname")},
|
||||
|
@ -15,10 +15,15 @@ class LabelSerializer(BulkOrgResourceModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Label
|
||||
fields = [
|
||||
'id', 'name', 'value', 'category', 'is_active', 'comment',
|
||||
'date_created', 'asset_count', 'assets', 'category_display'
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'value', 'category', 'category_display',
|
||||
'is_active',
|
||||
'date_created',
|
||||
'comment',
|
||||
]
|
||||
fields_m2m = ['asset_count', 'assets']
|
||||
fields = fields_small + fields_m2m
|
||||
read_only_fields = (
|
||||
'category', 'date_created', 'asset_count',
|
||||
)
|
||||
|
@ -26,16 +26,18 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = [
|
||||
'id', 'name', 'username', 'protocol',
|
||||
'password', 'public_key', 'private_key',
|
||||
'login_mode', 'login_mode_display',
|
||||
'priority', 'username_same_with_user',
|
||||
'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment',
|
||||
'auto_generate_key', 'sftp_root', 'token',
|
||||
'assets_amount', 'date_created', 'date_updated', 'created_by',
|
||||
'home', 'system_groups', 'ad_domain'
|
||||
fields_mini = ['id', 'name', 'username']
|
||||
fields_write_only = ['password', 'public_key', 'private_key']
|
||||
fields_small = fields_mini + fields_write_only + [
|
||||
'protocol', 'login_mode', 'login_mode_display', 'priority',
|
||||
'sudo', 'shell', 'sftp_root', 'token',
|
||||
'home', 'system_groups', 'ad_domain',
|
||||
'username_same_with_user', 'auto_push', 'auto_generate_key',
|
||||
'date_created', 'date_updated',
|
||||
'comment', 'created_by',
|
||||
]
|
||||
fields_m2m = [ 'cmd_filters', 'assets_amount']
|
||||
fields = fields_small + fields_m2m
|
||||
extra_kwargs = {
|
||||
'password': {"write_only": True},
|
||||
'public_key': {"write_only": True},
|
||||
@ -147,17 +149,18 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
class SystemUserListSerializer(SystemUserSerializer):
|
||||
|
||||
class Meta(SystemUserSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'name', 'username', 'protocol',
|
||||
'password', 'public_key', 'private_key',
|
||||
'login_mode', 'login_mode_display',
|
||||
'priority', "username_same_with_user",
|
||||
'auto_push', 'sudo', 'shell', 'comment',
|
||||
"assets_amount", 'home', 'system_groups',
|
||||
'auto_generate_key', 'ad_domain',
|
||||
'sftp_root', 'created_by', 'date_created',
|
||||
'date_updated',
|
||||
fields_mini = ['id', 'name', 'username']
|
||||
fields_write_only = ['password', 'public_key', 'private_key']
|
||||
fields_small = fields_mini + fields_write_only + [
|
||||
'protocol', 'login_mode', 'login_mode_display', 'priority',
|
||||
'sudo', 'shell', 'home', 'system_groups',
|
||||
'ad_domain', 'sftp_root',
|
||||
"username_same_with_user", 'auto_push', 'auto_generate_key',
|
||||
'date_created', 'date_updated',
|
||||
'comment', 'created_by',
|
||||
]
|
||||
fields_m2m = ["assets_amount",]
|
||||
fields = fields_small + fields_m2m
|
||||
extra_kwargs = {
|
||||
'password': {"write_only": True},
|
||||
'public_key': {"write_only": True},
|
||||
@ -178,15 +181,15 @@ class SystemUserListSerializer(SystemUserSerializer):
|
||||
|
||||
class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
|
||||
class Meta(SystemUserSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'name', 'username', 'protocol',
|
||||
'password', 'public_key', 'private_key',
|
||||
'login_mode', 'login_mode_display',
|
||||
'priority', 'username_same_with_user',
|
||||
'auto_push', 'sudo', 'shell', 'comment',
|
||||
'auto_generate_key', 'sftp_root', 'token',
|
||||
'ad_domain',
|
||||
fields_mini = ['id', 'name', 'username']
|
||||
fields_write_only = ['password', 'public_key', 'private_key']
|
||||
fields_small = fields_mini + fields_write_only + [
|
||||
'protocol', 'login_mode', 'login_mode_display', 'priority',
|
||||
'sudo', 'shell', 'ad_domain', 'sftp_root', 'token',
|
||||
"username_same_with_user", 'auto_push', 'auto_generate_key',
|
||||
'comment',
|
||||
]
|
||||
fields = fields_small
|
||||
extra_kwargs = {
|
||||
'nodes_amount': {'label': _('Node')},
|
||||
'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,
|
||||
'state': 'present',
|
||||
'home': system_user.home or Empty,
|
||||
'expires': -1,
|
||||
'groups': groups or Empty,
|
||||
'expires': 99999,
|
||||
'comment': comment
|
||||
}
|
||||
|
||||
|
@ -63,6 +63,9 @@ urlpatterns = [
|
||||
|
||||
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 = [
|
||||
|
@ -16,10 +16,14 @@ class FTPLogSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.FTPLog
|
||||
fields = (
|
||||
'id', 'user', 'remote_addr', 'asset', 'system_user', 'org_id',
|
||||
'operate', 'filename', 'is_success', 'date_start', 'operate_display'
|
||||
)
|
||||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'user', 'remote_addr', 'asset', 'system_user', 'org_id',
|
||||
'operate', 'filename', 'operate_display',
|
||||
'is_success',
|
||||
'date_start',
|
||||
]
|
||||
fields = fields_small
|
||||
|
||||
|
||||
class UserLoginLogSerializer(serializers.ModelSerializer):
|
||||
@ -29,11 +33,14 @@ class UserLoginLogSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.UserLoginLog
|
||||
fields = (
|
||||
'id', 'username', 'type', 'type_display', 'ip', 'city', 'user_agent',
|
||||
'mfa', 'reason', 'status', 'status_display', 'datetime', 'mfa_display',
|
||||
'backend'
|
||||
)
|
||||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'username', 'type', 'type_display', 'ip', 'city', 'user_agent',
|
||||
'mfa', 'mfa_display', 'reason', 'backend',
|
||||
'status', 'status_display',
|
||||
'datetime',
|
||||
]
|
||||
fields = fields_small
|
||||
extra_kwargs = {
|
||||
"user_agent": {'label': _('User agent')}
|
||||
}
|
||||
@ -42,10 +49,13 @@ class UserLoginLogSerializer(serializers.ModelSerializer):
|
||||
class OperateLogSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = models.OperateLog
|
||||
fields = (
|
||||
'id', 'user', 'action', 'resource_type', 'resource',
|
||||
'remote_addr', 'datetime', 'org_id'
|
||||
)
|
||||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'user', 'action', 'resource_type', 'resource', 'remote_addr',
|
||||
'datetime',
|
||||
'org_id'
|
||||
]
|
||||
fields = fields_small
|
||||
|
||||
|
||||
class PasswordChangeLogSerializer(serializers.ModelSerializer):
|
||||
|
@ -27,11 +27,23 @@ json_render = JSONRenderer()
|
||||
|
||||
|
||||
MODELS_NEED_RECORD = (
|
||||
'User', 'UserGroup', 'Asset', 'Node', 'AdminUser', 'SystemUser',
|
||||
'Domain', 'Gateway', 'Organization', 'AssetPermission', 'CommandFilter',
|
||||
'CommandFilterRule', 'License', 'Setting', 'Account', 'SyncInstanceTask',
|
||||
'Platform', 'ChangeAuthPlan', 'GatherUserTask',
|
||||
'RemoteApp', 'RemoteAppPermission', 'DatabaseApp', 'DatabaseAppPermission',
|
||||
# users
|
||||
'User', 'UserGroup',
|
||||
# acls
|
||||
'LoginACL', 'LoginAssetACL',
|
||||
# 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_MODEL] = _('Password')
|
||||
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
|
||||
|
||||
def _setup(self):
|
||||
|
@ -7,3 +7,6 @@ from .mfa import *
|
||||
from .access_key import *
|
||||
from .login_confirm import *
|
||||
from .sso import *
|
||||
from .wecom import *
|
||||
from .dingtalk import *
|
||||
from .password import *
|
||||
|
@ -1,5 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import urllib.parse
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import get_object_or_404
|
||||
@ -77,7 +79,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||
})
|
||||
|
||||
key = self.CACHE_KEY_PREFIX.format(token)
|
||||
cache.set(key, value, timeout=20)
|
||||
cache.set(key, value, timeout=30*60)
|
||||
return token
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
@ -100,7 +102,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||
'desktopwidth:i': '1280',
|
||||
'desktopheight:i': '800',
|
||||
'use multimon:i': '1',
|
||||
'session bpp:i': '24',
|
||||
'session bpp:i': '32',
|
||||
'audiomode:i': '0',
|
||||
'disable wallpaper:i': '0',
|
||||
'disable full window drag:i': '0',
|
||||
@ -140,15 +142,16 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||
# Todo: 上线后地址是 JumpServerAddr:3389
|
||||
address = self.request.query_params.get('address') or '1.1.1.1'
|
||||
options['full address:s'] = address
|
||||
options['username:s'] = '{}@{}'.format(user.username, token)
|
||||
options['username:s'] = '{}|{}'.format(user.username, token)
|
||||
options['desktopwidth:i'] = width
|
||||
options['desktopheight:i'] = height
|
||||
data = ''
|
||||
for k, v in options.items():
|
||||
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)
|
||||
response['Content-Disposition'] = 'attachment; filename={}'.format(filename)
|
||||
filename = urllib.parse.quote(filename)
|
||||
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
|
||||
return response
|
||||
|
||||
@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):
|
||||
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_backend_not_match = 'backend_not_match'
|
||||
reason_acl_not_allow = 'acl_not_allow'
|
||||
only_local_users_are_allowed = 'only_local_users_are_allowed'
|
||||
|
||||
reason_choices = {
|
||||
reason_password_failed: _('Username/password check failed'),
|
||||
@ -32,6 +33,7 @@ reason_choices = {
|
||||
reason_user_expired: _("This account is expired"),
|
||||
reason_backend_not_match: _("Auth backend not match"),
|
||||
reason_acl_not_allow: _("ACL is not allowed"),
|
||||
only_local_users_are_allowed: _("Only local users are allowed")
|
||||
}
|
||||
old_reason_choices = {
|
||||
'0': '-',
|
||||
@ -275,6 +277,15 @@ class PasswdTooSimple(JMSException):
|
||||
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):
|
||||
default_code = 'passwd_has_expired'
|
||||
default_detail = _('Your password has expired, please reset before logging in')
|
||||
@ -282,3 +293,28 @@ class PasswordRequireResetError(JMSException):
|
||||
def __init__(self, url, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
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
|
||||
import time
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.conf import settings
|
||||
from django.contrib import auth
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.contrib.auth import (
|
||||
BACKEND_SESSION_KEY, _get_backends,
|
||||
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.utils import LoginBlockUtil, MFABlockUtils
|
||||
from . import errors
|
||||
@ -81,6 +83,8 @@ class AuthMixin:
|
||||
request = None
|
||||
partial_credential_error = None
|
||||
|
||||
key_prefix_captcha = "_LOGIN_INVALID_{}"
|
||||
|
||||
def get_user_from_session(self):
|
||||
if self.request.session.is_empty():
|
||||
raise errors.SessionEmptyError()
|
||||
@ -109,11 +113,7 @@ class AuthMixin:
|
||||
ip = ip or get_request_ip(self.request)
|
||||
return ip
|
||||
|
||||
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")
|
||||
def _check_is_block(self, username, raise_exception=True):
|
||||
ip = self.get_request_ip()
|
||||
if LoginBlockUtil(username, ip).is_block():
|
||||
logger.warn('Ip was blocked' + ': ' + username + ':' + ip)
|
||||
@ -123,6 +123,13 @@ class AuthMixin:
|
||||
else:
|
||||
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):
|
||||
# 获取解密密钥,对密码进行解密
|
||||
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
|
||||
@ -139,6 +146,9 @@ class AuthMixin:
|
||||
def raise_credential_error(self, 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):
|
||||
request = self.request
|
||||
if hasattr(request, 'data'):
|
||||
@ -150,7 +160,7 @@ class AuthMixin:
|
||||
username, password, challenge, public_key, auto_login = bulk_get(data, *items, default='')
|
||||
password = password + challenge.strip()
|
||||
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:
|
||||
password = self.decrypt_passwd(password)
|
||||
@ -183,6 +193,21 @@ class AuthMixin:
|
||||
if not is_allowed:
|
||||
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):
|
||||
self.check_is_block()
|
||||
request = self.request
|
||||
@ -194,6 +219,7 @@ class AuthMixin:
|
||||
self._check_login_acl(user, ip)
|
||||
self._check_password_require_reset_or_not(user)
|
||||
self._check_passwd_is_too_simple(user, password)
|
||||
self._check_passwd_need_update(user)
|
||||
|
||||
LoginBlockUtil(username, ip).clean_failed_count()
|
||||
request.session['auth_password'] = 1
|
||||
@ -202,34 +228,62 @@ class AuthMixin:
|
||||
request.session['auth_backend'] = getattr(user, 'backend', settings.AUTH_BACKEND_MODEL)
|
||||
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
|
||||
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')
|
||||
query_str = urlencode({
|
||||
'token': user.generate_reset_token()
|
||||
})
|
||||
reset_passwd_url = f'{reset_passwd_url}?{query_str}'
|
||||
|
||||
flash_page_url = reverse(flash_view_name)
|
||||
query_str = urlencode({
|
||||
'redirect_url': reset_passwd_url
|
||||
})
|
||||
return f'{flash_page_url}?{query_str}'
|
||||
message_data = {
|
||||
'title': _('Please change your password'),
|
||||
'message': message,
|
||||
'interval': 3,
|
||||
'redirect_url': reset_passwd_url,
|
||||
}
|
||||
return FlashMessageUtil.gen_message_url(message_data)
|
||||
|
||||
@classmethod
|
||||
def _check_passwd_is_too_simple(cls, user: User, password):
|
||||
if user.is_superuser and password == 'admin':
|
||||
url = cls.generate_reset_password_url_with_flash_msg(
|
||||
user, 'authentication:passwd-too-simple-flash-msg'
|
||||
)
|
||||
message = _('Your password is too simple, please change it for security')
|
||||
url = cls.generate_reset_password_url_with_flash_msg(user, message=message)
|
||||
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
|
||||
def _check_password_require_reset_or_not(cls, user: User):
|
||||
if user.password_has_expired:
|
||||
url = cls.generate_reset_password_url_with_flash_msg(
|
||||
user, 'authentication:passwd-has-expired-flash-msg'
|
||||
)
|
||||
message = _('Your password has expired, please reset before logging in')
|
||||
url = cls.generate_reset_password_url_with_flash_msg(user, message)
|
||||
raise errors.PasswordRequireResetError(url)
|
||||
|
||||
def check_user_auth_if_need(self, decrypt_passwd=False):
|
||||
@ -345,3 +399,10 @@ class AuthMixin:
|
||||
sender=self.__class__, username=username,
|
||||
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 applications.models import Application
|
||||
from users.serializers import UserProfileSerializer
|
||||
from assets.serializers import ProtocolsField
|
||||
from perms.serializers.asset.permission import ActionsField
|
||||
from .models import AccessKey, LoginConfirmSetting, SSOToken
|
||||
from .models import AccessKey, LoginConfirmSetting
|
||||
|
||||
|
||||
__all__ = [
|
||||
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer',
|
||||
'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)
|
||||
|
||||
|
||||
class PasswordVerifySerializer(serializers.Serializer):
|
||||
password = serializers.CharField()
|
||||
|
||||
|
||||
class BearerTokenSerializer(serializers.Serializer):
|
||||
username = serializers.CharField(allow_null=True, required=False, write_only=True)
|
||||
password = serializers.CharField(write_only=True, allow_null=True,
|
||||
@ -150,9 +156,11 @@ class ConnectionTokenUserSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
|
||||
protocols = ProtocolsField(label='Protocols', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = ['id', 'hostname', 'ip', 'port', 'org_id']
|
||||
fields = ['id', 'hostname', 'ip', 'protocols', 'org_id']
|
||||
|
||||
|
||||
class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer):
|
||||
|
@ -117,6 +117,15 @@
|
||||
float: right;
|
||||
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>
|
||||
</head>
|
||||
@ -182,10 +191,10 @@
|
||||
</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 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 %}
|
||||
<a href="{% url 'authentication:openid:login' %}" class="more-login-item">
|
||||
<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' %}
|
||||
</a>
|
||||
{% 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>
|
||||
{% else %}
|
||||
<div class="text-center" style="display: inline-block;">
|
||||
|
@ -14,10 +14,17 @@ router.register('connection-token', api.UserConnectionTokenViewSet, 'connection-
|
||||
|
||||
urlpatterns = [
|
||||
# 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('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
|
||||
path('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'),
|
||||
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-settings/<uuid:user_id>/', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update')
|
||||
]
|
||||
|
@ -18,14 +18,25 @@ urlpatterns = [
|
||||
|
||||
# 原来在users中的
|
||||
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/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('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
|
||||
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'),
|
||||
|
@ -2,3 +2,5 @@
|
||||
#
|
||||
from .login 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
|
||||
import os
|
||||
import datetime
|
||||
from django.core.cache import cache
|
||||
from django.contrib.auth import login as auth_login, logout as auth_logout
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import reverse, redirect
|
||||
@ -19,7 +18,7 @@ from django.conf import settings
|
||||
from django.urls import reverse_lazy
|
||||
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 (
|
||||
redirect_user_first_login_or_index
|
||||
)
|
||||
@ -31,7 +30,6 @@ from ..forms import get_user_login_form_cls
|
||||
__all__ = [
|
||||
'UserLoginView', 'UserLogoutView',
|
||||
'UserLoginGuardView', 'UserLoginWaitConfirmView',
|
||||
'FlashPasswdTooSimpleMsgView', 'FlashPasswdHasExpiredMsgView'
|
||||
]
|
||||
|
||||
|
||||
@ -39,16 +37,45 @@ __all__ = [
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class UserLoginView(mixins.AuthMixin, FormView):
|
||||
key_prefix_captcha = "_LOGIN_INVALID_{}"
|
||||
redirect_field_name = 'next'
|
||||
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):
|
||||
if request.user.is_staff:
|
||||
first_login_url = redirect_user_first_login_or_index(
|
||||
request, self.redirect_field_name
|
||||
)
|
||||
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()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
@ -61,31 +88,22 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||
try:
|
||||
self.check_user_auth(decrypt_passwd=True)
|
||||
except errors.AuthFailedError as e:
|
||||
e = self.check_is_block(raise_exception=False) or e
|
||||
form.add_error(None, e.msg)
|
||||
ip = self.get_request_ip()
|
||||
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
|
||||
self.set_login_failed_mark()
|
||||
|
||||
form_cls = get_user_login_form_cls(captcha=True)
|
||||
new_form = form_cls(data=form.data)
|
||||
new_form._errors = form.errors
|
||||
context = self.get_context_data(form=new_form)
|
||||
self.request.session.set_test_cookie()
|
||||
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)
|
||||
self.clear_rsa_key()
|
||||
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):
|
||||
ip = get_request_ip(self.request)
|
||||
if cache.get(self.key_prefix_captcha.format(ip)):
|
||||
if self.check_is_need_captcha():
|
||||
return get_user_login_form_cls(captcha=True)
|
||||
else:
|
||||
return get_user_login_form_cls()
|
||||
@ -113,6 +131,8 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||
'demo_mode': os.environ.get("DEMO_MODE"),
|
||||
'AUTH_OPENID': settings.AUTH_OPENID,
|
||||
'AUTH_CAS': settings.AUTH_CAS,
|
||||
'AUTH_WECOM': settings.AUTH_WECOM,
|
||||
'AUTH_DINGTALK': settings.AUTH_DINGTALK,
|
||||
'rsa_public_key': rsa_public_key,
|
||||
'forgot_password_url': forgot_password_url
|
||||
}
|
||||
@ -218,39 +238,9 @@ class UserLogoutView(TemplateView):
|
||||
context = {
|
||||
'title': _('Logout success'),
|
||||
'messages': _('Logout success, return login page'),
|
||||
'interval': 1,
|
||||
'interval': 3,
|
||||
'redirect_url': reverse('authentication:login'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
kwargs.update(context)
|
||||
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
|
||||
default_code = 'referenced_by_others'
|
||||
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 itertools import chain
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models.signals import m2m_changed
|
||||
from django.core.cache import cache
|
||||
from django.http import JsonResponse
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.decorators import action
|
||||
@ -25,6 +27,9 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
|
||||
class JSONResponseMixin(object):
|
||||
"""JSON mixin"""
|
||||
@staticmethod
|
||||
@ -332,3 +337,21 @@ class AllowBulkDestoryMixin:
|
||||
"""
|
||||
query = str(filtered.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
|
||||
|
||||
|
||||
__all__ = ["DatetimeSearchMixin"]
|
||||
__all__ = ["DatetimeSearchMixin", "PermissionsMixin"]
|
||||
|
||||
from rest_framework import permissions
|
||||
|
||||
@ -51,4 +51,4 @@ class PermissionsMixin(UserPassesTestMixin):
|
||||
for permission_class in permission_classes:
|
||||
if not permission_class().has_permission(self.request, self):
|
||||
return False
|
||||
return True
|
||||
return True
|
||||
|
@ -3,6 +3,7 @@
|
||||
import time
|
||||
from rest_framework import permissions
|
||||
from django.conf import settings
|
||||
from common.exceptions import MFAVerifyRequired
|
||||
|
||||
from orgs.utils import current_org
|
||||
|
||||
@ -114,7 +115,7 @@ class NeedMFAVerify(permissions.BasePermission):
|
||||
mfa_verify_time = request.session.get('MFA_VERIFY_TIME', 0)
|
||||
if time.time() - mfa_verify_time < settings.SECURITY_MFA_VERIFY_TTL:
|
||||
return True
|
||||
return False
|
||||
raise MFAVerifyRequired()
|
||||
|
||||
|
||||
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 .crypto 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)
|
||||
except paramiko.SSHException:
|
||||
pass
|
||||
else:
|
||||
return key
|
||||
|
||||
try:
|
||||
key = paramiko.DSSKey.from_private_key(StringIO(text), password=password)
|
||||
except paramiko.SSHException:
|
||||
pass
|
||||
else:
|
||||
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 socket
|
||||
import string
|
||||
import secrets
|
||||
|
||||
|
||||
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 common.permissions import IsOrgAdmin, IsOrgAuditor
|
||||
from common.utils import lazyproperty
|
||||
from orgs.caches import OrgResourceStatisticsCache
|
||||
|
||||
__all__ = ['IndexApi']
|
||||
|
||||
@ -99,7 +100,7 @@ class DatesLoginMetricMixin:
|
||||
if count is not None:
|
||||
return count
|
||||
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)
|
||||
return count
|
||||
|
||||
@ -129,7 +130,7 @@ class DatesLoginMetricMixin:
|
||||
|
||||
@lazyproperty
|
||||
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
|
||||
|
||||
@lazyproperty
|
||||
@ -161,10 +162,10 @@ class DatesLoginMetricMixin:
|
||||
@lazyproperty
|
||||
def dates_total_count_disabled_assets(self):
|
||||
return Asset.objects.filter(is_active=False).count()
|
||||
|
||||
|
||||
# 以下是从week中而来
|
||||
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 = [
|
||||
{'user': user, 'total': total}
|
||||
for user, total in Counter(users).most_common(5)
|
||||
@ -172,7 +173,7 @@ class DatesLoginMetricMixin:
|
||||
return users
|
||||
|
||||
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):
|
||||
return self.sessions_queryset.count()
|
||||
@ -186,8 +187,8 @@ class DatesLoginMetricMixin:
|
||||
return list(assets)
|
||||
|
||||
def get_dates_login_times_top10_users(self):
|
||||
users = self.sessions_queryset.values("user") \
|
||||
.annotate(total=Count("user")) \
|
||||
users = self.sessions_queryset.values("user_id") \
|
||||
.annotate(total=Count("user_id")) \
|
||||
.annotate(last=Max("date_start")).order_by("-total")[:10]
|
||||
for user in users:
|
||||
user['last'] = str(user['last'])
|
||||
@ -210,26 +211,7 @@ class DatesLoginMetricMixin:
|
||||
return sessions
|
||||
|
||||
|
||||
class TotalCountMixin:
|
||||
@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):
|
||||
class IndexApi(DatesLoginMetricMixin, APIView):
|
||||
permission_classes = (IsOrgAdmin | IsOrgAuditor,)
|
||||
http_method_names = ['get']
|
||||
|
||||
@ -238,26 +220,28 @@ class IndexApi(TotalCountMixin, DatesLoginMetricMixin, APIView):
|
||||
|
||||
query_params = self.request.query_params
|
||||
|
||||
caches = OrgResourceStatisticsCache(self.request.user.user_orgs[0])
|
||||
|
||||
_all = query_params.get('all')
|
||||
|
||||
if _all or query_params.get('total_count') or query_params.get('total_count_users'):
|
||||
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'):
|
||||
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'):
|
||||
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'):
|
||||
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'):
|
||||
|
@ -216,6 +216,16 @@ class Config(dict):
|
||||
'AUTH_SSO': False,
|
||||
'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_ISSUER_NAME': 'JumpServer',
|
||||
'EMAIL_SUFFIX': 'jumpserver.org',
|
||||
@ -259,6 +269,7 @@ class Config(dict):
|
||||
'FTP_LOG_KEEP_DAYS': 200,
|
||||
'ASSETS_PERM_CACHE_TIME': 3600 * 24,
|
||||
'SECURITY_MFA_VERIFY_TTL': 3600,
|
||||
'OLD_PASSWORD_HISTORY_LIMIT_COUNT': 5,
|
||||
'ASSETS_PERM_CACHE_ENABLE': HAS_XPACK,
|
||||
'SYSLOG_ADDR': '', # '192.168.0.1:514'
|
||||
'SYSLOG_FACILITY': 'user',
|
||||
|
@ -14,6 +14,8 @@ def jumpserver_processor(request):
|
||||
'LOGIN_IMAGE_URL': static('img/login_image.png'),
|
||||
'FAVICON_URL': static('img/facio.ico'),
|
||||
'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'),
|
||||
'VERSION': settings.VERSION,
|
||||
'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2021',
|
||||
|
@ -101,6 +101,19 @@ CAS_CHECK_NEXT = lambda _next_page: True
|
||||
AUTH_SSO = CONFIG.AUTH_SSO
|
||||
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
|
||||
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
|
||||
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_CAS = 'authentication.backends.cas.CASBackend'
|
||||
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]
|
||||
@ -128,6 +143,10 @@ if AUTH_RADIUS:
|
||||
AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_RADIUS)
|
||||
if AUTH_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
|
||||
|
@ -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_PASSWORD_EXPIRATION_TIME = CONFIG.SECURITY_PASSWORD_EXPIRATION_TIME # Unit: day
|
||||
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_LOWER_CASE = CONFIG.SECURITY_PASSWORD_LOWER_CASE
|
||||
SECURITY_PASSWORD_NUMBER = CONFIG.SECURITY_PASSWORD_NUMBER
|
||||
|
@ -23,12 +23,13 @@ api_v1 = [
|
||||
path('applications/', include('applications.urls.api_urls', namespace='api-applications')),
|
||||
path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')),
|
||||
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 = [
|
||||
path('auth/', include('authentication.urls.view_urls'), name='auth'),
|
||||
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'),
|
||||
]
|
||||
|
||||
|
@ -1,17 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import re
|
||||
import time
|
||||
|
||||
from django.http import HttpResponseRedirect, JsonResponse, Http404
|
||||
from django.conf import settings
|
||||
from django.views.generic import View
|
||||
from django.shortcuts import redirect
|
||||
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.http import HttpResponse
|
||||
from rest_framework.views import APIView
|
||||
|
||||
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>If you see this page, prove that you are not accessing the nginx listening port. Good luck.</div>")
|
||||
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 django.utils.translation import gettext_lazy as _
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class OpsConfig(AppConfig):
|
||||
name = 'ops'
|
||||
verbose_name = _('Operations')
|
||||
|
||||
def ready(self):
|
||||
from orgs.models import Organization
|
||||
|
@ -14,11 +14,15 @@ class AdHocExecutionSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = AdHocExecution
|
||||
fields = [
|
||||
'id', 'task', 'task_display', 'hosts_amount', 'adhoc', 'date_start', 'stat',
|
||||
'date_finished', 'timedelta', 'is_finished', 'is_success', 'result', 'summary',
|
||||
'short_id', 'adhoc_short_id', 'last_success', 'last_failure'
|
||||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'hosts_amount', 'timedelta', 'result', 'summary', 'short_id',
|
||||
'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
|
||||
def get_task(obj):
|
||||
@ -52,11 +56,16 @@ class TaskSerializer(BulkOrgResourceModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Task
|
||||
fields = [
|
||||
'id', 'name', 'interval', 'crontab', 'is_periodic',
|
||||
'is_deleted', 'comment', 'date_created',
|
||||
'date_updated', 'latest_execution', 'summary',
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'interval', 'crontab',
|
||||
'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 = [
|
||||
'is_deleted', 'date_created', 'date_updated',
|
||||
'latest_adhoc', 'latest_execution', 'total_run_amount',
|
||||
@ -77,12 +86,16 @@ class AdHocSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = AdHoc
|
||||
fields = [
|
||||
"id", "task", 'tasks', "pattern", "options",
|
||||
"hosts", "run_as_admin", "run_as", "become",
|
||||
"date_created", "short_id",
|
||||
"become_display",
|
||||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'tasks', "pattern", "options", "run_as",
|
||||
"become", "become_display", "short_id",
|
||||
"run_as_admin",
|
||||
"date_created",
|
||||
]
|
||||
fields_fk = ["task"]
|
||||
fields_m2m = ["hosts"]
|
||||
fields = fields_small + fields_fk + fields_m2m
|
||||
read_only_fields = [
|
||||
'date_created'
|
||||
]
|
||||
@ -99,8 +112,8 @@ class AdHocExecutionNestSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = AdHocExecution
|
||||
fields = (
|
||||
'last_success', 'last_failure', 'last_run', 'timedelta', 'is_finished',
|
||||
'is_success'
|
||||
'last_success', 'last_failure', 'last_run', 'timedelta',
|
||||
'is_finished', 'is_success'
|
||||
)
|
||||
|
||||
|
||||
@ -120,10 +133,15 @@ class CommandExecutionSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CommandExecution
|
||||
fields = [
|
||||
'id', 'hosts', 'run_as', 'command', 'result', 'log_url',
|
||||
'is_finished', 'date_created', 'date_finished'
|
||||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'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 = [
|
||||
'result', 'is_finished', 'log_url', 'date_created',
|
||||
'date_finished'
|
||||
|
@ -6,12 +6,12 @@ from orgs.utils import current_org, tmp_to_org
|
||||
from common.cache import Cache, IntegerField
|
||||
from common.utils import get_logger
|
||||
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 perms.models import AssetPermission, ApplicationPermission
|
||||
from .models import OrganizationMember
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
@ -64,6 +64,9 @@ class OrgResourceStatisticsCache(OrgRelatedCache):
|
||||
asset_perms_amount = IntegerField(queryset=AssetPermission.objects)
|
||||
app_perms_amount = IntegerField(queryset=ApplicationPermission.objects)
|
||||
|
||||
total_count_online_users = IntegerField()
|
||||
total_count_online_sessions = IntegerField()
|
||||
|
||||
def __init__(self, org):
|
||||
super().__init__()
|
||||
self.org = org
|
||||
@ -86,3 +89,9 @@ class OrgResourceStatisticsCache(OrgRelatedCache):
|
||||
def compute_assets_amount(self):
|
||||
node = Node.org_root()
|
||||
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
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'is_default', 'is_root', 'comment',
|
||||
'created_by', 'date_created', 'resource_statistics'
|
||||
'resource_statistics',
|
||||
'is_default', 'is_root',
|
||||
'date_created',
|
||||
'comment', 'created_by',
|
||||
]
|
||||
|
||||
fields_m2m = ['users', 'admins', 'auditors']
|
||||
@ -79,7 +81,12 @@ class OrgMemberSerializer(BulkModelSerializer):
|
||||
|
||||
class Meta:
|
||||
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
|
||||
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 post_save, pre_delete
|
||||
from django.db.models.signals import post_save, pre_delete, pre_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from orgs.models import Organization, OrganizationMember
|
||||
@ -7,6 +7,7 @@ from assets.models import Node
|
||||
from perms.models import (AssetPermission, ApplicationPermission)
|
||||
from users.models import UserGroup, User
|
||||
from applications.models import Application
|
||||
from terminal.models import Session
|
||||
from assets.models import Asset, AdminUser, SystemUser, Domain, Gateway
|
||||
from common.const.signals import POST_PREFIX
|
||||
from orgs.caches import OrgResourceStatisticsCache
|
||||
@ -47,16 +48,17 @@ def on_org_user_changed_refresh_cache(sender, action, instance, reverse, pk_set,
|
||||
|
||||
class OrgResourceStatisticsRefreshUtil:
|
||||
model_cache_field_mapper = {
|
||||
ApplicationPermission: 'app_perms_amount',
|
||||
AssetPermission: 'asset_perms_amount',
|
||||
Application: 'applications_amount',
|
||||
Gateway: 'gateways_amount',
|
||||
Domain: 'domains_amount',
|
||||
SystemUser: 'system_users_amount',
|
||||
AdminUser: 'admin_users_amount',
|
||||
Node: 'nodes_amount',
|
||||
Asset: 'assets_amount',
|
||||
UserGroup: 'groups_amount',
|
||||
ApplicationPermission: ['app_perms_amount'],
|
||||
AssetPermission: ['asset_perms_amount'],
|
||||
Application: ['applications_amount'],
|
||||
Gateway: ['gateways_amount'],
|
||||
Domain: ['domains_amount'],
|
||||
SystemUser: ['system_users_amount'],
|
||||
AdminUser: ['admin_users_amount'],
|
||||
Node: ['nodes_amount'],
|
||||
Asset: ['assets_amount'],
|
||||
UserGroup: ['groups_amount'],
|
||||
Session: ['total_count_online_users', 'total_count_online_sessions']
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@ -64,13 +66,12 @@ class OrgResourceStatisticsRefreshUtil:
|
||||
cache_field_name = cls.model_cache_field_mapper.get(type(instance))
|
||||
if cache_field_name:
|
||||
org_cache = OrgResourceStatisticsCache(instance.org)
|
||||
org_cache.expire(cache_field_name)
|
||||
org_cache.expire(*cache_field_name)
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
def on_post_save_refresh_org_resource_statistics_cache(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
OrgResourceStatisticsRefreshUtil.refresh_if_need(instance)
|
||||
@receiver(pre_save)
|
||||
def on_post_save_refresh_org_resource_statistics_cache(sender, instance, **kwargs):
|
||||
OrgResourceStatisticsRefreshUtil.refresh_if_need(instance)
|
||||
|
||||
|
||||
@receiver(pre_delete)
|
||||
|
@ -8,7 +8,6 @@ from django.dispatch import receiver
|
||||
from django.utils.functional import LazyObject
|
||||
from django.db.models.signals import m2m_changed
|
||||
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.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.utils import get_logger
|
||||
from common.utils.connection import RedisPubSub
|
||||
from common.exceptions import JMSException
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
@ -1,9 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import uuid
|
||||
import time
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.decorators import method_decorator
|
||||
from rest_framework.views import APIView, Response
|
||||
from rest_framework import status
|
||||
from rest_framework.generics import (
|
||||
ListAPIView, get_object_or_404
|
||||
)
|
||||
@ -12,7 +14,8 @@ from orgs.utils import tmp_to_root_org
|
||||
from applications.models import Application
|
||||
from perms.utils.application.permission import (
|
||||
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 common.permissions import IsOrgAdminOrAppUser
|
||||
@ -61,18 +64,13 @@ class ValidateUserApplicationPermissionApi(APIView):
|
||||
application_id = request.query_params.get('application_id', '')
|
||||
system_user_id = request.query_params.get('system_user_id', '')
|
||||
|
||||
try:
|
||||
user_id = uuid.UUID(user_id)
|
||||
application_id = uuid.UUID(application_id)
|
||||
system_user_id = uuid.UUID(system_user_id)
|
||||
except ValueError:
|
||||
return Response({'msg': False}, status=403)
|
||||
if not all((user_id, application_id, system_user_id)):
|
||||
return Response({'has_permission': False, 'expire_at': int(time.time())})
|
||||
|
||||
user = get_object_or_404(User, id=user_id)
|
||||
application = get_object_or_404(Application, id=application_id)
|
||||
system_user = get_object_or_404(SystemUser, id=system_user_id)
|
||||
user = User.objects.get(id=user_id)
|
||||
application = Application.objects.get(id=application_id)
|
||||
system_user = SystemUser.objects.get(id=system_user_id)
|
||||
|
||||
if has_application_system_permission(user, application, system_user):
|
||||
return Response({'msg': True}, status=200)
|
||||
|
||||
return Response({'msg': False}, status=403)
|
||||
has_permission, expire_at = validate_permission(user, application, system_user)
|
||||
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)
|
||||
|
@ -1,22 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import uuid
|
||||
import time
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.decorators import method_decorator
|
||||
from rest_framework.views import APIView, Response
|
||||
from rest_framework import status
|
||||
from rest_framework.generics import (
|
||||
ListAPIView, get_object_or_404, RetrieveAPIView, DestroyAPIView
|
||||
)
|
||||
|
||||
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.utils import get_logger, lazyproperty
|
||||
|
||||
from perms.hands import User, Asset, SystemUser
|
||||
from perms import serializers
|
||||
from perms.models import Action
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@ -65,32 +66,22 @@ class ValidateUserAssetPermissionApi(APIView):
|
||||
def get_cache_policy(self):
|
||||
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):
|
||||
user_id = self.request.query_params.get('user_id', '')
|
||||
asset_id = request.query_params.get('asset_id', '')
|
||||
system_id = request.query_params.get('system_user_id', '')
|
||||
action_name = request.query_params.get('action_name', '')
|
||||
|
||||
try:
|
||||
asset_id = uuid.UUID(asset_id)
|
||||
system_id = uuid.UUID(system_id)
|
||||
except ValueError:
|
||||
return Response({'msg': False}, status=403)
|
||||
if not all((user_id, asset_id, system_id, action_name)):
|
||||
return Response({'has_permission': False, 'expire_at': int(time.time())})
|
||||
|
||||
asset = get_object_or_404(Asset, id=asset_id, is_active=True)
|
||||
system_user = get_object_or_404(SystemUser, id=system_id)
|
||||
user = User.objects.get(id=user_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)
|
||||
actions = system_users_actions.get(system_user.id)
|
||||
if actions is None:
|
||||
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)
|
||||
has_permission, expire_at = validate_permission(user, asset, system_user, action_name)
|
||||
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)
|
||||
|
||||
|
||||
# TODO 删除
|
||||
|
@ -3,8 +3,9 @@
|
||||
from rest_framework.request import Request
|
||||
|
||||
from common.permissions import IsOrgAdminOrAppUser, IsValidUser
|
||||
from common.utils import lazyproperty
|
||||
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 users.models import User
|
||||
from perms.utils.asset.user_permission import UserGrantedTreeRefreshController
|
||||
@ -20,24 +21,13 @@ class PermBaseMixin:
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class RoleAdminMixin(PermBaseMixin):
|
||||
class RoleAdminMixin(PermBaseMixin, _RoleAdminMixin):
|
||||
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,)
|
||||
request: Request
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
with tmp_to_root_org():
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
@lazyproperty
|
||||
def user(self):
|
||||
return self.request.user
|
||||
|
@ -72,7 +72,7 @@ class Action:
|
||||
def value_to_choices_display(cls, value):
|
||||
choices = cls.value_to_choices(value)
|
||||
return [str(dict(cls.choices())[i]) for i in choices]
|
||||
|
||||
|
||||
@classmethod
|
||||
def choices_to_value(cls, value):
|
||||
if not isinstance(value, list):
|
||||
@ -143,6 +143,26 @@ class AssetPermission(BasePermission):
|
||||
assets = Asset.objects.filter(id__in=asset_ids)
|
||||
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 NodeFrom(ChoiceSet):
|
||||
|
@ -20,16 +20,16 @@ class ApplicationPermissionSerializer(BulkOrgResourceModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ApplicationPermission
|
||||
mini_fields = ['id', 'name']
|
||||
small_fields = mini_fields + [
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'category', 'category_display', 'type', 'type_display', 'is_active', 'is_expired',
|
||||
'is_valid', 'created_by', 'date_created', 'date_expired', 'date_start', 'comment'
|
||||
]
|
||||
m2m_fields = [
|
||||
fields_m2m = [
|
||||
'users', 'user_groups', 'applications', 'system_users',
|
||||
'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']
|
||||
|
||||
@classmethod
|
||||
|
@ -3,7 +3,9 @@
|
||||
|
||||
from rest_framework import serializers
|
||||
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 perms.models import AssetPermission, Action
|
||||
@ -41,21 +43,27 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
|
||||
actions = ActionsField(required=False, allow_null=True)
|
||||
is_valid = serializers.BooleanField(read_only=True)
|
||||
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:
|
||||
model = AssetPermission
|
||||
mini_fields = ['id', 'name']
|
||||
small_fields = mini_fields + [
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'is_active', 'is_expired', 'is_valid', 'actions',
|
||||
'created_by', 'date_created', 'date_expired',
|
||||
'date_start', 'comment'
|
||||
]
|
||||
m2m_fields = [
|
||||
'users', 'user_groups', 'assets', 'nodes', 'system_users',
|
||||
fields_m2m = [
|
||||
'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',
|
||||
'nodes_amount', 'system_users_amount',
|
||||
]
|
||||
fields = small_fields + m2m_fields
|
||||
fields = fields_small + fields_m2m
|
||||
read_only_fields = ['created_by', 'date_created']
|
||||
extra_kwargs = {
|
||||
'is_expired': {'label': _('Is expired')},
|
||||
@ -71,11 +79,44 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset.prefetch_related(
|
||||
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'))
|
||||
)
|
||||
queryset = queryset.prefetch_related('users', 'user_groups', 'assets', 'nodes', 'system_users')
|
||||
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 common.utils import get_logger
|
||||
@ -6,6 +8,58 @@ from perms.models import ApplicationPermission
|
||||
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):
|
||||
queryset = ApplicationPermission.objects.valid()\
|
||||
.filter(
|
||||
|
@ -1,15 +1,61 @@
|
||||
import time
|
||||
from collections import defaultdict
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
from common.utils import get_logger
|
||||
from perms.models import AssetPermission
|
||||
from perms.hands import Asset, User, UserGroup, SystemUser
|
||||
from perms.models import AssetPermission, Action
|
||||
from perms.hands import Asset, User, UserGroup, SystemUser, Node
|
||||
from perms.utils.asset.user_permission import get_user_all_asset_perm_ids
|
||||
|
||||
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):
|
||||
nodes = asset.get_nodes()
|
||||
node_keys = set()
|
||||
|
@ -1,2 +1,4 @@
|
||||
from .common 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,
|
||||
"SECURITY_VIEW_AUTH_NEED_MFA": settings.SECURITY_VIEW_AUTH_NEED_MFA,
|
||||
"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_PASSWORD_EXPIRATION_TIME": settings.SECURITY_PASSWORD_EXPIRATION_TIME,
|
||||
"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_NUMBER': settings.SECURITY_PASSWORD_NUMBER,
|
||||
'SECURITY_PASSWORD_SPECIAL_CHAR': settings.SECURITY_PASSWORD_SPECIAL_CHAR,
|
||||
}
|
||||
},
|
||||
"AUTH_WECOM": settings.AUTH_WECOM,
|
||||
"AUTH_DINGTALK": settings.AUTH_DINGTALK,
|
||||
}
|
||||
}
|
||||
return instance
|
||||
@ -140,6 +143,8 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
|
||||
'ldap': serializers.LDAPSettingSerializer,
|
||||
'email': serializers.EmailSettingSerializer,
|
||||
'email_content': serializers.EmailContentSettingSerializer,
|
||||
'wecom': serializers.WeComSettingSerializer,
|
||||
'dingtalk': serializers.DingTalkSettingSerializer,
|
||||
}
|
||||
|
||||
def get_serializer_class(self):
|
||||
@ -162,6 +167,8 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
|
||||
category = self.request.query_params.get('category', '')
|
||||
for name, value in serializer.validated_data.items():
|
||||
encrypted = name in encrypted_items
|
||||
if encrypted and value in ['', None]:
|
||||
continue
|
||||
data.append({
|
||||
'name': name, 'value': value,
|
||||
'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__ = [
|
||||
'BasicSettingSerializer', 'EmailSettingSerializer', 'EmailContentSettingSerializer',
|
||||
'LDAPSettingSerializer', 'TerminalSettingSerializer', 'SecuritySettingSerializer',
|
||||
'SettingsSerializer'
|
||||
'SettingsSerializer', 'WeComSettingSerializer', 'DingTalkSettingSerializer',
|
||||
]
|
||||
|
||||
|
||||
@ -121,7 +121,11 @@ class TerminalSettingSerializer(serializers.Serializer):
|
||||
('50', '50'),
|
||||
)
|
||||
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_PAGE_SIZE = serializers.ChoiceField(PAGE_SIZE_CHOICES, required=False, label=_('List page size'))
|
||||
TERMINAL_SESSION_KEEP_DURATION = serializers.IntegerField(
|
||||
@ -163,6 +167,11 @@ class SecuritySettingSerializer(serializers.Serializer):
|
||||
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')
|
||||
)
|
||||
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(
|
||||
min_value=6, max_value=30, required=True,
|
||||
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(
|
||||
BasicSettingSerializer,
|
||||
EmailSettingSerializer,
|
||||
EmailContentSettingSerializer,
|
||||
LDAPSettingSerializer,
|
||||
TerminalSettingSerializer,
|
||||
SecuritySettingSerializer
|
||||
SecuritySettingSerializer,
|
||||
WeComSettingSerializer,
|
||||
DingTalkSettingSerializer,
|
||||
):
|
||||
|
||||
# encrypt_fields 现在使用 write_only 来判断了
|
||||
|
@ -13,6 +13,8 @@ urlpatterns = [
|
||||
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/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('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 title %} {{ title }}{% endblock %}
|
||||
|
||||
{% block custom_head_css_js %}
|
||||
<style>
|
||||
.passwordBox {
|
||||
max-width: 660px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
{% if errors %}
|
||||
@ -30,8 +22,21 @@
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-lg-3">
|
||||
<a href="{{ redirect_url }}" class="btn btn-primary block full-width m-b">{% trans 'Return' %}</a>
|
||||
{% if has_cancel %}
|
||||
<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>
|
||||
|
@ -17,6 +17,7 @@ from terminal.filters import CommandFilter
|
||||
from orgs.utils import current_org
|
||||
from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsAppUser
|
||||
from common.const.http import GET
|
||||
from common.drf.api import JMSBulkModelViewSet
|
||||
from common.utils import get_logger
|
||||
from terminal.utils import send_command_alert_mail
|
||||
from terminal.serializers import InsecureCommandAlertSerializer
|
||||
@ -94,7 +95,7 @@ class CommandQueryMixin:
|
||||
return date_from_st, date_to_st
|
||||
|
||||
|
||||
class CommandViewSet(viewsets.ModelViewSet):
|
||||
class CommandViewSet(JMSBulkModelViewSet):
|
||||
"""接受app发送来的command log, 格式如下
|
||||
{
|
||||
"user": "admin",
|
||||
|
@ -1,10 +1,12 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TerminalConfig(AppConfig):
|
||||
name = 'terminal'
|
||||
verbose_name = _('Terminal')
|
||||
|
||||
def ready(self):
|
||||
from . import signals_handler
|
||||
|
@ -7,6 +7,7 @@ import pytz
|
||||
from uuid import UUID
|
||||
import inspect
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.db.models import QuerySet as DJQuerySet
|
||||
from elasticsearch import Elasticsearch
|
||||
from elasticsearch.helpers import bulk
|
||||
@ -14,12 +15,18 @@ from elasticsearch.exceptions import RequestError
|
||||
|
||||
from common.utils.common import lazyproperty
|
||||
from common.utils import get_logger
|
||||
from common.exceptions import JMSException
|
||||
from .models import AbstractSessionCommand
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class InvalidElasticsearch(JMSException):
|
||||
default_code = 'invalid_elasticsearch'
|
||||
default_detail = _('Invalid elasticsearch config')
|
||||
|
||||
|
||||
class CommandStore():
|
||||
def __init__(self, config):
|
||||
hosts = config.get("HOSTS")
|
||||
@ -27,12 +34,14 @@ class CommandStore():
|
||||
self.index = config.get("INDEX") or 'jumpserver'
|
||||
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:
|
||||
kwargs['verify_certs'] = None
|
||||
self.es = Elasticsearch(hosts=hosts, max_retries=0, **kwargs)
|
||||
|
||||
def pre_use_check(self):
|
||||
if not self.ping(timeout=3):
|
||||
raise InvalidElasticsearch
|
||||
self._ensure_index_exists()
|
||||
|
||||
def _ensure_index_exists(self):
|
||||
|
@ -11,6 +11,7 @@ from django.core.files.storage import default_storage
|
||||
from django.core.cache import cache
|
||||
|
||||
from assets.models import Asset
|
||||
from users.models import User
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from common.db.models import ChoiceSet
|
||||
from ..backends import get_multi_command_storage
|
||||
@ -79,6 +80,10 @@ class Session(OrgModelMixin):
|
||||
def asset_obj(self):
|
||||
return Asset.objects.get(id=self.asset_id)
|
||||
|
||||
@property
|
||||
def user_obj(self):
|
||||
return User.objects.get(id=self.user_id)
|
||||
|
||||
@property
|
||||
def _date_start_first_has_replay_rdp_session(self):
|
||||
if self.__class__._DATE_START_FIRST_HAS_REPLAY_RDP_SESSION is None:
|
||||
|
@ -17,14 +17,17 @@ class SessionSerializer(BulkOrgResourceModelSerializer):
|
||||
class Meta:
|
||||
model = Session
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = [
|
||||
"id", "user", "asset", "system_user",
|
||||
fields_mini = ["id"]
|
||||
fields_small = fields_mini + [
|
||||
"user", "asset", "system_user",
|
||||
"user_id", "asset_id", "system_user_id",
|
||||
"login_from", "login_from_display", "remote_addr",
|
||||
"is_success", "is_finished", "has_replay", "can_replay",
|
||||
"can_join", "can_terminate", "protocol", "date_start", "date_end",
|
||||
"terminal",
|
||||
"login_from", "login_from_display", "remote_addr", "protocol",
|
||||
"is_success", "is_finished", "has_replay",
|
||||
"date_start", "date_end",
|
||||
]
|
||||
fields_fk = ["terminal",]
|
||||
fields_custom = ["can_replay", "can_join", "can_terminate",]
|
||||
fields = fields_small + fields_fk + fields_custom
|
||||
extra_kwargs = {
|
||||
"protocol": {'label': _('Protocol')},
|
||||
'user_id': {'label': _('User ID')},
|
||||
|
@ -181,9 +181,9 @@ class CommandStorageTypeESSerializer(serializers.Serializer):
|
||||
max_length=1024, default='jumpserver', label=_('Index'), 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'),
|
||||
source='OTHER.ignore_verify_certs', allow_null=True,
|
||||
source='OTHER.IGNORE_VERIFY_CERTS', allow_null=True,
|
||||
)
|
||||
|
||||
# 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