mirror of https://github.com/jumpserver/jumpserver
commit
93846234f8
|
@ -61,7 +61,7 @@ class LoginAssetCheckAPI(CreateAPIView):
|
|||
'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()],
|
||||
'reviewers': [str(user) for user in ticket.current_node.first().ticket_assignees.all()],
|
||||
}
|
||||
return data
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ from .. import serializers
|
|||
|
||||
|
||||
class AccountFilterSet(BaseFilterSet):
|
||||
username = filters.CharFilter(field_name='username')
|
||||
username = filters.CharFilter(method='do_nothing')
|
||||
type = filters.CharFilter(field_name='type', lookup_expr='exact')
|
||||
category = filters.CharFilter(field_name='category', lookup_expr='exact')
|
||||
app_display = filters.CharFilter(field_name='app_display', lookup_expr='exact')
|
||||
|
|
|
@ -6,14 +6,15 @@ from rest_framework.decorators import action
|
|||
from rest_framework.response import Response
|
||||
|
||||
from common.tree import TreeNodeSerializer
|
||||
from ..hands import IsOrgAdminOrAppUser, IsValidUser
|
||||
from common.mixins.views import SuggestionMixin
|
||||
from ..hands import IsOrgAdminOrAppUser
|
||||
from .. import serializers
|
||||
from ..models import Application
|
||||
|
||||
__all__ = ['ApplicationViewSet']
|
||||
|
||||
|
||||
class ApplicationViewSet(OrgBulkModelViewSet):
|
||||
class ApplicationViewSet(SuggestionMixin, OrgBulkModelViewSet):
|
||||
model = Application
|
||||
filterset_fields = {
|
||||
'name': ['exact'],
|
||||
|
@ -24,7 +25,8 @@ class ApplicationViewSet(OrgBulkModelViewSet):
|
|||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_classes = {
|
||||
'default': serializers.AppSerializer,
|
||||
'get_tree': TreeNodeSerializer
|
||||
'get_tree': TreeNodeSerializer,
|
||||
'suggestion': serializers.MiniAppSerializer
|
||||
}
|
||||
|
||||
@action(methods=['GET'], detail=False, url_path='tree')
|
||||
|
@ -34,9 +36,3 @@ class ApplicationViewSet(OrgBulkModelViewSet):
|
|||
tree_nodes = Application.create_tree_nodes(queryset, show_count=show_count)
|
||||
serializer = self.get_serializer(tree_nodes, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(methods=['get'], detail=False, permission_classes=(IsValidUser,))
|
||||
def suggestion(self, request):
|
||||
queryset = self.filter_queryset(self.get_queryset())[:3]
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
|
|
@ -15,7 +15,7 @@ from .. import models
|
|||
from .. import const
|
||||
|
||||
__all__ = [
|
||||
'AppSerializer', 'AppSerializerMixin',
|
||||
'AppSerializer', 'MiniAppSerializer', 'AppSerializerMixin',
|
||||
'AppAccountSerializer', 'AppAccountSecretSerializer'
|
||||
]
|
||||
|
||||
|
@ -78,6 +78,12 @@ class AppSerializer(AppSerializerMixin, BulkOrgResourceModelSerializer):
|
|||
return _attrs
|
||||
|
||||
|
||||
class MiniAppSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = models.Application
|
||||
fields = AppSerializer.Meta.fields_mini
|
||||
|
||||
|
||||
class AppAccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
category = serializers.ChoiceField(label=_('Category'), choices=const.AppCategory.choices, read_only=True)
|
||||
category_display = serializers.SerializerMethodField(label=_('Category display'))
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from assets.api import FilterAssetByNodeMixin
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework.generics import RetrieveAPIView
|
||||
from rest_framework.response import Response
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser, IsValidUser
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser
|
||||
from common.mixins.views import SuggestionMixin
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.mixins import generics
|
||||
from ..models import Asset, Node, Platform
|
||||
|
@ -27,7 +26,7 @@ __all__ = [
|
|||
]
|
||||
|
||||
|
||||
class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet):
|
||||
class AssetViewSet(SuggestionMixin, FilterAssetByNodeMixin, OrgBulkModelViewSet):
|
||||
"""
|
||||
API endpoint that allows Asset to be viewed or edited.
|
||||
"""
|
||||
|
@ -64,12 +63,6 @@ class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet):
|
|||
assets = serializer.save()
|
||||
self.set_assets_node(assets)
|
||||
|
||||
@action(methods=['get'], detail=False, permission_classes=(IsValidUser,))
|
||||
def suggestion(self, request):
|
||||
queryset = self.filter_queryset(self.get_queryset())[:3]
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class AssetPlatformRetrieveApi(RetrieveAPIView):
|
||||
queryset = Platform.objects.all()
|
||||
|
|
|
@ -77,7 +77,7 @@ class CommandConfirmAPI(CreateAPIView):
|
|||
'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()]
|
||||
'reviewers': [str(user) for user in ticket.current_node.first().ticket_assignees.all()]
|
||||
}
|
||||
|
||||
@lazyproperty
|
||||
|
|
|
@ -6,6 +6,7 @@ from common.utils import get_logger
|
|||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsValidUser
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.mixins import generics
|
||||
from common.mixins.views import SuggestionMixin
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from ..models import SystemUser, Asset
|
||||
from .. import serializers
|
||||
|
@ -24,7 +25,7 @@ __all__ = [
|
|||
]
|
||||
|
||||
|
||||
class SystemUserViewSet(OrgBulkModelViewSet):
|
||||
class SystemUserViewSet(SuggestionMixin, OrgBulkModelViewSet):
|
||||
"""
|
||||
System user api set, for add,delete,update,list,retrieve resource
|
||||
"""
|
||||
|
@ -39,6 +40,7 @@ class SystemUserViewSet(OrgBulkModelViewSet):
|
|||
serializer_class = serializers.SystemUserSerializer
|
||||
serializer_classes = {
|
||||
'default': serializers.SystemUserSerializer,
|
||||
'suggestion': serializers.MiniSystemUserSerializer
|
||||
}
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
|
||||
|
|
|
@ -120,6 +120,7 @@ class Gateway(BaseUser):
|
|||
except(paramiko.AuthenticationException,
|
||||
paramiko.BadAuthenticationType,
|
||||
paramiko.SSHException,
|
||||
paramiko.ChannelException,
|
||||
paramiko.ssh_exception.NoValidConnectionsError,
|
||||
socket.gaierror) as e:
|
||||
err = str(e)
|
||||
|
@ -128,6 +129,8 @@ class Gateway(BaseUser):
|
|||
err = err.format(port=self.port, ip=self.ip)
|
||||
elif err == 'Authentication failed.':
|
||||
err = _('Authentication failed')
|
||||
elif err == 'Connect failed':
|
||||
err = _('Connect failed')
|
||||
self.is_connective = False
|
||||
return False, err
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ from .utils import validate_password_contains_left_double_curly_bracket
|
|||
from .base import AuthSerializerMixin
|
||||
|
||||
__all__ = [
|
||||
'SystemUserSerializer',
|
||||
'SystemUserSerializer', 'MiniSystemUserSerializer',
|
||||
'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer',
|
||||
'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer',
|
||||
'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer',
|
||||
|
@ -185,6 +185,12 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||
return queryset
|
||||
|
||||
|
||||
class MiniSystemUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
fields = SystemUserSerializer.Meta.fields_mini
|
||||
|
||||
|
||||
class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
|
||||
class Meta(SystemUserSerializer.Meta):
|
||||
fields_mini = ['id', 'name', 'username']
|
||||
|
@ -208,6 +214,7 @@ class SystemUserSimpleSerializer(serializers.ModelSerializer):
|
|||
"""
|
||||
系统用户最基本信息的数据结构
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
fields = ('id', 'name', 'username')
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.db.models.signals import post_save, post_delete, m2m_changed
|
||||
from django.db.models.signals import (
|
||||
post_save, post_delete, m2m_changed, pre_delete
|
||||
)
|
||||
from django.dispatch import receiver
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
|
@ -35,7 +37,7 @@ MODELS_NEED_RECORD = (
|
|||
# users
|
||||
'User', 'UserGroup',
|
||||
# acls
|
||||
'LoginACL', 'LoginAssetACL',
|
||||
'LoginACL', 'LoginAssetACL', 'LoginConfirmSetting',
|
||||
# assets
|
||||
'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule',
|
||||
'CommandFilter', 'Platform', 'AuthBook',
|
||||
|
@ -98,68 +100,68 @@ def create_operate_log(action, sender, resource):
|
|||
M2M_NEED_RECORD = {
|
||||
'OrganizationMember': (
|
||||
_('User and Organization'),
|
||||
_('{User} *JOINED* {Organization}'),
|
||||
_('{User} *LEFT* {Organization}')
|
||||
_('{User} JOINED {Organization}'),
|
||||
_('{User} LEFT {Organization}')
|
||||
),
|
||||
User.groups.through._meta.object_name: (
|
||||
_('User and Group'),
|
||||
_('{User} *JOINED* {UserGroup}'),
|
||||
_('{User} *LEFT* {UserGroup}')
|
||||
_('{User} JOINED {UserGroup}'),
|
||||
_('{User} LEFT {UserGroup}')
|
||||
),
|
||||
SystemUser.assets.through._meta.object_name: (
|
||||
_('Asset and SystemUser'),
|
||||
_('{Asset} *ADD* {SystemUser}'),
|
||||
_('{Asset} *REMOVE* {SystemUser}')
|
||||
_('{Asset} ADD {SystemUser}'),
|
||||
_('{Asset} REMOVE {SystemUser}')
|
||||
),
|
||||
Asset.nodes.through._meta.object_name: (
|
||||
_('Node and Asset'),
|
||||
_('{Node} *ADD* {Asset}'),
|
||||
_('{Node} *REMOVE* {Asset}')
|
||||
_('{Node} ADD {Asset}'),
|
||||
_('{Node} REMOVE {Asset}')
|
||||
),
|
||||
AssetPermission.users.through._meta.object_name: (
|
||||
_('User asset permissions'),
|
||||
_('{AssetPermission} *ADD* {User}'),
|
||||
_('{AssetPermission} *REMOVE* {User}'),
|
||||
_('{AssetPermission} ADD {User}'),
|
||||
_('{AssetPermission} REMOVE {User}'),
|
||||
),
|
||||
AssetPermission.user_groups.through._meta.object_name: (
|
||||
_('User group asset permissions'),
|
||||
_('{AssetPermission} *ADD* {UserGroup}'),
|
||||
_('{AssetPermission} *REMOVE* {UserGroup}'),
|
||||
_('{AssetPermission} ADD {UserGroup}'),
|
||||
_('{AssetPermission} REMOVE {UserGroup}'),
|
||||
),
|
||||
AssetPermission.assets.through._meta.object_name: (
|
||||
_('Asset permission'),
|
||||
_('{AssetPermission} *ADD* {Asset}'),
|
||||
_('{AssetPermission} *REMOVE* {Asset}'),
|
||||
_('{AssetPermission} ADD {Asset}'),
|
||||
_('{AssetPermission} REMOVE {Asset}'),
|
||||
),
|
||||
AssetPermission.nodes.through._meta.object_name: (
|
||||
_('Node permission'),
|
||||
_('{AssetPermission} *ADD* {Node}'),
|
||||
_('{AssetPermission} *REMOVE* {Node}'),
|
||||
_('{AssetPermission} ADD {Node}'),
|
||||
_('{AssetPermission} REMOVE {Node}'),
|
||||
),
|
||||
AssetPermission.system_users.through._meta.object_name: (
|
||||
_('Asset permission and SystemUser'),
|
||||
_('{AssetPermission} *ADD* {SystemUser}'),
|
||||
_('{AssetPermission} *REMOVE* {SystemUser}'),
|
||||
_('{AssetPermission} ADD {SystemUser}'),
|
||||
_('{AssetPermission} REMOVE {SystemUser}'),
|
||||
),
|
||||
ApplicationPermission.users.through._meta.object_name: (
|
||||
_('User application permissions'),
|
||||
_('{ApplicationPermission} *ADD* {User}'),
|
||||
_('{ApplicationPermission} *REMOVE* {User}'),
|
||||
_('{ApplicationPermission} ADD {User}'),
|
||||
_('{ApplicationPermission} REMOVE {User}'),
|
||||
),
|
||||
ApplicationPermission.user_groups.through._meta.object_name: (
|
||||
_('User group application permissions'),
|
||||
_('{ApplicationPermission} *ADD* {UserGroup}'),
|
||||
_('{ApplicationPermission} *REMOVE* {UserGroup}'),
|
||||
_('{ApplicationPermission} ADD {UserGroup}'),
|
||||
_('{ApplicationPermission} REMOVE {UserGroup}'),
|
||||
),
|
||||
ApplicationPermission.applications.through._meta.object_name: (
|
||||
_('Application permission'),
|
||||
_('{ApplicationPermission} *ADD* {Application}'),
|
||||
_('{ApplicationPermission} *REMOVE* {Application}'),
|
||||
_('{ApplicationPermission} ADD {Application}'),
|
||||
_('{ApplicationPermission} REMOVE {Application}'),
|
||||
),
|
||||
ApplicationPermission.system_users.through._meta.object_name: (
|
||||
_('Application permission and SystemUser'),
|
||||
_('{ApplicationPermission} *ADD* {SystemUser}'),
|
||||
_('{ApplicationPermission} *REMOVE* {SystemUser}'),
|
||||
_('{ApplicationPermission} ADD {SystemUser}'),
|
||||
_('{ApplicationPermission} REMOVE {SystemUser}'),
|
||||
),
|
||||
}
|
||||
|
||||
|
@ -226,7 +228,7 @@ def on_object_created_or_update(sender, instance=None, created=False, update_fie
|
|||
create_operate_log(action, sender, instance)
|
||||
|
||||
|
||||
@receiver(post_delete)
|
||||
@receiver(pre_delete)
|
||||
def on_object_delete(sender, instance=None, **kwargs):
|
||||
create_operate_log(models.OperateLog.ACTION_DELETE, sender, instance)
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ sms_failed_msg = _(
|
|||
"(The account will be temporarily locked for {block_time} minutes)"
|
||||
)
|
||||
mfa_type_failed_msg = _(
|
||||
"The MFA type({mfa_type}) is not supported"
|
||||
"The MFA type({mfa_type}) is not supported, "
|
||||
"You can also try {times_try} times "
|
||||
"(The account will be temporarily locked for {block_time} minutes)"
|
||||
)
|
||||
|
|
|
@ -346,6 +346,10 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
|||
def check_user_mfa_if_need(self, user):
|
||||
if self.request.session.get('auth_mfa'):
|
||||
return
|
||||
|
||||
if settings.OTP_IN_RADIUS:
|
||||
return
|
||||
|
||||
if not user.mfa_enabled:
|
||||
return
|
||||
unset, url = user.mfa_enabled_but_not_set()
|
||||
|
@ -411,11 +415,11 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
|||
return
|
||||
elif ticket.state_reject:
|
||||
raise errors.LoginConfirmOtherError(
|
||||
ticket.id, ticket.get_action_display()
|
||||
ticket.id, ticket.get_state_display()
|
||||
)
|
||||
elif ticket.state_close:
|
||||
raise errors.LoginConfirmOtherError(
|
||||
ticket.id, ticket.get_action_display()
|
||||
ticket.id, ticket.get_state_display()
|
||||
)
|
||||
else:
|
||||
raise errors.LoginConfirmOtherError(
|
||||
|
|
|
@ -45,6 +45,9 @@ class LoginConfirmSetting(CommonModelMixin):
|
|||
reviewers = models.ManyToManyField('users.User', verbose_name=_("Reviewers"), related_name="review_login_confirm_settings", blank=True)
|
||||
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Login Confirm')
|
||||
|
||||
@classmethod
|
||||
def get_user_confirm_setting(cls, user):
|
||||
return get_object_or_none(cls, user=user)
|
||||
|
@ -83,7 +86,8 @@ class LoginConfirmSetting(CommonModelMixin):
|
|||
return ticket
|
||||
|
||||
def __str__(self):
|
||||
return '{} confirm'.format(self.user.username)
|
||||
reviewers = [u.username for u in self.reviewers.all()]
|
||||
return _('{} need confirm by {}').format(self.user.username, reviewers)
|
||||
|
||||
|
||||
class SSOToken(models.JMSBaseModel):
|
||||
|
|
|
@ -14,7 +14,8 @@ from .signals import post_auth_success, post_auth_failed
|
|||
@receiver(user_logged_in)
|
||||
def on_user_auth_login_success(sender, user, request, **kwargs):
|
||||
# 开启了 MFA,且没有校验过
|
||||
if user.mfa_enabled and not request.session.get('auth_mfa'):
|
||||
|
||||
if user.mfa_enabled and not settings.OTP_IN_RADIUS and not request.session.get('auth_mfa'):
|
||||
request.session['auth_mfa_required'] = 1
|
||||
|
||||
if settings.USER_LOGIN_SINGLE_MACHINE_ENABLED:
|
||||
|
|
|
@ -52,10 +52,13 @@ class VerifyCodeUtil:
|
|||
ttl = self.ttl()
|
||||
if ttl > 0:
|
||||
raise CodeSendTooFrequently(ttl)
|
||||
|
||||
self.generate()
|
||||
self.save()
|
||||
self.send()
|
||||
try:
|
||||
self.generate()
|
||||
self.save()
|
||||
self.send()
|
||||
except JMSException:
|
||||
self.clear()
|
||||
raise
|
||||
|
||||
def generate(self):
|
||||
code = ''.join(random.sample('0123456789', 4))
|
||||
|
|
|
@ -19,19 +19,25 @@
|
|||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="code" placeholder="" required="" autofocus="autofocus">
|
||||
<span class="help-block">
|
||||
{% trans 'Please enter the verification code' %}
|
||||
</span>
|
||||
</div>
|
||||
<button id='send-sms-verify-code' class="btn btn-primary full-width m-b" onclick="sendSMSVerifyCode()" style="display: none">{% trans 'Send verification code' %}</button>
|
||||
<button type="submit" class="btn btn-primary block full-width m-b">{% trans 'Next' %}</button>
|
||||
<div class="form-group" style="display: flex">
|
||||
|
||||
<input id="mfa-code" required type="text" class="form-control" name="code" placeholder="{% trans 'Please enter the verification code' %}" autofocus="autofocus">
|
||||
<button id='send-sms-verify-code' type="button" class="btn btn-info full-width m-b" onclick="sendSMSVerifyCode()" style="width: 150px!important;">{% trans 'Send verification code' %}</button>
|
||||
|
||||
</div>
|
||||
|
||||
<button id='submit_button' type="submit" class="btn btn-primary block full-width m-b">{% trans 'Next' %}</button>
|
||||
<div>
|
||||
<small>{% trans "Can't provide security? Please contact the administrator!" %}</small>
|
||||
</div>
|
||||
</form>
|
||||
<style type="text/css">
|
||||
.disabledBtn {
|
||||
background: #e6e4e4!important;
|
||||
border-color: #d8d5d5!important;
|
||||
color: #949191!important;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
|
||||
var methodSelect = document.getElementById('verify-method-select');
|
||||
|
@ -44,19 +50,35 @@
|
|||
|
||||
if (type == "sms") {
|
||||
currentBtn.style.display = "block";
|
||||
currentBtn.disabled = false;
|
||||
}
|
||||
else {
|
||||
currentBtn.style.display = "none";
|
||||
currentBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
function sendSMSVerifyCode(){
|
||||
var currentBtn = document.getElementById('send-sms-verify-code');
|
||||
var time = 60
|
||||
var url = "{% url 'api-auth:sms-verify-code-send' %}";
|
||||
requestApi({
|
||||
url: url,
|
||||
method: "POST",
|
||||
success: function (data) {
|
||||
alert('验证码已发送');
|
||||
currentBtn.innerHTML = `{% trans 'Wait: ' %} ${time}`;
|
||||
currentBtn.disabled = true
|
||||
currentBtn.classList.add("disabledBtn" )
|
||||
var TimeInterval = setInterval(()=>{
|
||||
--time
|
||||
currentBtn.innerHTML = `{% trans 'Wait: ' %} ${time}`;
|
||||
if(time === 0) {
|
||||
currentBtn.innerHTML = "{% trans 'Send verification code' %}"
|
||||
currentBtn.disabled = false
|
||||
currentBtn.classList.remove("disabledBtn")
|
||||
clearInterval(TimeInterval)
|
||||
}
|
||||
},1000)
|
||||
alert("{% trans 'The verification code has been sent' %}");
|
||||
},
|
||||
error: function (text, data) {
|
||||
alert(data.detail)
|
||||
|
|
|
@ -66,10 +66,12 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
|||
return None
|
||||
|
||||
login_redirect = settings.LOGIN_REDIRECT_TO_BACKEND.lower()
|
||||
if login_redirect == ['CAS', 'cas'] and cas_auth_url:
|
||||
if login_redirect in ['cas'] and cas_auth_url:
|
||||
auth_url = cas_auth_url
|
||||
else:
|
||||
elif login_redirect in ['openid', 'oidc'] and openid_auth_url:
|
||||
auth_url = openid_auth_url
|
||||
else:
|
||||
auth_url = openid_auth_url or cas_auth_url
|
||||
|
||||
if settings.LOGIN_REDIRECT_TO_BACKEND or not settings.LOGIN_REDIRECT_MSG_ENABLED:
|
||||
redirect_url = auth_url
|
||||
|
@ -212,8 +214,10 @@ class UserLoginWaitConfirmView(TemplateView):
|
|||
if ticket:
|
||||
timestamp_created = datetime.datetime.timestamp(ticket.date_created)
|
||||
ticket_detail_url = TICKET_DETAIL_URL.format(id=ticket_id)
|
||||
assignees = ticket.current_node.first().ticket_assignees.all()
|
||||
assignees_display = ', '.join([str(i.assignee) for i in assignees])
|
||||
msg = _("""Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>
|
||||
Don't close this page""").format(ticket.assignees_display)
|
||||
Don't close this page""").format(assignees_display)
|
||||
else:
|
||||
timestamp_created = 0
|
||||
ticket_detail_url = ''
|
||||
|
|
|
@ -32,7 +32,7 @@ class UserLoginOtpView(mixins.AuthMixin, FormView):
|
|||
except Exception as e:
|
||||
logger.error(e)
|
||||
import traceback
|
||||
traceback.print_exception()
|
||||
traceback.print_exception(e)
|
||||
return redirect_to_guard_view()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
|
|
@ -11,37 +11,9 @@ from common.exceptions import JMSException
|
|||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class SMS_MESSAGE(TextChoices):
|
||||
"""
|
||||
定义短信的各种消息类型,会存到类似 `ALIBABA_SMS_SIGN_AND_TEMPLATES` settings 里
|
||||
|
||||
{
|
||||
'verification_code': {'sign_name': 'Jumpserver', 'template_code': 'SMS_222870834'},
|
||||
...
|
||||
}
|
||||
"""
|
||||
|
||||
"""
|
||||
验证码签名和模板。模板例子:
|
||||
`您的验证码:${code},您正进行身份验证,打死不告诉别人!`
|
||||
其中必须包含 `code` 变量
|
||||
"""
|
||||
VERIFICATION_CODE = 'verification_code'
|
||||
|
||||
def get_sign_and_tmpl(self, config: dict):
|
||||
try:
|
||||
data = config[self]
|
||||
return data['sign_name'], data['template_code']
|
||||
except KeyError as e:
|
||||
raise JMSException(
|
||||
code=f'{settings.SMS_BACKEND}_sign_and_tmpl_bad',
|
||||
detail=_('Invalid SMS sign and template: {}').format(e)
|
||||
)
|
||||
|
||||
|
||||
class BACKENDS(TextChoices):
|
||||
ALIBABA = 'alibaba', _('Alibaba')
|
||||
TENCENT = 'tencent', _('Tencent')
|
||||
ALIBABA = 'alibaba', _('Alibaba cloud')
|
||||
TENCENT = 'tencent', _('Tencent cloud')
|
||||
|
||||
|
||||
class BaseSMSClient:
|
||||
|
@ -49,11 +21,7 @@ class BaseSMSClient:
|
|||
短信终端的基类
|
||||
"""
|
||||
|
||||
SIGN_AND_TMPL_SETTING_FIELD: str
|
||||
|
||||
@property
|
||||
def sign_and_tmpl(self):
|
||||
return getattr(settings, self.SIGN_AND_TMPL_SETTING_FIELD, {})
|
||||
SIGN_AND_TMPL_SETTING_FIELD_PREFIX: str
|
||||
|
||||
@classmethod
|
||||
def new_from_settings(cls):
|
||||
|
@ -67,6 +35,12 @@ class SMS:
|
|||
client: BaseSMSClient
|
||||
|
||||
def __init__(self, backend=None):
|
||||
backend = backend or settings.SMS_BACKEND
|
||||
if backend not in BACKENDS:
|
||||
raise JMSException(
|
||||
code='sms_provider_not_support',
|
||||
detail=_('SMS provider not support: {}').format(backend)
|
||||
)
|
||||
m = importlib.import_module(f'.{backend or settings.SMS_BACKEND}', __package__)
|
||||
self.client = m.client.new_from_settings()
|
||||
|
||||
|
@ -80,5 +54,12 @@ class SMS:
|
|||
)
|
||||
|
||||
def send_verify_code(self, phone_number, code):
|
||||
sign_name, template_code = SMS_MESSAGE.VERIFICATION_CODE.get_sign_and_tmpl(self.client.sign_and_tmpl)
|
||||
sign_name = getattr(settings, f'{self.client.SIGN_AND_TMPL_SETTING_FIELD_PREFIX}_VERIFY_SIGN_NAME')
|
||||
template_code = getattr(settings, f'{self.client.SIGN_AND_TMPL_SETTING_FIELD_PREFIX}_VERIFY_TEMPLATE_CODE')
|
||||
|
||||
if not (sign_name and template_code):
|
||||
raise JMSException(
|
||||
code='verify_code_sign_tmpl_invalid',
|
||||
detail=_('SMS verification code signature or template invalid')
|
||||
)
|
||||
return self.send_sms([phone_number], sign_name, template_code, OrderedDict(code=code))
|
||||
|
|
|
@ -15,7 +15,7 @@ logger = get_logger(__file__)
|
|||
|
||||
|
||||
class AlibabaSMS(BaseSMSClient):
|
||||
SIGN_AND_TMPL_SETTING_FIELD = 'ALIBABA_SMS_SIGN_AND_TEMPLATES'
|
||||
SIGN_AND_TMPL_SETTING_FIELD_PREFIX = 'ALIBABA'
|
||||
|
||||
@classmethod
|
||||
def new_from_settings(cls):
|
||||
|
|
|
@ -20,7 +20,7 @@ class TencentSMS(BaseSMSClient):
|
|||
"""
|
||||
https://cloud.tencent.com/document/product/382/43196#.E5.8F.91.E9.80.81.E7.9F.AD.E4.BF.A1
|
||||
"""
|
||||
SIGN_AND_TMPL_SETTING_FIELD = 'TENCENT_SMS_SIGN_AND_TEMPLATES'
|
||||
SIGN_AND_TMPL_SETTING_FIELD_PREFIX = 'TENCENT'
|
||||
|
||||
@classmethod
|
||||
def new_from_settings(cls):
|
||||
|
|
|
@ -3,12 +3,14 @@
|
|||
# coding: utf-8
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.utils import timezone
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework import permissions
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.permissions import IsValidUser
|
||||
|
||||
__all__ = ["DatetimeSearchMixin", "PermissionsMixin"]
|
||||
|
||||
from rest_framework import permissions
|
||||
|
||||
|
||||
class DatetimeSearchMixin:
|
||||
date_format = '%Y-%m-%d'
|
||||
|
@ -52,3 +54,19 @@ class PermissionsMixin(UserPassesTestMixin):
|
|||
if not permission_class().has_permission(self.request, self):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class SuggestionMixin:
|
||||
suggestion_mini_count = 10
|
||||
|
||||
@action(methods=['get'], detail=False, permission_classes=(IsValidUser,))
|
||||
def suggestions(self, request, *args, **kwargs):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
queryset = queryset[:self.suggestion_mini_count]
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
|
|
@ -33,6 +33,8 @@ class IsSuperUser(IsValidUser):
|
|||
|
||||
class IsSuperUserOrAppUser(IsSuperUser):
|
||||
def has_permission(self, request, view):
|
||||
if request.user.is_anonymous:
|
||||
return False
|
||||
return super(IsSuperUserOrAppUser, self).has_permission(request, view) \
|
||||
or request.user.is_app
|
||||
|
||||
|
@ -67,6 +69,8 @@ class IsOrgAdminOrAppUser(IsValidUser):
|
|||
def has_permission(self, request, view):
|
||||
if not current_org:
|
||||
return False
|
||||
if request.user.is_anonymous:
|
||||
return False
|
||||
return super(IsOrgAdminOrAppUser, self).has_permission(request, view) \
|
||||
and (current_org.can_admin_by(request.user) or request.user.is_app)
|
||||
|
||||
|
|
|
@ -17,9 +17,10 @@ from terminal.models import Session
|
|||
from terminal.utils import ComponentsPrometheusMetricsUtil
|
||||
from orgs.utils import current_org
|
||||
from common.permissions import IsOrgAdmin, IsOrgAuditor
|
||||
from common.utils import lazyproperty
|
||||
from common.utils import lazyproperty, get_request_ip
|
||||
from orgs.caches import OrgResourceStatisticsCache
|
||||
|
||||
|
||||
__all__ = ['IndexApi']
|
||||
|
||||
|
||||
|
@ -297,19 +298,33 @@ class IndexApi(DatesLoginMetricMixin, APIView):
|
|||
|
||||
|
||||
class HealthApiMixin(APIView):
|
||||
def is_token_right(self):
|
||||
token = self.request.query_params.get('token')
|
||||
ok_token = settings.HEALTH_CHECK_TOKEN
|
||||
if ok_token and token != ok_token:
|
||||
return False
|
||||
return True
|
||||
pass
|
||||
|
||||
def check_permissions(self, request):
|
||||
if not self.is_token_right():
|
||||
msg = 'Health check token error, ' \
|
||||
'Please set query param in url and same with setting HEALTH_CHECK_TOKEN. ' \
|
||||
'eg: $PATH/?token=$HEALTH_CHECK_TOKEN'
|
||||
self.permission_denied(request, message={'error': msg}, code=403)
|
||||
# 先去掉 Health Api 的权限校验,方便各组件直接调用
|
||||
|
||||
# def is_token_right(self):
|
||||
# token = self.request.query_params.get('token')
|
||||
# ok_token = settings.HEALTH_CHECK_TOKEN
|
||||
# if ok_token and token != ok_token:
|
||||
# return False
|
||||
# return True
|
||||
|
||||
# def is_localhost(self):
|
||||
# ip = get_request_ip(self.request)
|
||||
# return ip in ['localhost', '127.0.0.1']
|
||||
|
||||
# def check_permissions(self, request):
|
||||
# if self.is_token_right():
|
||||
# return
|
||||
# if self.is_localhost():
|
||||
# return
|
||||
# msg = '''
|
||||
# Health check token error,
|
||||
# Please set query param in url and
|
||||
# same with setting HEALTH_CHECK_TOKEN.
|
||||
# eg: $PATH/?token=$HEALTH_CHECK_TOKEN
|
||||
# '''
|
||||
# self.permission_denied(request, message={'error': msg}, code=403)
|
||||
|
||||
|
||||
class HealthCheckView(HealthApiMixin):
|
||||
|
|
|
@ -249,12 +249,14 @@ class Config(dict):
|
|||
|
||||
'ALIBABA_ACCESS_KEY_ID': '',
|
||||
'ALIBABA_ACCESS_KEY_SECRET': '',
|
||||
'ALIBABA_SMS_SIGN_AND_TEMPLATES': {},
|
||||
'ALIBABA_VERIFY_SIGN_NAME': '',
|
||||
'ALIBABA_VERIFY_TEMPLATE_CODE': '',
|
||||
|
||||
'TENCENT_SECRET_ID': '',
|
||||
'TENCENT_SECRET_KEY': '',
|
||||
'TENCENT_SDKAPPID': '',
|
||||
'TENCENT_SMS_SIGN_AND_TEMPLATES': {},
|
||||
'TENCENT_VERIFY_SIGN_NAME': '',
|
||||
'TENCENT_VERIFY_TEMPLATE_CODE': '',
|
||||
|
||||
'OTP_VALID_WINDOW': 2,
|
||||
'OTP_ISSUER_NAME': 'JumpServer',
|
||||
|
|
|
@ -122,21 +122,7 @@ AUTH_FEISHU = CONFIG.AUTH_FEISHU
|
|||
FEISHU_APP_ID = CONFIG.FEISHU_APP_ID
|
||||
FEISHU_APP_SECRET = CONFIG.FEISHU_APP_SECRET
|
||||
|
||||
# SMS auth
|
||||
SMS_ENABLED = CONFIG.SMS_ENABLED
|
||||
SMS_BACKEND = CONFIG.SMS_BACKEND
|
||||
SMS_TEST_PHONE = CONFIG.SMS_TEST_PHONE
|
||||
|
||||
# Alibaba
|
||||
ALIBABA_ACCESS_KEY_ID = CONFIG.ALIBABA_ACCESS_KEY_ID
|
||||
ALIBABA_ACCESS_KEY_SECRET = CONFIG.ALIBABA_ACCESS_KEY_SECRET
|
||||
ALIBABA_SMS_SIGN_AND_TEMPLATES = CONFIG.ALIBABA_SMS_SIGN_AND_TEMPLATES
|
||||
|
||||
# TENCENT
|
||||
TENCENT_SECRET_ID = CONFIG.TENCENT_SECRET_ID
|
||||
TENCENT_SECRET_KEY = CONFIG.TENCENT_SECRET_KEY
|
||||
TENCENT_SDKAPPID = CONFIG.TENCENT_SDKAPPID
|
||||
TENCENT_SMS_SIGN_AND_TEMPLATES = CONFIG.TENCENT_SMS_SIGN_AND_TEMPLATES
|
||||
|
||||
# Other setting
|
||||
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
|
||||
|
@ -161,6 +147,7 @@ AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.api.AuthorizationTokenAuthent
|
|||
AUTHENTICATION_BACKENDS = [
|
||||
AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY, AUTH_BACKEND_WECOM,
|
||||
AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU, AUTH_BACKEND_AUTH_TOKEN,
|
||||
AUTH_BACKEND_SSO,
|
||||
]
|
||||
|
||||
if AUTH_CAS:
|
||||
|
@ -170,8 +157,6 @@ if AUTH_OPENID:
|
|||
AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_OIDC_CODE)
|
||||
if AUTH_RADIUS:
|
||||
AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_RADIUS)
|
||||
if AUTH_SSO:
|
||||
AUTHENTICATION_BACKENDS.append(AUTH_BACKEND_SSO)
|
||||
|
||||
|
||||
ONLY_ALLOW_EXIST_USER_AUTH = CONFIG.ONLY_ALLOW_EXIST_USER_AUTH
|
||||
|
|
|
@ -132,3 +132,20 @@ LOGIN_REDIRECT_MSG_ENABLED = CONFIG.LOGIN_REDIRECT_MSG_ENABLED
|
|||
CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS = CONFIG.CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS
|
||||
|
||||
XRDP_ENABLED = CONFIG.XRDP_ENABLED
|
||||
|
||||
|
||||
# SMS enabled
|
||||
SMS_ENABLED = CONFIG.SMS_ENABLED
|
||||
SMS_BACKEND = CONFIG.SMS_BACKEND
|
||||
SMS_TEST_PHONE = CONFIG.SMS_TEST_PHONE
|
||||
|
||||
# Alibaba
|
||||
ALIBABA_ACCESS_KEY_ID = CONFIG.ALIBABA_ACCESS_KEY_ID
|
||||
ALIBABA_ACCESS_KEY_SECRET = CONFIG.ALIBABA_ACCESS_KEY_SECRET
|
||||
ALIBABA_SMS_SIGN_AND_TEMPLATES = CONFIG.ALIBABA_SMS_SIGN_AND_TEMPLATES
|
||||
|
||||
# TENCENT
|
||||
TENCENT_SECRET_ID = CONFIG.TENCENT_SECRET_ID
|
||||
TENCENT_SECRET_KEY = CONFIG.TENCENT_SECRET_KEY
|
||||
TENCENT_SDKAPPID = CONFIG.TENCENT_SDKAPPID
|
||||
TENCENT_SMS_SIGN_AND_TEMPLATES = CONFIG.TENCENT_SMS_SIGN_AND_TEMPLATES
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -12,7 +12,7 @@ class BACKEND(models.TextChoices):
|
|||
DINGTALK = 'dingtalk', _('DingTalk')
|
||||
SITE_MSG = 'site_msg', _('Site message')
|
||||
FEISHU = 'feishu', _('FeiShu')
|
||||
SMS = 'sms', _('SMS')
|
||||
# SMS = 'sms', _('SMS')
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
from typing import Iterable
|
||||
import traceback
|
||||
from itertools import chain
|
||||
from collections import defaultdict
|
||||
import time
|
||||
|
||||
from celery import shared_task
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.utils.timezone import now
|
||||
from common.utils import lazyproperty
|
||||
from users.models import User
|
||||
from notifications.backends import BACKEND
|
||||
|
@ -90,32 +92,58 @@ class Message(metaclass=MessageType):
|
|||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
def send_test_msg(self):
|
||||
from users.models import User
|
||||
users = User.objects.filter(username='admin')
|
||||
self.send_msg(users, [])
|
||||
|
||||
def get_common_msg(self) -> dict:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_text_msg(self) -> dict:
|
||||
return self.common_msg
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
return self.common_msg
|
||||
|
||||
@lazyproperty
|
||||
def common_msg(self) -> dict:
|
||||
return self.get_common_msg()
|
||||
|
||||
@lazyproperty
|
||||
def text_msg(self) -> dict:
|
||||
return self.get_text_msg()
|
||||
|
||||
@lazyproperty
|
||||
def html_msg(self) -> dict:
|
||||
return self.get_html_msg()
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# 支持不同发送消息的方式定义自己的消息内容,比如有些支持 html 标签
|
||||
def get_dingtalk_msg(self) -> dict:
|
||||
return self.common_msg
|
||||
# 钉钉相同的消息一天只能发一次,所以给所有消息添加基于时间的序号,使他们不相同
|
||||
message = self.text_msg['message']
|
||||
suffix = _('\nTime: {}').format(now())
|
||||
|
||||
return {
|
||||
'subject': self.text_msg['subject'],
|
||||
'message': message + suffix
|
||||
}
|
||||
|
||||
def get_wecom_msg(self) -> dict:
|
||||
return self.common_msg
|
||||
return self.text_msg
|
||||
|
||||
def get_feishu_msg(self) -> dict:
|
||||
return self.common_msg
|
||||
return self.text_msg
|
||||
|
||||
def get_email_msg(self) -> dict:
|
||||
return self.common_msg
|
||||
return self.html_msg
|
||||
|
||||
def get_site_msg_msg(self) -> dict:
|
||||
return self.common_msg
|
||||
return self.html_msg
|
||||
|
||||
def get_sms_msg(self) -> dict:
|
||||
raise NotImplementedError
|
||||
return self.text_msg
|
||||
# --------------------------------------------------------------
|
||||
|
||||
|
||||
|
@ -136,6 +164,7 @@ class SystemMessage(Message):
|
|||
|
||||
self.send_msg(users, receive_backends)
|
||||
|
||||
|
||||
@classmethod
|
||||
def post_insert_to_db(cls, subscription: SystemMsgSubscription):
|
||||
pass
|
||||
|
|
|
@ -16,6 +16,7 @@ class ApplicationPermissionViewSet(BasePermissionViewSet):
|
|||
'name': ['exact'],
|
||||
'category': ['exact'],
|
||||
'type': ['exact', 'in'],
|
||||
'from_ticket': ['exact']
|
||||
}
|
||||
search_fields = ['name', 'category', 'type']
|
||||
custom_filter_fields = BasePermissionViewSet.custom_filter_fields + [
|
||||
|
|
|
@ -21,8 +21,8 @@ class PermissionBaseFilter(BaseFilterSet):
|
|||
|
||||
class Meta:
|
||||
fields = (
|
||||
'user_id', 'username', 'system_user_id', 'system_user', 'user_group_id',
|
||||
'user_group', 'name', 'all', 'is_valid',
|
||||
'user_id', 'username', 'system_user_id', 'system_user',
|
||||
'user_group_id', 'user_group', 'name', 'all', 'is_valid',
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -118,7 +118,7 @@ class AssetPermissionFilter(PermissionBaseFilter):
|
|||
fields = (
|
||||
'user_id', 'username', 'system_user_id', 'system_user', 'user_group_id',
|
||||
'user_group', 'node_id', 'node', 'asset_id', 'hostname', 'ip', 'name',
|
||||
'all', 'asset_id', 'is_valid', 'is_effective',
|
||||
'all', 'asset_id', 'is_valid', 'is_effective', 'from_ticket'
|
||||
)
|
||||
|
||||
@property
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 3.1.12 on 2021-09-10 03:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0076_delete_assetuser'),
|
||||
('perms', '0019_auto_20210906_1044'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='assetpermission',
|
||||
name='system_users',
|
||||
field=models.ManyToManyField(blank=True, related_name='granted_by_permissions', to='assets.SystemUser', verbose_name='System user'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='applicationpermission',
|
||||
name='system_users',
|
||||
field=models.ManyToManyField(blank=True, related_name='granted_by_application_permissions', to='assets.SystemUser', verbose_name='System user'),
|
||||
),
|
||||
]
|
|
@ -27,7 +27,8 @@ class ApplicationPermission(BasePermission):
|
|||
verbose_name=_("Application")
|
||||
)
|
||||
system_users = models.ManyToManyField(
|
||||
'assets.SystemUser', related_name='granted_by_application_permissions',
|
||||
'assets.SystemUser',
|
||||
related_name='granted_by_application_permissions', blank=True,
|
||||
verbose_name=_("System user")
|
||||
)
|
||||
|
||||
|
|
|
@ -98,7 +98,7 @@ class Action:
|
|||
class AssetPermission(BasePermission):
|
||||
assets = models.ManyToManyField('assets.Asset', related_name='granted_by_permissions', blank=True, verbose_name=_("Asset"))
|
||||
nodes = models.ManyToManyField('assets.Node', related_name='granted_by_permissions', blank=True, verbose_name=_("Nodes"))
|
||||
system_users = models.ManyToManyField('assets.SystemUser', related_name='granted_by_permissions', verbose_name=_("System user"))
|
||||
system_users = models.ManyToManyField('assets.SystemUser', related_name='granted_by_permissions', blank=True, verbose_name=_("System user"))
|
||||
actions = models.IntegerField(choices=Action.DB_CHOICES, default=Action.ALL, verbose_name=_("Actions"))
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -549,7 +549,7 @@ class UserGrantedNodesQueryUtils(UserGrantedUtilsBase):
|
|||
return self.get_top_level_nodes()
|
||||
|
||||
nodes = PermNode.objects.none()
|
||||
if key == PermNode.FAVORITE_NODE_KEY:
|
||||
if key in [PermNode.FAVORITE_NODE_KEY, PermNode.UNGROUPED_NODE_KEY]:
|
||||
return nodes
|
||||
|
||||
node = PermNode.objects.get(key=key)
|
||||
|
|
|
@ -4,7 +4,6 @@ from rest_framework.exceptions import APIException
|
|||
from rest_framework import status
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.message.backends.sms import SMS_MESSAGE
|
||||
from common.message.backends.sms.alibaba import AlibabaSMS
|
||||
from settings.models import Setting
|
||||
from common.permissions import IsSuperUser
|
||||
|
@ -23,7 +22,8 @@ class AlibabaSMSTestingAPI(GenericAPIView):
|
|||
|
||||
alibaba_access_key_id = serializer.validated_data['ALIBABA_ACCESS_KEY_ID']
|
||||
alibaba_access_key_secret = serializer.validated_data.get('ALIBABA_ACCESS_KEY_SECRET')
|
||||
alibaba_sms_sign_and_tmpl = serializer.validated_data['ALIBABA_SMS_SIGN_AND_TEMPLATES']
|
||||
alibaba_verify_sign_name = serializer.validated_data['ALIBABA_VERIFY_SIGN_NAME']
|
||||
alibaba_verify_template_code = serializer.validated_data['ALIBABA_VERIFY_TEMPLATE_CODE']
|
||||
test_phone = serializer.validated_data.get('SMS_TEST_PHONE')
|
||||
|
||||
if not test_phone:
|
||||
|
@ -41,12 +41,11 @@ class AlibabaSMSTestingAPI(GenericAPIView):
|
|||
access_key_id=alibaba_access_key_id,
|
||||
access_key_secret=alibaba_access_key_secret
|
||||
)
|
||||
sign, tmpl = SMS_MESSAGE.VERIFICATION_CODE.get_sign_and_tmpl(alibaba_sms_sign_and_tmpl)
|
||||
|
||||
client.send_sms(
|
||||
phone_numbers=[test_phone],
|
||||
sign_name=sign,
|
||||
template_code=tmpl,
|
||||
sign_name=alibaba_verify_sign_name,
|
||||
template_code=alibaba_verify_template_code,
|
||||
template_param={'code': 'test'}
|
||||
)
|
||||
return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')})
|
||||
|
|
|
@ -34,6 +34,7 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
|
|||
'sso': serializers.SSOSettingSerializer,
|
||||
'clean': serializers.CleaningSerializer,
|
||||
'other': serializers.OtherSettingSerializer,
|
||||
'sms': serializers.SMSSettingSerializer,
|
||||
'alibaba': serializers.AlibabaSMSSettingSerializer,
|
||||
'tencent': serializers.TencentSMSSettingSerializer,
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ from rest_framework.exceptions import APIException
|
|||
from rest_framework import status
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.message.backends.sms import SMS_MESSAGE
|
||||
from common.message.backends.sms.tencent import TencentSMS
|
||||
from settings.models import Setting
|
||||
from common.permissions import IsSuperUser
|
||||
|
@ -25,7 +24,8 @@ class TencentSMSTestingAPI(GenericAPIView):
|
|||
|
||||
tencent_secret_id = serializer.validated_data['TENCENT_SECRET_ID']
|
||||
tencent_secret_key = serializer.validated_data.get('TENCENT_SECRET_KEY')
|
||||
tencent_sms_sign_and_tmpl = serializer.validated_data['TENCENT_SMS_SIGN_AND_TEMPLATES']
|
||||
tencent_verify_sign_name = serializer.validated_data['TENCENT_VERIFY_SIGN_NAME']
|
||||
tencent_verify_template_code = serializer.validated_data['TENCENT_VERIFY_TEMPLATE_CODE']
|
||||
tencent_sdkappid = serializer.validated_data.get('TENCENT_SDKAPPID')
|
||||
|
||||
test_phone = serializer.validated_data.get('SMS_TEST_PHONE')
|
||||
|
@ -46,12 +46,11 @@ class TencentSMSTestingAPI(GenericAPIView):
|
|||
secret_key=tencent_secret_key,
|
||||
sdkappid=tencent_sdkappid
|
||||
)
|
||||
sign, tmpl = SMS_MESSAGE.VERIFICATION_CODE.get_sign_and_tmpl(tencent_sms_sign_and_tmpl)
|
||||
|
||||
client.send_sms(
|
||||
phone_numbers=[test_phone],
|
||||
sign_name=sign,
|
||||
template_code=tmpl,
|
||||
sign_name=tencent_verify_sign_name,
|
||||
template_code=tencent_verify_template_code,
|
||||
template_param=OrderedDict(code='test')
|
||||
)
|
||||
return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')})
|
||||
|
|
|
@ -86,20 +86,48 @@ class Setting(models.Model):
|
|||
setattr(settings, self.name, self.cleaned_value)
|
||||
|
||||
@classmethod
|
||||
def refresh_AUTH_LDAP(cls):
|
||||
setting = cls.objects.filter(name='AUTH_LDAP').first()
|
||||
def refresh_authentications(cls, name):
|
||||
setting = cls.objects.filter(name=name).first()
|
||||
if not setting:
|
||||
return
|
||||
ldap_backend = settings.AUTH_BACKEND_LDAP
|
||||
backends = settings.AUTHENTICATION_BACKENDS
|
||||
has = ldap_backend in backends
|
||||
if setting.cleaned_value and not has:
|
||||
settings.AUTHENTICATION_BACKENDS.insert(0, ldap_backend)
|
||||
|
||||
if not setting.cleaned_value and has:
|
||||
index = backends.index(ldap_backend)
|
||||
backends.pop(index)
|
||||
settings.AUTH_LDAP = setting.cleaned_value
|
||||
backends_map = {
|
||||
'AUTH_LDAP': [settings.AUTH_BACKEND_LDAP],
|
||||
'AUTH_OPENID': [settings.AUTH_BACKEND_OIDC_CODE, settings.AUTH_BACKEND_OIDC_PASSWORD],
|
||||
'AUTH_RADIUS': [settings.AUTH_BACKEND_RADIUS],
|
||||
'AUTH_CAS': [settings.AUTH_BACKEND_CAS],
|
||||
}
|
||||
setting_backends = backends_map[name]
|
||||
auth_backends = settings.AUTHENTICATION_BACKENDS
|
||||
|
||||
for backend in setting_backends:
|
||||
has = backend in auth_backends
|
||||
|
||||
# 添加
|
||||
if setting.cleaned_value and not has:
|
||||
logger.debug('Add auth backend: ', name)
|
||||
settings.AUTHENTICATION_BACKENDS.insert(0, backend)
|
||||
|
||||
# 去掉
|
||||
if not setting.cleaned_value and has:
|
||||
index = auth_backends.index(backend)
|
||||
logger.debug('Pop auth backend: ', name)
|
||||
auth_backends.pop(index)
|
||||
|
||||
# 设置内存值
|
||||
setattr(settings, name, setting.cleaned_value)
|
||||
|
||||
@classmethod
|
||||
def refresh_AUTH_LDAP(cls):
|
||||
cls.refresh_authentications('AUTH_LDAP')
|
||||
|
||||
@classmethod
|
||||
def refresh_AUTH_OPENID(cls):
|
||||
cls.refresh_authentications('AUTH_OPENID')
|
||||
|
||||
@classmethod
|
||||
def refresh_AUTH_RADIUS(cls):
|
||||
cls.refresh_authentications('AUTH_RADIUS')
|
||||
|
||||
@classmethod
|
||||
def update_or_create(cls, name='', value='', encrypted=False, category=''):
|
||||
|
|
|
@ -15,11 +15,12 @@ class AuthSettingSerializer(serializers.Serializer):
|
|||
AUTH_WECOM = serializers.BooleanField(default=False, label=_('WeCom Auth'))
|
||||
AUTH_SSO = serializers.BooleanField(default=False, label=_("SSO Auth"))
|
||||
FORGOT_PASSWORD_URL = serializers.CharField(
|
||||
required=False, max_length=1024, label=_("Forgot password url")
|
||||
)
|
||||
HEALTH_CHECK_TOKEN = serializers.CharField(
|
||||
required=False, max_length=1024, label=_("Health check token")
|
||||
required=False, allow_blank=True, max_length=1024,
|
||||
label=_("Forgot password url")
|
||||
)
|
||||
# HEALTH_CHECK_TOKEN = serializers.CharField(
|
||||
# required=False, max_length=1024, label=_("Health check token")
|
||||
# )
|
||||
LOGIN_REDIRECT_MSG_ENABLED = serializers.BooleanField(
|
||||
required=False, label=_("Enable login redirect msg")
|
||||
)
|
||||
|
|
|
@ -3,49 +3,42 @@ from rest_framework import serializers
|
|||
|
||||
from common.message.backends.sms import BACKENDS
|
||||
|
||||
__all__ = ['AlibabaSMSSettingSerializer', 'TencentSMSSettingSerializer']
|
||||
__all__ = ['SMSSettingSerializer', 'AlibabaSMSSettingSerializer', 'TencentSMSSettingSerializer']
|
||||
|
||||
|
||||
class SMSSettingSerializer(serializers.Serializer):
|
||||
SMS_ENABLED = serializers.BooleanField(default=False, label=_('Enable SMS'))
|
||||
SMS_BACKEND = serializers.ChoiceField(
|
||||
choices=BACKENDS.choices, default=BACKENDS.ALIBABA, label=_('SMS provider')
|
||||
)
|
||||
|
||||
|
||||
class SignTmplPairSerializer(serializers.Serializer):
|
||||
SIGN_NAME = serializers.CharField(max_length=256, required=True, label=_('Signature'))
|
||||
TEMPLATE_CODE = serializers.CharField(max_length=256, required=True, label=_('Template code'))
|
||||
|
||||
|
||||
class BaseSMSSettingSerializer(serializers.Serializer):
|
||||
SMS_ENABLED = serializers.BooleanField(default=False, label=_('Enable SMS'))
|
||||
SMS_TEST_PHONE = serializers.CharField(max_length=256, required=False, label=_('Test phone'))
|
||||
SMS_TEST_PHONE = serializers.CharField(max_length=256, required=False, allow_blank=True, label=_('Test phone'))
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = super().to_representation(instance)
|
||||
data['SMS_BACKEND'] = self.fields['SMS_BACKEND'].default
|
||||
# data['SMS_BACKEND'] = self.fields['SMS_BACKEND'].default
|
||||
return data
|
||||
|
||||
|
||||
class AlibabaSMSSettingSerializer(BaseSMSSettingSerializer):
|
||||
SMS_BACKEND = serializers.ChoiceField(choices=BACKENDS.choices, default=BACKENDS.ALIBABA)
|
||||
ALIBABA_ACCESS_KEY_ID = serializers.CharField(max_length=256, required=True, label='AccessKeyId')
|
||||
ALIBABA_ACCESS_KEY_SECRET = serializers.CharField(
|
||||
max_length=256, required=False, label='AccessKeySecret', write_only=True)
|
||||
ALIBABA_SMS_SIGN_AND_TEMPLATES = serializers.DictField(
|
||||
label=_('Signatures and Templates'), required=True, help_text=_('''
|
||||
Filling in JSON Data:
|
||||
{
|
||||
"verification_code": {
|
||||
"sign_name": "<Your signature name>",
|
||||
"template_code": "<Your template code>"
|
||||
}
|
||||
}
|
||||
''')
|
||||
max_length=256, required=False, label='AccessKeySecret', write_only=True
|
||||
)
|
||||
ALIBABA_VERIFY_SIGN_NAME = serializers.CharField(max_length=256, required=True, label=_('Signature'))
|
||||
ALIBABA_VERIFY_TEMPLATE_CODE = serializers.CharField(max_length=256, required=True, label=_('Template code'))
|
||||
|
||||
|
||||
class TencentSMSSettingSerializer(BaseSMSSettingSerializer):
|
||||
SMS_BACKEND = serializers.ChoiceField(choices=BACKENDS.choices, default=BACKENDS.TENCENT)
|
||||
TENCENT_SECRET_ID = serializers.CharField(max_length=256, required=True, label='Secret id')
|
||||
TENCENT_SECRET_KEY = serializers.CharField(max_length=256, required=False, label='Secret key', write_only=True)
|
||||
TENCENT_SDKAPPID = serializers.CharField(max_length=256, required=True, label='SDK app id')
|
||||
TENCENT_SMS_SIGN_AND_TEMPLATES = serializers.DictField(
|
||||
label=_('Signatures and Templates'), required=True, help_text=_('''
|
||||
Filling in JSON Data:
|
||||
{
|
||||
"verification_code": {
|
||||
"sign_name": "<Your signature name>",
|
||||
"template_code": "<Your template code>"
|
||||
}
|
||||
}
|
||||
'''))
|
||||
TENCENT_VERIFY_SIGN_NAME = serializers.CharField(max_length=256, required=True, label=_('Signature'))
|
||||
TENCENT_VERIFY_TEMPLATE_CODE = serializers.CharField(max_length=256, required=True, label=_('Template code'))
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
from abc import ABCMeta
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
|
@ -17,8 +15,13 @@ class OtherSettingSerializer(serializers.Serializer):
|
|||
OTP_VALID_WINDOW = serializers.IntegerField(label=_("OTP valid window"))
|
||||
|
||||
PERIOD_TASK_ENABLED = serializers.BooleanField(required=False, label=_("Enable period task"))
|
||||
WINDOWS_SSH_DEFAULT_SHELL = serializers.CharField(
|
||||
required=False, max_length=1024, label=_('Ansible windows default shell')
|
||||
WINDOWS_SSH_DEFAULT_SHELL = serializers.ChoiceField(
|
||||
choices=[
|
||||
('cmd', _("CMD")),
|
||||
('powershell', _("PowerShell"))
|
||||
],
|
||||
label=_('Shell (Windows)'),
|
||||
help_text=_('The shell type used when Windows assets perform ansible tasks')
|
||||
)
|
||||
|
||||
PERM_SINGLE_ASSET_TO_UNGROUP_NODE = serializers.BooleanField(
|
||||
|
|
|
@ -111,5 +111,8 @@ class SecuritySettingSerializer(SecurityPasswordRuleSerializer, SecurityAuthSeri
|
|||
required=True, label=_('Session share'),
|
||||
help_text=_("Enabled, Allows user active session to be shared with other users")
|
||||
)
|
||||
|
||||
LOGIN_CONFIRM_ENABLE = serializers.BooleanField(
|
||||
required=False, label=_('Login Confirm'),
|
||||
help_text=_("Enabled, please go to the user detail add approver")
|
||||
)
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from typing import Callable
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.conf import settings
|
||||
|
||||
|
@ -7,6 +9,7 @@ from notifications.notifications import SystemMessage
|
|||
from terminal.models import Session, Command
|
||||
from notifications.models import SystemMsgSubscription
|
||||
from notifications.backends import BACKEND
|
||||
from common.utils import lazyproperty
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
@ -17,19 +20,31 @@ CATEGORY_LABEL = _('Sessions')
|
|||
|
||||
|
||||
class CommandAlertMixin:
|
||||
def get_dingtalk_msg(self) -> str:
|
||||
msg = self._get_message()
|
||||
msg = msg.replace('<br>', '')
|
||||
return msg
|
||||
command: dict
|
||||
_get_message: Callable
|
||||
message_type_label: str
|
||||
|
||||
@lazyproperty
|
||||
def subject(self):
|
||||
_input = self.command['input']
|
||||
if isinstance(_input, str):
|
||||
_input = _input.replace('\r\n', ' ').replace('\r', ' ').replace('\n', ' ')
|
||||
|
||||
subject = self.message_type_label + "%(cmd)s" % {
|
||||
'cmd': _input
|
||||
}
|
||||
return subject
|
||||
|
||||
@classmethod
|
||||
def post_insert_to_db(cls, subscription: SystemMsgSubscription):
|
||||
"""
|
||||
兼容操作,试图用 `settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER` 的邮件地址assets_systemuser_assets找到
|
||||
用户,把用户设置为默认接收者
|
||||
兼容操作,试图用 `settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER` 的邮件地址
|
||||
assets_systemuser_assets找到用户,把用户设置为默认接收者
|
||||
"""
|
||||
from settings.models import Setting
|
||||
db_setting = Setting.objects.filter(name='SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER').first()
|
||||
db_setting = Setting.objects.filter(
|
||||
name='SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER'
|
||||
).first()
|
||||
if db_setting:
|
||||
emails = db_setting.value
|
||||
else:
|
||||
|
@ -52,61 +67,64 @@ class CommandAlertMessage(CommandAlertMixin, SystemMessage):
|
|||
def __init__(self, command):
|
||||
self.command = command
|
||||
|
||||
def _get_message(self):
|
||||
def get_text_msg(self) -> dict:
|
||||
command = self.command
|
||||
session_obj = Session.objects.get(id=command['session'])
|
||||
session = Session.objects.get(id=command['session'])
|
||||
session_detail_url = reverse(
|
||||
'api-terminal:session-detail', kwargs={'pk': command['session']},
|
||||
external=True, api_to_ui=True
|
||||
)
|
||||
|
||||
message = _("""
|
||||
Command: %(command)s
|
||||
<br>
|
||||
Asset: %(host_name)s (%(host_ip)s)
|
||||
<br>
|
||||
User: %(user)s
|
||||
<br>
|
||||
Level: %(risk_level)s
|
||||
<br>
|
||||
Session: <a href="%(session_detail_url)s">session detail</a>
|
||||
<br>
|
||||
""") % {
|
||||
Command: %(command)s
|
||||
Asset: %(hostname)s (%(host_ip)s)
|
||||
User: %(user)s
|
||||
Level: %(risk_level)s
|
||||
Session: %(session_detail_url)s?oid=%(oid)s
|
||||
""") % {
|
||||
'command': command['input'],
|
||||
'host_name': command['asset'],
|
||||
'host_ip': session_obj.asset_obj.ip,
|
||||
'hostname': command['asset'],
|
||||
'host_ip': session.asset_obj.ip,
|
||||
'user': command['user'],
|
||||
'risk_level': Command.get_risk_level_str(command['risk_level']),
|
||||
'session_detail_url': reverse('api-terminal:session-detail',
|
||||
kwargs={'pk': command['session']},
|
||||
external=True, api_to_ui=True),
|
||||
'session_detail_url': session_detail_url,
|
||||
'oid': session.org_id
|
||||
}
|
||||
|
||||
return message
|
||||
|
||||
def get_common_msg(self):
|
||||
msg = self._get_message()
|
||||
|
||||
return {
|
||||
'subject': msg[:80],
|
||||
'message': msg
|
||||
'subject': self.subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
def get_email_msg(self):
|
||||
def get_html_msg(self) -> dict:
|
||||
command = self.command
|
||||
session_obj = Session.objects.get(id=command['session'])
|
||||
session = Session.objects.get(id=command['session'])
|
||||
session_detail_url = reverse(
|
||||
'api-terminal:session-detail', kwargs={'pk': command['session']},
|
||||
external=True, api_to_ui=True
|
||||
)
|
||||
|
||||
input = command['input']
|
||||
if isinstance(input, str):
|
||||
input = input.replace('\r\n', ' ').replace('\r', ' ').replace('\n', ' ')
|
||||
|
||||
subject = _("Insecure Command Alert: [%(name)s->%(login_from)s@%(remote_addr)s] $%(command)s") % {
|
||||
'name': command['user'],
|
||||
'login_from': session_obj.get_login_from_display(),
|
||||
'remote_addr': session_obj.remote_addr,
|
||||
'command': input
|
||||
message = _("""
|
||||
Command: %(command)s
|
||||
<br>
|
||||
Asset: %(hostname)s (%(host_ip)s)
|
||||
<br>
|
||||
User: %(user)s
|
||||
<br>
|
||||
Level: %(risk_level)s
|
||||
<br>
|
||||
Session: <a href="%(session_detail_url)s?oid=%(oid)s">session detail</a>
|
||||
<br>
|
||||
""") % {
|
||||
'command': command['input'],
|
||||
'hostname': command['asset'],
|
||||
'host_ip': session.asset_obj.ip,
|
||||
'user': command['user'],
|
||||
'risk_level': Command.get_risk_level_str(command['risk_level']),
|
||||
'session_detail_url': session_detail_url,
|
||||
'oid': session.org_id
|
||||
}
|
||||
|
||||
message = self._get_message()
|
||||
|
||||
return {
|
||||
'subject': subject,
|
||||
'subject': self.subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
|
@ -119,10 +137,10 @@ class CommandExecutionAlert(CommandAlertMixin, SystemMessage):
|
|||
def __init__(self, command):
|
||||
self.command = command
|
||||
|
||||
def _get_message(self):
|
||||
def get_html_msg(self) -> dict:
|
||||
command = self.command
|
||||
input = command['input']
|
||||
input = input.replace('\n', '<br>')
|
||||
_input = command['input']
|
||||
_input = _input.replace('\n', '<br>')
|
||||
|
||||
assets = ', '.join([str(asset) for asset in command['assets']])
|
||||
message = _("""
|
||||
|
@ -137,22 +155,36 @@ class CommandExecutionAlert(CommandAlertMixin, SystemMessage):
|
|||
%(command)s <br>
|
||||
----------------- Commands ---------------- <br>
|
||||
""") % {
|
||||
'command': input,
|
||||
'command': _input,
|
||||
'assets': assets,
|
||||
'user': command['user'],
|
||||
'risk_level': Command.get_risk_level_str(command['risk_level']),
|
||||
'risk_level': Command.get_risk_level_str(command['risk_level'])
|
||||
}
|
||||
return message
|
||||
|
||||
def get_common_msg(self):
|
||||
command = self.command
|
||||
|
||||
subject = _("Insecure Web Command Execution Alert: [%(name)s]") % {
|
||||
'name': command['user'],
|
||||
}
|
||||
message = self._get_message()
|
||||
|
||||
return {
|
||||
'subject': subject,
|
||||
'subject': self.subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
def get_text_msg(self) -> dict:
|
||||
command = self.command
|
||||
_input = command['input']
|
||||
|
||||
assets = ', '.join([str(asset) for asset in command['assets']])
|
||||
message = _("""
|
||||
Assets: %(assets)s
|
||||
User: %(user)s
|
||||
Level: %(risk_level)s
|
||||
|
||||
Commands 👇 ------------
|
||||
%(command)s
|
||||
------------------------
|
||||
""") % {
|
||||
'command': _input,
|
||||
'assets': assets,
|
||||
'user': command['user'],
|
||||
'risk_level': Command.get_risk_level_str(command['risk_level'])
|
||||
}
|
||||
return {
|
||||
'subject': self.subject,
|
||||
'message': message
|
||||
}
|
||||
|
|
|
@ -50,13 +50,21 @@ class BaseTerminal(object):
|
|||
time.sleep(self.interval)
|
||||
|
||||
def get_or_register_terminal(self):
|
||||
terminal = Terminal.objects.filter(name=self.name, type=self.type, is_deleted=False).first()
|
||||
terminal = Terminal.objects.filter(
|
||||
name=self.name, type=self.type, is_deleted=False
|
||||
).first()
|
||||
if not terminal:
|
||||
terminal = self.register_terminal()
|
||||
|
||||
terminal.remote_addr = self.remote_addr
|
||||
terminal.save()
|
||||
return terminal
|
||||
|
||||
def register_terminal(self):
|
||||
data = {'name': self.name, 'type': self.type, 'remote_addr': self.remote_addr}
|
||||
data = {
|
||||
'name': self.name, 'type': self.type,
|
||||
'remote_addr': self.remote_addr
|
||||
}
|
||||
serializer = TerminalRegistrationSerializer(data=data)
|
||||
serializer.is_valid()
|
||||
terminal = serializer.save()
|
||||
|
@ -68,7 +76,8 @@ class CoreTerminal(BaseTerminal):
|
|||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
suffix_name=TerminalTypeChoices.core.label, _type=TerminalTypeChoices.core.value
|
||||
suffix_name=TerminalTypeChoices.core.label,
|
||||
_type=TerminalTypeChoices.core.value
|
||||
)
|
||||
|
||||
|
||||
|
@ -76,5 +85,6 @@ class CoreTerminal(BaseTerminal):
|
|||
class CeleryTerminal(BaseTerminal):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
suffix_name=TerminalTypeChoices.celery.label, _type=TerminalTypeChoices.celery.value
|
||||
suffix_name=TerminalTypeChoices.celery.label,
|
||||
_type=TerminalTypeChoices.celery.value
|
||||
)
|
||||
|
|
|
@ -37,8 +37,8 @@ class Handler(BaseHandler):
|
|||
def _construct_meta_body_of_open(self):
|
||||
apply_category_display = self.ticket.meta.get('apply_category_display')
|
||||
apply_type_display = self.ticket.meta.get('apply_type_display')
|
||||
apply_applications = self.ticket.meta.get('apply_applications', [])
|
||||
apply_system_users = self.ticket.meta.get('apply_system_users', [])
|
||||
apply_applications = self.ticket.meta.get('apply_applications_display', [])
|
||||
apply_system_users = self.ticket.meta.get('apply_system_users_display', [])
|
||||
apply_date_start = self.ticket.meta.get('apply_date_start')
|
||||
apply_date_expired = self.ticket.meta.get('apply_date_expired')
|
||||
applied_body = '''{}: {},
|
||||
|
|
|
@ -32,8 +32,8 @@ class Handler(BaseHandler):
|
|||
|
||||
# body
|
||||
def _construct_meta_body_of_open(self):
|
||||
apply_assets = self.ticket.meta.get('apply_assets', [])
|
||||
apply_system_users = self.ticket.meta.get('apply_system_users', [])
|
||||
apply_assets = self.ticket.meta.get('apply_assets_display', [])
|
||||
apply_system_users = self.ticket.meta.get('apply_system_users_display', [])
|
||||
apply_actions_display = self.ticket.meta.get('apply_actions_display', [])
|
||||
apply_date_start = self.ticket.meta.get('apply_date_start')
|
||||
apply_date_expired = self.ticket.meta.get('apply_date_expired')
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from common.mixins.models import CommonModelMixin
|
||||
from common.db.encoder import ModelJSONFieldEncoder
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from orgs.models import Organization
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from ..const import TicketType, TicketApprovalLevel, TicketApprovalStrategy
|
||||
from ..signals import post_or_update_change_ticket_flow_approval
|
||||
|
@ -67,5 +68,5 @@ class TicketFlow(CommonModelMixin, OrgModelMixin):
|
|||
flows = cls.objects.all()
|
||||
cur_flow_types = flows.values_list('type', flat=True)
|
||||
with tmp_to_root_org():
|
||||
diff_global_flows = cls.objects.exclude(type__in=cur_flow_types)
|
||||
diff_global_flows = cls.objects.exclude(type__in=cur_flow_types).filter(org_id=Organization.ROOT_ID)
|
||||
return flows | diff_global_flows
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from . import const
|
||||
from notifications.notifications import UserMessage
|
||||
from common.utils import get_logger
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
EMAIL_TEMPLATE = '''
|
||||
<div>
|
||||
<p>
|
||||
{title}
|
||||
<a href={ticket_detail_url}>
|
||||
<strong>{ticket_detail_url_description}</strong>
|
||||
</a>
|
||||
</p>
|
||||
<div>
|
||||
{body}
|
||||
</div>
|
||||
</div>
|
||||
'''
|
||||
|
||||
|
||||
class BaseTicketMessage(UserMessage):
|
||||
|
||||
@property
|
||||
def subject(self):
|
||||
return _(self.title).format(self.ticket.title, self.ticket.get_type_display())
|
||||
|
||||
@property
|
||||
def ticket_detail_url(self):
|
||||
return urljoin(settings.SITE_URL, const.TICKET_DETAIL_URL.format(id=str(self.ticket.id)))
|
||||
|
||||
def get_text_msg(self) -> dict:
|
||||
message = """
|
||||
{title}: {ticket_detail_url} -> {ticket_detail_url_description}
|
||||
{body}
|
||||
""".format(
|
||||
title=self.content_title,
|
||||
ticket_detail_url=self.ticket_detail_url,
|
||||
ticket_detail_url_description=_('click here to review'),
|
||||
body=self.ticket.body
|
||||
)
|
||||
return {
|
||||
'subject': self.subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
message = EMAIL_TEMPLATE.format(
|
||||
title=self.content_title,
|
||||
ticket_detail_url=self.ticket_detail_url,
|
||||
ticket_detail_url_description=_('click here to review'),
|
||||
body=self.ticket.body.replace('\n', '<br/>'),
|
||||
)
|
||||
return {
|
||||
'subject': self.subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
|
||||
class TicketAppliedToAssignee(BaseTicketMessage):
|
||||
title = 'New Ticket - {} ({})'
|
||||
|
||||
def __init__(self, user, ticket):
|
||||
self.ticket = ticket
|
||||
super().__init__(user)
|
||||
|
||||
@property
|
||||
def content_title(self):
|
||||
return _('Your has a new ticket, applicant - {}').format(str(self.ticket.applicant_display))
|
||||
|
||||
|
||||
class TicketProcessedToApplicant(BaseTicketMessage):
|
||||
title = 'Ticket has processed - {} ({})'
|
||||
|
||||
def __init__(self, user, ticket, processor):
|
||||
self.ticket = ticket
|
||||
self.processor = processor
|
||||
super().__init__(user)
|
||||
|
||||
@property
|
||||
def content_title(self):
|
||||
return _('Your ticket has been processed, processor - {}').format(str(self.processor))
|
|
@ -149,7 +149,7 @@ class TicketFlowApproveSerializer(serializers.ModelSerializer):
|
|||
fields = fields_small + fields_m2m
|
||||
read_only_fields = ['level', 'assignees_display']
|
||||
extra_kwargs = {
|
||||
'assignees': {'write_only': True, 'allow_empty': True}
|
||||
'assignees': {'write_only': True, 'allow_empty': True, 'required': False}
|
||||
}
|
||||
|
||||
def get_assignees_read_only(self, obj):
|
||||
|
@ -157,6 +157,12 @@ class TicketFlowApproveSerializer(serializers.ModelSerializer):
|
|||
return obj.assignees.values_list('id', flat=True)
|
||||
return []
|
||||
|
||||
def validate(self, attrs):
|
||||
if attrs['strategy'] == TicketApprovalStrategy.custom_user and not attrs.get('assignees'):
|
||||
error = _('Please select the Assignees')
|
||||
raise serializers.ValidationError(error)
|
||||
return super().validate(attrs)
|
||||
|
||||
|
||||
class TicketFlowSerializer(OrgResourceModelSerializerMixin):
|
||||
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
|
||||
|
|
|
@ -1,66 +1,32 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from urllib.parse import urljoin
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.tasks import send_mail_async
|
||||
from . import const
|
||||
from .notifications import TicketAppliedToAssignee, TicketProcessedToApplicant
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
EMAIL_TEMPLATE = '''
|
||||
<div>
|
||||
<p>
|
||||
{title}
|
||||
<a href={ticket_detail_url}>
|
||||
<strong>{ticket_detail_url_description}</strong>
|
||||
</a>
|
||||
</p>
|
||||
<div>
|
||||
{body}
|
||||
</div>
|
||||
</div>
|
||||
'''
|
||||
|
||||
|
||||
def send_ticket_applied_mail_to_assignees(ticket):
|
||||
assignees = ticket.current_node.first().ticket_assignees.all()
|
||||
if not assignees:
|
||||
logger.debug("Not found assignees, ticket: {}({}), assignees: {}".format(
|
||||
ticket, str(ticket.id), assignees)
|
||||
)
|
||||
logger.debug("Not found assignees, ticket: {}({}), assignees: {}".format(ticket, str(ticket.id), assignees))
|
||||
return
|
||||
|
||||
ticket_detail_url = urljoin(settings.SITE_URL, const.TICKET_DETAIL_URL.format(id=str(ticket.id)))
|
||||
subject = _('New Ticket - {} ({})').format(ticket.title, ticket.get_type_display())
|
||||
message = EMAIL_TEMPLATE.format(
|
||||
title=_('Your has a new ticket, applicant - {}').format(str(ticket.applicant_display)),
|
||||
ticket_detail_url=ticket_detail_url,
|
||||
ticket_detail_url_description=_('click here to review'),
|
||||
body=ticket.body.replace('\n', '<br/>'),
|
||||
)
|
||||
if settings.DEBUG:
|
||||
logger.debug(message)
|
||||
recipient_list = [i.assignee.email for i in assignees]
|
||||
send_mail_async.delay(subject, message, recipient_list, html_message=message)
|
||||
for assignee in assignees:
|
||||
instance = TicketAppliedToAssignee(assignee, ticket)
|
||||
if settings.DEBUG:
|
||||
logger.debug(instance)
|
||||
instance.publish_async()
|
||||
|
||||
|
||||
def send_ticket_processed_mail_to_applicant(ticket, processor):
|
||||
if not ticket.applicant:
|
||||
logger.error("Not found applicant: {}({})".format(ticket.title, ticket.id))
|
||||
return
|
||||
processor_display = str(processor)
|
||||
ticket_detail_url = urljoin(settings.SITE_URL, const.TICKET_DETAIL_URL.format(id=str(ticket.id)))
|
||||
subject = _('Ticket has processed - {} ({})').format(ticket.title, processor_display)
|
||||
message = EMAIL_TEMPLATE.format(
|
||||
title=_('Your ticket has been processed, processor - {}').format(processor_display),
|
||||
ticket_detail_url=ticket_detail_url,
|
||||
ticket_detail_url_description=_('click here to review'),
|
||||
body=ticket.body.replace('\n', '<br/>'),
|
||||
)
|
||||
|
||||
instance = TicketProcessedToApplicant(ticket.applicant, ticket, processor)
|
||||
if settings.DEBUG:
|
||||
logger.debug(message)
|
||||
recipient_list = [ticket.applicant.email, ]
|
||||
send_mail_async.delay(subject, message, recipient_list, html_message=message)
|
||||
logger.debug(instance)
|
||||
instance.publish_async()
|
||||
|
|
|
@ -72,6 +72,11 @@ class UserPasswordApi(generics.RetrieveUpdateAPIView):
|
|||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
resp = super().update(request, *args, **kwargs)
|
||||
ResetPasswordSuccessMsg(self.request.user, request).publish_async()
|
||||
return resp
|
||||
|
||||
|
||||
class UserPublicKeyApi(generics.RetrieveUpdateAPIView):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
|
|
@ -6,38 +6,7 @@ from common.utils import reverse, get_request_ip_or_data, get_request_user_agent
|
|||
from notifications.notifications import UserMessage
|
||||
|
||||
|
||||
class BaseUserMessage(UserMessage):
|
||||
def get_text_msg(self) -> dict:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
raise NotImplementedError
|
||||
|
||||
@lazyproperty
|
||||
def text_msg(self) -> dict:
|
||||
return self.get_text_msg()
|
||||
|
||||
@lazyproperty
|
||||
def html_msg(self) -> dict:
|
||||
return self.get_html_msg()
|
||||
|
||||
def get_dingtalk_msg(self) -> dict:
|
||||
return self.text_msg
|
||||
|
||||
def get_wecom_msg(self) -> dict:
|
||||
return self.text_msg
|
||||
|
||||
def get_feishu_msg(self) -> dict:
|
||||
return self.text_msg
|
||||
|
||||
def get_email_msg(self) -> dict:
|
||||
return self.html_msg
|
||||
|
||||
def get_site_msg_msg(self) -> dict:
|
||||
return self.html_msg
|
||||
|
||||
|
||||
class ResetPasswordMsg(BaseUserMessage):
|
||||
class ResetPasswordMsg(UserMessage):
|
||||
def get_text_msg(self) -> dict:
|
||||
user = self.user
|
||||
subject = _('Reset password')
|
||||
|
@ -104,7 +73,7 @@ Login direct 👇
|
|||
}
|
||||
|
||||
|
||||
class ResetPasswordSuccessMsg(BaseUserMessage):
|
||||
class ResetPasswordSuccessMsg(UserMessage):
|
||||
def __init__(self, user, request):
|
||||
super().__init__(user)
|
||||
self.ip_address = get_request_ip_or_data(request)
|
||||
|
@ -187,7 +156,7 @@ Browser: %(browser)s
|
|||
}
|
||||
|
||||
|
||||
class PasswordExpirationReminderMsg(BaseUserMessage):
|
||||
class PasswordExpirationReminderMsg(UserMessage):
|
||||
def get_text_msg(self) -> dict:
|
||||
user = self.user
|
||||
|
||||
|
@ -263,7 +232,7 @@ Login direct 👇
|
|||
}
|
||||
|
||||
|
||||
class UserExpirationReminderMsg(BaseUserMessage):
|
||||
class UserExpirationReminderMsg(UserMessage):
|
||||
def get_text_msg(self) -> dict:
|
||||
subject = _('Expiration notice')
|
||||
message = _("""
|
||||
|
@ -303,7 +272,7 @@ In order not to affect your normal work, please contact the administrator for co
|
|||
}
|
||||
|
||||
|
||||
class ResetSSHKeyMsg(BaseUserMessage):
|
||||
class ResetSSHKeyMsg(UserMessage):
|
||||
def get_text_msg(self) -> dict:
|
||||
subject = _('SSH Key Reset')
|
||||
message = _("""
|
||||
|
@ -347,7 +316,7 @@ Login direct 👇
|
|||
}
|
||||
|
||||
|
||||
class ResetMFAMsg(BaseUserMessage):
|
||||
class ResetMFAMsg(UserMessage):
|
||||
def get_text_msg(self) -> dict:
|
||||
subject = _('MFA Reset')
|
||||
message = _("""
|
||||
|
|
|
@ -101,7 +101,7 @@ class UserProfileSerializer(UserSerializer):
|
|||
)
|
||||
mfa_level = serializers.ChoiceField(choices=MFA_LEVEL_CHOICES, label=_('MFA'), required=False)
|
||||
guide_url = serializers.SerializerMethodField()
|
||||
receive_backends = serializers.ListField(child=serializers.CharField())
|
||||
receive_backends = serializers.ListField(child=serializers.CharField(), read_only=True)
|
||||
|
||||
class Meta(UserSerializer.Meta):
|
||||
fields = UserSerializer.Meta.fields + [
|
||||
|
|
|
@ -169,6 +169,16 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
|
|||
self.context['request'], self.context['view'], obj
|
||||
)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
request = self.context.get('request')
|
||||
if request:
|
||||
user = request.user
|
||||
if user.id == instance.id:
|
||||
# 用户自己不能禁用启用自己
|
||||
validated_data.pop('is_active', None)
|
||||
|
||||
return super(UserSerializer, self).update(instance, validated_data)
|
||||
|
||||
|
||||
class UserRetrieveSerializer(UserSerializer):
|
||||
login_confirm_settings = serializers.PrimaryKeyRelatedField(read_only=True,
|
||||
|
|
Loading…
Reference in New Issue