mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-24 04:52:39 +00:00
Compare commits
157 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
502657bad4 | ||
|
|
b5120e72c8 | ||
|
|
2ca659414e | ||
|
|
64f772e747 | ||
|
|
67a897f9c3 | ||
|
|
d0a9ccbdfe | ||
|
|
1a30675a86 | ||
|
|
f6273450bb | ||
|
|
8f35fcd6f9 | ||
|
|
1999cfdfeb | ||
|
|
c4af78c9f0 | ||
|
|
a3d02decd6 | ||
|
|
e623f63fcf | ||
|
|
4f1b2aceda | ||
|
|
94fc1fb53b | ||
|
|
937acbd0b5 | ||
|
|
067a70463e | ||
|
|
b115ed3b79 | ||
|
|
057fbdf0b1 | ||
|
|
5263a146e2 | ||
|
|
84070a558e | ||
|
|
e0604a3211 | ||
|
|
00e4c3cd07 | ||
|
|
97a0e27307 | ||
|
|
8d3c1bd783 | ||
|
|
db99ab80db | ||
|
|
1e8d9ba2ec | ||
|
|
7dddf0c3c2 | ||
|
|
891a5157a7 | ||
|
|
34b2a5fe0b | ||
|
|
de6908e5a6 | ||
|
|
d6527e3b02 | ||
|
|
33a29ae788 | ||
|
|
a2eb431015 | ||
|
|
8fbea2f702 | ||
|
|
af92271a52 | ||
|
|
391a5cb7d0 | ||
|
|
daf7d98f0e | ||
|
|
ed297fd1bd | ||
|
|
f91bef4105 | ||
|
|
a8d84fc6e1 | ||
|
|
0c7838d0e3 | ||
|
|
f26483c9cd | ||
|
|
5daca6592b | ||
|
|
0bced39f08 | ||
|
|
6d83dd0e3a | ||
|
|
46e99d10cb | ||
|
|
95eb11422a | ||
|
|
e8b3ee4565 | ||
|
|
1e99be1775 | ||
|
|
adae509bc0 | ||
|
|
7868e91844 | ||
|
|
a9bdbcf7c6 | ||
|
|
a809eac2b8 | ||
|
|
bdab93260f | ||
|
|
4ef3b2630a | ||
|
|
4eef25982d | ||
|
|
b82e9f860b | ||
|
|
6b46f5b48e | ||
|
|
fe717f0244 | ||
|
|
33fb063f78 | ||
|
|
7edc9c37f8 | ||
|
|
f8b4259a8c | ||
|
|
572d0e3f27 | ||
|
|
b334f3c2d9 | ||
|
|
6b4b9f4b02 | ||
|
|
d765e61991 | ||
|
|
9ccde03656 | ||
|
|
c66f366446 | ||
|
|
34d46897f8 | ||
|
|
2d9ce16601 | ||
|
|
0380be51dd | ||
|
|
47df0cfaab | ||
|
|
a2fb4a701e | ||
|
|
6e4381ac04 | ||
|
|
8ae03e4374 | ||
|
|
73f2022ff6 | ||
|
|
bc4258256a | ||
|
|
58dfe58ae0 | ||
|
|
53e3fa2590 | ||
|
|
23dbdaf6c0 | ||
|
|
3eba92548b | ||
|
|
ac5f2c560d | ||
|
|
f7f9331c48 | ||
|
|
77b4847bd9 | ||
|
|
0de9b29fa9 | ||
|
|
f9ca46dd67 | ||
|
|
ba28f3263d | ||
|
|
2e118665f5 | ||
|
|
bf53df46dc | ||
|
|
6449f36c7e | ||
|
|
ba35f5906b | ||
|
|
c8d7d42f66 | ||
|
|
20dacea260 | ||
|
|
d2dc2ab02c | ||
|
|
ba3b5a4027 | ||
|
|
3743761024 | ||
|
|
70055b8af2 | ||
|
|
726fd94f65 | ||
|
|
8b951ce12c | ||
|
|
189bc9d74a | ||
|
|
dd6c063478 | ||
|
|
5e9006d0c2 | ||
|
|
c42f69d1ba | ||
|
|
c7dfd0edce | ||
|
|
4382921c57 | ||
|
|
45feb468be | ||
|
|
c9b6b9a37a | ||
|
|
8010bdecea | ||
|
|
fc1c9c564a | ||
|
|
7c13b72739 | ||
|
|
6a4bc1f8b3 | ||
|
|
7d51d8c570 | ||
|
|
0ecd9fa32a | ||
|
|
b37c8b09bf | ||
|
|
23f22e92b8 | ||
|
|
c16319ec48 | ||
|
|
340547c889 | ||
|
|
54f5e65d36 | ||
|
|
4d6d4cbc22 | ||
|
|
7294f6e5e0 | ||
|
|
8ca2522c71 | ||
|
|
72f9d0d371 | ||
|
|
9a92e24e50 | ||
|
|
fea0170c5e | ||
|
|
5e5cd80bc2 | ||
|
|
e3511df4f8 | ||
|
|
11e5a97f14 | ||
|
|
4519ccfe1a | ||
|
|
657a2ac7e7 | ||
|
|
f5d8e125cb | ||
|
|
fd203c67c3 | ||
|
|
9fe5496ce9 | ||
|
|
c0875f6a87 | ||
|
|
d1a005f750 | ||
|
|
c52431b5ce | ||
|
|
4a9e83ba15 | ||
|
|
7712c1659e | ||
|
|
74c7b18dc4 | ||
|
|
5a3c67989b | ||
|
|
50918a3dd2 | ||
|
|
e9b174f342 | ||
|
|
63efbfe62e | ||
|
|
99cce185dd | ||
|
|
ab0fda93f6 | ||
|
|
d9552c0038 | ||
|
|
f0f493081a | ||
|
|
c4727e1eba | ||
|
|
ce8143c2ec | ||
|
|
65ad63272c | ||
|
|
4a4d5f3243 | ||
|
|
4563743f00 | ||
|
|
7b679f3e82 | ||
|
|
3d6aa15ece | ||
|
|
94a798eb01 | ||
|
|
ec393c1440 | ||
|
|
6571209864 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,6 +15,7 @@ dump.rdb
|
||||
.tox
|
||||
.cache/
|
||||
.idea/
|
||||
.vscode/
|
||||
db.sqlite3
|
||||
config.py
|
||||
config.yml
|
||||
|
||||
41
README.md
41
README.md
@@ -6,9 +6,10 @@
|
||||
|
||||
- [ENGLISH](https://github.com/jumpserver/jumpserver/blob/master/README_EN.md)
|
||||
|
||||
|安全通知|
|
||||
|
||||
|《新一代堡垒机建设指南》开放下载|
|
||||
|------------------|
|
||||
|2021年1月15日 JumpServer 发现远程执行漏洞,请速度修复 [详见](https://github.com/jumpserver/jumpserver/issues/5533), 非常感谢 **reactivity of Alibaba Hackerone bug bounty program**(瑞典) 向我们报告了此 BUG|
|
||||
|本白皮书由JumpServer开源项目组编著而成。编写团队从企业实践和技术演进的双重视角出发,结合自身在身份与访问安全领域长期研发及落地经验组织撰写,同时积极听取行业内专家的意见和建议,在此基础上完成了本白皮书的编写任务。下载链接:https://jinshuju.net/f/E0qAl8|
|
||||
|
||||
--------------------------
|
||||
|
||||
@@ -36,8 +37,8 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td rowspan="8">身份认证<br>Authentication</td>
|
||||
<td rowspan="5">登录认证</td>
|
||||
<td rowspan="11">身份认证<br>Authentication</td>
|
||||
<td rowspan="7">登录认证</td>
|
||||
<td>资源统一登录与认证</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -52,6 +53,12 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
<tr>
|
||||
<td>CAS 认证 (实现单点登录)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>钉钉认证 (扫码登录)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>企业微信认证 (扫码登录)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">MFA认证</td>
|
||||
<td>MFA 二次认证(Google Authenticator)</td>
|
||||
@@ -63,6 +70,10 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
<td>登录复核</td>
|
||||
<td>用户登录行为受管理员的监管与控制:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>登录限制</td>
|
||||
<td>用户登录来源 IP 受管理员控制(支持黑/白名单)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="11">账号管理<br>Account</td>
|
||||
<td rowspan="2">集中账号</td>
|
||||
@@ -104,7 +115,7 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
<td>统一对资产主机的用户密码进行查看、更新、测试操作:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="15">授权控制<br>Authorization</td>
|
||||
<td rowspan="17">授权控制<br>Authorization</td>
|
||||
<td>多维授权</td>
|
||||
<td>对用户、用户组、资产、资产节点、应用以及系统用户进行授权</td>
|
||||
</tr>
|
||||
@@ -156,17 +167,27 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
<td>工单管理</td>
|
||||
<td>支持对用户登录请求行为进行控制:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">访问控制</td>
|
||||
<td>登录资产复核(通过 SSH/Telnet 协议登录资产):small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>命令执行复核:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>组织管理</td>
|
||||
<td>实现多租户管理与权限隔离:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="7">安全审计<br>Audit</td>
|
||||
<td rowspan="8">安全审计<br>Audit</td>
|
||||
<td>操作审计</td>
|
||||
<td>用户操作行为审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">会话审计</td>
|
||||
<td rowspan="3">会话审计</td>
|
||||
<td>在线会话内容监控</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>在线会话内容审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -244,12 +265,13 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
- [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/)
|
||||
- [完整文档](https://docs.jumpserver.org)
|
||||
- [演示视频](https://www.bilibili.com/video/BV1ZV41127GB)
|
||||
- [手动安装](https://github.com/jumpserver/installer)
|
||||
|
||||
## 组件项目
|
||||
- [Lina](https://github.com/jumpserver/lina) JumpServer Web UI 项目
|
||||
- [Luna](https://github.com/jumpserver/luna) JumpServer Web Terminal 项目
|
||||
- [KoKo](https://github.com/jumpserver/koko) JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco)
|
||||
- [Guacamole](https://github.com/jumpserver/docker-guacamole) JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/)
|
||||
- [Lion](https://github.com/jumpserver/lion-release) JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/)
|
||||
|
||||
## 贡献
|
||||
如果有你好的想法创意,或者帮助我们修复了 Bug, 欢迎提交 Pull Request
|
||||
@@ -262,7 +284,7 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
|
||||
|
||||
## 致谢
|
||||
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC协议设备,JumpServer 图形化连接依赖
|
||||
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC协议设备,JumpServer 图形化组件 Lion 依赖
|
||||
- [OmniDB](https://omnidb.org/) Web页面连接使用数据库,JumpServer Web数据库依赖
|
||||
|
||||
|
||||
@@ -299,3 +321,4 @@ Licensed under The GNU General Public License version 2 (GPLv2) (the "License")
|
||||
https://www.gnu.org/licenses/gpl-2.0.html
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from rest_framework.generics import CreateAPIView, RetrieveDestroyAPIView
|
||||
from common.permissions import IsAppUser
|
||||
from common.utils import reverse, lazyproperty
|
||||
from orgs.utils import tmp_to_org, tmp_to_root_org
|
||||
from tickets.models import Ticket
|
||||
from tickets.api import GenericTicketStatusRetrieveCloseAPI
|
||||
from ..models import LoginAssetACL
|
||||
from .. import serializers
|
||||
|
||||
@@ -48,7 +48,7 @@ class LoginAssetCheckAPI(CreateAPIView):
|
||||
org_id=self.serializer.org.id
|
||||
)
|
||||
confirm_status_url = reverse(
|
||||
view_name='acls:login-asset-confirm-status',
|
||||
view_name='api-acls:login-asset-confirm-status',
|
||||
kwargs={'pk': str(ticket.id)}
|
||||
)
|
||||
ticket_detail_url = reverse(
|
||||
@@ -72,34 +72,6 @@ class LoginAssetCheckAPI(CreateAPIView):
|
||||
return serializer
|
||||
|
||||
|
||||
class LoginAssetConfirmStatusAPI(RetrieveDestroyAPIView):
|
||||
permission_classes = (IsAppUser, )
|
||||
class LoginAssetConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
|
||||
pass
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
if self.ticket.action_open:
|
||||
status = 'await'
|
||||
elif self.ticket.action_approve:
|
||||
status = 'approve'
|
||||
else:
|
||||
status = 'reject'
|
||||
data = {
|
||||
'status': status,
|
||||
'action': self.ticket.action,
|
||||
'processor': self.ticket.processor_display
|
||||
}
|
||||
return Response(data=data, status=200)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
if self.ticket.status_open:
|
||||
self.ticket.close(processor=self.ticket.applicant)
|
||||
data = {
|
||||
'action': self.ticket.action,
|
||||
'status': self.ticket.status,
|
||||
'processor': self.ticket.processor_display
|
||||
}
|
||||
return Response(data=data, status=200)
|
||||
|
||||
@lazyproperty
|
||||
def ticket(self):
|
||||
with tmp_to_root_org():
|
||||
return get_object_or_404(Ticket, pk=self.kwargs['pk'])
|
||||
|
||||
@@ -33,6 +33,9 @@ class LoginACL(BaseACL):
|
||||
class Meta:
|
||||
ordering = ('priority', '-date_updated', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def action_reject(self):
|
||||
return self.action == self.ActionChoices.reject
|
||||
|
||||
@@ -38,6 +38,9 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
|
||||
unique_together = ('name', 'org_id')
|
||||
ordering = ('priority', '-date_updated', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def filter(cls, user, asset, system_user, action):
|
||||
queryset = cls.objects.filter(action=action)
|
||||
|
||||
@@ -35,10 +35,15 @@ class LoginACLSerializer(BulkModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = LoginACL
|
||||
fields = [
|
||||
'id', 'name', 'priority', 'ip_group', 'user', 'user_display', 'action',
|
||||
'action_display', 'is_active', 'comment', 'created_by', 'date_created', 'date_updated'
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'priority', 'ip_group', 'action', 'action_display',
|
||||
'is_active',
|
||||
'date_created', 'date_updated',
|
||||
'comment', 'created_by',
|
||||
]
|
||||
fields_fk = ['user', 'user_display',]
|
||||
fields = fields_small + fields_fk
|
||||
extra_kwargs = {
|
||||
'priority': {'default': 50},
|
||||
'is_active': {'default': True},
|
||||
|
||||
@@ -54,7 +54,7 @@ class LoginAssetACLSystemUsersSerializer(serializers.Serializer):
|
||||
protocol_group = serializers.ListField(
|
||||
default=['*'], child=serializers.CharField(max_length=16), label=_('Protocol'),
|
||||
help_text=protocol_group_help_text.format(
|
||||
', '.join(SystemUser.ASSET_CATEGORY_PROTOCOLS)
|
||||
', '.join([SystemUser.PROTOCOL_SSH, SystemUser.PROTOCOL_TELNET])
|
||||
)
|
||||
)
|
||||
|
||||
@@ -76,11 +76,15 @@ class LoginAssetACLSerializer(BulkOrgResourceModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.LoginAssetACL
|
||||
fields = [
|
||||
'id', 'name', 'priority', 'users', 'system_users', 'assets', 'action', 'action_display',
|
||||
'is_active', 'comment', 'reviewers', 'reviewers_amount', 'created_by', 'date_created',
|
||||
'date_updated', 'org_id'
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'users', 'system_users', 'assets',
|
||||
'is_active',
|
||||
'date_created', 'date_updated',
|
||||
'priority', 'action', 'action_display', 'comment', 'created_by', 'org_id'
|
||||
]
|
||||
fields_m2m = ['reviewers', 'reviewers_amount']
|
||||
fields = fields_small + fields_m2m
|
||||
extra_kwargs = {
|
||||
"reviewers": {'allow_null': False, 'required': True},
|
||||
'priority': {'default': 50},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from .application import *
|
||||
from .application_user import *
|
||||
from .mixin import *
|
||||
from .remote_app import *
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
|
||||
from ..hands import IsOrgAdminOrAppUser
|
||||
from .. import models, serializers
|
||||
from .. import serializers
|
||||
from ..models import Application
|
||||
|
||||
|
||||
__all__ = ['ApplicationViewSet']
|
||||
|
||||
|
||||
class ApplicationViewSet(OrgBulkModelViewSet):
|
||||
model = models.Application
|
||||
model = Application
|
||||
filterset_fields = ('name', 'type', 'category')
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.ApplicationSerializer
|
||||
|
||||
|
||||
55
apps/applications/api/application_user.py
Normal file
55
apps/applications/api/application_user.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from rest_framework import generics
|
||||
from django.conf import settings
|
||||
|
||||
from ..hands import IsOrgAdminOrAppUser, IsOrgAdmin, NeedMFAVerify
|
||||
from .. import serializers
|
||||
from ..models import Application, ApplicationUser
|
||||
from perms.models import ApplicationPermission
|
||||
|
||||
|
||||
class ApplicationUserListApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin, )
|
||||
filterset_fields = ('name', 'username')
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.ApplicationUserSerializer
|
||||
_application = None
|
||||
|
||||
@property
|
||||
def application(self):
|
||||
if self._application is None:
|
||||
app_id = self.request.query_params.get('application_id')
|
||||
if app_id:
|
||||
self._application = Application.objects.get(id=app_id)
|
||||
return self._application
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
context.update({
|
||||
'application': self.application
|
||||
})
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = ApplicationUser.objects.none()
|
||||
if not self.application:
|
||||
return queryset
|
||||
system_user_ids = ApplicationPermission.objects.filter(applications=self.application)\
|
||||
.values_list('system_users', flat=True)
|
||||
if not system_user_ids:
|
||||
return queryset
|
||||
queryset = ApplicationUser.objects.filter(id__in=system_user_ids)
|
||||
return queryset
|
||||
|
||||
|
||||
class ApplicationUserAuthInfoListApi(ApplicationUserListApi):
|
||||
serializer_class = serializers.ApplicationUserWithAuthInfoSerializer
|
||||
http_method_names = ['get']
|
||||
permission_classes = [IsOrgAdminOrAppUser]
|
||||
|
||||
def get_permissions(self):
|
||||
if settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
||||
self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
|
||||
return super().get_permissions()
|
||||
@@ -11,5 +11,5 @@
|
||||
"""
|
||||
|
||||
|
||||
from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser
|
||||
from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser, NeedMFAVerify
|
||||
from users.models import User, UserGroup
|
||||
|
||||
@@ -3,7 +3,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from common.mixins import CommonModelMixin
|
||||
from assets.models import Asset
|
||||
from assets.models import Asset, SystemUser
|
||||
from .. import const
|
||||
|
||||
|
||||
@@ -68,3 +68,8 @@ class Application(CommonModelMixin, OrgModelMixin):
|
||||
raise ValueError("Remote App not has asset attr")
|
||||
asset = Asset.objects.filter(id=asset_id).first()
|
||||
return asset
|
||||
|
||||
|
||||
class ApplicationUser(SystemUser):
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
@@ -6,11 +6,12 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from common.drf.serializers import MethodSerializer
|
||||
from .attrs import category_serializer_classes_mapping, type_serializer_classes_mapping
|
||||
|
||||
from assets.serializers import SystemUserSerializer
|
||||
from .. import models
|
||||
|
||||
__all__ = [
|
||||
'ApplicationSerializer', 'ApplicationSerializerMixin',
|
||||
'ApplicationUserSerializer', 'ApplicationUserWithAuthInfoSerializer'
|
||||
]
|
||||
|
||||
|
||||
@@ -49,10 +50,14 @@ class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSeri
|
||||
|
||||
class Meta:
|
||||
model = models.Application
|
||||
fields = [
|
||||
'id', 'name', 'category', 'category_display', 'type', 'type_display', 'attrs',
|
||||
'domain', 'created_by', 'date_created', 'date_updated', 'comment'
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'category', 'category_display', 'type', 'type_display', 'attrs',
|
||||
'date_created', 'date_updated',
|
||||
'created_by', 'comment'
|
||||
]
|
||||
fields_fk = ['domain']
|
||||
fields = fields_small + fields_fk
|
||||
read_only_fields = [
|
||||
'created_by', 'date_created', 'date_updated', 'get_type_display',
|
||||
]
|
||||
@@ -62,3 +67,42 @@ class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSeri
|
||||
_attrs.update(attrs)
|
||||
return _attrs
|
||||
|
||||
|
||||
class ApplicationUserSerializer(SystemUserSerializer):
|
||||
application_name = serializers.SerializerMethodField(label=_('Application name'))
|
||||
application_category = serializers.SerializerMethodField(label=_('Application category'))
|
||||
application_type = serializers.SerializerMethodField(label=_('Application type'))
|
||||
|
||||
class Meta(SystemUserSerializer.Meta):
|
||||
model = models.ApplicationUser
|
||||
fields_mini = [
|
||||
'id', 'application_name', 'application_category', 'application_type', 'name', 'username'
|
||||
]
|
||||
fields_small = fields_mini + [
|
||||
'protocol', 'login_mode', 'login_mode_display', 'priority',
|
||||
"username_same_with_user", 'comment',
|
||||
]
|
||||
fields = fields_small
|
||||
extra_kwargs = {
|
||||
'login_mode_display': {'label': _('Login mode display')},
|
||||
'created_by': {'read_only': True},
|
||||
}
|
||||
|
||||
@property
|
||||
def application(self):
|
||||
return self.context['application']
|
||||
|
||||
def get_application_name(self, obj):
|
||||
return self.application.name
|
||||
|
||||
def get_application_category(self, obj):
|
||||
return self.application.get_category_display()
|
||||
|
||||
def get_application_type(self, obj):
|
||||
return self.application.get_type_display()
|
||||
|
||||
|
||||
class ApplicationUserWithAuthInfoSerializer(ApplicationUserSerializer):
|
||||
|
||||
class Meta(ApplicationUserSerializer.Meta):
|
||||
fields = ApplicationUserSerializer.Meta.fields + ['password', 'token']
|
||||
|
||||
@@ -39,14 +39,14 @@ class RemoteAppSerializer(serializers.Serializer):
|
||||
@staticmethod
|
||||
def get_asset_info(obj):
|
||||
asset_id = obj.get('asset')
|
||||
if not asset_id or is_uuid(asset_id):
|
||||
if not asset_id or not is_uuid(asset_id):
|
||||
return {}
|
||||
try:
|
||||
asset = Asset.objects.filter(id=str(asset_id)).values_list('id', 'hostname')
|
||||
asset = Asset.objects.get(id=str(asset_id))
|
||||
except ObjectDoesNotExist as e:
|
||||
logger.error(e)
|
||||
return {}
|
||||
if not asset:
|
||||
return {}
|
||||
asset_info = {'id': str(asset[0]), 'hostname': asset[1]}
|
||||
asset_info = {'id': str(asset.id), 'hostname': asset.hostname}
|
||||
return asset_info
|
||||
|
||||
@@ -14,6 +14,8 @@ router.register(r'applications', api.ApplicationViewSet, 'application')
|
||||
|
||||
urlpatterns = [
|
||||
path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'),
|
||||
path('application-users/', api.ApplicationUserListApi.as_view(), name='application-user'),
|
||||
path('application-user-auth-infos/', api.ApplicationUserAuthInfoListApi.as_view(), name='application-user-auth-info')
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ from common.permissions import IsOrgAdminOrAppUser, NeedMFAVerify
|
||||
from common.utils import get_object_or_none, get_logger
|
||||
from common.mixins import CommonApiMixin
|
||||
from ..backends import AssetUserManager
|
||||
from ..models import Asset, Node, SystemUser
|
||||
from ..models import Node
|
||||
from .. import serializers
|
||||
from ..tasks import (
|
||||
test_asset_users_connectivity_manual, push_system_user_a_asset_manual
|
||||
test_asset_users_connectivity_manual
|
||||
)
|
||||
|
||||
|
||||
@@ -100,12 +100,6 @@ class AssetUserViewSet(CommonApiMixin, BulkModelViewSet):
|
||||
obj = queryset.get(id=pk)
|
||||
return obj
|
||||
|
||||
def get_exception_handler(self):
|
||||
def handler(e, context):
|
||||
logger.error(e, exc_info=True)
|
||||
return Response({"error": str(e)}, status=400)
|
||||
return handler
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
manager = AssetUserManager()
|
||||
manager.delete(instance)
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.generics import CreateAPIView, RetrieveDestroyAPIView
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from common.utils import reverse
|
||||
from common.utils import lazyproperty
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from ..hands import IsOrgAdmin
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from tickets.models import Ticket
|
||||
from tickets.api import GenericTicketStatusRetrieveCloseAPI
|
||||
from ..hands import IsOrgAdmin, IsAppUser
|
||||
from ..models import CommandFilter, CommandFilterRule
|
||||
from .. import serializers
|
||||
|
||||
|
||||
__all__ = ['CommandFilterViewSet', 'CommandFilterRuleViewSet']
|
||||
__all__ = [
|
||||
'CommandFilterViewSet', 'CommandFilterRuleViewSet', 'CommandConfirmAPI',
|
||||
'CommandConfirmStatusAPI'
|
||||
]
|
||||
|
||||
|
||||
class CommandFilterViewSet(OrgBulkModelViewSet):
|
||||
@@ -35,3 +45,50 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet):
|
||||
return cmd_filter.rules.all()
|
||||
|
||||
|
||||
class CommandConfirmAPI(CreateAPIView):
|
||||
permission_classes = (IsAppUser, )
|
||||
serializer_class = serializers.CommandConfirmSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
ticket = self.create_command_confirm_ticket()
|
||||
response_data = self.get_response_data(ticket)
|
||||
return Response(data=response_data, status=200)
|
||||
|
||||
def create_command_confirm_ticket(self):
|
||||
ticket = self.serializer.cmd_filter_rule.create_command_confirm_ticket(
|
||||
run_command=self.serializer.data.get('run_command'),
|
||||
session=self.serializer.session,
|
||||
cmd_filter_rule=self.serializer.cmd_filter_rule,
|
||||
org_id=self.serializer.org.id
|
||||
)
|
||||
return ticket
|
||||
|
||||
@staticmethod
|
||||
def get_response_data(ticket):
|
||||
confirm_status_url = reverse(
|
||||
view_name='api-assets:command-confirm-status',
|
||||
kwargs={'pk': str(ticket.id)}
|
||||
)
|
||||
ticket_detail_url = reverse(
|
||||
view_name='api-tickets:ticket-detail',
|
||||
kwargs={'pk': str(ticket.id)},
|
||||
external=True, api_to_ui=True
|
||||
)
|
||||
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type)
|
||||
return {
|
||||
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
|
||||
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},
|
||||
'ticket_detail_url': ticket_detail_url,
|
||||
'reviewers': [str(user) for user in ticket.assignees.all()]
|
||||
}
|
||||
|
||||
@lazyproperty
|
||||
def serializer(self):
|
||||
serializer = self.get_serializer(data=self.request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return serializer
|
||||
|
||||
|
||||
class CommandConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
|
||||
pass
|
||||
|
||||
|
||||
@@ -71,8 +71,8 @@ class NodeViewSet(OrgModelViewSet):
|
||||
if node.is_org_root():
|
||||
error = _("You can't delete the root node ({})".format(node.value))
|
||||
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
|
||||
if node.has_children_or_has_assets():
|
||||
error = _("Deletion failed and the node contains children or assets")
|
||||
if node.has_offspring_assets():
|
||||
error = _("Deletion failed and the node contains assets")
|
||||
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -3,14 +3,13 @@ from django.shortcuts import get_object_or_404
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
|
||||
from common.drf.filters import CustomFilter
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsValidUser
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.mixins import generics
|
||||
from orgs.utils import tmp_to_org
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from ..models import SystemUser, Asset
|
||||
from .. import serializers
|
||||
from ..serializers import SystemUserWithAuthInfoSerializer
|
||||
from ..serializers import SystemUserWithAuthInfoSerializer, SystemUserTempAuthSerializer
|
||||
from ..tasks import (
|
||||
push_system_user_to_assets_manual, test_system_user_connectivity_manual,
|
||||
push_system_user_to_assets
|
||||
@@ -21,6 +20,7 @@ logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi',
|
||||
'SystemUserCommandFilterRuleListApi', 'SystemUserTaskApi', 'SystemUserAssetsListView',
|
||||
'SystemUserTempAuthInfoApi', 'SystemUserAppAuthInfoApi',
|
||||
]
|
||||
|
||||
|
||||
@@ -57,6 +57,25 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
|
||||
return Response(status=204)
|
||||
|
||||
|
||||
class SystemUserTempAuthInfoApi(generics.CreateAPIView):
|
||||
model = SystemUser
|
||||
permission_classes = (IsValidUser,)
|
||||
serializer_class = SystemUserTempAuthSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = super().get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
pk = kwargs.get('pk')
|
||||
user = self.request.user
|
||||
data = serializer.validated_data
|
||||
instance_id = data.get('instance_id')
|
||||
|
||||
with tmp_to_root_org():
|
||||
instance = get_object_or_404(SystemUser, pk=pk)
|
||||
instance.set_temp_auth(instance_id, user, data)
|
||||
return Response(serializer.data, status=201)
|
||||
|
||||
|
||||
class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Get system user with asset auth info
|
||||
@@ -65,22 +84,30 @@ class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = SystemUserWithAuthInfoSerializer
|
||||
|
||||
def get_exception_handler(self):
|
||||
def handler(e, context):
|
||||
return Response({"error": str(e)}, status=400)
|
||||
return handler
|
||||
def get_object(self):
|
||||
instance = super().get_object()
|
||||
asset_id = self.kwargs.get('asset_id')
|
||||
user_id = self.request.query_params.get("user_id")
|
||||
username = self.request.query_params.get("username")
|
||||
instance.load_asset_more_auth(asset_id=asset_id, user_id=user_id, username=username)
|
||||
return instance
|
||||
|
||||
|
||||
class SystemUserAppAuthInfoApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Get system user with asset auth info
|
||||
"""
|
||||
model = SystemUser
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = SystemUserWithAuthInfoSerializer
|
||||
|
||||
def get_object(self):
|
||||
instance = super().get_object()
|
||||
username = instance.username
|
||||
if instance.username_same_with_user:
|
||||
username = self.request.query_params.get("username")
|
||||
asset_id = self.kwargs.get('aid')
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
|
||||
with tmp_to_org(asset.org_id):
|
||||
instance.load_asset_special_auth(asset=asset, username=username)
|
||||
return instance
|
||||
app_id = self.kwargs.get('app_id')
|
||||
user_id = self.request.query_params.get("user_id")
|
||||
if user_id:
|
||||
instance.load_app_more_auth(app_id, user_id)
|
||||
return instance
|
||||
|
||||
|
||||
class SystemUserTaskApi(generics.CreateAPIView):
|
||||
@@ -98,8 +125,8 @@ class SystemUserTaskApi(generics.CreateAPIView):
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def do_test(system_user):
|
||||
task = test_system_user_connectivity_manual.delay(system_user)
|
||||
def do_test(system_user, asset_ids):
|
||||
task = test_system_user_connectivity_manual.delay(system_user, asset_ids)
|
||||
return task
|
||||
|
||||
def get_object(self):
|
||||
@@ -109,16 +136,20 @@ class SystemUserTaskApi(generics.CreateAPIView):
|
||||
def perform_create(self, serializer):
|
||||
action = serializer.validated_data["action"]
|
||||
asset = serializer.validated_data.get('asset')
|
||||
assets = serializer.validated_data.get('assets') or []
|
||||
|
||||
if asset:
|
||||
assets = [asset]
|
||||
else:
|
||||
assets = serializer.validated_data.get('assets') or []
|
||||
|
||||
asset_ids = [asset.id for asset in assets]
|
||||
asset_ids = asset_ids if asset_ids else None
|
||||
|
||||
system_user = self.get_object()
|
||||
if action == 'push':
|
||||
assets = [asset] if asset else assets
|
||||
asset_ids = [asset.id for asset in assets]
|
||||
asset_ids = asset_ids if asset_ids else None
|
||||
task = self.do_push(system_user, asset_ids)
|
||||
else:
|
||||
task = self.do_test(system_user)
|
||||
task = self.do_test(system_user, asset_ids)
|
||||
data = getattr(serializer, '_data', {})
|
||||
data["task"] = task.id
|
||||
setattr(serializer, '_data', data)
|
||||
|
||||
@@ -31,11 +31,11 @@ class BaseBackend:
|
||||
def qs_to_values(qs):
|
||||
values = qs.values(
|
||||
'hostname', 'ip', "asset_id",
|
||||
'username', 'password', 'private_key', 'public_key',
|
||||
'name', 'username', 'password', 'private_key', 'public_key',
|
||||
'score', 'version',
|
||||
"asset_username", "union_id",
|
||||
'date_created', 'date_updated',
|
||||
'org_id', 'backend',
|
||||
'org_id', 'backend', 'backend_display'
|
||||
)
|
||||
return values
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.utils.translation import ugettext as _
|
||||
from functools import reduce
|
||||
from django.db.models import F, CharField, Value, IntegerField, Q, Count
|
||||
from django.db.models.functions import Concat
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from common.utils import get_object_or_none
|
||||
from orgs.utils import current_org
|
||||
@@ -106,6 +107,7 @@ class DBBackend(BaseBackend):
|
||||
class SystemUserBackend(DBBackend):
|
||||
model = SystemUser.assets.through
|
||||
backend = 'system_user'
|
||||
backend_display = _('System user')
|
||||
prefer = backend
|
||||
base_score = 0
|
||||
union_id_length = 2
|
||||
@@ -138,6 +140,7 @@ class SystemUserBackend(DBBackend):
|
||||
kwargs = dict(
|
||||
hostname=F("asset__hostname"),
|
||||
ip=F("asset__ip"),
|
||||
name=F("systemuser__name"),
|
||||
username=F("systemuser__username"),
|
||||
password=F("systemuser__password"),
|
||||
private_key=F("systemuser__private_key"),
|
||||
@@ -152,7 +155,8 @@ class SystemUserBackend(DBBackend):
|
||||
union_id=Concat(F("systemuser_id"), Value("_"), F("asset_id"),
|
||||
output_field=CharField()),
|
||||
org_id=F("asset__org_id"),
|
||||
backend=Value(self.backend, CharField())
|
||||
backend=Value(self.backend, CharField()),
|
||||
backend_display=Value(self.backend_display, CharField()),
|
||||
)
|
||||
return kwargs
|
||||
|
||||
@@ -174,12 +178,17 @@ class SystemUserBackend(DBBackend):
|
||||
|
||||
class DynamicSystemUserBackend(SystemUserBackend):
|
||||
backend = 'system_user_dynamic'
|
||||
backend_display = _('System user(Dynamic)')
|
||||
prefer = 'system_user'
|
||||
union_id_length = 3
|
||||
|
||||
def get_annotate(self):
|
||||
kwargs = super().get_annotate()
|
||||
kwargs.update(dict(
|
||||
name=Concat(
|
||||
F("systemuser__users__name"), Value('('), F("systemuser__name"), Value(')'),
|
||||
output_field=CharField()
|
||||
),
|
||||
username=F("systemuser__users__username"),
|
||||
asset_username=Concat(
|
||||
F("asset__id"), Value("_"),
|
||||
@@ -221,6 +230,7 @@ class DynamicSystemUserBackend(SystemUserBackend):
|
||||
class AdminUserBackend(DBBackend):
|
||||
model = Asset
|
||||
backend = 'admin_user'
|
||||
backend_display = _('Admin user')
|
||||
prefer = backend
|
||||
base_score = 200
|
||||
|
||||
@@ -241,11 +251,12 @@ class AdminUserBackend(DBBackend):
|
||||
)
|
||||
|
||||
def _perform_delete_by_union_id(self, union_id_cleaned):
|
||||
raise PermissionError(_("Could not remove asset admin user"))
|
||||
raise PermissionDenied(_("Could not remove asset admin user"))
|
||||
|
||||
def all(self):
|
||||
qs = self.model.objects.all().annotate(
|
||||
asset_id=F("id"),
|
||||
name=F("admin_user__name"),
|
||||
username=F("admin_user__username"),
|
||||
password=F("admin_user__password"),
|
||||
private_key=F("admin_user__private_key"),
|
||||
@@ -256,6 +267,7 @@ class AdminUserBackend(DBBackend):
|
||||
asset_username=Concat(F("id"), Value("_"), F("admin_user__username"), output_field=CharField()),
|
||||
union_id=Concat(F("admin_user_id"), Value("_"), F("id"), output_field=CharField()),
|
||||
backend=Value(self.backend, CharField()),
|
||||
backend_display=Value(self.backend_display, CharField()),
|
||||
)
|
||||
qs = self.qs_to_values(qs)
|
||||
return qs
|
||||
@@ -264,6 +276,7 @@ class AdminUserBackend(DBBackend):
|
||||
class AuthbookBackend(DBBackend):
|
||||
model = AuthBook
|
||||
backend = 'db'
|
||||
backend_display = _('Database')
|
||||
prefer = backend
|
||||
base_score = 400
|
||||
|
||||
@@ -302,7 +315,7 @@ class AuthbookBackend(DBBackend):
|
||||
authbook_id, asset_id = union_id_cleaned
|
||||
authbook = get_object_or_none(AuthBook, pk=authbook_id)
|
||||
if authbook.is_latest:
|
||||
raise PermissionError(_("Latest version could not be delete"))
|
||||
raise PermissionDenied(_("Latest version could not be delete"))
|
||||
AuthBook.objects.filter(id=authbook_id).delete()
|
||||
|
||||
def all(self):
|
||||
@@ -313,6 +326,7 @@ class AuthbookBackend(DBBackend):
|
||||
asset_username=Concat(F("asset__id"), Value("_"), F("username"), output_field=CharField()),
|
||||
union_id=Concat(F("id"), Value("_"), F("asset_id"), output_field=CharField()),
|
||||
backend=Value(self.backend, CharField()),
|
||||
backend_display=Value(self.backend_display, CharField()),
|
||||
)
|
||||
qs = self.qs_to_values(qs)
|
||||
return qs
|
||||
|
||||
35
apps/assets/migrations/0002_auto_20180105_1807.py
Normal file
35
apps/assets/migrations/0002_auto_20180105_1807.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2018-01-05 10:07
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='adminuser',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Admin user'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='asset',
|
||||
options={'verbose_name': 'Asset'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='assetgroup',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Asset group'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='cluster',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Cluster'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='systemuser',
|
||||
options={'ordering': ['name'], 'verbose_name': 'System user'},
|
||||
),
|
||||
]
|
||||
22
apps/assets/migrations/0003_auto_20180109_2331.py
Normal file
22
apps/assets/migrations/0003_auto_20180109_2331.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2018-01-09 15:31
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import assets.models.asset
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0002_auto_20180105_1807'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='cluster',
|
||||
field=models.ForeignKey(default=assets.models.asset.default_cluster, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='assets', to='assets.Cluster', verbose_name='Cluster'),
|
||||
),
|
||||
]
|
||||
20
apps/assets/migrations/0004_auto_20180125_1218.py
Normal file
20
apps/assets/migrations/0004_auto_20180125_1218.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2018-01-25 04:18
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0003_auto_20180109_2331'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='assetgroup',
|
||||
name='created_by',
|
||||
field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by'),
|
||||
),
|
||||
]
|
||||
40
apps/assets/migrations/0005_auto_20180126_1637.py
Normal file
40
apps/assets/migrations/0005_auto_20180126_1637.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2018-01-26 08:37
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0004_auto_20180125_1218'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Label',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('value', models.CharField(max_length=128, verbose_name='Value')),
|
||||
('category', models.CharField(choices=[('S', 'System'), ('U', 'User')], default='U', max_length=128, verbose_name='Category')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
||||
('comment', models.TextField(blank=True, null=True, verbose_name='Comment')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'assets_label',
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='label',
|
||||
unique_together=set([('name', 'value')]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='labels',
|
||||
field=models.ManyToManyField(blank=True, related_name='assets', to='assets.Label', verbose_name='Labels'),
|
||||
),
|
||||
]
|
||||
39
apps/assets/migrations/0006_auto_20180130_1502.py
Normal file
39
apps/assets/migrations/0006_auto_20180130_1502.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2018-01-30 07:02
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0005_auto_20180126_1637'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='cabinet_no',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='cabinet_pos',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='env',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='remote_card_ip',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='status',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='type',
|
||||
),
|
||||
]
|
||||
60
apps/assets/migrations/0007_auto_20180225_1815.py
Normal file
60
apps/assets/migrations/0007_auto_20180225_1815.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2018-02-25 10:15
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import assets.models.asset
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0006_auto_20180130_1502'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Node',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('key', models.CharField(max_length=64, unique=True, verbose_name='Key')),
|
||||
('value', models.CharField(max_length=128, unique=True, verbose_name='Value')),
|
||||
('child_mark', models.IntegerField(default=0)),
|
||||
('date_create', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='cluster',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='groups',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='systemuser',
|
||||
name='cluster',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='admin_user',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.AdminUser', verbose_name='Admin user'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='protocol',
|
||||
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='nodes',
|
||||
field=models.ManyToManyField(default=assets.models.asset.default_node, related_name='assets', to='assets.Node', verbose_name='Nodes'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='nodes',
|
||||
field=models.ManyToManyField(blank=True, to='assets.Node', verbose_name='Nodes'),
|
||||
),
|
||||
]
|
||||
40
apps/assets/migrations/0008_auto_20180306_1804.py
Normal file
40
apps/assets/migrations/0008_auto_20180306_1804.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2018-03-06 10:04
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0007_auto_20180225_1815'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='created_by',
|
||||
field=models.CharField(max_length=128, null=True, verbose_name='Created by'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='username',
|
||||
field=models.CharField(max_length=128, verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='platform',
|
||||
field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='created_by',
|
||||
field=models.CharField(max_length=128, null=True, verbose_name='Created by'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='username',
|
||||
field=models.CharField(max_length=128, verbose_name='Username'),
|
||||
),
|
||||
]
|
||||
20
apps/assets/migrations/0009_auto_20180307_1212.py
Normal file
20
apps/assets/migrations/0009_auto_20180307_1212.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2018-03-07 04:12
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0008_auto_20180306_1804'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='node',
|
||||
name='value',
|
||||
field=models.CharField(max_length=128, verbose_name='Value'),
|
||||
),
|
||||
]
|
||||
20
apps/assets/migrations/0010_auto_20180307_1749.py
Normal file
20
apps/assets/migrations/0010_auto_20180307_1749.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2018-03-07 09:49
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0009_auto_20180307_1212'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='node',
|
||||
name='value',
|
||||
field=models.CharField(max_length=128, unique=True, verbose_name='Value'),
|
||||
),
|
||||
]
|
||||
55
apps/assets/migrations/0011_auto_20180326_0957.py
Normal file
55
apps/assets/migrations/0011_auto_20180326_0957.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2018-03-26 01:57
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import assets.models.utils
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0010_auto_20180307_1749'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Domain',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
|
||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Gateway',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
|
||||
('username', models.CharField(max_length=128, verbose_name='Username')),
|
||||
('_password', models.CharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('_private_key', models.TextField(blank=True, max_length=4096, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key')),
|
||||
('_public_key', models.TextField(blank=True, max_length=4096, verbose_name='SSH public key')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True)),
|
||||
('date_updated', models.DateTimeField(auto_now=True)),
|
||||
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
|
||||
('ip', models.GenericIPAddressField(db_index=True, verbose_name='IP')),
|
||||
('port', models.IntegerField(default=22, verbose_name='Port')),
|
||||
('protocol', models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol')),
|
||||
('comment', models.CharField(blank=True, max_length=128, null=True, verbose_name='Comment')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
||||
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Domain', verbose_name='Domain')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='domain',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='assets.Domain', verbose_name='Domain'),
|
||||
),
|
||||
]
|
||||
21
apps/assets/migrations/0012_auto_20180404_1302.py
Normal file
21
apps/assets/migrations/0012_auto_20180404_1302.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2018-04-04 05:02
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0011_auto_20180326_0957'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='domain',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.Domain', verbose_name='Domain'),
|
||||
),
|
||||
]
|
||||
25
apps/assets/migrations/0013_auto_20180411_1135.py
Normal file
25
apps/assets/migrations/0013_auto_20180411_1135.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2018-04-11 03:35
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0012_auto_20180404_1302'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='assets',
|
||||
field=models.ManyToManyField(blank=True, to='assets.Asset', verbose_name='Assets'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='sudo',
|
||||
field=models.TextField(default='/bin/whoami', verbose_name='Sudo'),
|
||||
),
|
||||
]
|
||||
31
apps/assets/migrations/0014_auto_20180427_1245.py
Normal file
31
apps/assets/migrations/0014_auto_20180427_1245.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2018-04-27 04:45
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0013_auto_20180411_1135'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='username',
|
||||
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='username',
|
||||
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='username',
|
||||
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
]
|
||||
31
apps/assets/migrations/0015_auto_20180510_1235.py
Normal file
31
apps/assets/migrations/0015_auto_20180510_1235.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2018-05-10 04:35
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0014_auto_20180427_1245'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='username',
|
||||
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='username',
|
||||
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='username',
|
||||
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
]
|
||||
20
apps/assets/migrations/0016_auto_20180511_1203.py
Normal file
20
apps/assets/migrations/0016_auto_20180511_1203.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2018-05-11 04:03
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0015_auto_20180510_1235'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='node',
|
||||
name='value',
|
||||
field=models.CharField(max_length=128, verbose_name='Value'),
|
||||
),
|
||||
]
|
||||
58
apps/assets/migrations/0017_auto_20180702_1415.py
Normal file
58
apps/assets/migrations/0017_auto_20180702_1415.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2018-07-02 06:15
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def migrate_win_to_ssh_protocol(apps, schema_editor):
|
||||
asset_model = apps.get_model("assets", "Asset")
|
||||
db_alias = schema_editor.connection.alias
|
||||
asset_model.objects.using(db_alias).filter(platform__startswith='Win').update(protocol='rdp')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0016_auto_20180511_1203'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='protocol',
|
||||
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=128, verbose_name='Protocol'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='login_mode',
|
||||
field=models.CharField(choices=[('auto', 'Automatic login'), ('manual', 'Manually login')], default='auto', max_length=10, verbose_name='Login mode'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='platform',
|
||||
field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Windows2016', 'Windows(2016)'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='protocol',
|
||||
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=16, verbose_name='Protocol'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.RunPython(migrate_win_to_ssh_protocol),
|
||||
]
|
||||
84
apps/assets/migrations/0018_auto_20180807_1116.py
Normal file
84
apps/assets/migrations/0018_auto_20180807_1116.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# Generated by Django 2.0.7 on 2018-08-07 03:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0017_auto_20180702_1415'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='adminuser',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default=None, max_length=36, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default=None, max_length=36, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='domain',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default=None, max_length=36, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='gateway',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default=None, max_length=36, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='label',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default=None, max_length=36, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='node',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default=None, max_length=36, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default=None, max_length=36, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='name',
|
||||
field=models.CharField(max_length=128, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='hostname',
|
||||
field=models.CharField(max_length=128, verbose_name='Hostname'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='name',
|
||||
field=models.CharField(max_length=128, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='name',
|
||||
field=models.CharField(max_length=128, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='adminuser',
|
||||
unique_together={('name', 'org_id')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='asset',
|
||||
unique_together={('org_id', 'hostname')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='gateway',
|
||||
unique_together={('name', 'org_id')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='systemuser',
|
||||
unique_together={('name', 'org_id')},
|
||||
),
|
||||
]
|
||||
22
apps/assets/migrations/0019_auto_20180816_1320.py
Normal file
22
apps/assets/migrations/0019_auto_20180816_1320.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 2.0.7 on 2018-08-16 05:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0018_auto_20180807_1116'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='cpu_vcpus',
|
||||
field=models.IntegerField(null=True, verbose_name='CPU vcpus'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='label',
|
||||
unique_together={('name', 'value', 'org_id')},
|
||||
),
|
||||
]
|
||||
25
apps/assets/migrations/0070_auto_20210426_1515.py
Normal file
25
apps/assets/migrations/0070_auto_20210426_1515.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.1 on 2021-04-26 07:15
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('assets', '0069_change_node_key0_to_key1'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='commandfilterrule',
|
||||
name='reviewers',
|
||||
field=models.ManyToManyField(blank=True, related_name='review_cmd_filter_rules', to=settings.AUTH_USER_MODEL, verbose_name='Reviewers'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='commandfilterrule',
|
||||
name='action',
|
||||
field=models.IntegerField(choices=[(0, 'Deny'), (1, 'Allow'), (2, 'Reconfirm')], default=0, verbose_name='Action'),
|
||||
),
|
||||
]
|
||||
@@ -35,7 +35,7 @@ def default_node():
|
||||
try:
|
||||
from .node import Node
|
||||
root = Node.org_root()
|
||||
return root
|
||||
return Node.objects.filter(id=root.id)
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ class AssetUser(AuthBook):
|
||||
hostname = ""
|
||||
ip = ""
|
||||
backend = ""
|
||||
backend_display = ""
|
||||
union_id = ""
|
||||
asset_username = ""
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Max
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from orgs.mixins.models import OrgManager
|
||||
from .base import BaseUser
|
||||
@@ -14,7 +15,7 @@ __all__ = ['AuthBook']
|
||||
class AuthBookQuerySet(models.QuerySet):
|
||||
def delete(self):
|
||||
if self.count() > 1:
|
||||
raise PermissionError(_("Bulk delete deny"))
|
||||
raise PermissionDenied(_("Bulk delete deny"))
|
||||
return super().delete()
|
||||
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
|
||||
from common.db.models import ChoiceSet
|
||||
from common.utils import random_string
|
||||
from common.utils import random_string, signer
|
||||
from common.utils import (
|
||||
ssh_key_string_to_obj, ssh_key_gen, get_logger, lazyproperty
|
||||
)
|
||||
|
||||
@@ -41,11 +41,12 @@ class CommandFilterRule(OrgModelMixin):
|
||||
(TYPE_COMMAND, _('Command')),
|
||||
)
|
||||
|
||||
ACTION_DENY, ACTION_ALLOW, ACTION_UNKNOWN = range(3)
|
||||
ACTION_CHOICES = (
|
||||
(ACTION_DENY, _('Deny')),
|
||||
(ACTION_ALLOW, _('Allow')),
|
||||
)
|
||||
ACTION_UNKNOWN = 10
|
||||
|
||||
class ActionChoices(models.IntegerChoices):
|
||||
deny = 0, _('Deny')
|
||||
allow = 1, _('Allow')
|
||||
confirm = 2, _('Reconfirm')
|
||||
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
filter = models.ForeignKey('CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules')
|
||||
@@ -53,7 +54,13 @@ class CommandFilterRule(OrgModelMixin):
|
||||
priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"),
|
||||
validators=[MinValueValidator(1), MaxValueValidator(100)])
|
||||
content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command"))
|
||||
action = models.IntegerField(default=ACTION_DENY, choices=ACTION_CHOICES, verbose_name=_("Action"))
|
||||
action = models.IntegerField(default=ActionChoices.deny, choices=ActionChoices.choices, verbose_name=_("Action"))
|
||||
# 动作: 附加字段
|
||||
# - confirm: 命令复核人
|
||||
reviewers = models.ManyToManyField(
|
||||
'users.User', related_name='review_cmd_filter_rules', blank=True,
|
||||
verbose_name=_("Reviewers")
|
||||
)
|
||||
comment = models.CharField(max_length=64, blank=True, default='', verbose_name=_("Comment"))
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
date_updated = models.DateTimeField(auto_now=True)
|
||||
@@ -89,10 +96,32 @@ class CommandFilterRule(OrgModelMixin):
|
||||
if not found:
|
||||
return self.ACTION_UNKNOWN, ''
|
||||
|
||||
if self.action == self.ACTION_ALLOW:
|
||||
return self.ACTION_ALLOW, found.group()
|
||||
if self.action == self.ActionChoices.allow:
|
||||
return self.ActionChoices.allow, found.group()
|
||||
else:
|
||||
return self.ACTION_DENY, found.group()
|
||||
return self.ActionChoices.deny, found.group()
|
||||
|
||||
def __str__(self):
|
||||
return '{} % {}'.format(self.type, self.content)
|
||||
|
||||
def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id):
|
||||
from tickets.const import TicketTypeChoices
|
||||
from tickets.models import Ticket
|
||||
data = {
|
||||
'title': _('Command confirm') + ' ({})'.format(session.user),
|
||||
'type': TicketTypeChoices.command_confirm,
|
||||
'meta': {
|
||||
'apply_run_user': session.user,
|
||||
'apply_run_asset': session.asset,
|
||||
'apply_run_system_user': session.system_user,
|
||||
'apply_run_command': run_command,
|
||||
'apply_from_session_id': str(session.id),
|
||||
'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id),
|
||||
'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id)
|
||||
},
|
||||
'org_id': org_id,
|
||||
}
|
||||
ticket = Ticket.objects.create(**data)
|
||||
ticket.assignees.set(self.reviewers.all())
|
||||
ticket.open(applicant=session.user_obj)
|
||||
return ticket
|
||||
|
||||
@@ -38,8 +38,7 @@ def compute_parent_key(key):
|
||||
|
||||
|
||||
class NodeQuerySet(models.QuerySet):
|
||||
def delete(self):
|
||||
raise NotImplementedError
|
||||
pass
|
||||
|
||||
|
||||
class FamilyMixin:
|
||||
@@ -622,14 +621,14 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
|
||||
tree_node = TreeNode(**data)
|
||||
return tree_node
|
||||
|
||||
def has_children_or_has_assets(self):
|
||||
if self.children or self.get_assets().exists():
|
||||
return True
|
||||
return False
|
||||
def has_offspring_assets(self):
|
||||
# 拥有后代资产
|
||||
return self.get_all_assets().exists()
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
if self.has_children_or_has_assets():
|
||||
if self.has_offspring_assets():
|
||||
return
|
||||
self.all_children.delete()
|
||||
return super().delete(using=using, keep_parents=keep_parents)
|
||||
|
||||
def update_child_full_value(self):
|
||||
|
||||
@@ -7,9 +7,10 @@ import logging
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.core.cache import cache
|
||||
|
||||
from common.utils import signer
|
||||
from common.fields.model import JsonListCharField
|
||||
from common.utils import signer, get_object_or_none
|
||||
from common.exceptions import JMSException
|
||||
from .base import BaseUser
|
||||
from .asset import Asset
|
||||
|
||||
@@ -185,6 +186,81 @@ class SystemUser(BaseUser):
|
||||
if self.username_same_with_user:
|
||||
self.username = other.username
|
||||
|
||||
def set_temp_auth(self, asset_or_app_id, user_id, auth, ttl=300):
|
||||
if not auth:
|
||||
raise ValueError('Auth not set')
|
||||
key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id)
|
||||
logger.debug(f'Set system user temp auth: {key}')
|
||||
cache.set(key, auth, ttl)
|
||||
|
||||
def get_temp_auth(self, asset_or_app_id, user_id):
|
||||
key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id)
|
||||
logger.debug(f'Get system user temp auth: {key}')
|
||||
password = cache.get(key)
|
||||
return password
|
||||
|
||||
def load_tmp_auth_if_has(self, asset_or_app_id, user):
|
||||
if not asset_or_app_id or not user:
|
||||
return
|
||||
if self.login_mode != self.LOGIN_MANUAL:
|
||||
pass
|
||||
|
||||
auth = self.get_temp_auth(asset_or_app_id, user)
|
||||
if not auth:
|
||||
return
|
||||
username = auth.get('username')
|
||||
password = auth.get('password')
|
||||
|
||||
if username:
|
||||
self.username = username
|
||||
if password:
|
||||
self.password = password
|
||||
|
||||
def load_app_more_auth(self, app_id=None, user_id=None):
|
||||
from users.models import User
|
||||
|
||||
if self.login_mode == self.LOGIN_MANUAL:
|
||||
self.password = ''
|
||||
self.private_key = ''
|
||||
if not user_id:
|
||||
return
|
||||
user = get_object_or_none(User, pk=user_id)
|
||||
if not user:
|
||||
return
|
||||
self.load_tmp_auth_if_has(app_id, user)
|
||||
|
||||
def load_asset_more_auth(self, asset_id=None, username=None, user_id=None):
|
||||
from users.models import User
|
||||
|
||||
if self.login_mode == self.LOGIN_MANUAL:
|
||||
self.password = ''
|
||||
self.private_key = ''
|
||||
|
||||
asset = None
|
||||
if asset_id:
|
||||
asset = get_object_or_none(Asset, pk=asset_id)
|
||||
# 没有资产就没有必要继续了
|
||||
if not asset:
|
||||
logger.debug('Asset not found, pass')
|
||||
return
|
||||
|
||||
user = None
|
||||
if user_id:
|
||||
user = get_object_or_none(User, pk=user_id)
|
||||
|
||||
if self.username_same_with_user:
|
||||
if user and not username:
|
||||
username = user.username
|
||||
|
||||
# 加载某个资产的特殊配置认证信息
|
||||
try:
|
||||
self.load_asset_special_auth(asset, username)
|
||||
except Exception as e:
|
||||
logger.error('Load special auth Error: ', e)
|
||||
pass
|
||||
|
||||
self.load_tmp_auth_if_has(asset_id, user)
|
||||
|
||||
@property
|
||||
def cmd_filter_rules(self):
|
||||
from .cmd_filter import CommandFilterRule
|
||||
@@ -196,9 +272,9 @@ class SystemUser(BaseUser):
|
||||
def is_command_can_run(self, command):
|
||||
for rule in self.cmd_filter_rules:
|
||||
action, matched_cmd = rule.match(command)
|
||||
if action == rule.ACTION_ALLOW:
|
||||
if action == rule.ActionChoices.allow:
|
||||
return True, None
|
||||
elif action == rule.ACTION_DENY:
|
||||
elif action == rule.ActionChoices.deny:
|
||||
return False, matched_cmd
|
||||
return True, None
|
||||
|
||||
|
||||
@@ -16,10 +16,14 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = AdminUser
|
||||
fields = [
|
||||
'id', 'name', 'username', 'password', 'private_key', 'public_key',
|
||||
'comment', 'assets_amount', 'date_created', 'date_updated', 'created_by',
|
||||
fields_mini = ['id', 'name', 'username']
|
||||
fields_write_only = ['password', 'private_key', 'public_key']
|
||||
fields_small = fields_mini + fields_write_only + [
|
||||
'date_created', 'date_updated',
|
||||
'comment', 'created_by'
|
||||
]
|
||||
fields_fk = ['assets_amount']
|
||||
fields = fields_small + fields_fk
|
||||
read_only_fields = ['date_created', 'date_updated', 'created_by', 'assets_amount']
|
||||
|
||||
extra_kwargs = {
|
||||
|
||||
@@ -65,7 +65,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
|
||||
platform = serializers.SlugRelatedField(
|
||||
slug_field='name', queryset=Platform.objects.all(), label=_("Platform")
|
||||
)
|
||||
protocols = ProtocolsField(label=_('Protocols'), required=False)
|
||||
protocols = ProtocolsField(label=_('Protocols'), required=False, default=['ssh/22'])
|
||||
domain_display = serializers.ReadOnlyField(source='domain.name', label=_('Domain name'))
|
||||
admin_user_display = serializers.ReadOnlyField(source='admin_user.name', label=_('Admin user name'))
|
||||
nodes_display = serializers.ListField(child=serializers.CharField(), label=_('Nodes name'), required=False)
|
||||
|
||||
@@ -22,10 +22,11 @@ class AssetUserWriteSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializ
|
||||
class Meta:
|
||||
model = AuthBook
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = [
|
||||
'id', 'username', 'password', 'private_key', "public_key",
|
||||
'asset', 'comment',
|
||||
]
|
||||
fields_mini = ['id', 'username']
|
||||
fields_write_only = ['password', 'private_key', "public_key"]
|
||||
fields_small = fields_mini + fields_write_only + ['comment']
|
||||
fields_fk = ['asset']
|
||||
fields = fields_small + fields_fk
|
||||
extra_kwargs = {
|
||||
'username': {'required': True},
|
||||
'password': {'write_only': True},
|
||||
@@ -46,18 +47,24 @@ class AssetUserReadSerializer(AssetUserWriteSerializer):
|
||||
ip = serializers.CharField(read_only=True, label=_("IP"))
|
||||
asset = serializers.CharField(source='asset_id', label=_('Asset'))
|
||||
backend = serializers.CharField(read_only=True, label=_("Backend"))
|
||||
backend_display = serializers.CharField(read_only=True, label=_("Source"))
|
||||
|
||||
class Meta(AssetUserWriteSerializer.Meta):
|
||||
read_only_fields = (
|
||||
'date_created', 'date_updated',
|
||||
'created_by', 'version',
|
||||
)
|
||||
fields = [
|
||||
'id', 'username', 'password', 'private_key', "public_key",
|
||||
'asset', 'hostname', 'ip', 'backend', 'version',
|
||||
'date_created', "date_updated", 'comment',
|
||||
fields_mini = ['id', 'name', 'username']
|
||||
fields_write_only = ['password', 'private_key', "public_key"]
|
||||
fields_small = fields_mini + fields_write_only + [
|
||||
'backend', 'backend_display', 'version',
|
||||
'date_created', "date_updated",
|
||||
'comment'
|
||||
]
|
||||
fields_fk = ['asset', 'hostname', 'ip']
|
||||
fields = fields_small + fields_fk
|
||||
extra_kwargs = {
|
||||
'name': {'required': False},
|
||||
'username': {'required': True},
|
||||
'password': {'write_only': True},
|
||||
'private_key': {'write_only': True},
|
||||
|
||||
@@ -4,8 +4,11 @@ import re
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.serializers import AdaptedBulkListSerializer
|
||||
from ..models import CommandFilter, CommandFilterRule, SystemUser
|
||||
from ..models import CommandFilter, CommandFilterRule
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from common.utils import get_object_or_none, lazyproperty
|
||||
from terminal.models import Session
|
||||
|
||||
|
||||
class CommandFilterSerializer(BulkOrgResourceModelSerializer):
|
||||
@@ -13,11 +16,16 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer):
|
||||
class Meta:
|
||||
model = CommandFilter
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = [
|
||||
'id', 'name', 'org_id', 'org_name', 'is_active', 'comment',
|
||||
'created_by', 'date_created', 'date_updated', 'rules', 'system_users'
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'org_id', 'org_name',
|
||||
'is_active',
|
||||
'date_created', 'date_updated',
|
||||
'comment', 'created_by',
|
||||
]
|
||||
|
||||
fields_fk = ['rules']
|
||||
fields_m2m = ['system_users']
|
||||
fields = fields_small + fields_fk + fields_m2m
|
||||
extra_kwargs = {
|
||||
'rules': {'read_only': True},
|
||||
'system_users': {'required': False},
|
||||
@@ -34,13 +42,28 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
|
||||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'type', 'type_display', 'content', 'priority',
|
||||
'action', 'action_display',
|
||||
'comment', 'created_by', 'date_created', 'date_updated'
|
||||
'action', 'action_display', 'reviewers',
|
||||
'date_created', 'date_updated',
|
||||
'comment', 'created_by',
|
||||
]
|
||||
fields_fk = ['filter']
|
||||
fields = '__all__'
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_action_choices()
|
||||
|
||||
def set_action_choices(self):
|
||||
from django.conf import settings
|
||||
action = self.fields.get('action')
|
||||
if not action:
|
||||
return
|
||||
choices = action._choices
|
||||
if not settings.XPACK_ENABLED:
|
||||
choices.pop(CommandFilterRule.ActionChoices.confirm, None)
|
||||
action._choices = choices
|
||||
|
||||
# def validate_content(self, content):
|
||||
# tp = self.initial_data.get("type")
|
||||
# if tp == CommandFilterRule.TYPE_REGEX:
|
||||
@@ -50,3 +73,35 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
|
||||
# msg = _("Content should not be contain: {}").format(invalid_char)
|
||||
# raise serializers.ValidationError(msg)
|
||||
# return content
|
||||
|
||||
|
||||
class CommandConfirmSerializer(serializers.Serializer):
|
||||
session_id = serializers.UUIDField(required=True, allow_null=False)
|
||||
cmd_filter_rule_id = serializers.UUIDField(required=True, allow_null=False)
|
||||
run_command = serializers.CharField(required=True, allow_null=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.session = None
|
||||
self.cmd_filter_rule = None
|
||||
|
||||
def validate_session_id(self, session_id):
|
||||
self.session = self.validate_object_exist(Session, session_id)
|
||||
return session_id
|
||||
|
||||
def validate_cmd_filter_rule_id(self, cmd_filter_rule_id):
|
||||
self.cmd_filter_rule = self.validate_object_exist(CommandFilterRule, cmd_filter_rule_id)
|
||||
return cmd_filter_rule_id
|
||||
|
||||
@staticmethod
|
||||
def validate_object_exist(model, field_id):
|
||||
with tmp_to_root_org():
|
||||
obj = get_object_or_none(model, id=field_id)
|
||||
if not obj:
|
||||
error = '{} Model object does not exist'.format(model.__name__)
|
||||
raise serializers.ValidationError(error)
|
||||
return obj
|
||||
|
||||
@lazyproperty
|
||||
def org(self):
|
||||
return self.session.org
|
||||
|
||||
@@ -48,13 +48,22 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
class Meta:
|
||||
model = Gateway
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = [
|
||||
'id', 'name', 'ip', 'port', 'protocol', 'username', 'password',
|
||||
'private_key', 'public_key', 'domain', 'is_active', 'date_created',
|
||||
'date_updated', 'created_by', 'comment',
|
||||
fields_mini = ['id', 'name']
|
||||
fields_write_only = [
|
||||
'password', 'private_key', 'public_key',
|
||||
]
|
||||
fields_small = fields_mini + fields_write_only + [
|
||||
'username', 'ip', 'port', 'protocol',
|
||||
'is_active',
|
||||
'date_created', 'date_updated',
|
||||
'created_by', 'comment',
|
||||
]
|
||||
fields_fk = ['domain']
|
||||
fields = fields_small + fields_fk
|
||||
extra_kwargs = {
|
||||
'password': {'validators': [NoSpecialChars()]}
|
||||
'password': {'write_only': True, 'validators': [NoSpecialChars()]},
|
||||
'private_key': {"write_only": True},
|
||||
'public_key': {"write_only": True},
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -69,12 +78,12 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
|
||||
|
||||
class GatewayWithAuthSerializer(GatewaySerializer):
|
||||
def get_field_names(self, declared_fields, info):
|
||||
fields = super().get_field_names(declared_fields, info)
|
||||
fields.extend(
|
||||
['password', 'private_key']
|
||||
)
|
||||
return fields
|
||||
class Meta(GatewaySerializer.Meta):
|
||||
extra_kwargs = {
|
||||
'password': {'write_only': False, 'validators': [NoSpecialChars()]},
|
||||
'private_key': {"write_only": False},
|
||||
'public_key': {"write_only": False},
|
||||
}
|
||||
|
||||
|
||||
class DomainWithGatewaySerializer(BulkOrgResourceModelSerializer):
|
||||
|
||||
@@ -10,11 +10,14 @@ from ..models import GatheredUser
|
||||
class GatheredUserSerializer(OrgResourceModelSerializerMixin):
|
||||
class Meta:
|
||||
model = GatheredUser
|
||||
fields = [
|
||||
'id', 'asset', 'hostname', 'ip', 'username',
|
||||
'date_last_login', 'ip_last_login',
|
||||
'present', 'date_created', 'date_updated'
|
||||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'username', 'ip_last_login',
|
||||
'present',
|
||||
'date_last_login', 'date_created', 'date_updated'
|
||||
]
|
||||
fields_fk = ['asset', 'hostname', 'ip']
|
||||
fields = fields_small + fields_fk
|
||||
read_only_fields = fields
|
||||
extra_kwargs = {
|
||||
'hostname': {'label': _("Hostname")},
|
||||
|
||||
@@ -15,10 +15,15 @@ class LabelSerializer(BulkOrgResourceModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Label
|
||||
fields = [
|
||||
'id', 'name', 'value', 'category', 'is_active', 'comment',
|
||||
'date_created', 'asset_count', 'assets', 'category_display'
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'value', 'category', 'category_display',
|
||||
'is_active',
|
||||
'date_created',
|
||||
'comment',
|
||||
]
|
||||
fields_m2m = ['asset_count', 'assets']
|
||||
fields = fields_small + fields_m2m
|
||||
read_only_fields = (
|
||||
'category', 'date_created', 'asset_count',
|
||||
)
|
||||
|
||||
@@ -14,6 +14,7 @@ __all__ = [
|
||||
'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer',
|
||||
'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer',
|
||||
'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer',
|
||||
'SystemUserTempAuthSerializer',
|
||||
]
|
||||
|
||||
|
||||
@@ -26,16 +27,18 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = [
|
||||
'id', 'name', 'username', 'protocol',
|
||||
'password', 'public_key', 'private_key',
|
||||
'login_mode', 'login_mode_display',
|
||||
'priority', 'username_same_with_user',
|
||||
'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment',
|
||||
'auto_generate_key', 'sftp_root', 'token',
|
||||
'assets_amount', 'date_created', 'date_updated', 'created_by',
|
||||
'home', 'system_groups', 'ad_domain'
|
||||
fields_mini = ['id', 'name', 'username']
|
||||
fields_write_only = ['password', 'public_key', 'private_key']
|
||||
fields_small = fields_mini + fields_write_only + [
|
||||
'protocol', 'login_mode', 'login_mode_display', 'priority',
|
||||
'sudo', 'shell', 'sftp_root', 'token',
|
||||
'home', 'system_groups', 'ad_domain',
|
||||
'username_same_with_user', 'auto_push', 'auto_generate_key',
|
||||
'date_created', 'date_updated',
|
||||
'comment', 'created_by',
|
||||
]
|
||||
fields_m2m = [ 'cmd_filters', 'assets_amount']
|
||||
fields = fields_small + fields_m2m
|
||||
extra_kwargs = {
|
||||
'password': {"write_only": True},
|
||||
'public_key': {"write_only": True},
|
||||
@@ -101,6 +104,12 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
raise serializers.ValidationError(msg)
|
||||
return username
|
||||
|
||||
def validate_home(self, home):
|
||||
username_same_with_user = self.initial_data.get("username_same_with_user")
|
||||
if username_same_with_user:
|
||||
return ''
|
||||
return home
|
||||
|
||||
def validate_sftp_root(self, value):
|
||||
if value in ['home', 'tmp']:
|
||||
return value
|
||||
@@ -147,17 +156,18 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
class SystemUserListSerializer(SystemUserSerializer):
|
||||
|
||||
class Meta(SystemUserSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'name', 'username', 'protocol',
|
||||
'password', 'public_key', 'private_key',
|
||||
'login_mode', 'login_mode_display',
|
||||
'priority', "username_same_with_user",
|
||||
'auto_push', 'sudo', 'shell', 'comment',
|
||||
"assets_amount", 'home', 'system_groups',
|
||||
'auto_generate_key', 'ad_domain',
|
||||
'sftp_root', 'created_by', 'date_created',
|
||||
'date_updated',
|
||||
fields_mini = ['id', 'name', 'username']
|
||||
fields_write_only = ['password', 'public_key', 'private_key']
|
||||
fields_small = fields_mini + fields_write_only + [
|
||||
'protocol', 'login_mode', 'login_mode_display', 'priority',
|
||||
'sudo', 'shell', 'home', 'system_groups',
|
||||
'ad_domain', 'sftp_root',
|
||||
"username_same_with_user", 'auto_push', 'auto_generate_key',
|
||||
'date_created', 'date_updated',
|
||||
'comment', 'created_by',
|
||||
]
|
||||
fields_m2m = ["assets_amount",]
|
||||
fields = fields_small + fields_m2m
|
||||
extra_kwargs = {
|
||||
'password': {"write_only": True},
|
||||
'public_key': {"write_only": True},
|
||||
@@ -178,15 +188,15 @@ class SystemUserListSerializer(SystemUserSerializer):
|
||||
|
||||
class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
|
||||
class Meta(SystemUserSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'name', 'username', 'protocol',
|
||||
'password', 'public_key', 'private_key',
|
||||
'login_mode', 'login_mode_display',
|
||||
'priority', 'username_same_with_user',
|
||||
'auto_push', 'sudo', 'shell', 'comment',
|
||||
'auto_generate_key', 'sftp_root', 'token',
|
||||
'ad_domain',
|
||||
fields_mini = ['id', 'name', 'username']
|
||||
fields_write_only = ['password', 'public_key', 'private_key']
|
||||
fields_small = fields_mini + fields_write_only + [
|
||||
'protocol', 'login_mode', 'login_mode_display', 'priority',
|
||||
'sudo', 'shell', 'ad_domain', 'sftp_root', 'token',
|
||||
"username_same_with_user", 'auto_push', 'auto_generate_key',
|
||||
'comment',
|
||||
]
|
||||
fields = fields_small
|
||||
extra_kwargs = {
|
||||
'nodes_amount': {'label': _('Node')},
|
||||
'assets_amount': {'label': _('Asset')},
|
||||
@@ -263,3 +273,10 @@ class SystemUserTaskSerializer(serializers.Serializer):
|
||||
many=True
|
||||
)
|
||||
task = serializers.CharField(read_only=True)
|
||||
|
||||
|
||||
class SystemUserTempAuthSerializer(SystemUserSerializer):
|
||||
instance_id = serializers.CharField()
|
||||
|
||||
class Meta(SystemUserSerializer.Meta):
|
||||
fields = ['instance_id', 'username', 'password']
|
||||
|
||||
@@ -56,8 +56,8 @@ def get_push_unixlike_system_user_tasks(system_user, username=None):
|
||||
'shell': system_user.shell or Empty,
|
||||
'state': 'present',
|
||||
'home': system_user.home or Empty,
|
||||
'expires': -1,
|
||||
'groups': groups or Empty,
|
||||
'expires': 99999,
|
||||
'comment': comment
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from collections import defaultdict
|
||||
from celery import shared_task
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from assets.models import Asset
|
||||
from common.utils import get_logger
|
||||
from orgs.utils import tmp_to_org, org_aware_func
|
||||
from ..models import SystemUser
|
||||
@@ -96,9 +97,12 @@ def test_system_user_connectivity_util(system_user, assets, task_name):
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
@org_aware_func("system_user")
|
||||
def test_system_user_connectivity_manual(system_user):
|
||||
def test_system_user_connectivity_manual(system_user, asset_ids=None):
|
||||
task_name = _("Test system user connectivity: {}").format(system_user)
|
||||
assets = system_user.get_related_assets()
|
||||
if asset_ids:
|
||||
assets = Asset.objects.filter(id__in=asset_ids)
|
||||
else:
|
||||
assets = system_user.get_related_assets()
|
||||
test_system_user_connectivity_util(system_user, assets, task_name)
|
||||
|
||||
|
||||
|
||||
@@ -46,7 +46,9 @@ urlpatterns = [
|
||||
|
||||
path('system-users/<uuid:pk>/auth-info/', api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
|
||||
path('system-users/<uuid:pk>/assets/', api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
|
||||
path('system-users/<uuid:pk>/assets/<uuid:aid>/auth-info/', api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'),
|
||||
path('system-users/<uuid:pk>/assets/<uuid:asset_id>/auth-info/', api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'),
|
||||
path('system-users/<uuid:pk>/applications/<uuid:app_id>/auth-info/', api.SystemUserAppAuthInfoApi.as_view(), name='system-user-app-auth-info'),
|
||||
path('system-users/<uuid:pk>/temp-auth/', api.SystemUserTempAuthInfoApi.as_view(), name='system-user-asset-temp-info'),
|
||||
path('system-users/<uuid:pk>/tasks/', api.SystemUserTaskApi.as_view(), name='system-user-task-create'),
|
||||
path('system-users/<uuid:pk>/cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'),
|
||||
|
||||
@@ -63,6 +65,9 @@ urlpatterns = [
|
||||
|
||||
path('gateways/<uuid:pk>/test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
|
||||
|
||||
path('cmd-filters/command-confirm/', api.CommandConfirmAPI.as_view(), name='command-confirm'),
|
||||
path('cmd-filters/command-confirm/<uuid:pk>/status/', api.CommandConfirmStatusAPI.as_view(), name='command-confirm-status')
|
||||
|
||||
]
|
||||
|
||||
old_version_urlpatterns = [
|
||||
|
||||
@@ -94,8 +94,14 @@ class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
|
||||
date_range_filter_fields = [
|
||||
('date_start', ('date_from', 'date_to'))
|
||||
]
|
||||
filterset_fields = ['user__name', 'command', 'run_as__name', 'is_finished']
|
||||
search_fields = ['command', 'user__name', 'run_as__name']
|
||||
filterset_fields = [
|
||||
'user__name', 'user__username', 'command',
|
||||
'run_as__name', 'run_as__username', 'is_finished'
|
||||
]
|
||||
search_fields = [
|
||||
'command', 'user__name', 'user__username',
|
||||
'run_as__name', 'run_as__username',
|
||||
]
|
||||
ordering = ['-date_created']
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
@@ -16,10 +16,14 @@ class FTPLogSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.FTPLog
|
||||
fields = (
|
||||
'id', 'user', 'remote_addr', 'asset', 'system_user', 'org_id',
|
||||
'operate', 'filename', 'is_success', 'date_start', 'operate_display'
|
||||
)
|
||||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'user', 'remote_addr', 'asset', 'system_user', 'org_id',
|
||||
'operate', 'filename', 'operate_display',
|
||||
'is_success',
|
||||
'date_start',
|
||||
]
|
||||
fields = fields_small
|
||||
|
||||
|
||||
class UserLoginLogSerializer(serializers.ModelSerializer):
|
||||
@@ -29,11 +33,14 @@ class UserLoginLogSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.UserLoginLog
|
||||
fields = (
|
||||
'id', 'username', 'type', 'type_display', 'ip', 'city', 'user_agent',
|
||||
'mfa', 'reason', 'status', 'status_display', 'datetime', 'mfa_display',
|
||||
'backend'
|
||||
)
|
||||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'username', 'type', 'type_display', 'ip', 'city', 'user_agent',
|
||||
'mfa', 'mfa_display', 'reason', 'backend',
|
||||
'status', 'status_display',
|
||||
'datetime',
|
||||
]
|
||||
fields = fields_small
|
||||
extra_kwargs = {
|
||||
"user_agent": {'label': _('User agent')}
|
||||
}
|
||||
@@ -42,10 +49,13 @@ class UserLoginLogSerializer(serializers.ModelSerializer):
|
||||
class OperateLogSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = models.OperateLog
|
||||
fields = (
|
||||
'id', 'user', 'action', 'resource_type', 'resource',
|
||||
'remote_addr', 'datetime', 'org_id'
|
||||
)
|
||||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'user', 'action', 'resource_type', 'resource', 'remote_addr',
|
||||
'datetime',
|
||||
'org_id'
|
||||
]
|
||||
fields = fields_small
|
||||
|
||||
|
||||
class PasswordChangeLogSerializer(serializers.ModelSerializer):
|
||||
@@ -72,7 +82,7 @@ class CommandExecutionSerializer(serializers.ModelSerializer):
|
||||
model = CommandExecution
|
||||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'run_as', 'command', 'user', 'is_finished',
|
||||
'run_as', 'command', 'is_finished', 'user',
|
||||
'date_start', 'result', 'is_success', 'org_id'
|
||||
]
|
||||
fields = fields_small + ['hosts', 'hosts_display', 'run_as_display', 'user_display']
|
||||
|
||||
@@ -27,11 +27,23 @@ json_render = JSONRenderer()
|
||||
|
||||
|
||||
MODELS_NEED_RECORD = (
|
||||
'User', 'UserGroup', 'Asset', 'Node', 'AdminUser', 'SystemUser',
|
||||
'Domain', 'Gateway', 'Organization', 'AssetPermission', 'CommandFilter',
|
||||
'CommandFilterRule', 'License', 'Setting', 'Account', 'SyncInstanceTask',
|
||||
'Platform', 'ChangeAuthPlan', 'GatherUserTask',
|
||||
'RemoteApp', 'RemoteAppPermission', 'DatabaseApp', 'DatabaseAppPermission',
|
||||
# users
|
||||
'User', 'UserGroup',
|
||||
# acls
|
||||
'LoginACL', 'LoginAssetACL',
|
||||
# assets
|
||||
'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule',
|
||||
'CommandFilter', 'Platform',
|
||||
# applications
|
||||
'Application',
|
||||
# orgs
|
||||
'Organization',
|
||||
# settings
|
||||
'Setting',
|
||||
# perms
|
||||
'AssetPermission', 'ApplicationPermission',
|
||||
# xpack
|
||||
'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'GatherUserTask',
|
||||
)
|
||||
|
||||
|
||||
@@ -45,6 +57,9 @@ class AuthBackendLabelMapping(LazyObject):
|
||||
backend_label_mapping[settings.AUTH_BACKEND_PUBKEY] = _('SSH Key')
|
||||
backend_label_mapping[settings.AUTH_BACKEND_MODEL] = _('Password')
|
||||
backend_label_mapping[settings.AUTH_BACKEND_SSO] = _('SSO')
|
||||
backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _('Auth Token')
|
||||
backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom')
|
||||
backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk')
|
||||
return backend_label_mapping
|
||||
|
||||
def _setup(self):
|
||||
@@ -142,12 +157,13 @@ def get_login_backend(request):
|
||||
return backend_label
|
||||
|
||||
|
||||
def generate_data(username, request):
|
||||
def generate_data(username, request, login_type=None):
|
||||
user_agent = request.META.get('HTTP_USER_AGENT', '')
|
||||
login_ip = get_request_ip(request) or '0.0.0.0'
|
||||
if isinstance(request, Request):
|
||||
|
||||
if login_type is None and isinstance(request, Request):
|
||||
login_type = request.META.get('HTTP_X_JMS_LOGIN_TYPE', 'U')
|
||||
else:
|
||||
if login_type is None:
|
||||
login_type = 'W'
|
||||
|
||||
data = {
|
||||
@@ -162,9 +178,9 @@ def generate_data(username, request):
|
||||
|
||||
|
||||
@receiver(post_auth_success)
|
||||
def on_user_auth_success(sender, user, request, **kwargs):
|
||||
def on_user_auth_success(sender, user, request, login_type=None, **kwargs):
|
||||
logger.debug('User login success: {}'.format(user.username))
|
||||
data = generate_data(user.username, request)
|
||||
data = generate_data(user.username, request, login_type=login_type)
|
||||
data.update({'mfa': int(user.mfa_enabled), 'status': True})
|
||||
write_login_log(**data)
|
||||
|
||||
|
||||
@@ -7,3 +7,6 @@ from .mfa import *
|
||||
from .access_key import *
|
||||
from .login_confirm import *
|
||||
from .sso import *
|
||||
from .wecom import *
|
||||
from .dingtalk import *
|
||||
from .password import *
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import urllib.parse
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.http import HttpResponse
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework import serializers
|
||||
|
||||
from authentication.signals import post_auth_failed, post_auth_success
|
||||
from common.utils import get_logger, random_string
|
||||
from common.drf.api import SerializerMixin2
|
||||
from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser
|
||||
@@ -47,11 +52,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||
raise PermissionDenied(error)
|
||||
return True
|
||||
|
||||
def create_token(self, user, asset, application, system_user):
|
||||
if not settings.CONNECTION_TOKEN_ENABLED:
|
||||
raise PermissionDenied('Connection token disabled')
|
||||
if not user:
|
||||
user = self.request.user
|
||||
def create_token(self, user, asset, application, system_user, ttl=5*60):
|
||||
if not self.request.user.is_superuser and user != self.request.user:
|
||||
raise PermissionDenied('Only super user can create user token')
|
||||
self.check_resource_permission(user, asset, application, system_user)
|
||||
@@ -77,7 +78,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||
})
|
||||
|
||||
key = self.CACHE_KEY_PREFIX.format(token)
|
||||
cache.set(key, value, timeout=20)
|
||||
cache.set(key, value, timeout=ttl)
|
||||
return token
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
@@ -91,16 +92,16 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||
token = self.create_token(user, asset, application, system_user)
|
||||
return Response({"token": token}, status=201)
|
||||
|
||||
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
|
||||
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file', permission_classes=[IsValidUser])
|
||||
def get_rdp_file(self, request, *args, **kwargs):
|
||||
options = {
|
||||
'full address:s': '',
|
||||
'username:s': '',
|
||||
'screen mode id:i': '0',
|
||||
'desktopwidth:i': '1280',
|
||||
'desktopheight:i': '800',
|
||||
# 'desktopwidth:i': '1280',
|
||||
# 'desktopheight:i': '800',
|
||||
'use multimon:i': '1',
|
||||
'session bpp:i': '24',
|
||||
'session bpp:i': '32',
|
||||
'audiomode:i': '0',
|
||||
'disable wallpaper:i': '0',
|
||||
'disable full window drag:i': '0',
|
||||
@@ -118,6 +119,8 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||
'autoreconnection enabled:i': '1',
|
||||
'bookmarktype:i': '3',
|
||||
'use redirection server name:i': '0',
|
||||
'smart sizing:i': '0',
|
||||
# 'domain:s': ''
|
||||
# 'alternate shell:s:': '||MySQLWorkbench',
|
||||
# 'remoteapplicationname:s': 'Firefox',
|
||||
# 'remoteapplicationcmdline:s': '',
|
||||
@@ -132,30 +135,35 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||
asset = serializer.validated_data.get('asset')
|
||||
application = serializer.validated_data.get('application')
|
||||
system_user = serializer.validated_data['system_user']
|
||||
user = serializer.validated_data.get('user')
|
||||
height = serializer.validated_data.get('height')
|
||||
width = serializer.validated_data.get('width')
|
||||
user = request.user
|
||||
token = self.create_token(user, asset, application, system_user)
|
||||
|
||||
# Todo: 上线后地址是 JumpServerAddr:3389
|
||||
address = self.request.query_params.get('address') or '1.1.1.1'
|
||||
address = settings.TERMINAL_RDP_ADDR
|
||||
if not address or address == 'localhost:3389':
|
||||
address = request.get_host().split(':')[0] + ':3389'
|
||||
options['full address:s'] = address
|
||||
options['username:s'] = '{}@{}'.format(user.username, token)
|
||||
options['desktopwidth:i'] = width
|
||||
options['desktopheight:i'] = height
|
||||
options['username:s'] = '{}|{}'.format(user.username, token)
|
||||
if system_user.ad_domain:
|
||||
options['domain:s'] = system_user.ad_domain
|
||||
if width and height:
|
||||
options['desktopwidth:i'] = width
|
||||
options['desktopheight:i'] = height
|
||||
else:
|
||||
options['smart sizing:i'] = '1'
|
||||
data = ''
|
||||
for k, v in options.items():
|
||||
data += f'{k}:{v}\n'
|
||||
response = HttpResponse(data, content_type='text/plain')
|
||||
response = HttpResponse(data, content_type='application/octet-stream')
|
||||
filename = "{}-{}-jumpserver.rdp".format(user.username, asset.hostname)
|
||||
response['Content-Disposition'] = 'attachment; filename={}'.format(filename)
|
||||
filename = urllib.parse.quote(filename)
|
||||
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def _get_application_secret_detail(value):
|
||||
from applications.models import Application
|
||||
def _get_application_secret_detail(application):
|
||||
from perms.models import Action
|
||||
application = get_object_or_404(Application, id=value.get('application'))
|
||||
gateway = None
|
||||
|
||||
if not application.category_remote_app:
|
||||
@@ -181,15 +189,15 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _get_asset_secret_detail(value, user, system_user):
|
||||
from assets.models import Asset
|
||||
def _get_asset_secret_detail(asset, user, system_user):
|
||||
from perms.utils.asset import get_asset_system_user_ids_with_actions_by_user
|
||||
asset = get_object_or_404(Asset, id=value.get('asset'))
|
||||
systemuserid_actions_mapper = get_asset_system_user_ids_with_actions_by_user(user, asset)
|
||||
actions = systemuserid_actions_mapper.get(system_user.id, [])
|
||||
|
||||
gateway = None
|
||||
if asset and asset.domain and asset.domain.has_gateway():
|
||||
gateway = asset.domain.random_gateway()
|
||||
|
||||
return {
|
||||
'asset': asset,
|
||||
'application': None,
|
||||
@@ -198,29 +206,65 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||
'actions': actions,
|
||||
}
|
||||
|
||||
@action(methods=['POST'], detail=False, permission_classes=[IsSuperUserOrAppUser], url_path='secret-info/detail')
|
||||
def get_secret_detail(self, request, *args, **kwargs):
|
||||
def valid_token(self, token):
|
||||
from users.models import User
|
||||
from assets.models import SystemUser
|
||||
from assets.models import SystemUser, Asset
|
||||
from applications.models import Application
|
||||
|
||||
token = request.data.get('token', '')
|
||||
key = self.CACHE_KEY_PREFIX.format(token)
|
||||
value = cache.get(key, None)
|
||||
if not value:
|
||||
return Response(status=404)
|
||||
user = get_object_or_404(User, id=value.get('user'))
|
||||
system_user = get_object_or_404(SystemUser, id=value.get('system_user'))
|
||||
data = dict(user=user, system_user=system_user)
|
||||
raise serializers.ValidationError('Token not found')
|
||||
|
||||
user = get_object_or_404(User, id=value.get('user'))
|
||||
if not user.is_valid:
|
||||
raise serializers.ValidationError("User not valid, disabled or expired")
|
||||
|
||||
system_user = get_object_or_404(SystemUser, id=value.get('system_user'))
|
||||
|
||||
asset = None
|
||||
app = None
|
||||
if value.get('type') == 'asset':
|
||||
asset_detail = self._get_asset_secret_detail(value, user=user, system_user=system_user)
|
||||
asset = get_object_or_404(Asset, id=value.get('asset'))
|
||||
else:
|
||||
app = get_object_or_404(Application, id=value.get('application'))
|
||||
|
||||
if asset and not asset.is_active:
|
||||
raise serializers.ValidationError("Asset disabled")
|
||||
|
||||
try:
|
||||
self.check_resource_permission(user, asset, app, system_user)
|
||||
except PermissionDenied:
|
||||
raise serializers.ValidationError('Permission expired or invalid')
|
||||
return value, user, system_user, asset, app
|
||||
|
||||
@action(methods=['POST'], detail=False, permission_classes=[IsSuperUserOrAppUser], url_path='secret-info/detail')
|
||||
def get_secret_detail(self, request, *args, **kwargs):
|
||||
token = request.data.get('token', '')
|
||||
try:
|
||||
value, user, system_user, asset, app = self.valid_token(token)
|
||||
except serializers.ValidationError as e:
|
||||
post_auth_failed.send(
|
||||
sender=self.__class__, username='', request=self.request,
|
||||
reason=_('Invalid token')
|
||||
)
|
||||
raise e
|
||||
|
||||
data = dict(user=user, system_user=system_user)
|
||||
if asset:
|
||||
asset_detail = self._get_asset_secret_detail(asset, user=user, system_user=system_user)
|
||||
system_user.load_asset_more_auth(asset.id, user.username, user.id)
|
||||
data['type'] = 'asset'
|
||||
data.update(asset_detail)
|
||||
else:
|
||||
app_detail = self._get_application_secret_detail(value)
|
||||
app_detail = self._get_application_secret_detail(app)
|
||||
system_user.load_app_more_auth(app.id, user.id)
|
||||
data['type'] = 'application'
|
||||
data.update(app_detail)
|
||||
|
||||
self.request.session['auth_backend'] = settings.AUTH_BACKEND_AUTH_TOKEN
|
||||
post_auth_success.send(sender=self.__class__, user=user, request=self.request, login_type='T')
|
||||
|
||||
serializer = self.get_serializer(data)
|
||||
return Response(data=serializer.data, status=200)
|
||||
|
||||
|
||||
35
apps/authentication/api/dingtalk.py
Normal file
35
apps/authentication/api/dingtalk.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from users.permissions import IsAuthPasswdTimeValid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.permissions import IsOrgAdmin
|
||||
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||
from authentication import errors
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class DingTalkQRUnBindBase(APIView):
|
||||
user: User
|
||||
|
||||
def post(self, request: Request, **kwargs):
|
||||
user = self.user
|
||||
|
||||
if not user.dingtalk_id:
|
||||
raise errors.DingTalkNotBound
|
||||
|
||||
user.dingtalk_id = None
|
||||
user.save()
|
||||
return Response()
|
||||
|
||||
|
||||
class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase):
|
||||
permission_classes = (IsAuthPasswdTimeValid,)
|
||||
|
||||
|
||||
class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase):
|
||||
user_id_url_kwarg = 'user_id'
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
26
apps/authentication/api/password.py
Normal file
26
apps/authentication/api/password.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from rest_framework.generics import CreateAPIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
from authentication.serializers import PasswordVerifySerializer
|
||||
from common.permissions import IsValidUser
|
||||
from authentication.mixins import authenticate
|
||||
from authentication.errors import PasswdInvalid
|
||||
from authentication.mixins import AuthMixin
|
||||
|
||||
|
||||
class UserPasswordVerifyApi(AuthMixin, CreateAPIView):
|
||||
permission_classes = (IsValidUser,)
|
||||
serializer_class = PasswordVerifySerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
password = serializer.validated_data['password']
|
||||
user = self.request.user
|
||||
|
||||
user = authenticate(request=request, username=user.username, password=password)
|
||||
if not user:
|
||||
raise PasswdInvalid
|
||||
|
||||
self.set_passwd_verify_on_session(user)
|
||||
return Response()
|
||||
35
apps/authentication/api/wecom.py
Normal file
35
apps/authentication/api/wecom.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from users.permissions import IsAuthPasswdTimeValid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.permissions import IsOrgAdmin
|
||||
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||
from authentication import errors
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class WeComQRUnBindBase(APIView):
|
||||
user: User
|
||||
|
||||
def post(self, request: Request, **kwargs):
|
||||
user = self.user
|
||||
|
||||
if not user.wecom_id:
|
||||
raise errors.WeComNotBound
|
||||
|
||||
user.wecom_id = None
|
||||
user.save()
|
||||
return Response()
|
||||
|
||||
|
||||
class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase):
|
||||
permission_classes = (IsAuthPasswdTimeValid,)
|
||||
|
||||
|
||||
class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase):
|
||||
user_id_url_kwarg = 'user_id'
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
@@ -8,7 +8,7 @@ from django.core.cache import cache
|
||||
from django.utils.translation import ugettext as _
|
||||
from six import text_type
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.contrib.auth.backends import ModelBackend as DJModelBackend
|
||||
from rest_framework import HTTP_HEADER_ENCODING
|
||||
from rest_framework import authentication, exceptions
|
||||
from common.auth import signature
|
||||
@@ -25,6 +25,11 @@ def get_request_date_header(request):
|
||||
return date
|
||||
|
||||
|
||||
class ModelBackend(DJModelBackend):
|
||||
def user_can_authenticate(self, user):
|
||||
return user.is_valid
|
||||
|
||||
|
||||
class AccessKeyAuthentication(authentication.BaseAuthentication):
|
||||
"""App使用Access key进行签名认证, 目前签名算法比较简单,
|
||||
app注册或者手动建立后,会生成 access_key_id 和 access_key_secret,
|
||||
@@ -205,3 +210,29 @@ class SSOAuthentication(ModelBackend):
|
||||
|
||||
def authenticate(self, request, sso_token=None, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class WeComAuthentication(ModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
|
||||
def authenticate(self, request, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class DingTalkAuthentication(ModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
|
||||
def authenticate(self, request, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class AuthorizationTokenAuthentication(ModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
def authenticate(self, request, **kwargs):
|
||||
pass
|
||||
|
||||
@@ -19,6 +19,7 @@ reason_user_inactive = 'user_inactive'
|
||||
reason_user_expired = 'user_expired'
|
||||
reason_backend_not_match = 'backend_not_match'
|
||||
reason_acl_not_allow = 'acl_not_allow'
|
||||
only_local_users_are_allowed = 'only_local_users_are_allowed'
|
||||
|
||||
reason_choices = {
|
||||
reason_password_failed: _('Username/password check failed'),
|
||||
@@ -32,6 +33,7 @@ reason_choices = {
|
||||
reason_user_expired: _("This account is expired"),
|
||||
reason_backend_not_match: _("Auth backend not match"),
|
||||
reason_acl_not_allow: _("ACL is not allowed"),
|
||||
only_local_users_are_allowed: _("Only local users are allowed")
|
||||
}
|
||||
old_reason_choices = {
|
||||
'0': '-',
|
||||
@@ -275,6 +277,15 @@ class PasswdTooSimple(JMSException):
|
||||
self.url = url
|
||||
|
||||
|
||||
class PasswdNeedUpdate(JMSException):
|
||||
default_code = 'passwd_need_update'
|
||||
default_detail = _('You should to change your password before login')
|
||||
|
||||
def __init__(self, url, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.url = url
|
||||
|
||||
|
||||
class PasswordRequireResetError(JMSException):
|
||||
default_code = 'passwd_has_expired'
|
||||
default_detail = _('Your password has expired, please reset before logging in')
|
||||
@@ -282,3 +293,28 @@ class PasswordRequireResetError(JMSException):
|
||||
def __init__(self, url, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.url = url
|
||||
|
||||
|
||||
class WeComCodeInvalid(JMSException):
|
||||
default_code = 'wecom_code_invalid'
|
||||
default_detail = 'Code invalid, can not get user info'
|
||||
|
||||
|
||||
class WeComBindAlready(JMSException):
|
||||
default_code = 'wecom_bind_already'
|
||||
default_detail = 'WeCom already binded'
|
||||
|
||||
|
||||
class WeComNotBound(JMSException):
|
||||
default_code = 'wecom_not_bound'
|
||||
default_detail = 'WeCom is not bound'
|
||||
|
||||
|
||||
class DingTalkNotBound(JMSException):
|
||||
default_code = 'dingtalk_not_bound'
|
||||
default_detail = 'DingTalk is not bound'
|
||||
|
||||
|
||||
class PasswdInvalid(JMSException):
|
||||
default_code = 'passwd_invalid'
|
||||
default_detail = _('Your password is invalid')
|
||||
|
||||
@@ -5,15 +5,17 @@ from urllib.parse import urlencode
|
||||
from functools import partial
|
||||
import time
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.conf import settings
|
||||
from django.contrib import auth
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.contrib.auth import (
|
||||
BACKEND_SESSION_KEY, _get_backends,
|
||||
PermissionDenied, user_login_failed, _clean_credentials
|
||||
)
|
||||
from django.shortcuts import reverse
|
||||
from django.shortcuts import reverse, redirect
|
||||
|
||||
from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get
|
||||
from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get, FlashMessageUtil
|
||||
from users.models import User
|
||||
from users.utils import LoginBlockUtil, MFABlockUtils
|
||||
from . import errors
|
||||
@@ -81,6 +83,8 @@ class AuthMixin:
|
||||
request = None
|
||||
partial_credential_error = None
|
||||
|
||||
key_prefix_captcha = "_LOGIN_INVALID_{}"
|
||||
|
||||
def get_user_from_session(self):
|
||||
if self.request.session.is_empty():
|
||||
raise errors.SessionEmptyError()
|
||||
@@ -109,11 +113,7 @@ class AuthMixin:
|
||||
ip = ip or get_request_ip(self.request)
|
||||
return ip
|
||||
|
||||
def check_is_block(self, raise_exception=True):
|
||||
if hasattr(self.request, 'data'):
|
||||
username = self.request.data.get("username")
|
||||
else:
|
||||
username = self.request.POST.get("username")
|
||||
def _check_is_block(self, username, raise_exception=True):
|
||||
ip = self.get_request_ip()
|
||||
if LoginBlockUtil(username, ip).is_block():
|
||||
logger.warn('Ip was blocked' + ': ' + username + ':' + ip)
|
||||
@@ -123,6 +123,13 @@ class AuthMixin:
|
||||
else:
|
||||
return exception
|
||||
|
||||
def check_is_block(self, raise_exception=True):
|
||||
if hasattr(self.request, 'data'):
|
||||
username = self.request.data.get("username")
|
||||
else:
|
||||
username = self.request.POST.get("username")
|
||||
self._check_is_block(username, raise_exception)
|
||||
|
||||
def decrypt_passwd(self, raw_passwd):
|
||||
# 获取解密密钥,对密码进行解密
|
||||
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
|
||||
@@ -139,6 +146,9 @@ class AuthMixin:
|
||||
def raise_credential_error(self, error):
|
||||
raise self.partial_credential_error(error=error)
|
||||
|
||||
def _set_partial_credential_error(self, username, ip, request):
|
||||
self.partial_credential_error = partial(errors.CredentialError, username=username, ip=ip, request=request)
|
||||
|
||||
def get_auth_data(self, decrypt_passwd=False):
|
||||
request = self.request
|
||||
if hasattr(request, 'data'):
|
||||
@@ -150,7 +160,7 @@ class AuthMixin:
|
||||
username, password, challenge, public_key, auto_login = bulk_get(data, *items, default='')
|
||||
password = password + challenge.strip()
|
||||
ip = self.get_request_ip()
|
||||
self.partial_credential_error = partial(errors.CredentialError, username=username, ip=ip, request=request)
|
||||
self._set_partial_credential_error(username=username, ip=ip, request=request)
|
||||
|
||||
if decrypt_passwd:
|
||||
password = self.decrypt_passwd(password)
|
||||
@@ -183,6 +193,21 @@ class AuthMixin:
|
||||
if not is_allowed:
|
||||
raise errors.LoginIPNotAllowed(username=user.username, request=self.request)
|
||||
|
||||
def set_login_failed_mark(self):
|
||||
ip = self.get_request_ip()
|
||||
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
|
||||
|
||||
def set_passwd_verify_on_session(self, user: User):
|
||||
self.request.session['user_id'] = str(user.id)
|
||||
self.request.session['auth_password'] = 1
|
||||
self.request.session['auth_password_expired_at'] = time.time() + settings.AUTH_EXPIRED_SECONDS
|
||||
|
||||
def check_is_need_captcha(self):
|
||||
# 最近有登录失败时需要填写验证码
|
||||
ip = get_request_ip(self.request)
|
||||
need = cache.get(self.key_prefix_captcha.format(ip))
|
||||
return need
|
||||
|
||||
def check_user_auth(self, decrypt_passwd=False):
|
||||
self.check_is_block()
|
||||
request = self.request
|
||||
@@ -194,6 +219,7 @@ class AuthMixin:
|
||||
self._check_login_acl(user, ip)
|
||||
self._check_password_require_reset_or_not(user)
|
||||
self._check_passwd_is_too_simple(user, password)
|
||||
self._check_passwd_need_update(user)
|
||||
|
||||
LoginBlockUtil(username, ip).clean_failed_count()
|
||||
request.session['auth_password'] = 1
|
||||
@@ -202,34 +228,62 @@ class AuthMixin:
|
||||
request.session['auth_backend'] = getattr(user, 'backend', settings.AUTH_BACKEND_MODEL)
|
||||
return user
|
||||
|
||||
def _check_is_local_user(self, user: User):
|
||||
if user.source != User.Source.local:
|
||||
raise self.raise_credential_error(error=errors.only_local_users_are_allowed)
|
||||
|
||||
def check_oauth2_auth(self, user: User, auth_backend):
|
||||
ip = self.get_request_ip()
|
||||
request = self.request
|
||||
|
||||
self._set_partial_credential_error(user.username, ip, request)
|
||||
self._check_is_local_user(user)
|
||||
self._check_is_block(user.username)
|
||||
self._check_login_acl(user, ip)
|
||||
|
||||
LoginBlockUtil(user.username, ip).clean_failed_count()
|
||||
MFABlockUtils(user.username, ip).clean_failed_count()
|
||||
|
||||
request.session['auth_password'] = 1
|
||||
request.session['user_id'] = str(user.id)
|
||||
request.session['auth_backend'] = auth_backend
|
||||
return user
|
||||
|
||||
@classmethod
|
||||
def generate_reset_password_url_with_flash_msg(cls, user: User, flash_view_name):
|
||||
def generate_reset_password_url_with_flash_msg(cls, user, message):
|
||||
reset_passwd_url = reverse('authentication:reset-password')
|
||||
query_str = urlencode({
|
||||
'token': user.generate_reset_token()
|
||||
})
|
||||
reset_passwd_url = f'{reset_passwd_url}?{query_str}'
|
||||
|
||||
flash_page_url = reverse(flash_view_name)
|
||||
query_str = urlencode({
|
||||
'redirect_url': reset_passwd_url
|
||||
})
|
||||
return f'{flash_page_url}?{query_str}'
|
||||
message_data = {
|
||||
'title': _('Please change your password'),
|
||||
'message': message,
|
||||
'interval': 3,
|
||||
'redirect_url': reset_passwd_url,
|
||||
}
|
||||
return FlashMessageUtil.gen_message_url(message_data)
|
||||
|
||||
@classmethod
|
||||
def _check_passwd_is_too_simple(cls, user: User, password):
|
||||
if user.is_superuser and password == 'admin':
|
||||
url = cls.generate_reset_password_url_with_flash_msg(
|
||||
user, 'authentication:passwd-too-simple-flash-msg'
|
||||
)
|
||||
message = _('Your password is too simple, please change it for security')
|
||||
url = cls.generate_reset_password_url_with_flash_msg(user, message=message)
|
||||
raise errors.PasswdTooSimple(url)
|
||||
|
||||
@classmethod
|
||||
def _check_passwd_need_update(cls, user: User):
|
||||
if user.need_update_password:
|
||||
message = _('You should to change your password before login')
|
||||
url = cls.generate_reset_password_url_with_flash_msg(user, message)
|
||||
raise errors.PasswdNeedUpdate(url)
|
||||
|
||||
@classmethod
|
||||
def _check_password_require_reset_or_not(cls, user: User):
|
||||
if user.password_has_expired:
|
||||
url = cls.generate_reset_password_url_with_flash_msg(
|
||||
user, 'authentication:passwd-has-expired-flash-msg'
|
||||
)
|
||||
message = _('Your password has expired, please reset before logging in')
|
||||
url = cls.generate_reset_password_url_with_flash_msg(user, message)
|
||||
raise errors.PasswordRequireResetError(url)
|
||||
|
||||
def check_user_auth_if_need(self, decrypt_passwd=False):
|
||||
@@ -345,3 +399,10 @@ class AuthMixin:
|
||||
sender=self.__class__, username=username,
|
||||
request=self.request, reason=reason
|
||||
)
|
||||
|
||||
def redirect_to_guard_view(self):
|
||||
guard_url = reverse('authentication:login-guard')
|
||||
args = self.request.META.get('QUERY_STRING', '')
|
||||
if args:
|
||||
guard_url = "%s?%s" % (guard_url, args)
|
||||
return redirect(guard_url)
|
||||
|
||||
@@ -8,14 +8,16 @@ from users.models import User
|
||||
from assets.models import Asset, SystemUser, Gateway
|
||||
from applications.models import Application
|
||||
from users.serializers import UserProfileSerializer
|
||||
from assets.serializers import ProtocolsField
|
||||
from perms.serializers.asset.permission import ActionsField
|
||||
from .models import AccessKey, LoginConfirmSetting, SSOToken
|
||||
from .models import AccessKey, LoginConfirmSetting
|
||||
|
||||
|
||||
__all__ = [
|
||||
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer',
|
||||
'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', 'SSOTokenSerializer',
|
||||
'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer', 'RDPFileSerializer'
|
||||
'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer', 'RDPFileSerializer',
|
||||
'PasswordVerifySerializer',
|
||||
]
|
||||
|
||||
|
||||
@@ -30,6 +32,10 @@ class OtpVerifySerializer(serializers.Serializer):
|
||||
code = serializers.CharField(max_length=6, min_length=6)
|
||||
|
||||
|
||||
class PasswordVerifySerializer(serializers.Serializer):
|
||||
password = serializers.CharField()
|
||||
|
||||
|
||||
class BearerTokenSerializer(serializers.Serializer):
|
||||
username = serializers.CharField(allow_null=True, required=False, write_only=True)
|
||||
password = serializers.CharField(write_only=True, allow_null=True,
|
||||
@@ -150,9 +156,11 @@ class ConnectionTokenUserSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
|
||||
protocols = ProtocolsField(label='Protocols', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = ['id', 'hostname', 'ip', 'port', 'org_id']
|
||||
fields = ['id', 'hostname', 'ip', 'protocols', 'org_id']
|
||||
|
||||
|
||||
class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer):
|
||||
@@ -191,5 +199,5 @@ class ConnectionTokenSecretSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class RDPFileSerializer(ConnectionTokenSerializer):
|
||||
width = serializers.IntegerField(default=1280)
|
||||
height = serializers.IntegerField(default=800)
|
||||
width = serializers.IntegerField(allow_null=True, max_value=3112, min_value=100, required=False)
|
||||
height = serializers.IntegerField(allow_null=True, max_value=4096, min_value=100, required=False)
|
||||
|
||||
@@ -117,6 +117,15 @@
|
||||
float: right;
|
||||
margin: 10px 10px 0 0;
|
||||
}
|
||||
.more-login-item {
|
||||
border-right: 1px dashed #dedede;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.more-login-item:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
@@ -182,10 +191,10 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{% if AUTH_OPENID or AUTH_CAS %}
|
||||
{% if AUTH_OPENID or AUTH_CAS or AUTH_WECOM or AUTH_DINGTALK %}
|
||||
<div class="hr-line-dashed"></div>
|
||||
<div style="display: inline-block; float: left">
|
||||
<b class="text-muted text-left" style="margin-right: 10px">{% trans "More login options" %}</b>
|
||||
<b class="text-muted text-left" >{% trans "More login options" %}</b>
|
||||
{% if AUTH_OPENID %}
|
||||
<a href="{% url 'authentication:openid:login' %}" class="more-login-item">
|
||||
<i class="fa fa-openid"></i> {% trans 'OpenID' %}
|
||||
@@ -196,6 +205,17 @@
|
||||
<i class="fa"><img src="{{ LOGIN_CAS_LOGO_URL }}" height="13" width="13"></i> {% trans 'CAS' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if AUTH_WECOM %}
|
||||
<a href="{% url 'authentication:wecom-qr-login' %}" class="more-login-item">
|
||||
<i class="fa"><img src="{{ LOGIN_WECOM_LOGO_URL }}" height="13" width="13"></i> {% trans 'WeCom' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if AUTH_DINGTALK %}
|
||||
<a href="{% url 'authentication:dingtalk-qr-login' %}" class="more-login-item">
|
||||
<i class="fa"><img src="{{ LOGIN_DINGTALK_LOGO_URL }}" height="13" width="13"></i> {% trans 'DingTalk' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center" style="display: inline-block;">
|
||||
|
||||
@@ -14,10 +14,17 @@ router.register('connection-token', api.UserConnectionTokenViewSet, 'connection-
|
||||
|
||||
urlpatterns = [
|
||||
# path('token/', api.UserToken.as_view(), name='user-token'),
|
||||
path('wecom/qr/unbind/', api.WeComQRUnBindForUserApi.as_view(), name='wecom-qr-unbind'),
|
||||
path('wecom/qr/unbind/<uuid:user_id>/', api.WeComQRUnBindForAdminApi.as_view(), name='wecom-qr-unbind-for-admin'),
|
||||
|
||||
path('dingtalk/qr/unbind/', api.DingTalkQRUnBindForUserApi.as_view(), name='dingtalk-qr-unbind'),
|
||||
path('dingtalk/qr/unbind/<uuid:user_id>/', api.DingTalkQRUnBindForAdminApi.as_view(), name='dingtalk-qr-unbind-for-admin'),
|
||||
|
||||
path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
|
||||
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
|
||||
path('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'),
|
||||
path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'),
|
||||
path('password/verify/', api.UserPasswordVerifyApi.as_view(), name='user-password-verify'),
|
||||
path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'),
|
||||
path('login-confirm-settings/<uuid:user_id>/', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update')
|
||||
]
|
||||
|
||||
@@ -18,14 +18,25 @@ urlpatterns = [
|
||||
|
||||
# 原来在users中的
|
||||
path('password/forgot/', users_view.UserForgotPasswordView.as_view(), name='forgot-password'),
|
||||
path('password/forgot/sendmail-success/', users_view.UserForgotPasswordSendmailSuccessView.as_view(),
|
||||
name='forgot-password-sendmail-success'),
|
||||
path('password/reset/', users_view.UserResetPasswordView.as_view(), name='reset-password'),
|
||||
path('password/too-simple-flash-msg/', views.FlashPasswdTooSimpleMsgView.as_view(), name='passwd-too-simple-flash-msg'),
|
||||
path('password/has-expired-msg/', views.FlashPasswdHasExpiredMsgView.as_view(), name='passwd-has-expired-flash-msg'),
|
||||
path('password/reset/success/', users_view.UserResetPasswordSuccessView.as_view(), name='reset-password-success'),
|
||||
path('password/verify/', users_view.UserVerifyPasswordView.as_view(), name='user-verify-password'),
|
||||
|
||||
path('wecom/bind/success-flash-msg/', views.FlashWeComBindSucceedMsgView.as_view(), name='wecom-bind-success-flash-msg'),
|
||||
path('wecom/bind/failed-flash-msg/', views.FlashWeComBindFailedMsgView.as_view(), name='wecom-bind-failed-flash-msg'),
|
||||
path('wecom/bind/start/', views.WeComEnableStartView.as_view(), name='wecom-bind-start'),
|
||||
path('wecom/qr/bind/', views.WeComQRBindView.as_view(), name='wecom-qr-bind'),
|
||||
path('wecom/qr/login/', views.WeComQRLoginView.as_view(), name='wecom-qr-login'),
|
||||
path('wecom/qr/bind/<uuid:user_id>/callback/', views.WeComQRBindCallbackView.as_view(), name='wecom-qr-bind-callback'),
|
||||
path('wecom/qr/login/callback/', views.WeComQRLoginCallbackView.as_view(), name='wecom-qr-login-callback'),
|
||||
|
||||
path('dingtalk/bind/success-flash-msg/', views.FlashDingTalkBindSucceedMsgView.as_view(), name='dingtalk-bind-success-flash-msg'),
|
||||
path('dingtalk/bind/failed-flash-msg/', views.FlashDingTalkBindFailedMsgView.as_view(), name='dingtalk-bind-failed-flash-msg'),
|
||||
path('dingtalk/bind/start/', views.DingTalkEnableStartView.as_view(), name='dingtalk-bind-start'),
|
||||
path('dingtalk/qr/bind/', views.DingTalkQRBindView.as_view(), name='dingtalk-qr-bind'),
|
||||
path('dingtalk/qr/login/', views.DingTalkQRLoginView.as_view(), name='dingtalk-qr-login'),
|
||||
path('dingtalk/qr/bind/<uuid:user_id>/callback/', views.DingTalkQRBindCallbackView.as_view(), name='dingtalk-qr-bind-callback'),
|
||||
path('dingtalk/qr/login/callback/', views.DingTalkQRLoginCallbackView.as_view(), name='dingtalk-qr-login-callback'),
|
||||
|
||||
# Profile
|
||||
path('profile/pubkey/generate/', users_view.UserPublicKeyGenerateView.as_view(), name='user-pubkey-generate'),
|
||||
path('profile/otp/enable/start/', users_view.UserOtpEnableStartView.as_view(), name='user-otp-enable-start'),
|
||||
|
||||
@@ -2,3 +2,5 @@
|
||||
#
|
||||
from .login import *
|
||||
from .mfa import *
|
||||
from .wecom import *
|
||||
from .dingtalk import *
|
||||
|
||||
266
apps/authentication/views/dingtalk.py
Normal file
266
apps/authentication/views/dingtalk.py
Normal file
@@ -0,0 +1,266 @@
|
||||
import urllib
|
||||
|
||||
from django.http.response import HttpResponseRedirect
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.generic import TemplateView
|
||||
from django.views import View
|
||||
from django.conf import settings
|
||||
from django.http.request import HttpRequest
|
||||
from django.db.utils import IntegrityError
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
from users.views import UserVerifyPasswordView
|
||||
from users.utils import is_auth_password_time_valid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.utils.random import random_string
|
||||
from common.utils.django import reverse, get_object_or_none
|
||||
from common.message.backends.dingtalk import URL
|
||||
from common.mixins.views import PermissionsMixin
|
||||
from authentication import errors
|
||||
from authentication.mixins import AuthMixin
|
||||
from common.message.backends.dingtalk import DingTalk
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
DINGTALK_STATE_SESSION_KEY = '_dingtalk_state'
|
||||
|
||||
|
||||
class DingTalkQRMixin(PermissionsMixin, View):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
except APIException as e:
|
||||
try:
|
||||
msg = e.detail['errmsg']
|
||||
except Exception:
|
||||
msg = _('DingTalk Error, Please contact your system administrator')
|
||||
return self.get_failed_reponse(
|
||||
'/',
|
||||
_('DingTalk Error'),
|
||||
msg
|
||||
)
|
||||
|
||||
def verify_state(self):
|
||||
state = self.request.GET.get('state')
|
||||
session_state = self.request.session.get(DINGTALK_STATE_SESSION_KEY)
|
||||
if state != session_state:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_verify_state_failed_response(self, redirect_uri):
|
||||
msg = _("You've been hacked")
|
||||
return self.get_failed_reponse(redirect_uri, msg, msg)
|
||||
|
||||
def get_qr_url(self, redirect_uri):
|
||||
state = random_string(16)
|
||||
self.request.session[DINGTALK_STATE_SESSION_KEY] = state
|
||||
|
||||
params = {
|
||||
'appid': settings.DINGTALK_APPKEY,
|
||||
'response_type': 'code',
|
||||
'scope': 'snsapi_login',
|
||||
'state': state,
|
||||
'redirect_uri': redirect_uri,
|
||||
}
|
||||
url = URL.QR_CONNECT + '?' + urllib.parse.urlencode(params)
|
||||
return url
|
||||
|
||||
def get_success_reponse(self, redirect_url, title, msg):
|
||||
ok_flash_msg_url = reverse('authentication:dingtalk-bind-success-flash-msg')
|
||||
ok_flash_msg_url += '?' + urllib.parse.urlencode({
|
||||
'redirect_url': redirect_url,
|
||||
'title': title,
|
||||
'msg': msg
|
||||
})
|
||||
return HttpResponseRedirect(ok_flash_msg_url)
|
||||
|
||||
def get_failed_reponse(self, redirect_url, title, msg):
|
||||
failed_flash_msg_url = reverse('authentication:dingtalk-bind-failed-flash-msg')
|
||||
failed_flash_msg_url += '?' + urllib.parse.urlencode({
|
||||
'redirect_url': redirect_url,
|
||||
'title': title,
|
||||
'msg': msg
|
||||
})
|
||||
return HttpResponseRedirect(failed_flash_msg_url)
|
||||
|
||||
def get_already_bound_response(self, redirect_url):
|
||||
msg = _('DingTalk is already bound')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
|
||||
class DingTalkQRBindView(DingTalkQRMixin, View):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
user = request.user
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
if not is_auth_password_time_valid(request.session):
|
||||
msg = _('Please verify your password first')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
redirect_uri = reverse('authentication:dingtalk-qr-bind-callback', kwargs={'user_id': user.id}, external=True)
|
||||
redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url})
|
||||
|
||||
url = self.get_qr_url(redirect_uri)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class DingTalkQRBindCallbackView(DingTalkQRMixin, View):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def get(self, request: HttpRequest, user_id):
|
||||
code = request.GET.get('code')
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
if not self.verify_state():
|
||||
return self.get_verify_state_failed_response(redirect_url)
|
||||
|
||||
user = get_object_or_none(User, id=user_id)
|
||||
if user is None:
|
||||
logger.error(f'DingTalkQR bind callback error, user_id invalid: user_id={user_id}')
|
||||
msg = _('Invalid user_id')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
if user.dingtalk_id:
|
||||
response = self.get_already_bound_response(redirect_url)
|
||||
return response
|
||||
|
||||
dingtalk = DingTalk(
|
||||
appid=settings.DINGTALK_APPKEY,
|
||||
appsecret=settings.DINGTALK_APPSECRET,
|
||||
agentid=settings.DINGTALK_AGENTID
|
||||
)
|
||||
userid = dingtalk.get_userid_by_code(code)
|
||||
|
||||
if not userid:
|
||||
msg = _('DingTalk query user failed')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
try:
|
||||
user.dingtalk_id = userid
|
||||
user.save()
|
||||
except IntegrityError as e:
|
||||
if e.args[0] == 1062:
|
||||
msg = _('The DingTalk is already bound to another user')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
return response
|
||||
raise e
|
||||
|
||||
msg = _('Binding DingTalk successfully')
|
||||
response = self.get_success_reponse(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
|
||||
class DingTalkEnableStartView(UserVerifyPasswordView):
|
||||
|
||||
def get_success_url(self):
|
||||
referer = self.request.META.get('HTTP_REFERER')
|
||||
redirect_url = self.request.GET.get("redirect_url")
|
||||
|
||||
success_url = reverse('authentication:dingtalk-qr-bind')
|
||||
|
||||
success_url += '?' + urllib.parse.urlencode({
|
||||
'redirect_url': redirect_url or referer
|
||||
})
|
||||
|
||||
return success_url
|
||||
|
||||
|
||||
class DingTalkQRLoginView(DingTalkQRMixin, View):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
redirect_uri = reverse('authentication:dingtalk-qr-login-callback', external=True)
|
||||
redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url})
|
||||
|
||||
url = self.get_qr_url(redirect_uri)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class DingTalkQRLoginCallbackView(AuthMixin, DingTalkQRMixin, View):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
code = request.GET.get('code')
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
login_url = reverse('authentication:login')
|
||||
|
||||
if not self.verify_state():
|
||||
return self.get_verify_state_failed_response(redirect_url)
|
||||
|
||||
dingtalk = DingTalk(
|
||||
appid=settings.DINGTALK_APPKEY,
|
||||
appsecret=settings.DINGTALK_APPSECRET,
|
||||
agentid=settings.DINGTALK_AGENTID
|
||||
)
|
||||
userid = dingtalk.get_userid_by_code(code)
|
||||
if not userid:
|
||||
# 正常流程不会出这个错误,hack 行为
|
||||
msg = _('Failed to get user from DingTalk')
|
||||
response = self.get_failed_reponse(login_url, title=msg, msg=msg)
|
||||
return response
|
||||
|
||||
user = get_object_or_none(User, dingtalk_id=userid)
|
||||
if user is None:
|
||||
title = _('DingTalk is not bound')
|
||||
msg = _('Please login with a password and then bind the WeCom')
|
||||
response = self.get_failed_reponse(login_url, title=title, msg=msg)
|
||||
return response
|
||||
|
||||
try:
|
||||
self.check_oauth2_auth(user, settings.AUTH_BACKEND_DINGTALK)
|
||||
except errors.AuthFailedError as e:
|
||||
self.set_login_failed_mark()
|
||||
msg = e.msg
|
||||
response = self.get_failed_reponse(login_url, title=msg, msg=msg)
|
||||
return response
|
||||
|
||||
return self.redirect_to_guard_view()
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class FlashDingTalkBindSucceedMsgView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
title = request.GET.get('title')
|
||||
msg = request.GET.get('msg')
|
||||
|
||||
context = {
|
||||
'title': title or _('Binding DingTalk successfully'),
|
||||
'messages': msg or _('Binding DingTalk successfully'),
|
||||
'interval': 5,
|
||||
'redirect_url': request.GET.get('redirect_url'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class FlashDingTalkBindFailedMsgView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
title = request.GET.get('title')
|
||||
msg = request.GET.get('msg')
|
||||
|
||||
context = {
|
||||
'title': title or _('Binding DingTalk failed'),
|
||||
'messages': msg or _('Binding DingTalk failed'),
|
||||
'interval': 5,
|
||||
'redirect_url': request.GET.get('redirect_url'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
@@ -4,7 +4,6 @@
|
||||
from __future__ import unicode_literals
|
||||
import os
|
||||
import datetime
|
||||
from django.core.cache import cache
|
||||
from django.contrib.auth import login as auth_login, logout as auth_logout
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import reverse, redirect
|
||||
@@ -19,7 +18,7 @@ from django.conf import settings
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib.auth import BACKEND_SESSION_KEY
|
||||
|
||||
from common.utils import get_request_ip, get_object_or_none
|
||||
from common.utils import get_request_ip, FlashMessageUtil
|
||||
from users.utils import (
|
||||
redirect_user_first_login_or_index
|
||||
)
|
||||
@@ -31,7 +30,6 @@ from ..forms import get_user_login_form_cls
|
||||
__all__ = [
|
||||
'UserLoginView', 'UserLogoutView',
|
||||
'UserLoginGuardView', 'UserLoginWaitConfirmView',
|
||||
'FlashPasswdTooSimpleMsgView', 'FlashPasswdHasExpiredMsgView'
|
||||
]
|
||||
|
||||
|
||||
@@ -39,16 +37,45 @@ __all__ = [
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class UserLoginView(mixins.AuthMixin, FormView):
|
||||
key_prefix_captcha = "_LOGIN_INVALID_{}"
|
||||
redirect_field_name = 'next'
|
||||
template_name = 'authentication/login.html'
|
||||
|
||||
def redirect_third_party_auth_if_need(self, request):
|
||||
# show jumpserver login page if request http://{JUMP-SERVER}/?admin=1
|
||||
if self.request.GET.get("admin", 0):
|
||||
return None
|
||||
auth_type = ''
|
||||
auth_url = ''
|
||||
if settings.AUTH_OPENID:
|
||||
auth_type = 'OIDC'
|
||||
auth_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME)
|
||||
elif settings.AUTH_CAS:
|
||||
auth_type = 'CAS'
|
||||
auth_url = reverse(settings.CAS_LOGIN_URL_NAME)
|
||||
if not auth_url:
|
||||
return None
|
||||
|
||||
message_data = {
|
||||
'title': _('Redirecting'),
|
||||
'message': _("Redirecting to {} authentication").format(auth_type),
|
||||
'redirect_url': auth_url,
|
||||
'has_cancel': True,
|
||||
'cancel_url': reverse('authentication:login') + '?admin=1'
|
||||
}
|
||||
redirect_url = FlashMessageUtil.gen_message_url(message_data)
|
||||
query_string = request.GET.urlencode()
|
||||
redirect_url = "{}&{}".format(redirect_url, query_string)
|
||||
return redirect_url
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if request.user.is_staff:
|
||||
first_login_url = redirect_user_first_login_or_index(
|
||||
request, self.redirect_field_name
|
||||
)
|
||||
return redirect(first_login_url)
|
||||
redirect_url = self.redirect_third_party_auth_if_need(request)
|
||||
if redirect_url:
|
||||
return redirect(redirect_url)
|
||||
request.session.set_test_cookie()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
@@ -61,31 +88,22 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||
try:
|
||||
self.check_user_auth(decrypt_passwd=True)
|
||||
except errors.AuthFailedError as e:
|
||||
e = self.check_is_block(raise_exception=False) or e
|
||||
form.add_error(None, e.msg)
|
||||
ip = self.get_request_ip()
|
||||
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
|
||||
self.set_login_failed_mark()
|
||||
|
||||
form_cls = get_user_login_form_cls(captcha=True)
|
||||
new_form = form_cls(data=form.data)
|
||||
new_form._errors = form.errors
|
||||
context = self.get_context_data(form=new_form)
|
||||
self.request.session.set_test_cookie()
|
||||
return self.render_to_response(context)
|
||||
except (errors.PasswdTooSimple, errors.PasswordRequireResetError) as e:
|
||||
except (errors.PasswdTooSimple, errors.PasswordRequireResetError, errors.PasswdNeedUpdate) as e:
|
||||
return redirect(e.url)
|
||||
self.clear_rsa_key()
|
||||
return self.redirect_to_guard_view()
|
||||
|
||||
def redirect_to_guard_view(self):
|
||||
guard_url = reverse('authentication:login-guard')
|
||||
args = self.request.META.get('QUERY_STRING', '')
|
||||
if args:
|
||||
guard_url = "%s?%s" % (guard_url, args)
|
||||
return redirect(guard_url)
|
||||
|
||||
def get_form_class(self):
|
||||
ip = get_request_ip(self.request)
|
||||
if cache.get(self.key_prefix_captcha.format(ip)):
|
||||
if self.check_is_need_captcha():
|
||||
return get_user_login_form_cls(captcha=True)
|
||||
else:
|
||||
return get_user_login_form_cls()
|
||||
@@ -113,6 +131,8 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||
'demo_mode': os.environ.get("DEMO_MODE"),
|
||||
'AUTH_OPENID': settings.AUTH_OPENID,
|
||||
'AUTH_CAS': settings.AUTH_CAS,
|
||||
'AUTH_WECOM': settings.AUTH_WECOM,
|
||||
'AUTH_DINGTALK': settings.AUTH_DINGTALK,
|
||||
'rsa_public_key': rsa_public_key,
|
||||
'forgot_password_url': forgot_password_url
|
||||
}
|
||||
@@ -218,39 +238,9 @@ class UserLogoutView(TemplateView):
|
||||
context = {
|
||||
'title': _('Logout success'),
|
||||
'messages': _('Logout success, return login page'),
|
||||
'interval': 1,
|
||||
'interval': 3,
|
||||
'redirect_url': reverse('authentication:login'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class FlashPasswdTooSimpleMsgView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = {
|
||||
'title': _('Please change your password'),
|
||||
'messages': _('Your password is too simple, please change it for security'),
|
||||
'interval': 5,
|
||||
'redirect_url': request.GET.get('redirect_url'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class FlashPasswdHasExpiredMsgView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = {
|
||||
'title': _('Please change your password'),
|
||||
'messages': _('Your password has expired, please reset before logging in'),
|
||||
'interval': 5,
|
||||
'redirect_url': request.GET.get('redirect_url'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
|
||||
264
apps/authentication/views/wecom.py
Normal file
264
apps/authentication/views/wecom.py
Normal file
@@ -0,0 +1,264 @@
|
||||
import urllib
|
||||
|
||||
from django.http.response import HttpResponseRedirect
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.generic import TemplateView
|
||||
from django.views import View
|
||||
from django.conf import settings
|
||||
from django.http.request import HttpRequest
|
||||
from django.db.utils import IntegrityError
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
from users.views import UserVerifyPasswordView
|
||||
from users.utils import is_auth_password_time_valid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.utils.random import random_string
|
||||
from common.utils.django import reverse, get_object_or_none
|
||||
from common.message.backends.wecom import URL
|
||||
from common.message.backends.wecom import WeCom
|
||||
from common.mixins.views import PermissionsMixin
|
||||
from authentication import errors
|
||||
from authentication.mixins import AuthMixin
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
WECOM_STATE_SESSION_KEY = '_wecom_state'
|
||||
|
||||
|
||||
class WeComQRMixin(PermissionsMixin, View):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
except APIException as e:
|
||||
try:
|
||||
msg = e.detail['errmsg']
|
||||
except Exception:
|
||||
msg = _('WeCom Error, Please contact your system administrator')
|
||||
return self.get_failed_reponse(
|
||||
'/',
|
||||
_('WeCom Error'),
|
||||
msg
|
||||
)
|
||||
|
||||
def verify_state(self):
|
||||
state = self.request.GET.get('state')
|
||||
session_state = self.request.session.get(WECOM_STATE_SESSION_KEY)
|
||||
if state != session_state:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_verify_state_failed_response(self, redirect_uri):
|
||||
msg = _("You've been hacked")
|
||||
return self.get_failed_reponse(redirect_uri, msg, msg)
|
||||
|
||||
def get_qr_url(self, redirect_uri):
|
||||
state = random_string(16)
|
||||
self.request.session[WECOM_STATE_SESSION_KEY] = state
|
||||
|
||||
params = {
|
||||
'appid': settings.WECOM_CORPID,
|
||||
'agentid': settings.WECOM_AGENTID,
|
||||
'state': state,
|
||||
'redirect_uri': redirect_uri,
|
||||
}
|
||||
url = URL.QR_CONNECT + '?' + urllib.parse.urlencode(params)
|
||||
return url
|
||||
|
||||
def get_success_reponse(self, redirect_url, title, msg):
|
||||
ok_flash_msg_url = reverse('authentication:wecom-bind-success-flash-msg')
|
||||
ok_flash_msg_url += '?' + urllib.parse.urlencode({
|
||||
'redirect_url': redirect_url,
|
||||
'title': title,
|
||||
'msg': msg
|
||||
})
|
||||
return HttpResponseRedirect(ok_flash_msg_url)
|
||||
|
||||
def get_failed_reponse(self, redirect_url, title, msg):
|
||||
failed_flash_msg_url = reverse('authentication:wecom-bind-failed-flash-msg')
|
||||
failed_flash_msg_url += '?' + urllib.parse.urlencode({
|
||||
'redirect_url': redirect_url,
|
||||
'title': title,
|
||||
'msg': msg
|
||||
})
|
||||
return HttpResponseRedirect(failed_flash_msg_url)
|
||||
|
||||
def get_already_bound_response(self, redirect_url):
|
||||
msg = _('WeCom is already bound')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
|
||||
class WeComQRBindView(WeComQRMixin, View):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
user = request.user
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
if not is_auth_password_time_valid(request.session):
|
||||
msg = _('Please verify your password first')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
redirect_uri = reverse('authentication:wecom-qr-bind-callback', kwargs={'user_id': user.id}, external=True)
|
||||
redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url})
|
||||
|
||||
url = self.get_qr_url(redirect_uri)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class WeComQRBindCallbackView(WeComQRMixin, View):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def get(self, request: HttpRequest, user_id):
|
||||
code = request.GET.get('code')
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
if not self.verify_state():
|
||||
return self.get_verify_state_failed_response(redirect_url)
|
||||
|
||||
user = get_object_or_none(User, id=user_id)
|
||||
if user is None:
|
||||
logger.error(f'WeComQR bind callback error, user_id invalid: user_id={user_id}')
|
||||
msg = _('Invalid user_id')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
if user.wecom_id:
|
||||
response = self.get_already_bound_response(redirect_url)
|
||||
return response
|
||||
|
||||
wecom = WeCom(
|
||||
corpid=settings.WECOM_CORPID,
|
||||
corpsecret=settings.WECOM_SECRET,
|
||||
agentid=settings.WECOM_AGENTID
|
||||
)
|
||||
wecom_userid, __ = wecom.get_user_id_by_code(code)
|
||||
if not wecom_userid:
|
||||
msg = _('WeCom query user failed')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
try:
|
||||
user.wecom_id = wecom_userid
|
||||
user.save()
|
||||
except IntegrityError as e:
|
||||
if e.args[0] == 1062:
|
||||
msg = _('The WeCom is already bound to another user')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
return response
|
||||
raise e
|
||||
|
||||
msg = _('Binding WeCom successfully')
|
||||
response = self.get_success_reponse(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
|
||||
class WeComEnableStartView(UserVerifyPasswordView):
|
||||
|
||||
def get_success_url(self):
|
||||
referer = self.request.META.get('HTTP_REFERER')
|
||||
redirect_url = self.request.GET.get("redirect_url")
|
||||
|
||||
success_url = reverse('authentication:wecom-qr-bind')
|
||||
|
||||
success_url += '?' + urllib.parse.urlencode({
|
||||
'redirect_url': redirect_url or referer
|
||||
})
|
||||
|
||||
return success_url
|
||||
|
||||
|
||||
class WeComQRLoginView(WeComQRMixin, View):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
redirect_uri = reverse('authentication:wecom-qr-login-callback', external=True)
|
||||
redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url})
|
||||
|
||||
url = self.get_qr_url(redirect_uri)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class WeComQRLoginCallbackView(AuthMixin, WeComQRMixin, View):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
code = request.GET.get('code')
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
login_url = reverse('authentication:login')
|
||||
|
||||
if not self.verify_state():
|
||||
return self.get_verify_state_failed_response(redirect_url)
|
||||
|
||||
wecom = WeCom(
|
||||
corpid=settings.WECOM_CORPID,
|
||||
corpsecret=settings.WECOM_SECRET,
|
||||
agentid=settings.WECOM_AGENTID
|
||||
)
|
||||
wecom_userid, __ = wecom.get_user_id_by_code(code)
|
||||
if not wecom_userid:
|
||||
# 正常流程不会出这个错误,hack 行为
|
||||
msg = _('Failed to get user from WeCom')
|
||||
response = self.get_failed_reponse(login_url, title=msg, msg=msg)
|
||||
return response
|
||||
|
||||
user = get_object_or_none(User, wecom_id=wecom_userid)
|
||||
if user is None:
|
||||
title = _('WeCom is not bound')
|
||||
msg = _('Please login with a password and then bind the WeCom')
|
||||
response = self.get_failed_reponse(login_url, title=title, msg=msg)
|
||||
return response
|
||||
|
||||
try:
|
||||
self.check_oauth2_auth(user, settings.AUTH_BACKEND_WECOM)
|
||||
except errors.AuthFailedError as e:
|
||||
self.set_login_failed_mark()
|
||||
msg = e.msg
|
||||
response = self.get_failed_reponse(login_url, title=msg, msg=msg)
|
||||
return response
|
||||
|
||||
return self.redirect_to_guard_view()
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class FlashWeComBindSucceedMsgView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
title = request.GET.get('title')
|
||||
msg = request.GET.get('msg')
|
||||
|
||||
context = {
|
||||
'title': title or _('Binding WeCom successfully'),
|
||||
'messages': msg or _('Binding WeCom successfully'),
|
||||
'interval': 5,
|
||||
'redirect_url': request.GET.get('redirect_url'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class FlashWeComBindFailedMsgView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
title = request.GET.get('title')
|
||||
msg = request.GET.get('msg')
|
||||
|
||||
context = {
|
||||
'title': title or _('Binding WeCom failed'),
|
||||
'messages': msg or _('Binding WeCom failed'),
|
||||
'interval': 5,
|
||||
'redirect_url': request.GET.get('redirect_url'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
@@ -18,7 +18,7 @@ from rest_framework.request import clone_request
|
||||
class SimpleMetadataWithFilters(SimpleMetadata):
|
||||
"""Override SimpleMetadata, adding info about filters"""
|
||||
|
||||
methods = {"PUT", "POST", "GET"}
|
||||
methods = {"PUT", "POST", "GET", "PATCH"}
|
||||
attrs = [
|
||||
'read_only', 'label', 'help_text',
|
||||
'min_length', 'max_length',
|
||||
@@ -32,6 +32,9 @@ class SimpleMetadataWithFilters(SimpleMetadata):
|
||||
"""
|
||||
actions = {}
|
||||
for method in self.methods & set(view.allowed_methods):
|
||||
if hasattr(view, 'action_map'):
|
||||
view.action = view.action_map.get(method.lower(), view.action)
|
||||
|
||||
view.request = clone_request(request, method)
|
||||
try:
|
||||
# Test global permissions
|
||||
|
||||
@@ -47,8 +47,13 @@ class BaseFileParser(BaseParser):
|
||||
def convert_to_field_names(self, column_titles):
|
||||
fields_map = {}
|
||||
fields = self.serializer_fields
|
||||
fields_map.update({v.label: k for k, v in fields.items()})
|
||||
fields_map.update({k: k for k, _ in fields.items()})
|
||||
for k, v in fields.items():
|
||||
if v.read_only:
|
||||
continue
|
||||
fields_map.update({
|
||||
v.label: k,
|
||||
k: k
|
||||
})
|
||||
field_names = [
|
||||
fields_map.get(column_title.strip('*'), '')
|
||||
for column_title in column_titles
|
||||
@@ -57,6 +62,8 @@ class BaseFileParser(BaseParser):
|
||||
|
||||
@staticmethod
|
||||
def _replace_chinese_quote(s):
|
||||
if not isinstance(s, str):
|
||||
return s
|
||||
trans_table = str.maketrans({
|
||||
'“': '"',
|
||||
'”': '"',
|
||||
@@ -92,7 +99,7 @@ class BaseFileParser(BaseParser):
|
||||
new_row_data = {}
|
||||
serializer_fields = self.serializer_fields
|
||||
for k, v in row_data.items():
|
||||
if isinstance(v, list) or isinstance(v, dict) or isinstance(v, str) and k.strip() and v.strip():
|
||||
if type(v) in [list, dict, int] or (isinstance(v, str) and k.strip() and v.strip()):
|
||||
# 解决类似disk_info为字符串的'{}'的问题
|
||||
if not isinstance(v, str) and isinstance(serializer_fields[k], serializers.CharField):
|
||||
v = str(v)
|
||||
|
||||
@@ -39,3 +39,9 @@ class ReferencedByOthers(JMSException):
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
default_code = 'referenced_by_others'
|
||||
default_detail = _('Is referenced by other objects and cannot be deleted')
|
||||
|
||||
|
||||
class MFAVerifyRequired(JMSException):
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
default_code = 'mfa_verify_required'
|
||||
default_detail = _('This action require verify your MFA')
|
||||
|
||||
19
apps/common/management/commands/expire_caches.py
Normal file
19
apps/common/management/commands/expire_caches.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from assets.signals_handler.node_assets_mapping import expire_node_assets_mapping_for_memory
|
||||
from orgs.models import Organization
|
||||
|
||||
|
||||
def expire_node_assets_mapping():
|
||||
org_ids = Organization.objects.all().values_list('id', flat=True)
|
||||
org_ids = [*org_ids, '00000000-0000-0000-0000-000000000000']
|
||||
|
||||
for org_id in org_ids:
|
||||
expire_node_assets_mapping_for_memory(org_id)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Expire caches'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
expire_node_assets_mapping()
|
||||
0
apps/common/message/__init__.py
Normal file
0
apps/common/message/__init__.py
Normal file
0
apps/common/message/backends/__init__.py
Normal file
0
apps/common/message/backends/__init__.py
Normal file
168
apps/common/message/backends/dingtalk/__init__.py
Normal file
168
apps/common/message/backends/dingtalk/__init__.py
Normal file
@@ -0,0 +1,168 @@
|
||||
import time
|
||||
import hmac
|
||||
import base64
|
||||
|
||||
from common.message.backends.utils import request
|
||||
from common.message.backends.utils import digest
|
||||
from common.message.backends.mixin import BaseRequest
|
||||
|
||||
|
||||
def sign(secret, data):
|
||||
|
||||
digest = hmac.HMAC(
|
||||
key=secret.encode('utf8'),
|
||||
msg=data.encode('utf8'),
|
||||
digestmod=hmac._hashlib.sha256).digest()
|
||||
signature = base64.standard_b64encode(digest).decode('utf8')
|
||||
# signature = urllib.parse.quote(signature, safe='')
|
||||
# signature = signature.replace('+', '%20').replace('*', '%2A').replace('~', '%7E').replace('/', '%2F')
|
||||
return signature
|
||||
|
||||
|
||||
class ErrorCode:
|
||||
INVALID_TOKEN = 88
|
||||
|
||||
|
||||
class URL:
|
||||
QR_CONNECT = 'https://oapi.dingtalk.com/connect/qrconnect'
|
||||
GET_USER_INFO_BY_CODE = 'https://oapi.dingtalk.com/sns/getuserinfo_bycode'
|
||||
GET_TOKEN = 'https://oapi.dingtalk.com/gettoken'
|
||||
SEND_MESSAGE_BY_TEMPLATE = 'https://oapi.dingtalk.com/topapi/message/corpconversation/sendbytemplate'
|
||||
SEND_MESSAGE = 'https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2'
|
||||
GET_SEND_MSG_PROGRESS = 'https://oapi.dingtalk.com/topapi/message/corpconversation/getsendprogress'
|
||||
GET_USERID_BY_UNIONID = 'https://oapi.dingtalk.com/topapi/user/getbyunionid'
|
||||
|
||||
|
||||
class DingTalkRequests(BaseRequest):
|
||||
invalid_token_errcode = ErrorCode.INVALID_TOKEN
|
||||
|
||||
def __init__(self, appid, appsecret, agentid, timeout=None):
|
||||
self._appid = appid
|
||||
self._appsecret = appsecret
|
||||
self._agentid = agentid
|
||||
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
def get_access_token_cache_key(self):
|
||||
return digest(self._appid, self._appsecret)
|
||||
|
||||
def request_access_token(self):
|
||||
# https://developers.dingtalk.com/document/app/obtain-orgapp-token?spm=ding_open_doc.document.0.0.3a256573JEWqIL#topic-1936350
|
||||
params = {'appkey': self._appid, 'appsecret': self._appsecret}
|
||||
data = self.raw_request('get', url=URL.GET_TOKEN, params=params)
|
||||
|
||||
access_token = data['access_token']
|
||||
expires_in = data['expires_in']
|
||||
return access_token, expires_in
|
||||
|
||||
@request
|
||||
def get(self, url, params=None,
|
||||
with_token=False, with_sign=False,
|
||||
check_errcode_is_0=True,
|
||||
**kwargs):
|
||||
pass
|
||||
|
||||
@request
|
||||
def post(self, url, json=None, params=None,
|
||||
with_token=False, with_sign=False,
|
||||
check_errcode_is_0=True,
|
||||
**kwargs):
|
||||
pass
|
||||
|
||||
def _add_sign(self, params: dict):
|
||||
timestamp = str(int(time.time() * 1000))
|
||||
signature = sign(self._appsecret, timestamp)
|
||||
accessKey = self._appid
|
||||
|
||||
params['timestamp'] = timestamp
|
||||
params['signature'] = signature
|
||||
params['accessKey'] = accessKey
|
||||
|
||||
def request(self, method, url, params=None,
|
||||
with_token=False, with_sign=False,
|
||||
check_errcode_is_0=True,
|
||||
**kwargs):
|
||||
if not isinstance(params, dict):
|
||||
params = {}
|
||||
|
||||
if with_token:
|
||||
params['access_token'] = self.access_token
|
||||
|
||||
if with_sign:
|
||||
self._add_sign(params)
|
||||
|
||||
data = self.raw_request(method, url, params=params, **kwargs)
|
||||
if check_errcode_is_0:
|
||||
self.check_errcode_is_0(data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class DingTalk:
|
||||
def __init__(self, appid, appsecret, agentid, timeout=None):
|
||||
self._appid = appid
|
||||
self._appsecret = appsecret
|
||||
self._agentid = agentid
|
||||
|
||||
self._request = DingTalkRequests(
|
||||
appid=appid, appsecret=appsecret, agentid=agentid,
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
def get_userinfo_bycode(self, code):
|
||||
# https://developers.dingtalk.com/document/app/obtain-the-user-information-based-on-the-sns-temporary-authorization?spm=ding_open_doc.document.0.0.3a256573y8Y7yg#topic-1995619
|
||||
body = {
|
||||
"tmp_auth_code": code
|
||||
}
|
||||
|
||||
data = self._request.post(URL.GET_USER_INFO_BY_CODE, json=body, with_sign=True)
|
||||
return data['user_info']
|
||||
|
||||
def get_userid_by_code(self, code):
|
||||
user_info = self.get_userinfo_bycode(code)
|
||||
unionid = user_info['unionid']
|
||||
userid = self.get_userid_by_unionid(unionid)
|
||||
return userid
|
||||
|
||||
def get_userid_by_unionid(self, unionid):
|
||||
body = {
|
||||
'unionid': unionid
|
||||
}
|
||||
data = self._request.post(URL.GET_USERID_BY_UNIONID, json=body, with_token=True)
|
||||
userid = data['result']['userid']
|
||||
return userid
|
||||
|
||||
def send_by_template(self, template_id, user_ids, dept_ids, data):
|
||||
body = {
|
||||
'agent_id': self._agentid,
|
||||
'template_id': template_id,
|
||||
'userid_list': ','.join(user_ids),
|
||||
'dept_id_list': ','.join(dept_ids),
|
||||
'data': data
|
||||
}
|
||||
data = self._request.post(URL.SEND_MESSAGE_BY_TEMPLATE, json=body, with_token=True)
|
||||
|
||||
def send_text(self, user_ids, msg):
|
||||
body = {
|
||||
'agent_id': self._agentid,
|
||||
'userid_list': ','.join(user_ids),
|
||||
# 'dept_id_list': '',
|
||||
'to_all_user': False,
|
||||
'msg': {
|
||||
'msgtype': 'text',
|
||||
'text': {
|
||||
'content': msg
|
||||
}
|
||||
}
|
||||
}
|
||||
data = self._request.post(URL.SEND_MESSAGE, json=body, with_token=True)
|
||||
return data
|
||||
|
||||
def get_send_msg_progress(self, task_id):
|
||||
body = {
|
||||
'agent_id': self._agentid,
|
||||
'task_id': task_id
|
||||
}
|
||||
|
||||
data = self._request.post(URL.GET_SEND_MSG_PROGRESS, json=body, with_token=True)
|
||||
return data
|
||||
23
apps/common/message/backends/exceptions.py
Normal file
23
apps/common/message/backends/exceptions.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
|
||||
class HTTPNot200(APIException):
|
||||
default_code = 'http_not_200'
|
||||
default_detail = 'HTTP status is not 200'
|
||||
|
||||
|
||||
class ErrCodeNot0(APIException):
|
||||
default_code = 'errcode_not_0'
|
||||
default_detail = 'Error code is not 0'
|
||||
|
||||
|
||||
class ResponseDataKeyError(APIException):
|
||||
default_code = 'response_data_key_error'
|
||||
default_detail = 'Response data key error'
|
||||
|
||||
|
||||
class NetError(APIException):
|
||||
default_code = 'net_error'
|
||||
default_detail = _('Network error, please contact system administrator')
|
||||
94
apps/common/message/backends/mixin.py
Normal file
94
apps/common/message/backends/mixin.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import requests
|
||||
from requests import exceptions as req_exce
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from django.core.cache import cache
|
||||
|
||||
from .utils import DictWrapper
|
||||
from common.utils.common import get_logger
|
||||
from common.utils import lazyproperty
|
||||
from common.message.backends.utils import set_default
|
||||
|
||||
from . import exceptions as exce
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class RequestMixin:
|
||||
def check_errcode_is_0(self, data: DictWrapper):
|
||||
errcode = data['errcode']
|
||||
if errcode != 0:
|
||||
# 如果代码写的对,配置没问题,这里不该出错,系统性错误,直接抛异常
|
||||
errmsg = data['errmsg']
|
||||
logger.error(f'Response 200 but errcode is not 0: '
|
||||
f'errcode={errcode} '
|
||||
f'errmsg={errmsg} ')
|
||||
raise exce.ErrCodeNot0(detail=data.raw_data)
|
||||
|
||||
def check_http_is_200(self, response):
|
||||
if response.status_code != 200:
|
||||
# 正常情况下不会返回非 200 响应码
|
||||
logger.error(f'Response error: '
|
||||
f'status_code={response.status_code} '
|
||||
f'url={response.url}'
|
||||
f'\ncontent={response.content}')
|
||||
raise exce.HTTPNot200(detail=response.json())
|
||||
|
||||
|
||||
class BaseRequest(RequestMixin):
|
||||
invalid_token_errcode = -1
|
||||
|
||||
def __init__(self, timeout=None):
|
||||
self._request_kwargs = {
|
||||
'timeout': timeout
|
||||
}
|
||||
self.init_access_token()
|
||||
|
||||
def request_access_token(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_access_token_cache_key(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def is_token_invalid(self, data):
|
||||
errcode = data['errcode']
|
||||
if errcode == self.invalid_token_errcode:
|
||||
return True
|
||||
return False
|
||||
|
||||
@lazyproperty
|
||||
def access_token_cache_key(self):
|
||||
return self.get_access_token_cache_key()
|
||||
|
||||
def init_access_token(self):
|
||||
access_token = cache.get(self.access_token_cache_key)
|
||||
if access_token:
|
||||
self.access_token = access_token
|
||||
return
|
||||
self.refresh_access_token()
|
||||
|
||||
def refresh_access_token(self):
|
||||
access_token, expires_in = self.request_access_token()
|
||||
self.access_token = access_token
|
||||
cache.set(self.access_token_cache_key, access_token, expires_in)
|
||||
|
||||
def raw_request(self, method, url, **kwargs):
|
||||
set_default(kwargs, self._request_kwargs)
|
||||
raw_data = ''
|
||||
for i in range(3):
|
||||
# 循环为了防止 access_token 失效
|
||||
try:
|
||||
response = getattr(requests, method)(url, **kwargs)
|
||||
self.check_http_is_200(response)
|
||||
raw_data = response.json()
|
||||
data = DictWrapper(raw_data)
|
||||
|
||||
if self.is_token_invalid(data):
|
||||
self.refresh_access_token()
|
||||
continue
|
||||
|
||||
return data
|
||||
except req_exce.ReadTimeout as e:
|
||||
logger.exception(e)
|
||||
raise exce.NetError
|
||||
logger.error(f'Get access_token error, check config: url={url} data={raw_data}')
|
||||
raise PermissionDenied(raw_data)
|
||||
78
apps/common/message/backends/utils.py
Normal file
78
apps/common/message/backends/utils.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import hashlib
|
||||
import inspect
|
||||
from inspect import Parameter
|
||||
|
||||
from common.utils.common import get_logger
|
||||
from common.message.backends import exceptions as exce
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def digest(corpid, corpsecret):
|
||||
md5 = hashlib.md5()
|
||||
md5.update(corpid.encode())
|
||||
md5.update(corpsecret.encode())
|
||||
digest = md5.hexdigest()
|
||||
return digest
|
||||
|
||||
|
||||
def update_values(default: dict, others: dict):
|
||||
for key in default.keys():
|
||||
if key in others:
|
||||
default[key] = others[key]
|
||||
|
||||
|
||||
def set_default(data: dict, default: dict):
|
||||
for key in default.keys():
|
||||
if key not in data:
|
||||
data[key] = default[key]
|
||||
|
||||
|
||||
class DictWrapper:
|
||||
def __init__(self, data:dict):
|
||||
self.raw_data = data
|
||||
|
||||
def __getitem__(self, item):
|
||||
# 网络请求返回的数据,不能完全信任,所以字典操作包在异常里
|
||||
try:
|
||||
return self.raw_data[item]
|
||||
except KeyError as e:
|
||||
msg = f'Response 200 but get field from json error: error={e} data={self.raw_data}'
|
||||
logger.error(msg)
|
||||
raise exce.ResponseDataKeyError(detail=self.raw_data)
|
||||
|
||||
def __getattr__(self, item):
|
||||
return getattr(self.raw_data, item)
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self.raw_data
|
||||
|
||||
def __str__(self):
|
||||
return str(self.raw_data)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.raw_data)
|
||||
|
||||
|
||||
def request(func):
|
||||
def inner(*args, **kwargs):
|
||||
signature = inspect.signature(func)
|
||||
bound_args = signature.bind(*args, **kwargs)
|
||||
bound_args.apply_defaults()
|
||||
|
||||
arguments = bound_args.arguments
|
||||
self = arguments['self']
|
||||
request_method = func.__name__
|
||||
|
||||
parameters = {}
|
||||
for k, v in signature.parameters.items():
|
||||
if k == 'self':
|
||||
continue
|
||||
if v.kind is Parameter.VAR_KEYWORD:
|
||||
parameters.update(arguments[k])
|
||||
continue
|
||||
parameters[k] = arguments[k]
|
||||
|
||||
response = self.request(request_method, **parameters)
|
||||
return response
|
||||
return inner
|
||||
197
apps/common/message/backends/wecom/__init__.py
Normal file
197
apps/common/message/backends/wecom/__init__.py
Normal file
@@ -0,0 +1,197 @@
|
||||
from typing import Iterable, AnyStr
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.exceptions import APIException
|
||||
from requests.exceptions import ReadTimeout
|
||||
import requests
|
||||
from django.core.cache import cache
|
||||
|
||||
from common.utils.common import get_logger
|
||||
from common.message.backends.utils import digest, DictWrapper, update_values, set_default
|
||||
from common.message.backends.utils import request
|
||||
from common.message.backends.mixin import RequestMixin, BaseRequest
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class WeComError(APIException):
|
||||
default_code = 'wecom_error'
|
||||
default_detail = _('WeCom error, please contact system administrator')
|
||||
|
||||
|
||||
class URL:
|
||||
GET_TOKEN = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken'
|
||||
SEND_MESSAGE = 'https://qyapi.weixin.qq.com/cgi-bin/message/send'
|
||||
QR_CONNECT = 'https://open.work.weixin.qq.com/wwopen/sso/qrConnect'
|
||||
|
||||
# https://open.work.weixin.qq.com/api/doc/90000/90135/91437
|
||||
GET_USER_ID_BY_CODE = 'https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo'
|
||||
GET_USER_DETAIL = 'https://qyapi.weixin.qq.com/cgi-bin/user/get'
|
||||
|
||||
|
||||
class ErrorCode:
|
||||
# https://open.work.weixin.qq.com/api/doc/90000/90139/90313#%E9%94%99%E8%AF%AF%E7%A0%81%EF%BC%9A81013
|
||||
RECIPIENTS_INVALID = 81013 # UserID、部门ID、标签ID全部非法或无权限。
|
||||
|
||||
# https: // open.work.weixin.qq.com / devtool / query?e = 82001
|
||||
RECIPIENTS_EMPTY = 82001 # 指定的成员/部门/标签全部为空
|
||||
|
||||
# https://open.work.weixin.qq.com/api/doc/90000/90135/91437
|
||||
INVALID_CODE = 40029
|
||||
|
||||
INVALID_TOKEN = 40014 # 无效的 access_token
|
||||
|
||||
|
||||
class WeComRequests(BaseRequest):
|
||||
"""
|
||||
处理系统级错误,抛出 API 异常,直接生成 HTTP 响应,业务代码无需关心这些错误
|
||||
- 确保 status_code == 200
|
||||
- 确保 access_token 无效时重试
|
||||
"""
|
||||
invalid_token_errcode = ErrorCode.INVALID_TOKEN
|
||||
|
||||
def __init__(self, corpid, corpsecret, agentid, timeout=None):
|
||||
self._corpid = corpid
|
||||
self._corpsecret = corpsecret
|
||||
self._agentid = agentid
|
||||
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
def get_access_token_cache_key(self):
|
||||
return digest(self._corpid, self._corpsecret)
|
||||
|
||||
def request_access_token(self):
|
||||
params = {'corpid': self._corpid, 'corpsecret': self._corpsecret}
|
||||
data = self.raw_request('get', url=URL.GET_TOKEN, params=params)
|
||||
|
||||
access_token = data['access_token']
|
||||
expires_in = data['expires_in']
|
||||
return access_token, expires_in
|
||||
|
||||
@request
|
||||
def get(self, url, params=None, with_token=True,
|
||||
check_errcode_is_0=True, **kwargs):
|
||||
# self.request ...
|
||||
pass
|
||||
|
||||
@request
|
||||
def post(self, url, params=None, json=None,
|
||||
with_token=True, check_errcode_is_0=True,
|
||||
**kwargs):
|
||||
# self.request ...
|
||||
pass
|
||||
|
||||
def request(self, method, url,
|
||||
params=None,
|
||||
with_token=True,
|
||||
check_errcode_is_0=True,
|
||||
**kwargs):
|
||||
|
||||
if not isinstance(params, dict):
|
||||
params = {}
|
||||
|
||||
if with_token:
|
||||
params['access_token'] = self.access_token
|
||||
|
||||
data = self.raw_request(method, url, params=params, **kwargs)
|
||||
if check_errcode_is_0:
|
||||
self.check_errcode_is_0(data)
|
||||
return data
|
||||
|
||||
|
||||
class WeCom(RequestMixin):
|
||||
"""
|
||||
非业务数据导致的错误直接抛异常,说明是系统配置错误,业务代码不用理会
|
||||
"""
|
||||
|
||||
def __init__(self, corpid, corpsecret, agentid, timeout=None):
|
||||
self._corpid = corpid
|
||||
self._corpsecret = corpsecret
|
||||
self._agentid = agentid
|
||||
|
||||
self._requests = WeComRequests(
|
||||
corpid=corpid,
|
||||
corpsecret=corpsecret,
|
||||
agentid=agentid,
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
def send_text(self, users: Iterable, msg: AnyStr, **kwargs):
|
||||
"""
|
||||
https://open.work.weixin.qq.com/api/doc/90000/90135/90236
|
||||
|
||||
对于业务代码,只需要关心由 用户id 或 消息不对 导致的错误,其他错误不予理会
|
||||
"""
|
||||
users = tuple(users)
|
||||
|
||||
extra_params = {
|
||||
"safe": 0,
|
||||
"enable_id_trans": 0,
|
||||
"enable_duplicate_check": 0,
|
||||
"duplicate_check_interval": 1800
|
||||
}
|
||||
update_values(extra_params, kwargs)
|
||||
|
||||
body = {
|
||||
"touser": '|'.join(users),
|
||||
"msgtype": "text",
|
||||
"agentid": self._agentid,
|
||||
"text": {
|
||||
"content": msg
|
||||
},
|
||||
**extra_params
|
||||
}
|
||||
data = self._requests.post(URL.SEND_MESSAGE, json=body, check_errcode_is_0=False)
|
||||
|
||||
errcode = data['errcode']
|
||||
if errcode in (ErrorCode.RECIPIENTS_INVALID, ErrorCode.RECIPIENTS_EMPTY):
|
||||
# 全部接收人无权限或不存在
|
||||
return users
|
||||
self.check_errcode_is_0(data)
|
||||
|
||||
invaliduser = data['invaliduser']
|
||||
if not invaliduser:
|
||||
return ()
|
||||
|
||||
if isinstance(invaliduser, str):
|
||||
logger.error(f'WeCom send text 200, but invaliduser is not str: invaliduser={invaliduser}')
|
||||
raise WeComError
|
||||
|
||||
invalid_users = invaliduser.split('|')
|
||||
return invalid_users
|
||||
|
||||
def get_user_id_by_code(self, code):
|
||||
# # https://open.work.weixin.qq.com/api/doc/90000/90135/91437
|
||||
|
||||
params = {
|
||||
'code': code,
|
||||
}
|
||||
data = self._requests.get(URL.GET_USER_ID_BY_CODE, params=params, check_errcode_is_0=False)
|
||||
|
||||
errcode = data['errcode']
|
||||
if errcode == ErrorCode.INVALID_CODE:
|
||||
logger.warn(f'WeCom get_user_id_by_code invalid code: code={code}')
|
||||
return None, None
|
||||
|
||||
self.check_errcode_is_0(data)
|
||||
|
||||
USER_ID = 'UserId'
|
||||
OPEN_ID = 'OpenId'
|
||||
|
||||
if USER_ID in data:
|
||||
return data[USER_ID], USER_ID
|
||||
elif OPEN_ID in data:
|
||||
return data[OPEN_ID], OPEN_ID
|
||||
else:
|
||||
logger.error(f'WeCom response 200 but get field from json error: fields=UserId|OpenId')
|
||||
raise WeComError
|
||||
|
||||
def get_user_detail(self, id):
|
||||
# https://open.work.weixin.qq.com/api/doc/90000/90135/90196
|
||||
|
||||
params = {
|
||||
'userid': id,
|
||||
}
|
||||
|
||||
data = self._requests.get(URL.GET_USER_DETAIL, params)
|
||||
return data
|
||||
@@ -6,10 +6,12 @@ from threading import Thread
|
||||
from collections import defaultdict
|
||||
from itertools import chain
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models.signals import m2m_changed
|
||||
from django.core.cache import cache
|
||||
from django.http import JsonResponse
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.decorators import action
|
||||
@@ -25,6 +27,9 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
|
||||
class JSONResponseMixin(object):
|
||||
"""JSON mixin"""
|
||||
@staticmethod
|
||||
@@ -332,3 +337,21 @@ class AllowBulkDestoryMixin:
|
||||
"""
|
||||
query = str(filtered.query)
|
||||
return '`id` IN (' in query or '`id` =' in query
|
||||
|
||||
|
||||
class RoleAdminMixin:
|
||||
kwargs: dict
|
||||
user_id_url_kwarg = 'pk'
|
||||
|
||||
@lazyproperty
|
||||
def user(self):
|
||||
user_id = self.kwargs.get(self.user_id_url_kwarg)
|
||||
return UserModel.objects.get(id=user_id)
|
||||
|
||||
|
||||
class RoleUserMixin:
|
||||
request: Request
|
||||
|
||||
@lazyproperty
|
||||
def user(self):
|
||||
return self.request.user
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
__all__ = ["DatetimeSearchMixin"]
|
||||
__all__ = ["DatetimeSearchMixin", "PermissionsMixin"]
|
||||
|
||||
from rest_framework import permissions
|
||||
|
||||
@@ -51,4 +51,4 @@ class PermissionsMixin(UserPassesTestMixin):
|
||||
for permission_class in permission_classes:
|
||||
if not permission_class().has_permission(self.request, self):
|
||||
return False
|
||||
return True
|
||||
return True
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import time
|
||||
from rest_framework import permissions
|
||||
from django.conf import settings
|
||||
from common.exceptions import MFAVerifyRequired
|
||||
|
||||
from orgs.utils import current_org
|
||||
|
||||
@@ -114,7 +115,7 @@ class NeedMFAVerify(permissions.BasePermission):
|
||||
mfa_verify_time = request.session.get('MFA_VERIFY_TIME', 0)
|
||||
if time.time() - mfa_verify_time < settings.SECURITY_MFA_VERIFY_TTL:
|
||||
return True
|
||||
return False
|
||||
raise MFAVerifyRequired()
|
||||
|
||||
|
||||
class CanUpdateDeleteUser(permissions.BasePermission):
|
||||
|
||||
15
apps/common/request_log.py
Normal file
15
apps/common/request_log.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from django.http.request import HttpRequest
|
||||
from django.http.response import HttpResponse
|
||||
|
||||
from orgs.utils import current_org
|
||||
|
||||
|
||||
class RequestLogMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request: HttpRequest):
|
||||
print(f'Request {request.method} --> ', request.get_raw_uri())
|
||||
response: HttpResponse = self.get_response(request)
|
||||
print(f'Response {current_org.name} {request.method} {response.status_code} --> ', request.get_raw_uri())
|
||||
return response
|
||||
10
apps/common/urls/view_urls.py
Normal file
10
apps/common/urls/view_urls.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.urls import path
|
||||
|
||||
from .. import views
|
||||
|
||||
app_name = 'common'
|
||||
|
||||
urlpatterns = [
|
||||
# login
|
||||
path('flash-message/', views.FlashMessageMsgView.as_view(), name='flash-message'),
|
||||
]
|
||||
@@ -8,3 +8,4 @@ from .http import *
|
||||
from .ipip import *
|
||||
from .crypto import *
|
||||
from .random import *
|
||||
from .jumpserver import *
|
||||
@@ -75,11 +75,16 @@ def ssh_key_string_to_obj(text, password=None):
|
||||
key = paramiko.RSAKey.from_private_key(StringIO(text), password=password)
|
||||
except paramiko.SSHException:
|
||||
pass
|
||||
else:
|
||||
return key
|
||||
|
||||
try:
|
||||
key = paramiko.DSSKey.from_private_key(StringIO(text), password=password)
|
||||
except paramiko.SSHException:
|
||||
pass
|
||||
else:
|
||||
return key
|
||||
|
||||
return key
|
||||
|
||||
|
||||
|
||||
31
apps/common/utils/jumpserver.py
Normal file
31
apps/common/utils/jumpserver.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import reverse
|
||||
|
||||
from .random import random_string
|
||||
|
||||
|
||||
__all__ = ['FlashMessageUtil']
|
||||
|
||||
|
||||
class FlashMessageUtil:
|
||||
@staticmethod
|
||||
def get_key(code):
|
||||
key = 'MESSAGE_{}'.format(code)
|
||||
return key
|
||||
|
||||
@classmethod
|
||||
def get_message_code(cls, message_data):
|
||||
code = random_string(12)
|
||||
key = cls.get_key(code)
|
||||
cache.set(key, message_data, 60)
|
||||
return code
|
||||
|
||||
@classmethod
|
||||
def get_message_by_code(cls, code):
|
||||
key = cls.get_key(code)
|
||||
return cache.get(key)
|
||||
|
||||
@classmethod
|
||||
def gen_message_url(cls, message_data):
|
||||
code = cls.get_message_code(message_data)
|
||||
return reverse('common:flash-message') + f'?code={code}'
|
||||
@@ -4,7 +4,6 @@ import struct
|
||||
import random
|
||||
import socket
|
||||
import string
|
||||
import secrets
|
||||
|
||||
|
||||
string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_{}~'
|
||||
|
||||
40
apps/common/views.py
Normal file
40
apps/common/views.py
Normal file
@@ -0,0 +1,40 @@
|
||||
#
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.http import HttpResponse
|
||||
from django.views.generic.base import TemplateView
|
||||
|
||||
from common.utils import bulk_get, FlashMessageUtil
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class FlashMessageMsgView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
code = request.GET.get('code')
|
||||
if not code:
|
||||
return HttpResponse('Not found the code')
|
||||
|
||||
message_data = FlashMessageUtil.get_message_by_code(code)
|
||||
if not message_data:
|
||||
return HttpResponse('Message code error')
|
||||
|
||||
title, message, redirect_url, confirm_button, cancel_url = bulk_get(
|
||||
message_data, 'title', 'message', 'redirect_url', 'confirm_button', 'cancel_url'
|
||||
)
|
||||
|
||||
interval = message_data.get('interval', 3)
|
||||
auto_redirect = message_data.get('auto_redirect', True)
|
||||
has_cancel = message_data.get('has_cancel', False)
|
||||
context = {
|
||||
'title': title,
|
||||
'messages': message,
|
||||
'interval': interval,
|
||||
'redirect_url': redirect_url,
|
||||
'auto_redirect': auto_redirect,
|
||||
'confirm_button': confirm_button,
|
||||
'has_cancel': has_cancel,
|
||||
'cancel_url': cancel_url,
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
@@ -18,6 +18,7 @@ from terminal.utils import ComponentsPrometheusMetricsUtil
|
||||
from orgs.utils import current_org
|
||||
from common.permissions import IsOrgAdmin, IsOrgAuditor
|
||||
from common.utils import lazyproperty
|
||||
from orgs.caches import OrgResourceStatisticsCache
|
||||
|
||||
__all__ = ['IndexApi']
|
||||
|
||||
@@ -99,7 +100,7 @@ class DatesLoginMetricMixin:
|
||||
if count is not None:
|
||||
return count
|
||||
ds, de = self.get_date_start_2_end(date)
|
||||
count = len(set(Session.objects.filter(date_start__range=(ds, de)).values_list('user', flat=True)))
|
||||
count = len(set(Session.objects.filter(date_start__range=(ds, de)).values_list('user_id', flat=True)))
|
||||
self.__set_data_to_cache(date, tp, count)
|
||||
return count
|
||||
|
||||
@@ -129,7 +130,7 @@ class DatesLoginMetricMixin:
|
||||
|
||||
@lazyproperty
|
||||
def dates_total_count_active_users(self):
|
||||
count = len(set(self.sessions_queryset.values_list('user', flat=True)))
|
||||
count = len(set(self.sessions_queryset.values_list('user_id', flat=True)))
|
||||
return count
|
||||
|
||||
@lazyproperty
|
||||
@@ -161,10 +162,10 @@ class DatesLoginMetricMixin:
|
||||
@lazyproperty
|
||||
def dates_total_count_disabled_assets(self):
|
||||
return Asset.objects.filter(is_active=False).count()
|
||||
|
||||
|
||||
# 以下是从week中而来
|
||||
def get_dates_login_times_top5_users(self):
|
||||
users = self.sessions_queryset.values_list('user', flat=True)
|
||||
users = self.sessions_queryset.values_list('user_id', flat=True)
|
||||
users = [
|
||||
{'user': user, 'total': total}
|
||||
for user, total in Counter(users).most_common(5)
|
||||
@@ -172,7 +173,7 @@ class DatesLoginMetricMixin:
|
||||
return users
|
||||
|
||||
def get_dates_total_count_login_users(self):
|
||||
return len(set(self.sessions_queryset.values_list('user', flat=True)))
|
||||
return len(set(self.sessions_queryset.values_list('user_id', flat=True)))
|
||||
|
||||
def get_dates_total_count_login_times(self):
|
||||
return self.sessions_queryset.count()
|
||||
@@ -186,8 +187,9 @@ class DatesLoginMetricMixin:
|
||||
return list(assets)
|
||||
|
||||
def get_dates_login_times_top10_users(self):
|
||||
users = self.sessions_queryset.values("user") \
|
||||
.annotate(total=Count("user")) \
|
||||
users = self.sessions_queryset.values("user_id") \
|
||||
.annotate(total=Count("user_id")) \
|
||||
.annotate(user=Max('user')) \
|
||||
.annotate(last=Max("date_start")).order_by("-total")[:10]
|
||||
for user in users:
|
||||
user['last'] = str(user['last'])
|
||||
@@ -210,26 +212,7 @@ class DatesLoginMetricMixin:
|
||||
return sessions
|
||||
|
||||
|
||||
class TotalCountMixin:
|
||||
@staticmethod
|
||||
def get_total_count_users():
|
||||
return current_org.get_members().count()
|
||||
|
||||
@staticmethod
|
||||
def get_total_count_assets():
|
||||
return Asset.objects.all().count()
|
||||
|
||||
@staticmethod
|
||||
def get_total_count_online_users():
|
||||
count = len(set(Session.objects.filter(is_finished=False).values_list('user', flat=True)))
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
def get_total_count_online_sessions():
|
||||
return Session.objects.filter(is_finished=False).count()
|
||||
|
||||
|
||||
class IndexApi(TotalCountMixin, DatesLoginMetricMixin, APIView):
|
||||
class IndexApi(DatesLoginMetricMixin, APIView):
|
||||
permission_classes = (IsOrgAdmin | IsOrgAuditor,)
|
||||
http_method_names = ['get']
|
||||
|
||||
@@ -238,26 +221,28 @@ class IndexApi(TotalCountMixin, DatesLoginMetricMixin, APIView):
|
||||
|
||||
query_params = self.request.query_params
|
||||
|
||||
caches = OrgResourceStatisticsCache(current_org)
|
||||
|
||||
_all = query_params.get('all')
|
||||
|
||||
if _all or query_params.get('total_count') or query_params.get('total_count_users'):
|
||||
data.update({
|
||||
'total_count_users': self.get_total_count_users(),
|
||||
'total_count_users': caches.users_amount,
|
||||
})
|
||||
|
||||
if _all or query_params.get('total_count') or query_params.get('total_count_assets'):
|
||||
data.update({
|
||||
'total_count_assets': self.get_total_count_assets(),
|
||||
'total_count_assets': caches.assets_amount,
|
||||
})
|
||||
|
||||
if _all or query_params.get('total_count') or query_params.get('total_count_online_users'):
|
||||
data.update({
|
||||
'total_count_online_users': self.get_total_count_online_users(),
|
||||
'total_count_online_users': caches.total_count_online_users,
|
||||
})
|
||||
|
||||
if _all or query_params.get('total_count') or query_params.get('total_count_online_sessions'):
|
||||
data.update({
|
||||
'total_count_online_sessions': self.get_total_count_online_sessions(),
|
||||
'total_count_online_sessions': caches.total_count_online_sessions,
|
||||
})
|
||||
|
||||
if _all or query_params.get('dates_metrics'):
|
||||
|
||||
@@ -216,6 +216,16 @@ class Config(dict):
|
||||
'AUTH_SSO': False,
|
||||
'AUTH_SSO_AUTHKEY_TTL': 60 * 15,
|
||||
|
||||
'AUTH_WECOM': False,
|
||||
'WECOM_CORPID': '',
|
||||
'WECOM_AGENTID': '',
|
||||
'WECOM_SECRET': '',
|
||||
|
||||
'AUTH_DINGTALK': False,
|
||||
'DINGTALK_AGENTID': '',
|
||||
'DINGTALK_APPKEY': '',
|
||||
'DINGTALK_APPSECRET': '',
|
||||
|
||||
'OTP_VALID_WINDOW': 2,
|
||||
'OTP_ISSUER_NAME': 'JumpServer',
|
||||
'EMAIL_SUFFIX': 'jumpserver.org',
|
||||
@@ -249,6 +259,7 @@ class Config(dict):
|
||||
'SECURITY_INSECURE_COMMAND': False,
|
||||
'SECURITY_INSECURE_COMMAND_LEVEL': 5,
|
||||
'SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER': '',
|
||||
'SECURITY_LUNA_REMEMBER_AUTH': True,
|
||||
|
||||
'HTTP_BIND_HOST': '0.0.0.0',
|
||||
'HTTP_LISTEN_PORT': 8080,
|
||||
@@ -259,6 +270,7 @@ class Config(dict):
|
||||
'FTP_LOG_KEEP_DAYS': 200,
|
||||
'ASSETS_PERM_CACHE_TIME': 3600 * 24,
|
||||
'SECURITY_MFA_VERIFY_TTL': 3600,
|
||||
'OLD_PASSWORD_HISTORY_LIMIT_COUNT': 5,
|
||||
'ASSETS_PERM_CACHE_ENABLE': HAS_XPACK,
|
||||
'SYSLOG_ADDR': '', # '192.168.0.1:514'
|
||||
'SYSLOG_FACILITY': 'user',
|
||||
@@ -268,7 +280,7 @@ class Config(dict):
|
||||
'WINDOWS_SSH_DEFAULT_SHELL': 'cmd',
|
||||
'FLOWER_URL': "127.0.0.1:5555",
|
||||
'DEFAULT_ORG_SHOW_ALL_USERS': True,
|
||||
'PERIOD_TASK_ENABLE': True,
|
||||
'PERIOD_TASK_ENABLED': True,
|
||||
'FORCE_SCRIPT_NAME': '',
|
||||
'LOGIN_CONFIRM_ENABLE': False,
|
||||
'WINDOWS_SKIP_ALL_MANUAL_PASSWORD': False,
|
||||
@@ -289,7 +301,9 @@ class Config(dict):
|
||||
'SESSION_SAVE_EVERY_REQUEST': True,
|
||||
'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False,
|
||||
'FORGOT_PASSWORD_URL': '',
|
||||
'HEALTH_CHECK_TOKEN': ''
|
||||
'HEALTH_CHECK_TOKEN': '',
|
||||
|
||||
'TERMINAL_RDP_ADDR': ''
|
||||
}
|
||||
|
||||
def compatible_auth_openid_of_key(self):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user