Merge pull request #6834 from jumpserver/dev

v2.14.0 rc2
pull/6892/head
Jiangjie.Bai 2021-09-13 21:27:15 +08:00 committed by GitHub
commit 93846234f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 1164 additions and 769 deletions

View File

@ -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

View File

@ -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')

View File

@ -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)

View File

@ -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'))

View File

@ -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()

View File

@ -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

View File

@ -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,)

View File

@ -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

View File

@ -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')

View File

@ -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)

View File

@ -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)"
)

View File

@ -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(

View File

@ -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):

View File

@ -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:

View File

@ -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))

View File

@ -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)

View File

@ -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 = ''

View File

@ -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):

View File

@ -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))

View File

@ -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):

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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',

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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 + [

View File

@ -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

View File

@ -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'),
),
]

View File

@ -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")
)

View File

@ -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:

View File

@ -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)

View File

@ -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')})

View File

@ -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,
}

View File

@ -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')})

View File

@ -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=''):

View File

@ -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")
)

View File

@ -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'))

View File

@ -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(

View File

@ -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")
)

View File

@ -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
}

View File

@ -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
)

View File

@ -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 = '''{}: {},

View File

@ -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')

View File

@ -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

View File

@ -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))

View File

@ -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'))

View File

@ -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()

View File

@ -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,)

View File

@ -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 = _("""

View File

@ -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 + [

View File

@ -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,