Compare commits

..

14 Commits

Author SHA1 Message Date
xinwen
3b95487221 fix: 添加 0069_change_node_key0_to_key1 迁移脚本 2021-04-13 21:35:21 -05:00
xinwen
eba7a7288a fix: 修复 test cookie 问题,删掉key=0迁移脚本 2021-04-09 03:13:56 -05:00
xinwen
bd11018cef fix: 管理用户输入带密码的秘钥报错 2021-04-06 19:45:40 +08:00
xinwen
8a2b0a7845 fix: 修正 key 为 0 的节点 2021-04-06 18:27:18 +08:00
xinwen
b3dbdd5389 fix: revert 一些代码 2021-03-30 10:44:48 +08:00
xinwen
a4643fe4b4 fix: Default 组织下出现 app user 2021-03-30 10:23:13 +08:00
xinwen
dce9b50ac0 feat: 增加 es 忽略 https 证书验证 2021-03-30 10:23:13 +08:00
xinwen
e2b7c902a5 perf: delete_test_cookie 2021-03-30 10:23:13 +08:00
ibuler
4055306cf8 perf: 修改表结构迁移,增加rdp terminal 2021-03-24 10:24:09 +08:00
xinwen
9e1c5bb64d fix: 授权树节点排序 2021-03-24 10:13:45 +08:00
ibuler
32df722e9d perf: 合并 2021-03-23 18:46:30 +08:00
ibuler
22f6f5c34c perf: session add rdp terminal login from 2021-03-23 18:31:42 +08:00
ibuler
6fb819ca53 perf: 优化登录ip限制提示 2021-03-23 18:31:24 +08:00
老广
043c4a7a0b Merge pull request #5813 from jumpserver/master
v2.8.1
2021-03-19 20:05:29 +08:00
184 changed files with 4470 additions and 4935 deletions

View File

@@ -1,15 +1,14 @@
# JumpServer 多云环境下更好用的堡垒机
[![License](https://shields.io/github/license/jumpserver/jumpserver)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.html)
[![Release Downloads](https://shields.io/github/downloads/jumpserver/jumpserver/total)](https://github.com/jumpserver/jumpserver/releases)
[![Python3](https://img.shields.io/badge/python-3.6-green.svg?style=plastic)](https://www.python.org/)
[![Django](https://img.shields.io/badge/django-2.2-brightgreen.svg?style=plastic)](https://www.djangoproject.com/)
[![Docker Pulls](https://img.shields.io/docker/pulls/jumpserver/jms_all.svg)](https://hub.docker.com/u/jumpserver)
- [ENGLISH](https://github.com/jumpserver/jumpserver/blob/master/README_EN.md)
|《新一代堡垒机建设指南》开放下载|
|![notification](https://raw.githubusercontent.com/goharbor/website/master/docs/img/readme/bell-outline-badged.svg)安全通知|
|------------------|
|本白皮书由JumpServer开源项目组编著而成。编写团队从企业实践和技术演进的双重视角出发结合自身在身份与访问安全领域长期研发及落地经验组织撰写同时积极听取行业内专家的意见和建议在此基础上完成了本白皮书的编写任务。下载链接https://jinshuju.net/f/E0qAl8|
|2021年1月15日 JumpServer 发现远程执行漏洞,请速度修复 [详见](https://github.com/jumpserver/jumpserver/issues/5533) 非常感谢 **reactivity of Alibaba Hackerone bug bounty program**(瑞典) 向我们报告了此 BUG|
--------------------------
@@ -250,7 +249,7 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
- [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)
- [Lion](https://github.com/jumpserver/lion-release) JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/)
- [Guacamole](https://github.com/jumpserver/docker-guacamole) JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/)
## 贡献
如果有你好的想法创意,或者帮助我们修复了 Bug, 欢迎提交 Pull Request
@@ -263,7 +262,7 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
## 致谢
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC协议设备JumpServer 图形化组件 Lion 依赖
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC协议设备JumpServer 图形化连接依赖
- [OmniDB](https://omnidb.org/) Web页面连接使用数据库JumpServer Web数据库依赖

View File

@@ -1,245 +1,143 @@
# Jumpserver - The Bastion Host for Multi-Cloud Environment
## Jumpserver
[![Python3](https://img.shields.io/badge/python-3.6-green.svg?style=plastic)](https://www.python.org/)
[![Django](https://img.shields.io/badge/django-2.2-brightgreen.svg?style=plastic)](https://www.djangoproject.com/)
[![Docker Pulls](https://img.shields.io/docker/pulls/jumpserver/jms_all.svg)](https://hub.docker.com/u/jumpserver)
- [中文版](https://github.com/jumpserver/jumpserver/blob/master/README.md)
----
## CRITICAL BUG WARNING
|![notification](https://raw.githubusercontent.com/goharbor/website/master/docs/img/readme/bell-outline-badged.svg)Security Notice|
|------------------|
|On 15th January 2021, JumpServer found a critical bug for remote execution vulnerability. Please fix it asap! [For more detail](https://github.com/jumpserver/jumpserver/issues/5533) Thanks for **reactivity of Alibaba Hackerone bug bounty program** report use the bug|
Recently we have found a critical bug for remote execution vulnerability which leads to pre-auth and info leak, please fix it as soon as possible.
Thanks for **reactivity from Alibaba Hackerone bug bounty program** report us this bug
**Vulnerable version:**
```
< v2.6.2
< v2.5.4
< v2.4.5
= v1.5.9
>= v1.5.3
```
**Safe and Stable version:**
```
>= v2.6.2
>= v2.5.4
>= v2.4.5
= v1.5.9 version tag didn't change
< v1.5.3
```
**Bug Fix Solution:**
Upgrade to the latest version or the version mentioned above
**Temporary Solution (upgrade asap):**
Modify the Nginx config file and disable the vulnerable api listed below
```
/api/v1/authentication/connection-token/
/api/v1/users/connection-token/
```
Path to Nginx config file
```
# Previous Community version
/etc/nginx/conf.d/jumpserver.conf
# Previous Enterprise version
jumpserver-release/nginx/http_server.conf
# Latest version
jumpserver-release/compose/config_static/http_server.conf
```
Changes in Nginx config file
```
### Put the following code on top of location server, or before /api and /
location /api/v1/authentication/connection-token/ {
return 403;
}
location /api/v1/users/connection-token/ {
return 403;
}
### End right here
location /api/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://core:8080;
}
...
```
Save the file and restart Nginx
```
docker deployment:
$ docker restart jms_nginx
rpm or other deployment:
$ systemctl restart nginx
```
**Bug Fix Verification**
```
# Download the following script to check if it is fixed
$ wget https://github.com/jumpserver/jumpserver/releases/download/v2.6.2/jms_bug_check.sh
# Run the code to verify it
$ bash jms_bug_check.sh demo.jumpserver.org
漏洞已修复 (It means the bug is fixed)
漏洞未修复 (It means the bug is not fixed and the system is still vulnerable)
```
**Attack Simulation**
Go to the logs directory which should contain gunicorn.log file. Then download the "attack" script and execute it
```
$ pwd
/opt/jumpserver/core/logs
$ ls gunicorn.log
gunicorn.log
$ wget 'https://github.com/jumpserver/jumpserver/releases/download/v2.6.2/jms_check_attack.sh'
$ bash jms_check_attack.sh
系统未被入侵 (It means the system is safe)
系统已被入侵 (It means the system is being attacked)
```
--------------------------
Jumpserver is the world's first open-source Bastion Host and is licensed under the GNU GPL v2.0. It is a 4A-compliant professional operation and maintenance security audit system.
----
- [中文版](https://github.com/jumpserver/jumpserver/blob/master/README.md)
Jumpserver is the world's first open-source PAM (Privileged Access Management System) and is licensed under the GNU GPL v2.0. It is a 4A-compliant professional operation and maintenance security audit system.
Jumpserver uses Python / Django for development, follows Web 2.0 specifications, and is equipped with an industry-leading Web Terminal solution that provides a beautiful user interface and great user experience
Jumpserver adopts a distributed architecture to support multi-branch deployment across multiple cross-regional areas. The central node provides APIs, and login nodes are deployed in each branch. It can be scaled horizontally without concurrency restrictions.
Change the world by taking every little step
Change the world, starting from little things.
----
### Advantages
- Open Source: huge transparency and free to access with quick installation process.
- Distributed: support large-scale concurrent access with ease.
- No Plugin required: all you need is a browser, the ultimate Web Terminal experience.
- Multi-Cloud supported: a unified system to manage assets on different clouds at the same time
- Cloud storage: audit records are stored in the cloud. Data lost no more!
- Multi-Tenant system: multiple subsidiary companies or departments access the same system simultaneously.
- Many applications supported: link to databases, windows remote applications, and Kubernetes cluster, etc.
### Features
## Features List
<table>
<tr>
<td rowspan="8">Authentication</td>
<td rowspan="5">Login</td>
<td>Unified way to access and authenticate resources</td>
</tr>
<tr>
<td>LDAP/AD Authentication</td>
</tr>
<tr>
<td>RADIUS Authentication</td>
</tr>
<tr>
<td>OpenID AuthenticationSingle Sign-On</td>
</tr>
<tr>
<td>CAS Authentication Single Sign-On</td>
</tr>
<tr>
<td rowspan="2">MFA (Multi-Factor Authentication)</td>
<td>Use Google Authenticator for MFA</td>
</tr>
<tr>
<td>RADIUS (Remote Authentication Dial In User Service)</td>
</tr>
<tr>
<td>Login Supervision</td>
<td>Any users login behavior is supervised and controlled by the administrator:small_orange_diamond:</td>
</tr>
<tr>
<td rowspan="11">Accounting</td>
<td rowspan="2">Centralized Accounts Management</td>
<td>Admin Users management</td>
</tr>
<tr>
<td>System Users management</td>
</tr>
<tr>
<td rowspan="4">Unified Password Management</td>
<td>Asset password custody (a matrix storing all asset password with dense security)</td>
</tr>
<tr>
<td>Auto-generated passwords</td>
</tr>
<tr>
<td>Automatic password handling (auto login assets)</td>
</tr>
<tr>
<td>Password expiration settings</td>
</tr>
<tr>
<td rowspan="2">Password change Schedular</td>
<td>Support regular batch Linux/Windows assets password changing:small_orange_diamond:</td>
</tr>
<tr>
<td>Implement multiple password strategies:small_orange_diamond:</td>
</tr>
<tr>
<td>Multi-Cloud Management</td>
<td>Automatically manage private cloud and public cloud assets in a unified platform :small_orange_diamond:</td>
</tr>
<tr>
<td>Users Acquisition </td>
<td>Create regular custom tasks to collect system users in selected assets to identify and track the privileges ownership:small_orange_diamond:</td>
</tr>
<tr>
<td>Password Vault </td>
<td>Unified operations to check, update, and test system user password to prevent stealing or unauthorised sharing of passwords:small_orange_diamond:</td>
</tr>
<tr>
<td rowspan="15">Authorization</td>
<td>Multi-Dimensional</td>
<td>Granting users or user groups to access assets, asset nodes, or applications through system users. Providing precise access control to different roles of users</td>
</tr>
<tr>
<td rowspan="4">Assets</td>
<td>Assets are arranged and displayed in a tree structure </td>
</tr>
<tr>
<td>Assets and Nodes have immense flexibility for authorizing</td>
</tr>
<tr>
<td>Assets in nodes inherit authorization automatically</td>
</tr>
<tr>
<td>child nodes automatically inherit authorization from parent nodes</td>
</tr>
<tr>
<td rowspan="2">Application</td>
<td>Provides granular access control for privileged users on application level to protect from unauthorized access and unintentional errors</td>
</tr>
<tr>
<td>Database applications (MySQL, Oracle, PostgreSQL, MariaDB, etc.) and Remote App:small_orange_diamond: </td>
</tr>
<tr>
<td>Actions</td>
<td>Deeper restriction on the control of file upload, download and connection actions of authorized assets. Control the permission of clipboard copy/paste (from outer terminal to current asset)</td>
</tr>
<tr>
<td>Time Bound</td>
<td>Sharply limited the available (accessible) time for account access to the authorized resources to reduce the risk and attack surface drastically</td>
</tr>
<tr>
<td>Privileged Assignment</td>
<td>Assign the denied/allowed command lists to different system users as privilege elevation, with the latter taking the form of allowing particular commands to be run with a higher level of privileges. (Minimize insider threat)</td>
</tr>
<tr>
<td>Command Filtering</td>
<td>Creating list of restriction commands that you would like to assign to different authorized system users for filtering purpose</td>
</tr>
<tr>
<td>File Transfer and Management</td>
<td>Support SFTP file upload/download</td>
</tr>
<tr>
<td>File Management</td>
<td>Provide a Web UI for SFTP file management</td>
</tr>
<tr>
<td>Workflow Management</td>
<td>Manage user login confirmation requests and assets or applications authorization requests for Just-In-Time Privileges functionality:small_orange_diamond:</td>
</tr>
<tr>
<td>Group Management </td>
<td>Establishing a multi-tenant ecosystem that able authority isolation to keep malicious actors away from sensitive administrative backends:small_orange_diamond:</td>
</tr>
<tr>
<td rowspan="8">Auditing</td>
<td>Operations</td>
<td>Auditing user operation behaviors for any access or usage of given privileged accounts</td>
</tr>
<tr>
<td rowspan="2">Session</td>
<td>Support real-time session audit</td>
</tr>
<tr>
<td>Full history of all previous session audits</td>
</tr>
<tr>
<td rowspan="3">Video</td>
<td>Complete session audit and playback recordings on assets operation (Linux, Windows)</td>
</tr>
<tr>
<td>Full recordings of RemoteApp, MySQL, and Kubernetes:small_orange_diamond:</td>
</tr>
<tr>
<td>Supports uploading recordings to public clouds</td>
</tr>
<tr>
<td>Command</td>
<td>Command auditing on assets and applications operation. Send warning alerts when executing illegal commands</td>
</tr>
<tr>
<td>File Transfer</td>
<td>Full recordings of file upload and download</td>
</tr>
<tr>
<td rowspan="20">Database</td>
<td rowspan="2">How to connect</td>
<td>Command line</td>
</tr>
<tr>
<td>Built-in Web UI:small_orange_diamond:</td>
</tr>
<tr>
<td rowspan="4">Supported Database</td>
<td>MySQL</td>
</tr>
<tr>
<td>Oracle :small_orange_diamond:</td>
</tr>
<tr>
<td>MariaDB :small_orange_diamond:</td>
</tr>
<tr>
<td>PostgreSQL :small_orange_diamond:</td>
</tr>
<tr>
<td rowspan="6">Feature Highlights</td>
<td>Syntax highlights</td>
</tr>
<tr>
<td>Prettier SQL formmating</td>
</tr>
<tr>
<td>Support Shortcuts</td>
</tr>
<tr>
<td>Support selected SQL statements</td>
</tr>
<tr>
<td>SQL commands history query</td>
</tr>
<tr>
<td>Support page creation: DB, TABLE</td>
</tr>
<tr>
<td rowspan="2">Session Auditing</td>
<td>Full records of command</td>
</tr>
<tr>
<td>Playback videos</td>
</tr>
</table>
**Note**: Rows with :small_orange_diamond: at the end of the sentence means that it is X-PACK features exclusive ([Apply for X-PACK Trial](https://jinshuju.net/f/kyOYpi))
![Jumpserver 功能](https://jumpserver-release.oss-cn-hangzhou.aliyuncs.com/Jumpserver148.jpeg "Jumpserver 功能")
### Start
@@ -264,50 +162,6 @@ We provide the SDK for your other systems to quickly interact with the Jumpserve
- [Python](https://github.com/jumpserver/jumpserver-python-sdk) Jumpserver other components use this SDK to complete the interaction.
- [Java](https://github.com/KaiJunYan/jumpserver-java-sdk.git) Thanks to 恺珺 for providing his Java SDK vesrion.
## JumpServer Component Projects
- [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 Character protocaol Connector, replace original Python Version [Coco](https://github.com/jumpserver/coco)
- [Guacamole](https://github.com/jumpserver/docker-guacamole) JumpServer Graphics protocol Connectorrely on [Apache Guacamole](https://guacamole.apache.org/)
## Contribution
If you have any good ideas or helping us to fix bugs, please submit a Pull Request and accept our thanks :)
Thanks to the following contributors for making JumpServer better everyday!
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/jumpserver" />
</a>
## Thanks to
- [Apache Guacamole](https://guacamole.apache.org/) Web page connection RDP, SSH, VNC protocol equipment. JumpServer graphical connection dependent.
- [OmniDB](https://omnidb.org/) Web page connection to databases. JumpServer Web database dependent.
## JumpServer Enterprise Version
- [Apply for it](https://jinshuju.net/f/kyOYpi)
## Case Study
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
- [携程 JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
- [小红书的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516)
- [JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732)
- [中通快递JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708)
- [东方明珠JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687)
- [江苏农信JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)。
## For safety instructions
JumpServer is a security product. Please refer to [Basic Security Recommendations](https://docs.jumpserver.org/zh/master/install/install_security/) for deployment and installation.
If you find a security problem, please contact us directly
- ibuler@fit2cloud.com
- support@fit2cloud.com
- 400-052-0755
### License & Copyright
Copyright (c) 2014-2019 Beijing Duizhan Tech, Inc., All rights reserved.

View File

@@ -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.api import GenericTicketStatusRetrieveCloseAPI
from tickets.models import Ticket
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='api-acls:login-asset-confirm-status',
view_name='acls:login-asset-confirm-status',
kwargs={'pk': str(ticket.id)}
)
ticket_detail_url = reverse(
@@ -72,6 +72,34 @@ class LoginAssetCheckAPI(CreateAPIView):
return serializer
class LoginAssetConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
pass
class LoginAssetConfirmStatusAPI(RetrieveDestroyAPIView):
permission_classes = (IsAppUser, )
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'])

9
apps/acls/const.py Normal file
View File

@@ -0,0 +1,9 @@
from django.utils.translation import ugettext as _
common_help_text = _('Format for comma-delimited string, with * indicating a match all. ')
ip_group_help_text = common_help_text + _(
'Such as: '
'192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 '
)

View File

@@ -33,9 +33,6 @@ 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

View File

@@ -38,9 +38,6 @@ 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)

View File

@@ -4,6 +4,7 @@ from common.drf.serializers import BulkModelSerializer
from orgs.utils import current_org
from ..models import LoginACL
from ..utils import is_ip_address, is_ip_network, is_ip_segment
from .. import const
__all__ = ['LoginACLSerializer', ]
@@ -20,14 +21,8 @@ def ip_group_child_validator(ip_group_child):
class LoginACLSerializer(BulkModelSerializer):
ip_group_help_text = _(
'Format for comma-delimited string, with * indicating a match all. '
'Such as: '
'192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 '
)
ip_group = serializers.ListField(
default=['*'], label=_('IP'), help_text=ip_group_help_text,
default=['*'], label=_('IP'), help_text=const.ip_group_help_text,
child=serializers.CharField(max_length=1024, validators=[ip_group_child_validator])
)
user_display = serializers.ReadOnlyField(source='user.name', label=_('User'))
@@ -35,15 +30,10 @@ class LoginACLSerializer(BulkModelSerializer):
class Meta:
model = LoginACL
fields_mini = ['id', 'name']
fields_small = fields_mini + [
'priority', 'ip_group', 'action', 'action_display',
'is_active',
'date_created', 'date_updated',
'comment', 'created_by',
fields = [
'id', 'name', 'priority', 'ip_group', 'user', 'user_display', 'action',
'action_display', 'is_active', 'comment', 'created_by', 'date_created', 'date_updated'
]
fields_fk = ['user', 'user_display',]
fields = fields_small + fields_fk
extra_kwargs = {
'priority': {'default': 50},
'is_active': {'default': True},

View File

@@ -1,60 +1,46 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from assets.models import SystemUser
from acls import models
from orgs.models import Organization
from .. import const
__all__ = ['LoginAssetACLSerializer']
common_help_text = _('Format for comma-delimited string, with * indicating a match all. ')
class LoginAssetACLUsersSerializer(serializers.Serializer):
username_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=128), label=_('Username'),
help_text=common_help_text
help_text=const.common_help_text
)
class LoginAssetACLAssestsSerializer(serializers.Serializer):
ip_group_help_text = _(
'Format for comma-delimited string, with * indicating a match all. '
'Such as: '
'192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 '
'(Domain name support)'
)
ip_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=1024), label=_('IP'),
help_text=ip_group_help_text
help_text=const.ip_group_help_text + _('(Domain name support)')
)
hostname_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=128), label=_('Hostname'),
help_text=common_help_text
help_text=const.common_help_text
)
class LoginAssetACLSystemUsersSerializer(serializers.Serializer):
protocol_group_help_text = _(
'Format for comma-delimited string, with * indicating a match all. '
'Protocol options: {}'
)
name_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=128), label=_('Name'),
help_text=common_help_text
help_text=const.common_help_text
)
username_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=128), label=_('Username'),
help_text=common_help_text
help_text=const.common_help_text
)
protocol_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=16), label=_('Protocol'),
help_text=protocol_group_help_text.format(
', '.join([SystemUser.PROTOCOL_SSH, SystemUser.PROTOCOL_TELNET])
help_text=const.common_help_text + _('Protocol options: {}').format(
', '.join(SystemUser.ASSET_CATEGORY_PROTOCOLS)
)
)
@@ -76,15 +62,11 @@ class LoginAssetACLSerializer(BulkOrgResourceModelSerializer):
class Meta:
model = models.LoginAssetACL
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 = [
'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_m2m = ['reviewers', 'reviewers_amount']
fields = fields_small + fields_m2m
extra_kwargs = {
"reviewers": {'allow_null': False, 'required': True},
'priority': {'default': 50},

View File

@@ -44,19 +44,15 @@ class ApplicationSerializerMixin(serializers.Serializer):
class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSerializer):
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category(Display)'))
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type(Dispaly)'))
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category'))
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type'))
class Meta:
model = models.Application
fields_mini = ['id', 'name']
fields_small = fields_mini + [
'category', 'category_display', 'type', 'type_display', 'attrs',
'date_created', 'date_updated',
'created_by', 'comment'
fields = [
'id', 'name', 'category', 'category_display', 'type', 'type_display', 'attrs',
'domain', 'created_by', 'date_created', 'date_updated', 'comment'
]
fields_fk = ['domain']
fields = fields_small + fields_fk
read_only_fields = [
'created_by', 'date_created', 'date_updated', 'get_type_display',
]

View File

@@ -39,14 +39,14 @@ class RemoteAppSerializer(serializers.Serializer):
@staticmethod
def get_asset_info(obj):
asset_id = obj.get('asset')
if not asset_id or not is_uuid(asset_id):
if not asset_id or is_uuid(asset_id):
return {}
try:
asset = Asset.objects.get(id=str(asset_id))
asset = Asset.objects.filter(id=str(asset_id)).values_list('id', 'hostname')
except ObjectDoesNotExist as e:
logger.error(e)
return {}
if not asset:
return {}
asset_info = {'id': str(asset.id), 'hostname': asset.hostname}
asset_info = {'id': str(asset[0]), 'hostname': asset[1]}
return asset_info

View File

@@ -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 Node
from ..models import Asset, Node, SystemUser
from .. import serializers
from ..tasks import (
test_asset_users_connectivity_manual
test_asset_users_connectivity_manual, push_system_user_a_asset_manual
)
@@ -100,6 +100,12 @@ 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)

View File

@@ -1,25 +1,15 @@
# -*- 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 orgs.utils import tmp_to_root_org
from tickets.models import Ticket
from tickets.api import GenericTicketStatusRetrieveCloseAPI
from ..hands import IsOrgAdmin, IsAppUser
from ..hands import IsOrgAdmin
from ..models import CommandFilter, CommandFilterRule
from .. import serializers
__all__ = [
'CommandFilterViewSet', 'CommandFilterRuleViewSet', 'CommandConfirmAPI',
'CommandConfirmStatusAPI'
]
__all__ = ['CommandFilterViewSet', 'CommandFilterRuleViewSet']
class CommandFilterViewSet(OrgBulkModelViewSet):
@@ -45,50 +35,3 @@ 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

View File

@@ -28,7 +28,6 @@ from ..tasks import (
)
from .. import serializers
from .mixin import SerializeToTreeNodeMixin
from assets.locks import NodeAddChildrenLock
logger = get_logger(__file__)
@@ -71,8 +70,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_offspring_assets():
error = _("Deletion failed and the node contains assets")
if node.has_children_or_has_assets():
error = _("Deletion failed and the node contains children or assets")
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
return super().destroy(request, *args, **kwargs)
@@ -115,16 +114,15 @@ class NodeChildrenApi(generics.ListCreateAPIView):
return super().initial(request, *args, **kwargs)
def perform_create(self, serializer):
with NodeAddChildrenLock(self.instance):
data = serializer.validated_data
_id = data.get("id")
value = data.get("value")
if not value:
value = self.instance.get_next_child_preset_name()
node = self.instance.create_child(value=value, _id=_id)
# 避免查询 full value
node._full_value = node.value
serializer.instance = node
data = serializer.validated_data
_id = data.get("id")
value = data.get("value")
if not value:
value = self.instance.get_next_child_preset_name()
node = self.instance.create_child(value=value, _id=_id)
# 避免查询 full value
node._full_value = node.value
serializer.instance = node
def get_object(self):
pk = self.kwargs.get('pk') or self.request.query_params.get('id')
@@ -223,8 +221,7 @@ class NodeAddChildrenApi(generics.UpdateAPIView):
serializer_class = serializers.NodeAddChildrenSerializer
instance = None
def update(self, request, *args, **kwargs):
""" 同时支持 put 和 patch 方法"""
def put(self, request, *args, **kwargs):
instance = self.get_object()
node_ids = request.data.get("nodes")
children = Node.objects.filter(id__in=node_ids)

View File

@@ -98,8 +98,8 @@ class SystemUserTaskApi(generics.CreateAPIView):
return task
@staticmethod
def do_test(system_user, asset_ids):
task = test_system_user_connectivity_manual.delay(system_user, asset_ids)
def do_test(system_user):
task = test_system_user_connectivity_manual.delay(system_user)
return task
def get_object(self):
@@ -109,20 +109,16 @@ class SystemUserTaskApi(generics.CreateAPIView):
def perform_create(self, serializer):
action = serializer.validated_data["action"]
asset = serializer.validated_data.get('asset')
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
assets = serializer.validated_data.get('assets') or []
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, asset_ids)
task = self.do_test(system_user)
data = getattr(serializer, '_data', {})
data["task"] = task.id
setattr(serializer, '_data', data)

View File

@@ -1,6 +1,5 @@
from orgs.utils import current_org
from common.utils.lock import DistributedLock
from assets.models import Node
class NodeTreeUpdateLock(DistributedLock):
@@ -19,11 +18,3 @@ class NodeTreeUpdateLock(DistributedLock):
def __init__(self):
name = self.get_name()
super().__init__(name=name, release_on_transaction_commit=True, reentrant=True)
class NodeAddChildrenLock(DistributedLock):
name_template = 'assets.node.add_children.<org_id:{org_id}>'
def __init__(self, node: Node):
name = self.name_template.format(org_id=node.org_id)
super().__init__(name=name, release_on_transaction_commit=True)

View File

@@ -42,9 +42,7 @@ def change_key0_to_key1(apps, schema_editor):
key_list = n.key.split(':')
key_list[0] = '1'
new_key = ':'.join(key_list)
new_parent_key = ':'.join(key_list[:-1])
n.key = new_key
n.parent_key = new_parent_key
n.save()
print('--> Modify key ( {} > {} )'.format(old_key, new_key))

View File

@@ -1,25 +0,0 @@
# 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'),
),
]

View File

@@ -35,7 +35,7 @@ def default_node():
try:
from .node import Node
root = Node.org_root()
return Node.objects.filter(id=root.id)
return root
except:
return None

View File

@@ -41,12 +41,11 @@ class CommandFilterRule(OrgModelMixin):
(TYPE_COMMAND, _('Command')),
)
ACTION_UNKNOWN = 10
class ActionChoices(models.IntegerChoices):
deny = 0, _('Deny')
allow = 1, _('Allow')
confirm = 2, _('Reconfirm')
ACTION_DENY, ACTION_ALLOW, ACTION_UNKNOWN = range(3)
ACTION_CHOICES = (
(ACTION_DENY, _('Deny')),
(ACTION_ALLOW, _('Allow')),
)
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
filter = models.ForeignKey('CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules')
@@ -54,13 +53,7 @@ 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=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")
)
action = models.IntegerField(default=ACTION_DENY, choices=ACTION_CHOICES, verbose_name=_("Action"))
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)
@@ -96,32 +89,10 @@ class CommandFilterRule(OrgModelMixin):
if not found:
return self.ACTION_UNKNOWN, ''
if self.action == self.ActionChoices.allow:
return self.ActionChoices.allow, found.group()
if self.action == self.ACTION_ALLOW:
return self.ACTION_ALLOW, found.group()
else:
return self.ActionChoices.deny, found.group()
return self.ACTION_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

View File

@@ -38,7 +38,8 @@ def compute_parent_key(key):
class NodeQuerySet(models.QuerySet):
pass
def delete(self):
raise NotImplementedError
class FamilyMixin:
@@ -306,15 +307,6 @@ class NodeAllAssetsMappingMixin:
org_id = str(org_id)
cls.orgid_nodekey_assetsid_mapping.pop(org_id, None)
@classmethod
def expire_all_orgs_node_all_asset_ids_mapping_from_memory(cls):
orgs = Organization.objects.all()
org_ids = [str(org.id) for org in orgs]
org_ids.append(Organization.ROOT_ID)
for id in org_ids:
cls.expire_node_all_asset_ids_mapping_from_memory(id)
# get order: from memory -> (from cache -> to generate)
@classmethod
def get_node_all_asset_ids_mapping_from_cache_or_generate_to_cache(cls, org_id):
@@ -621,14 +613,14 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
tree_node = TreeNode(**data)
return tree_node
def has_offspring_assets(self):
# 拥有后代资产
return self.get_all_assets().exists()
def has_children_or_has_assets(self):
if self.children or self.get_assets().exists():
return True
return False
def delete(self, using=None, keep_parents=False):
if self.has_offspring_assets():
if self.has_children_or_has_assets():
return
self.all_children.delete()
return super().delete(using=using, keep_parents=keep_parents)
def update_child_full_value(self):

View File

@@ -196,9 +196,9 @@ class SystemUser(BaseUser):
def is_command_can_run(self, command):
for rule in self.cmd_filter_rules:
action, matched_cmd = rule.match(command)
if action == rule.ActionChoices.allow:
if action == rule.ACTION_ALLOW:
return True, None
elif action == rule.ActionChoices.deny:
elif action == rule.ACTION_DENY:
return False, matched_cmd
return True, None

View File

@@ -16,14 +16,10 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class Meta:
model = AdminUser
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 = [
'id', 'name', 'username', 'password', 'private_key', 'public_key',
'comment', 'assets_amount', 'date_created', 'date_updated', 'created_by',
]
fields_fk = ['assets_amount']
fields = fields_small + fields_fk
read_only_fields = ['date_created', 'date_updated', 'created_by', 'assets_amount']
extra_kwargs = {

View File

@@ -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, default=['ssh/22'])
protocols = ProtocolsField(label=_('Protocols'), required=False)
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)

View File

@@ -22,11 +22,10 @@ class AssetUserWriteSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializ
class Meta:
model = AuthBook
list_serializer_class = AdaptedBulkListSerializer
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
fields = [
'id', 'username', 'password', 'private_key', "public_key",
'asset', 'comment',
]
extra_kwargs = {
'username': {'required': True},
'password': {'write_only': True},
@@ -53,15 +52,11 @@ class AssetUserReadSerializer(AssetUserWriteSerializer):
'date_created', 'date_updated',
'created_by', 'version',
)
fields_mini = ['id', 'username']
fields_write_only = ['password', 'private_key', "public_key"]
fields_small = fields_mini + fields_write_only + [
'backend', 'version',
'date_created', "date_updated",
'comment'
fields = [
'id', 'username', 'password', 'private_key', "public_key",
'asset', 'hostname', 'ip', 'backend', 'version',
'date_created', "date_updated", 'comment',
]
fields_fk = ['asset', 'hostname', 'ip']
fields = fields_small + fields_fk
extra_kwargs = {
'username': {'required': True},
'password': {'write_only': True},

View File

@@ -4,11 +4,8 @@ import re
from rest_framework import serializers
from common.drf.serializers import AdaptedBulkListSerializer
from ..models import CommandFilter, CommandFilterRule
from ..models import CommandFilter, CommandFilterRule, SystemUser
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from orgs.utils import tmp_to_root_org
from common.utils import get_object_or_none, lazyproperty
from terminal.models import Session
class CommandFilterSerializer(BulkOrgResourceModelSerializer):
@@ -16,16 +13,11 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer):
class Meta:
model = CommandFilter
list_serializer_class = AdaptedBulkListSerializer
fields_mini = ['id', 'name']
fields_small = fields_mini + [
'org_id', 'org_name',
'is_active',
'date_created', 'date_updated',
'comment', 'created_by',
fields = [
'id', 'name', 'org_id', 'org_name', 'is_active', 'comment',
'created_by', 'date_created', 'date_updated', 'rules', 'system_users'
]
fields_fk = ['rules']
fields_m2m = ['system_users']
fields = fields_small + fields_fk + fields_m2m
extra_kwargs = {
'rules': {'read_only': True},
'system_users': {'required': False},
@@ -42,28 +34,13 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
fields_mini = ['id']
fields_small = fields_mini + [
'type', 'type_display', 'content', 'priority',
'action', 'action_display', 'reviewers',
'date_created', 'date_updated',
'comment', 'created_by',
'action', 'action_display',
'comment', 'created_by', 'date_created', 'date_updated'
]
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:
@@ -73,35 +50,3 @@ 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

View File

@@ -48,22 +48,13 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class Meta:
model = Gateway
list_serializer_class = AdaptedBulkListSerializer
fields_mini = ['id', 'name']
fields_write_only = [
'password', 'private_key', 'public_key',
fields = [
'id', 'name', 'ip', 'port', 'protocol', 'username', 'password',
'private_key', 'public_key', 'domain', 'is_active', 'date_created',
'date_updated', 'created_by', 'comment',
]
fields_small = fields_mini + fields_write_only + [
'username', 'ip', 'port', 'protocol',
'is_active',
'date_created', 'date_updated',
'created_by', 'comment',
]
fields_fk = ['domain']
fields = fields_small + fields_fk
extra_kwargs = {
'password': {'write_only': True, 'validators': [NoSpecialChars()]},
'private_key': {"write_only": True},
'public_key': {"write_only": True},
'password': {'validators': [NoSpecialChars()]}
}
def __init__(self, *args, **kwargs):
@@ -78,12 +69,12 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class GatewayWithAuthSerializer(GatewaySerializer):
class Meta(GatewaySerializer.Meta):
extra_kwargs = {
'password': {'write_only': False, 'validators': [NoSpecialChars()]},
'private_key': {"write_only": False},
'public_key': {"write_only": False},
}
def get_field_names(self, declared_fields, info):
fields = super().get_field_names(declared_fields, info)
fields.extend(
['password', 'private_key']
)
return fields
class DomainWithGatewaySerializer(BulkOrgResourceModelSerializer):

View File

@@ -10,14 +10,11 @@ from ..models import GatheredUser
class GatheredUserSerializer(OrgResourceModelSerializerMixin):
class Meta:
model = GatheredUser
fields_mini = ['id']
fields_small = fields_mini + [
'username', 'ip_last_login',
'present',
'date_last_login', 'date_created', 'date_updated'
fields = [
'id', 'asset', 'hostname', 'ip', 'username',
'date_last_login', 'ip_last_login',
'present', 'date_created', 'date_updated'
]
fields_fk = ['asset', 'hostname', 'ip']
fields = fields_small + fields_fk
read_only_fields = fields
extra_kwargs = {
'hostname': {'label': _("Hostname")},

View File

@@ -15,15 +15,10 @@ class LabelSerializer(BulkOrgResourceModelSerializer):
class Meta:
model = Label
fields_mini = ['id', 'name']
fields_small = fields_mini + [
'value', 'category', 'category_display',
'is_active',
'date_created',
'comment',
fields = [
'id', 'name', 'value', 'category', 'is_active', 'comment',
'date_created', 'asset_count', 'assets', 'category_display'
]
fields_m2m = ['asset_count', 'assets']
fields = fields_small + fields_m2m
read_only_fields = (
'category', 'date_created', 'asset_count',
)

View File

@@ -26,18 +26,16 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class Meta:
model = SystemUser
list_serializer_class = AdaptedBulkListSerializer
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 = [
'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_m2m = [ 'cmd_filters', 'assets_amount']
fields = fields_small + fields_m2m
extra_kwargs = {
'password': {"write_only": True},
'public_key': {"write_only": True},
@@ -103,12 +101,6 @@ 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
@@ -155,18 +147,17 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class SystemUserListSerializer(SystemUserSerializer):
class Meta(SystemUserSerializer.Meta):
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 = [
'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_m2m = ["assets_amount",]
fields = fields_small + fields_m2m
extra_kwargs = {
'password': {"write_only": True},
'public_key': {"write_only": True},
@@ -187,15 +178,15 @@ class SystemUserListSerializer(SystemUserSerializer):
class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
class Meta(SystemUserSerializer.Meta):
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 = [
'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 = fields_small
extra_kwargs = {
'nodes_amount': {'label': _('Node')},
'assets_amount': {'label': _('Asset')},

View File

@@ -13,7 +13,6 @@ from common.signals import django_ready
from common.utils.connection import RedisPubSub
from common.utils import get_logger
from assets.models import Asset, Node
from orgs.models import Organization
logger = get_logger(__file__)
@@ -37,18 +36,13 @@ node_assets_mapping_for_memory_pub_sub = NodeAssetsMappingForMemoryPubSub()
def expire_node_assets_mapping_for_memory(org_id):
# 所有进程清除(自己的 memory 数据)
org_id = str(org_id)
root_org_id = Organization.ROOT_ID
node_assets_mapping_for_memory_pub_sub.publish(org_id)
# 当前进程清除(cache 数据)
logger.debug(
"Expire node assets id mapping from cache of org={}, pid={}"
"".format(org_id, os.getpid())
)
Node.expire_node_all_asset_ids_mapping_from_cache(org_id)
Node.expire_node_all_asset_ids_mapping_from_cache(root_org_id)
node_assets_mapping_for_memory_pub_sub.publish(org_id)
node_assets_mapping_for_memory_pub_sub.publish(root_org_id)
@receiver(post_save, sender=Node)
@@ -79,22 +73,16 @@ def subscribe_node_assets_mapping_expire(sender, **kwargs):
logger.debug("Start subscribe for expire node assets id mapping from memory")
def keep_subscribe():
while True:
try:
subscribe = node_assets_mapping_for_memory_pub_sub.subscribe()
for message in subscribe.listen():
if message["type"] != "message":
continue
org_id = message['data'].decode()
Node.expire_node_all_asset_ids_mapping_from_memory(org_id)
logger.debug(
"Expire node assets id mapping from memory of org={}, pid={}"
"".format(str(org_id), os.getpid())
)
except Exception as e:
logger.exception(f'subscribe_node_assets_mapping_expire: {e}')
Node.expire_all_orgs_node_all_asset_ids_mapping_from_memory()
subscribe = node_assets_mapping_for_memory_pub_sub.subscribe()
for message in subscribe.listen():
if message["type"] != "message":
continue
org_id = message['data'].decode()
Node.expire_node_all_asset_ids_mapping_from_memory(org_id)
logger.debug(
"Expire node assets id mapping from memory of org={}, pid={}"
"".format(str(org_id), os.getpid())
)
t = threading.Thread(target=keep_subscribe)
t.daemon = True
t.start()

View File

@@ -56,7 +56,6 @@ 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,
'comment': comment
}

View File

@@ -5,7 +5,6 @@ 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
@@ -97,12 +96,9 @@ 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, asset_ids=None):
def test_system_user_connectivity_manual(system_user):
task_name = _("Test system user connectivity: {}").format(system_user)
if asset_ids:
assets = Asset.objects.filter(id__in=asset_ids)
else:
assets = system_user.get_related_assets()
assets = system_user.get_related_assets()
test_system_user_connectivity_util(system_user, assets, task_name)

View File

@@ -63,9 +63,6 @@ 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 = [

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.1 on 2021-04-14 06:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('audits', '0011_userloginlog_backend'),
]
operations = [
migrations.AlterField(
model_name='userloginlog',
name='type',
field=models.CharField(choices=[('W', 'Web'), ('T', 'Terminal'), ('U', 'Unknown')], max_length=2, verbose_name='Login type'),
),
]

View File

@@ -79,7 +79,6 @@ class UserLoginLog(models.Model):
LOGIN_TYPE_CHOICE = (
('W', 'Web'),
('T', 'Terminal'),
('U', 'Unknown'),
)
MFA_DISABLED = 0

View File

@@ -16,14 +16,10 @@ class FTPLogSerializer(serializers.ModelSerializer):
class Meta:
model = models.FTPLog
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
fields = (
'id', 'user', 'remote_addr', 'asset', 'system_user', 'org_id',
'operate', 'filename', 'is_success', 'date_start', 'operate_display'
)
class UserLoginLogSerializer(serializers.ModelSerializer):
@@ -33,14 +29,11 @@ class UserLoginLogSerializer(serializers.ModelSerializer):
class Meta:
model = models.UserLoginLog
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
fields = (
'id', 'username', 'type', 'type_display', 'ip', 'city', 'user_agent',
'mfa', 'reason', 'status', 'status_display', 'datetime', 'mfa_display',
'backend'
)
extra_kwargs = {
"user_agent": {'label': _('User agent')}
}
@@ -49,13 +42,10 @@ class UserLoginLogSerializer(serializers.ModelSerializer):
class OperateLogSerializer(serializers.ModelSerializer):
class Meta:
model = models.OperateLog
fields_mini = ['id']
fields_small = fields_mini + [
'user', 'action', 'resource_type', 'resource', 'remote_addr',
'datetime',
'org_id'
]
fields = fields_small
fields = (
'id', 'user', 'action', 'resource_type', 'resource',
'remote_addr', 'datetime', 'org_id'
)
class PasswordChangeLogSerializer(serializers.ModelSerializer):

View File

@@ -27,23 +27,11 @@ json_render = JSONRenderer()
MODELS_NEED_RECORD = (
# 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',
'User', 'UserGroup', 'Asset', 'Node', 'AdminUser', 'SystemUser',
'Domain', 'Gateway', 'Organization', 'AssetPermission', 'CommandFilter',
'CommandFilterRule', 'License', 'Setting', 'Account', 'SyncInstanceTask',
'Platform', 'ChangeAuthPlan', 'GatherUserTask',
'RemoteApp', 'RemoteAppPermission', 'DatabaseApp', 'DatabaseAppPermission',
)
@@ -56,9 +44,6 @@ class AuthBackendLabelMapping(LazyObject):
backend_label_mapping[backend] = source.label
backend_label_mapping[settings.AUTH_BACKEND_PUBKEY] = _('SSH Key')
backend_label_mapping[settings.AUTH_BACKEND_MODEL] = _('Password')
backend_label_mapping[settings.AUTH_BACKEND_SSO] = _('SSO')
backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom')
backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk')
return backend_label_mapping
def _setup(self):
@@ -160,7 +145,7 @@ def generate_data(username, request):
user_agent = request.META.get('HTTP_USER_AGENT', '')
login_ip = get_request_ip(request) or '0.0.0.0'
if isinstance(request, Request):
login_type = request.META.get('HTTP_X_JMS_LOGIN_TYPE', 'U')
login_type = request.META.get('HTTP_X_JMS_LOGIN_TYPE', '')
else:
login_type = 'W'
@@ -168,7 +153,7 @@ def generate_data(username, request):
'username': username,
'ip': login_ip,
'type': login_type,
'user_agent': user_agent[0:254],
'user_agent': user_agent,
'datetime': timezone.now(),
'backend': get_login_backend(request)
}

View File

@@ -7,6 +7,3 @@ from .mfa import *
from .access_key import *
from .login_confirm import *
from .sso import *
from .wecom import *
from .dingtalk import *
from .password import *

View File

@@ -1,7 +1,5 @@
# -*- 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
@@ -79,7 +77,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
})
key = self.CACHE_KEY_PREFIX.format(token)
cache.set(key, value, timeout=30*60)
cache.set(key, value, timeout=20)
return token
def create(self, request, *args, **kwargs):
@@ -102,7 +100,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
'desktopwidth:i': '1280',
'desktopheight:i': '800',
'use multimon:i': '1',
'session bpp:i': '32',
'session bpp:i': '24',
'audiomode:i': '0',
'disable wallpaper:i': '0',
'disable full window drag:i': '0',
@@ -142,16 +140,15 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
# Todo: 上线后地址是 JumpServerAddr:3389
address = self.request.query_params.get('address') or '1.1.1.1'
options['full address:s'] = address
options['username:s'] = '{}|{}'.format(user.username, token)
options['username:s'] = '{}@{}'.format(user.username, token)
options['desktopwidth:i'] = width
options['desktopheight:i'] = height
data = ''
for k, v in options.items():
data += f'{k}:{v}\n'
response = HttpResponse(data, content_type='application/octet-stream')
response = HttpResponse(data, content_type='text/plain')
filename = "{}-{}-jumpserver.rdp".format(user.username, asset.hostname)
filename = urllib.parse.quote(filename)
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
response['Content-Disposition'] = 'attachment; filename={}'.format(filename)
return response
@staticmethod

View File

@@ -1,35 +0,0 @@
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,)

View File

@@ -29,7 +29,7 @@ class MFAChallengeApi(AuthMixin, CreateAPIView):
if not valid:
self.request.session['auth_mfa'] = ''
raise errors.MFAFailedError(
username=user.username, request=self.request, ip=self.get_request_ip()
username=user.username, request=self.request
)
else:
self.request.session['auth_mfa'] = '1'

View File

@@ -1,26 +0,0 @@
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()

View File

@@ -1,35 +0,0 @@
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,)

View File

@@ -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 as DJModelBackend
from django.contrib.auth.backends import ModelBackend
from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import authentication, exceptions
from common.auth import signature
@@ -25,11 +25,6 @@ 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,
@@ -210,21 +205,3 @@ 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

View File

@@ -6,7 +6,9 @@ from django.conf import settings
from common.exceptions import JMSException
from .signals import post_auth_failed
from users.utils import LoginBlockUtil, MFABlockUtils
from users.utils import (
increase_login_failed_count, get_login_failed_count
)
reason_password_failed = 'password_failed'
reason_password_decrypt_failed = 'password_decrypt_failed'
@@ -16,10 +18,8 @@ reason_user_not_exist = 'user_not_exist'
reason_password_expired = 'password_expired'
reason_user_invalid = 'user_invalid'
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'),
@@ -30,10 +30,8 @@ reason_choices = {
reason_password_expired: _("Password expired"),
reason_user_invalid: _('Disabled or expired'),
reason_user_inactive: _("This account is inactive."),
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")
reason_acl_not_allow: _("Login IP is not allowed")
}
old_reason_choices = {
'0': '-',
@@ -54,15 +52,7 @@ block_login_msg = _(
"The account has been locked "
"(please contact admin to unlock it or try again after {} minutes)"
)
block_mfa_msg = _(
"The account has been locked "
"(please contact admin to unlock it or try again after {} minutes)"
)
mfa_failed_msg = _(
"MFA code invalid, or ntp sync server time, "
"You can also try {times_try} times "
"(The account will be temporarily locked for {block_time} minutes)"
)
mfa_failed_msg = _("MFA code invalid, or ntp sync server time")
mfa_required_msg = _("MFA required")
mfa_unset_msg = _("MFA not set, please set it first")
@@ -90,7 +80,7 @@ class AuthFailedNeedBlockMixin:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
LoginBlockUtil(self.username, self.ip).incr_failed_count()
increase_login_failed_count(self.username, self.ip)
class AuthFailedError(Exception):
@@ -117,12 +107,13 @@ class AuthFailedError(Exception):
class CredentialError(AuthFailedNeedLogMixin, AuthFailedNeedBlockMixin, AuthFailedError):
def __init__(self, error, username, ip, request):
super().__init__(error=error, username=username, ip=ip, request=request)
util = LoginBlockUtil(username, ip)
times_remainder = util.get_remainder_times()
times_up = settings.SECURITY_LOGIN_LIMIT_COUNT
times_failed = get_login_failed_count(username, ip)
times_try = int(times_up) - int(times_failed)
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
default_msg = invalid_login_msg.format(
times_try=times_remainder, block_time=block_time
times_try=times_try, block_time=block_time
)
if error == reason_password_failed:
self.msg = default_msg
@@ -132,32 +123,12 @@ class CredentialError(AuthFailedNeedLogMixin, AuthFailedNeedBlockMixin, AuthFail
class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError):
error = reason_mfa_failed
msg: str
msg = mfa_failed_msg
def __init__(self, username, request, ip):
util = MFABlockUtils(username, ip)
util.incr_failed_count()
times_remainder = util.get_remainder_times()
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
if times_remainder:
self.msg = mfa_failed_msg.format(
times_try=times_remainder, block_time=block_time
)
else:
self.msg = block_mfa_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
def __init__(self, username, request):
super().__init__(username=username, request=request)
class BlockMFAError(AuthFailedNeedLogMixin, AuthFailedError):
error = 'block_mfa'
def __init__(self, username, request, ip):
self.msg = block_mfa_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
super().__init__(username=username, request=request, ip=ip)
class MFAUnsetError(AuthFailedNeedLogMixin, AuthFailedError):
error = reason_mfa_unset
msg = mfa_unset_msg
@@ -213,28 +184,6 @@ class MFARequiredError(NeedMoreInfoError):
}
class ACLError(AuthFailedNeedLogMixin, AuthFailedError):
msg = reason_acl_not_allow
error = 'acl_error'
def __init__(self, msg, **kwargs):
self.msg = msg
super().__init__(**kwargs)
def as_data(self):
return {
"error": reason_acl_not_allow,
"msg": self.msg
}
class LoginIPNotAllowed(ACLError):
def __init__(self, username, request, **kwargs):
self.username = username
self.request = request
super().__init__(_("IP is not allowed"), **kwargs)
class LoginConfirmBaseError(NeedMoreInfoError):
def __init__(self, ticket_id, **kwargs):
self.ticket_id = ticket_id
@@ -277,15 +226,6 @@ 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')
@@ -293,28 +233,3 @@ 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')

View File

@@ -5,19 +5,19 @@ 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, redirect
from django.shortcuts import reverse
from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get, FlashMessageUtil
from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get
from users.models import User
from users.utils import LoginBlockUtil, MFABlockUtils
from users.utils import (
is_block_login, clean_failed_count
)
from . import errors
from .utils import rsa_decrypt
from .signals import post_auth_success, post_auth_failed
@@ -83,8 +83,6 @@ 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()
@@ -113,9 +111,13 @@ class AuthMixin:
ip = ip or get_request_ip(self.request)
return ip
def _check_is_block(self, username, raise_exception=True):
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")
ip = self.get_request_ip()
if LoginBlockUtil(username, ip).is_block():
if is_block_login(username, ip):
logger.warn('Ip was blocked' + ': ' + username + ':' + ip)
exception = errors.BlockLoginError(username=username, ip=ip)
if raise_exception:
@@ -123,13 +125,6 @@ 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)
@@ -146,9 +141,6 @@ 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'):
@@ -160,7 +152,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._set_partial_credential_error(username=username, ip=ip, request=request)
self.partial_credential_error = partial(errors.CredentialError, username=username, ip=ip, request=request)
if decrypt_passwd:
password = self.decrypt_passwd(password)
@@ -181,7 +173,7 @@ class AuthMixin:
if not user:
self.raise_credential_error(errors.reason_password_failed)
elif user.is_expired:
self.raise_credential_error(errors.reason_user_expired)
self.raise_credential_error(errors.reason_user_inactive)
elif not user.is_active:
self.raise_credential_error(errors.reason_user_inactive)
return user
@@ -191,22 +183,7 @@ class AuthMixin:
from acls.models import LoginACL
is_allowed = LoginACL.allow_user_to_login(user, ip)
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
raise self.raise_credential_error(error=errors.reason_acl_not_allow)
def check_user_auth(self, decrypt_passwd=False):
self.check_is_block()
@@ -219,71 +196,42 @@ 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()
clean_failed_count(username, ip)
request.session['auth_password'] = 1
request.session['user_id'] = str(user.id)
request.session['auto_login'] = auto_login
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, message):
def generate_reset_password_url_with_flash_msg(cls, user: User, flash_view_name):
reset_passwd_url = reverse('authentication:reset-password')
query_str = urlencode({
'token': user.generate_reset_token()
})
reset_passwd_url = f'{reset_passwd_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)
flash_page_url = reverse(flash_view_name)
query_str = urlencode({
'redirect_url': reset_passwd_url
})
return f'{flash_page_url}?{query_str}'
@classmethod
def _check_passwd_is_too_simple(cls, user: User, password):
if user.is_superuser and password == 'admin':
message = _('Your password is too simple, please change it for security')
url = cls.generate_reset_password_url_with_flash_msg(user, message=message)
url = cls.generate_reset_password_url_with_flash_msg(
user, 'authentication:passwd-too-simple-flash-msg'
)
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:
message = _('Your password has expired, please reset before logging in')
url = cls.generate_reset_password_url_with_flash_msg(user, message)
url = cls.generate_reset_password_url_with_flash_msg(
user, 'authentication:passwd-has-expired-flash-msg'
)
raise errors.PasswordRequireResetError(url)
def check_user_auth_if_need(self, decrypt_passwd=False):
@@ -305,34 +253,15 @@ class AuthMixin:
raise errors.MFAUnsetError(user, self.request, url)
raise errors.MFARequiredError()
def mark_mfa_ok(self):
self.request.session['auth_mfa'] = 1
self.request.session['auth_mfa_time'] = time.time()
self.request.session['auth_mfa_type'] = 'otp'
def check_mfa_is_block(self, username, ip, raise_exception=True):
if MFABlockUtils(username, ip).is_block():
logger.warn('Ip was blocked' + ': ' + username + ':' + ip)
exception = errors.BlockMFAError(username=username, request=self.request, ip=ip)
if raise_exception:
raise exception
else:
return exception
def check_user_mfa(self, code):
user = self.get_user_from_session()
ip = self.get_request_ip()
self.check_mfa_is_block(user.username, ip)
ok = user.check_mfa(code)
if ok:
self.mark_mfa_ok()
self.request.session['auth_mfa'] = 1
self.request.session['auth_mfa_time'] = time.time()
self.request.session['auth_mfa_type'] = 'otp'
return
raise errors.MFAFailedError(
username=user.username,
request=self.request,
ip=ip
)
raise errors.MFAFailedError(username=user.username, request=self.request)
def get_ticket(self):
from tickets.models import Ticket
@@ -399,10 +328,3 @@ 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)

View File

@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
#
from django.utils import timezone
from rest_framework import serializers
from common.utils import get_object_or_none
@@ -8,16 +7,14 @@ 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
from .models import AccessKey, LoginConfirmSetting, SSOToken
__all__ = [
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer',
'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', 'SSOTokenSerializer',
'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer', 'RDPFileSerializer',
'PasswordVerifySerializer',
'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer', 'RDPFileSerializer'
]
@@ -32,10 +29,6 @@ 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,
@@ -51,10 +44,6 @@ class BearerTokenSerializer(serializers.Serializer):
def get_keyword(obj):
return 'Bearer'
def update_last_login(self, user):
user.last_login = timezone.now()
user.save(update_fields=['last_login'])
def create(self, validated_data):
request = self.context.get('request')
if request.user and not request.user.is_anonymous:
@@ -67,8 +56,6 @@ class BearerTokenSerializer(serializers.Serializer):
"user id {} not exist".format(user_id)
)
token, date_expired = user.create_bearer_token(request)
self.update_last_login(user)
instance = {
"token": token,
"date_expired": date_expired,
@@ -156,11 +143,9 @@ class ConnectionTokenUserSerializer(serializers.ModelSerializer):
class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
protocols = ProtocolsField(label='Protocols', read_only=True)
class Meta:
model = Asset
fields = ['id', 'hostname', 'ip', 'protocols', 'org_id']
fields = ['id', 'hostname', 'ip', 'port', 'org_id']
class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer):

View File

@@ -117,15 +117,6 @@
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>
@@ -191,10 +182,10 @@
</div>
<div>
{% if AUTH_OPENID or AUTH_CAS or AUTH_WECOM or AUTH_DINGTALK %}
{% if AUTH_OPENID or AUTH_CAS %}
<div class="hr-line-dashed"></div>
<div style="display: inline-block; float: left">
<b class="text-muted text-left" >{% trans "More login options" %}</b>
<b class="text-muted text-left" style="margin-right: 10px">{% 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' %}
@@ -205,17 +196,6 @@
<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;">

View File

@@ -14,19 +14,13 @@ 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')
]
urlpatterns += router.urls

View File

@@ -18,25 +18,14 @@ 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'),

View File

@@ -2,5 +2,3 @@
#
from .login import *
from .mfa import *
from .wecom import *
from .dingtalk import *

View File

@@ -1,266 +0,0 @@
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)

View File

@@ -4,6 +4,7 @@
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
@@ -18,7 +19,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, FlashMessageUtil
from common.utils import get_request_ip, get_object_or_none
from users.utils import (
redirect_user_first_login_or_index
)
@@ -30,6 +31,7 @@ from ..forms import get_user_login_form_cls
__all__ = [
'UserLoginView', 'UserLogoutView',
'UserLoginGuardView', 'UserLoginWaitConfirmView',
'FlashPasswdTooSimpleMsgView', 'FlashPasswdHasExpiredMsgView'
]
@@ -37,45 +39,16 @@ __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)
@@ -88,22 +61,31 @@ 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)
self.set_login_failed_mark()
ip = self.get_request_ip()
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
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, errors.PasswdNeedUpdate) as e:
except (errors.PasswdTooSimple, errors.PasswordRequireResetError) 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):
if self.check_is_need_captcha():
ip = get_request_ip(self.request)
if cache.get(self.key_prefix_captcha.format(ip)):
return get_user_login_form_cls(captcha=True)
else:
return get_user_login_form_cls()
@@ -131,8 +113,6 @@ 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
}
@@ -238,9 +218,39 @@ class UserLogoutView(TemplateView):
context = {
'title': _('Logout success'),
'messages': _('Logout success, return login page'),
'interval': 3,
'interval': 1,
'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)

View File

@@ -22,12 +22,10 @@ class UserLoginOtpView(mixins.AuthMixin, FormView):
try:
self.check_user_mfa(otp_code)
return redirect_to_guard_view()
except (errors.MFAFailedError, errors.BlockMFAError) as e:
except errors.MFAFailedError as e:
form.add_error('otp_code', e.msg)
return super().form_invalid(form)
except Exception as e:
logger.error(e)
import traceback
traceback.print_exception()
return redirect_to_guard_view()

View File

@@ -1,264 +0,0 @@
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)

View File

@@ -57,8 +57,6 @@ class BaseFileParser(BaseParser):
@staticmethod
def _replace_chinese_quote(s):
if not isinstance(s, str):
return s
trans_table = str.maketrans({
'': '"',
'': '"',
@@ -94,7 +92,7 @@ class BaseFileParser(BaseParser):
new_row_data = {}
serializer_fields = self.serializer_fields
for k, v in row_data.items():
if type(v) in [list, dict, int] or (isinstance(v, str) and k.strip() and v.strip()):
if isinstance(v, list) or isinstance(v, dict) 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)
@@ -145,5 +143,5 @@ class BaseFileParser(BaseParser):
return data
except Exception as e:
logger.error(e, exc_info=True)
raise ParseError(_('Parse file error: {}').format(e))
raise ParseError('Parse error! ({})'.format(self.media_type))

View File

@@ -39,9 +39,3 @@ 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')

View File

@@ -1,19 +0,0 @@
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()

View File

@@ -1,168 +0,0 @@
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

View File

@@ -1,23 +0,0 @@
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')

View File

@@ -1,94 +0,0 @@
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)

View File

@@ -1,78 +0,0 @@
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

View File

@@ -1,197 +0,0 @@
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

View File

@@ -6,12 +6,9 @@ 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
@@ -27,9 +24,6 @@ __all__ = [
]
UserModel = get_user_model()
class JSONResponseMixin(object):
"""JSON mixin"""
@staticmethod
@@ -53,9 +47,6 @@ class RenderToJsonMixin:
column_title_field_pairs = jms_context.get('column_title_field_pairs', ())
data['title'] = column_title_field_pairs
if isinstance(request.data, (list, tuple)) and not any(request.data):
error = _("Request file format may be wrong")
return Response(data={"error": error}, status=400)
return Response(data=data)
@@ -337,21 +328,3 @@ 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

View File

@@ -1,13 +1,11 @@
# -*- coding: utf-8 -*-
#
# coding: utf-8
from django.contrib.auth.mixins import UserPassesTestMixin
from django.utils import timezone
__all__ = ["DatetimeSearchMixin", "PermissionsMixin"]
from rest_framework import permissions
__all__ = ["DatetimeSearchMixin"]
class DatetimeSearchMixin:
@@ -38,17 +36,3 @@ class DatetimeSearchMixin:
def get(self, request, *args, **kwargs):
self.get_date_range()
return super().get(request, *args, **kwargs)
class PermissionsMixin(UserPassesTestMixin):
permission_classes = [permissions.IsAuthenticated]
def get_permissions(self):
return self.permission_classes
def test_func(self):
permission_classes = self.get_permissions()
for permission_class in permission_classes:
if not permission_class().has_permission(self.request, self):
return False
return True

View File

@@ -2,8 +2,8 @@
#
import time
from rest_framework import permissions
from django.contrib.auth.mixins import UserPassesTestMixin
from django.conf import settings
from common.exceptions import MFAVerifyRequired
from orgs.utils import current_org
@@ -95,6 +95,20 @@ class WithBootstrapToken(permissions.BasePermission):
return settings.BOOTSTRAP_TOKEN == request_bootstrap_token
class PermissionsMixin(UserPassesTestMixin):
permission_classes = [permissions.IsAuthenticated]
def get_permissions(self):
return self.permission_classes
def test_func(self):
permission_classes = self.get_permissions()
for permission_class in permission_classes:
if not permission_class().has_permission(self.request, self):
return False
return True
class UserCanAnyPermCurrentOrg(permissions.BasePermission):
def has_permission(self, request, view):
return current_org.can_any_by(request.user)
@@ -115,7 +129,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
raise MFAVerifyRequired()
return False
class CanUpdateDeleteUser(permissions.BasePermission):

View File

@@ -1,15 +0,0 @@
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

View File

@@ -1,10 +0,0 @@
from django.urls import path
from .. import views
app_name = 'common'
urlpatterns = [
# login
path('flash-message/', views.FlashMessageMsgView.as_view(), name='flash-message'),
]

View File

@@ -8,4 +8,3 @@ from .http import *
from .ipip import *
from .crypto import *
from .random import *
from .jumpserver import *

View File

@@ -75,16 +75,11 @@ 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

View File

@@ -1,31 +0,0 @@
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}'

View File

@@ -4,6 +4,7 @@ import struct
import random
import socket
import string
import secrets
string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_{}~'

View File

@@ -1,40 +0,0 @@
#
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)

View File

@@ -1,5 +1,3 @@
import time
from django.core.cache import cache
from django.utils import timezone
from django.utils.timesince import timesince
@@ -8,8 +6,6 @@ from django.http.response import JsonResponse, HttpResponse
from rest_framework.views import APIView
from rest_framework.permissions import AllowAny
from collections import Counter
from django.conf import settings
from rest_framework.response import Response
from users.models import User
from assets.models import Asset
@@ -18,7 +14,6 @@ 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']
@@ -100,7 +95,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_id', flat=True)))
count = len(set(Session.objects.filter(date_start__range=(ds, de)).values_list('user', flat=True)))
self.__set_data_to_cache(date, tp, count)
return count
@@ -130,7 +125,7 @@ class DatesLoginMetricMixin:
@lazyproperty
def dates_total_count_active_users(self):
count = len(set(self.sessions_queryset.values_list('user_id', flat=True)))
count = len(set(self.sessions_queryset.values_list('user', flat=True)))
return count
@lazyproperty
@@ -162,10 +157,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_id', flat=True)
users = self.sessions_queryset.values_list('user', flat=True)
users = [
{'user': user, 'total': total}
for user, total in Counter(users).most_common(5)
@@ -173,7 +168,7 @@ class DatesLoginMetricMixin:
return users
def get_dates_total_count_login_users(self):
return len(set(self.sessions_queryset.values_list('user_id', flat=True)))
return len(set(self.sessions_queryset.values_list('user', flat=True)))
def get_dates_total_count_login_times(self):
return self.sessions_queryset.count()
@@ -187,9 +182,8 @@ class DatesLoginMetricMixin:
return list(assets)
def get_dates_login_times_top10_users(self):
users = self.sessions_queryset.values("user_id") \
.annotate(total=Count("user_id")) \
.annotate(user=Max('user')) \
users = self.sessions_queryset.values("user") \
.annotate(total=Count("user")) \
.annotate(last=Max("date_start")).order_by("-total")[:10]
for user in users:
user['last'] = str(user['last'])
@@ -212,7 +206,26 @@ class DatesLoginMetricMixin:
return sessions
class IndexApi(DatesLoginMetricMixin, APIView):
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):
permission_classes = (IsOrgAdmin | IsOrgAuditor,)
http_method_names = ['get']
@@ -221,28 +234,26 @@ class IndexApi(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': caches.users_amount,
'total_count_users': self.get_total_count_users(),
})
if _all or query_params.get('total_count') or query_params.get('total_count_assets'):
data.update({
'total_count_assets': caches.assets_amount,
'total_count_assets': self.get_total_count_assets(),
})
if _all or query_params.get('total_count') or query_params.get('total_count_online_users'):
data.update({
'total_count_online_users': caches.total_count_online_users,
'total_count_online_users': self.get_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': caches.total_count_online_sessions,
'total_count_online_sessions': self.get_total_count_online_sessions(),
})
if _all or query_params.get('dates_metrics'):
@@ -296,68 +307,7 @@ class IndexApi(DatesLoginMetricMixin, APIView):
return JsonResponse(data, status=200)
class HealthApiMixin(APIView):
def is_token_right(self):
token = self.request.query_params.get('token')
ok_token = settings.HEALTH_CHECK_TOKEN
if ok_token and token != ok_token:
return False
return True
def check_permissions(self, request):
if not self.is_token_right():
msg = 'Health check token error, ' \
'Please set query param in url and same with setting HEALTH_CHECK_TOKEN. ' \
'eg: $PATH/?token=$HEALTH_CHECK_TOKEN'
self.permission_denied(request, message={'error': msg}, code=403)
class HealthCheckView(HealthApiMixin):
permission_classes = (AllowAny,)
@staticmethod
def get_db_status():
t1 = time.time()
try:
User.objects.first()
t2 = time.time()
return True, t2 - t1
except:
t2 = time.time()
return False, t2 - t1
def get_redis_status(self):
key = 'HEALTH_CHECK'
t1 = time.time()
try:
value = '1'
cache.set(key, '1', 10)
got = cache.get(key)
t2 = time.time()
if value == got:
return True, t2 -t1
return False, t2 -t1
except:
t2 = time.time()
return False, t2 - t1
def get(self, request):
redis_status, redis_time = self.get_redis_status()
db_status, db_time = self.get_db_status()
status = all([redis_status, db_status])
data = {
'status': status,
'db_status': db_status,
'db_time': db_time,
'redis_status': redis_status,
'redis_time': redis_time,
'time': int(time.time())
}
return Response(data)
class PrometheusMetricsApi(HealthApiMixin):
class PrometheusMetricsApi(APIView):
permission_classes = (AllowAny,)
def get(self, request, *args, **kwargs):

View File

@@ -143,7 +143,6 @@ class Config(dict):
'REDIS_DB_SESSION': 5,
'REDIS_DB_WS': 6,
'GLOBAL_ORG_DISPLAY_NAME': '',
'SITE_URL': 'http://localhost:8080',
'CAPTCHA_TEST_MODE': None,
'TOKEN_EXPIRATION': 3600 * 24,
@@ -216,16 +215,6 @@ 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',
@@ -269,7 +258,6 @@ 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',
@@ -279,7 +267,7 @@ class Config(dict):
'WINDOWS_SSH_DEFAULT_SHELL': 'cmd',
'FLOWER_URL': "127.0.0.1:5555",
'DEFAULT_ORG_SHOW_ALL_USERS': True,
'PERIOD_TASK_ENABLED': True,
'PERIOD_TASK_ENABLE': True,
'FORCE_SCRIPT_NAME': '',
'LOGIN_CONFIRM_ENABLE': False,
'WINDOWS_SKIP_ALL_MANUAL_PASSWORD': False,
@@ -300,7 +288,6 @@ class Config(dict):
'SESSION_SAVE_EVERY_REQUEST': True,
'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False,
'FORGOT_PASSWORD_URL': '',
'HEALTH_CHECK_TOKEN': ''
}
def compatible_auth_openid_of_key(self):

View File

@@ -14,8 +14,6 @@ def jumpserver_processor(request):
'LOGIN_IMAGE_URL': static('img/login_image.png'),
'FAVICON_URL': static('img/facio.ico'),
'LOGIN_CAS_LOGO_URL': static('img/login_cas_logo.png'),
'LOGIN_WECOM_LOGO_URL': static('img/login_wecom_log.png'),
'LOGIN_DINGTALK_LOGO_URL': static('img/login_dingtalk_log.png'),
'JMS_TITLE': _('JumpServer Open Source Bastion Host'),
'VERSION': settings.VERSION,
'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2021',

View File

@@ -101,26 +101,13 @@ CAS_CHECK_NEXT = lambda _next_page: True
AUTH_SSO = CONFIG.AUTH_SSO
AUTH_SSO_AUTHKEY_TTL = CONFIG.AUTH_SSO_AUTHKEY_TTL
# WECOM Auth
AUTH_WECOM = CONFIG.AUTH_WECOM
WECOM_CORPID = CONFIG.WECOM_CORPID
WECOM_AGENTID = CONFIG.WECOM_AGENTID
WECOM_SECRET = CONFIG.WECOM_SECRET
# DingDing auth
AUTH_DINGTALK = CONFIG.AUTH_DINGTALK
DINGTALK_AGENTID = CONFIG.DINGTALK_AGENTID
DINGTALK_APPKEY = CONFIG.DINGTALK_APPKEY
DINGTALK_APPSECRET = CONFIG.DINGTALK_APPSECRET
# Other setting
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE
OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS
AUTH_BACKEND_MODEL = 'authentication.backends.api.ModelBackend'
AUTH_BACKEND_MODEL = 'django.contrib.auth.backends.ModelBackend'
AUTH_BACKEND_PUBKEY = 'authentication.backends.pubkey.PublicKeyAuthBackend'
AUTH_BACKEND_LDAP = 'authentication.backends.ldap.LDAPAuthorizationBackend'
AUTH_BACKEND_OIDC_PASSWORD = 'jms_oidc_rp.backends.OIDCAuthPasswordBackend'
@@ -128,13 +115,9 @@ AUTH_BACKEND_OIDC_CODE = 'jms_oidc_rp.backends.OIDCAuthCodeBackend'
AUTH_BACKEND_RADIUS = 'authentication.backends.radius.RadiusBackend'
AUTH_BACKEND_CAS = 'authentication.backends.cas.CASBackend'
AUTH_BACKEND_SSO = 'authentication.backends.api.SSOAuthentication'
AUTH_BACKEND_WECOM = 'authentication.backends.api.WeComAuthentication'
AUTH_BACKEND_DINGTALK = 'authentication.backends.api.DingTalkAuthentication'
AUTHENTICATION_BACKENDS = [
AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY, AUTH_BACKEND_WECOM, AUTH_BACKEND_DINGTALK
]
AUTHENTICATION_BACKENDS = [AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY]
if AUTH_CAS:
AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_CAS)

View File

@@ -16,7 +16,7 @@ PROJECT_DIR = const.PROJECT_DIR
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = CONFIG.SECRET_KEY
# SECURITY WARNING: keep the token secret, remove it if all koko, lion ok
# SECURITY WARNING: keep the token secret, remove it if all coco, guacamole ok
BOOTSTRAP_TOKEN = CONFIG.BOOTSTRAP_TOKEN
# SECURITY WARNING: don't run with debug turned on in production!

View File

@@ -38,7 +38,6 @@ SECURITY_LOGIN_LIMIT_TIME = CONFIG.SECURITY_LOGIN_LIMIT_TIME # Unit: minute
SECURITY_MAX_IDLE_TIME = CONFIG.SECURITY_MAX_IDLE_TIME # Unit: minute
SECURITY_PASSWORD_EXPIRATION_TIME = CONFIG.SECURITY_PASSWORD_EXPIRATION_TIME # Unit: day
SECURITY_PASSWORD_MIN_LENGTH = CONFIG.SECURITY_PASSWORD_MIN_LENGTH # Unit: bit
OLD_PASSWORD_HISTORY_LIMIT_COUNT = CONFIG.OLD_PASSWORD_HISTORY_LIMIT_COUNT
SECURITY_PASSWORD_UPPER_CASE = CONFIG.SECURITY_PASSWORD_UPPER_CASE
SECURITY_PASSWORD_LOWER_CASE = CONFIG.SECURITY_PASSWORD_LOWER_CASE
SECURITY_PASSWORD_NUMBER = CONFIG.SECURITY_PASSWORD_NUMBER
@@ -121,7 +120,3 @@ CONNECTION_TOKEN_ENABLED = CONFIG.CONNECTION_TOKEN_ENABLED
DISK_CHECK_ENABLED = CONFIG.DISK_CHECK_ENABLED
FORGOT_PASSWORD_URL = CONFIG.FORGOT_PASSWORD_URL
# 自定义默认组织名
GLOBAL_ORG_DISPLAY_NAME = CONFIG.GLOBAL_ORG_DISPLAY_NAME
HEALTH_CHECK_TOKEN = CONFIG.HEALTH_CHECK_TOKEN

View File

@@ -23,13 +23,12 @@ api_v1 = [
path('applications/', include('applications.urls.api_urls', namespace='api-applications')),
path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')),
path('acls/', include('acls.urls.api_urls', namespace='api-acls')),
path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()),
path('prometheus/metrics/', api.PrometheusMetricsApi.as_view())
]
app_view_patterns = [
path('auth/', include('authentication.urls.view_urls'), name='auth'),
path('ops/', include('ops.urls.view_urls'), name='ops'),
path('common/', include('common.urls.view_urls'), name='common'),
re_path(r'flower/(?P<path>.*)', views.celery_flower_view, name='flower-view'),
]
@@ -49,8 +48,7 @@ urlpatterns = [
path('', views.IndexView.as_view(), name='index'),
path('api/v1/', include(api_v1)),
re_path('api/(?P<app>\w+)/(?P<version>v\d)/.*', views.redirect_format_api),
path('api/health/', api.HealthCheckView.as_view(), name="health"),
path('api/v1/health/', api.HealthCheckView.as_view(), name="health_v1"),
path('api/health/', views.HealthCheckView.as_view(), name="health"),
# External apps url
path('core/auth/captcha/', include('captcha.urls')),
path('core/', include(app_view_patterns)),

View File

@@ -1,7 +1,6 @@
from django.views.generic import TemplateView
from django.shortcuts import redirect
from common.permissions import IsValidUser
from common.mixins.views import PermissionsMixin
from common.permissions import PermissionsMixin, IsValidUser
__all__ = ['IndexView']

View File

@@ -1,21 +1,23 @@
# -*- coding: utf-8 -*-
#
import re
import time
from django.http import HttpResponseRedirect, JsonResponse, Http404
from django.conf import settings
from django.views.generic import View
from django.shortcuts import redirect
from django.utils.translation import ugettext_lazy as _
from rest_framework.views import APIView
from rest_framework.permissions import AllowAny
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
from rest_framework.views import APIView
from common.http import HttpResponseTemporaryRedirect
__all__ = [
'LunaView', 'I18NView', 'KokoView', 'WsView',
'LunaView', 'I18NView', 'KokoView', 'WsView', 'HealthCheckView',
'redirect_format_api', 'redirect_old_apps_view', 'UIView'
]
@@ -62,6 +64,13 @@ def redirect_old_apps_view(request, *args, **kwargs):
return HttpResponseTemporaryRedirect(new_path)
class HealthCheckView(APIView):
permission_classes = (AllowAny,)
def get(self, request):
return JsonResponse({"status": 1, "time": int(time.time())})
class WsView(APIView):
ws_port = settings.HTTP_LISTEN_PORT + 1
@@ -83,4 +92,3 @@ class KokoView(View):
"<div>Koko is a separately deployed program, you need to deploy Koko, configure nginx for url distribution,</div> "
"</div>If you see this page, prove that you are not accessing the nginx listening port. Good luck.</div>")
return HttpResponse(msg)

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,10 @@
from __future__ import unicode_literals
from django.utils.translation import gettext_lazy as _
from django.apps import AppConfig
class OpsConfig(AppConfig):
name = 'ops'
verbose_name = _('Operations')
def ready(self):
from orgs.models import Organization

View File

@@ -72,7 +72,6 @@ def create_or_update_celery_periodic_tasks(tasks):
crontab=crontab,
name=name,
task=detail['task'],
enabled=detail.get('enabled', True),
args=json.dumps(detail.get('args', [])),
kwargs=json.dumps(detail.get('kwargs', {})),
description=detail.get('description') or ''

View File

@@ -103,14 +103,12 @@ class PeriodTaskModelMixin(models.Model):
class PeriodTaskSerializerMixin(serializers.Serializer):
is_periodic = serializers.BooleanField(default=True, label=_("Periodic perform"))
is_periodic = serializers.BooleanField(default=False, label=_("Periodic perform"))
crontab = serializers.CharField(
max_length=128, allow_blank=True,
allow_null=True, required=False, label=_('Regularly perform')
)
interval = serializers.IntegerField(
default=24, allow_null=True, required=False, label=_('Interval')
)
interval = serializers.IntegerField(allow_null=True, required=False, label=_('Interval'))
INTERVAL_MAX = 65535
INTERVAL_MIN = 1
@@ -124,7 +122,7 @@ class PeriodTaskSerializerMixin(serializers.Serializer):
return crontab
def validate_interval(self, interval):
if not interval and not isinstance(interval, int):
if not interval:
return interval
msg = _("Range {} to {}").format(self.INTERVAL_MIN, self.INTERVAL_MAX)
if interval > self.INTERVAL_MAX or interval < self.INTERVAL_MIN:

View File

@@ -14,15 +14,11 @@ class AdHocExecutionSerializer(serializers.ModelSerializer):
class Meta:
model = AdHocExecution
fields_mini = ['id']
fields_small = fields_mini + [
'hosts_amount', 'timedelta', 'result', 'summary', 'short_id',
'is_finished', 'is_success',
'date_start', 'date_finished',
fields = [
'id', 'task', 'task_display', 'hosts_amount', 'adhoc', 'date_start', 'stat',
'date_finished', 'timedelta', 'is_finished', 'is_success', 'result', 'summary',
'short_id', 'adhoc_short_id', 'last_success', 'last_failure'
]
fields_fk = ['task', 'task_display', 'adhoc', 'adhoc_short_id',]
fields_custom = ['stat', 'last_success', 'last_failure']
fields = fields_small + fields_fk + fields_custom
@staticmethod
def get_task(obj):
@@ -56,16 +52,11 @@ class TaskSerializer(BulkOrgResourceModelSerializer):
class Meta:
model = Task
fields_mini = ['id', 'name']
fields_small = fields_mini + [
'interval', 'crontab',
'is_periodic', 'is_deleted',
'date_created', 'date_updated',
'comment',
fields = [
'id', 'name', 'interval', 'crontab', 'is_periodic',
'is_deleted', 'comment', 'date_created',
'date_updated', 'latest_execution', 'summary',
]
fields_fk = ['latest_execution']
fields_custom = ['summary']
fields = fields_small + fields_fk + fields_custom
read_only_fields = [
'is_deleted', 'date_created', 'date_updated',
'latest_adhoc', 'latest_execution', 'total_run_amount',
@@ -86,16 +77,12 @@ class AdHocSerializer(serializers.ModelSerializer):
class Meta:
model = AdHoc
fields_mini = ['id']
fields_small = fields_mini + [
'tasks', "pattern", "options", "run_as",
"become", "become_display", "short_id",
"run_as_admin",
"date_created",
fields = [
"id", "task", 'tasks', "pattern", "options",
"hosts", "run_as_admin", "run_as", "become",
"date_created", "short_id",
"become_display",
]
fields_fk = ["task"]
fields_m2m = ["hosts"]
fields = fields_small + fields_fk + fields_m2m
read_only_fields = [
'date_created'
]
@@ -112,8 +99,8 @@ class AdHocExecutionNestSerializer(serializers.ModelSerializer):
class Meta:
model = AdHocExecution
fields = (
'last_success', 'last_failure', 'last_run', 'timedelta',
'is_finished', 'is_success'
'last_success', 'last_failure', 'last_run', 'timedelta', 'is_finished',
'is_success'
)
@@ -133,15 +120,10 @@ class CommandExecutionSerializer(serializers.ModelSerializer):
class Meta:
model = CommandExecution
fields_mini = ['id']
fields_small = fields_mini + [
'command', 'result', 'log_url',
'is_finished',
'date_created', 'date_finished'
fields = [
'id', 'hosts', 'run_as', 'command', 'result', 'log_url',
'is_finished', 'date_created', 'date_finished'
]
fields_fk = ['run_as']
fields_m2m = ['hosts']
fields = fields_small + fields_fk + fields_m2m
read_only_fields = [
'result', 'is_finished', 'log_url', 'date_created',
'date_finished'

View File

@@ -3,8 +3,8 @@
from django.views.generic import TemplateView
from django.conf import settings
from common.permissions import IsOrgAdmin, IsOrgAuditor
from common.mixins.views import PermissionsMixin
from common.permissions import PermissionsMixin, IsOrgAdmin, IsOrgAuditor
__all__ = ['CeleryTaskLogView']

View File

@@ -48,6 +48,7 @@ class OrgViewSet(BulkModelViewSet):
queryset = Organization.objects.all()
serializer_class = OrgSerializer
permission_classes = (IsSuperUserOrAppUser,)
org = None
def get_serializer_class(self):
mapper = {
@@ -57,36 +58,29 @@ class OrgViewSet(BulkModelViewSet):
return mapper.get(self.action, super().get_serializer_class())
@tmp_to_root_org()
def get_data_from_model(self, org, model):
def get_data_from_model(self, model):
if model == User:
data = model.objects.filter(
orgs__id=org.id, m2m_org_members__role__in=[ROLE.USER, ROLE.ADMIN, ROLE.AUDITOR]
)
data = model.objects.filter(orgs__id=self.org.id, m2m_org_members__role=ROLE.USER)
elif model == Node:
# 节点不能手动删除,所以排除检查
data = model.objects.filter(org_id=org.id).exclude(parent_key='', key__regex=r'^[0-9]+$')
# 节点不能手动删除,所以排除检查
data = model.objects.filter(org_id=self.org.id).exclude(parent_key='', key__regex=r'^[0-9]+$')
else:
data = model.objects.filter(org_id=org.id)
data = model.objects.filter(org_id=self.org.id)
return data
def allow_bulk_destroy(self, qs, filtered):
return False
def perform_destroy(self, instance):
if str(current_org) == str(instance):
msg = _('The current organization ({}) cannot be deleted'.format(current_org))
raise PermissionDenied(detail=msg)
def destroy(self, request, *args, **kwargs):
self.org = self.get_object()
for model in org_related_models:
data = self.get_data_from_model(instance, model)
if not data:
continue
msg = _(
'The organization have resource ({}) cannot be deleted'
).format(model._meta.verbose_name)
raise PermissionDenied(detail=msg)
super().perform_destroy(instance)
data = self.get_data_from_model(model)
if data:
msg = _(f'Have `{model._meta.verbose_name}` exists, Please delete')
return Response(data={'error': msg}, status=status.HTTP_403_FORBIDDEN)
else:
if str(current_org) == str(self.org):
msg = _('The current organization cannot be deleted')
return Response(data={'error': msg}, status=status.HTTP_403_FORBIDDEN)
self.org.delete()
return Response({'msg': True}, status=status.HTTP_200_OK)
class OrgMemberRelationBulkViewSet(JMSBulkRelationModelViewSet):

View File

@@ -6,12 +6,12 @@ from orgs.utils import current_org, tmp_to_org
from common.cache import Cache, IntegerField
from common.utils import get_logger
from users.models import UserGroup, User
from assets.models import Node, AdminUser, SystemUser, Domain, Gateway, Asset
from terminal.models import Session
from assets.models import Node, AdminUser, SystemUser, Domain, Gateway
from applications.models import Application
from perms.models import AssetPermission, ApplicationPermission
from .models import OrganizationMember
logger = get_logger(__file__)
@@ -64,9 +64,6 @@ class OrgResourceStatisticsCache(OrgRelatedCache):
asset_perms_amount = IntegerField(queryset=AssetPermission.objects)
app_perms_amount = IntegerField(queryset=ApplicationPermission.objects)
total_count_online_users = IntegerField()
total_count_online_sessions = IntegerField()
def __init__(self, org):
super().__init__()
self.org = org
@@ -78,22 +75,14 @@ class OrgResourceStatisticsCache(OrgRelatedCache):
return self.org
def compute_users_amount(self):
users = User.objects.exclude(role='App')
if not self.org.is_root():
users = users.filter(m2m_org_members__org_id=self.org.id)
users_amount = users.values('id').distinct().count()
if self.org.is_root():
users_amount = User.objects.all().count()
else:
users_amount = OrganizationMember.objects.values(
'user_id'
).filter(org_id=self.org.id).distinct().count()
return users_amount
def compute_assets_amount(self):
if self.org.is_root():
return Asset.objects.all().count()
node = Node.org_root()
return node.assets_amount
def compute_total_count_online_users(self):
return len(set(Session.objects.filter(is_finished=False).values_list('user_id', flat=True)))
def compute_total_count_online_sessions(self):
return Session.objects.filter(is_finished=False).count()

View File

@@ -7,7 +7,7 @@ from django.db.models import signals
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from common.utils import lazyproperty, settings
from common.utils import is_uuid, lazyproperty
from common.const import choices
from common.db.models import ChoiceSet
@@ -193,8 +193,7 @@ class Organization(models.Model):
@classmethod
def root(cls):
name = settings.GLOBAL_ORG_DISPLAY_NAME or cls.ROOT_NAME
return cls(id=cls.ROOT_ID, name=name)
return cls(id=cls.ROOT_ID, name=cls.ROOT_NAME)
def is_root(self):
return self.id == self.ROOT_ID

View File

@@ -38,10 +38,8 @@ class OrgSerializer(ModelSerializer):
list_serializer_class = AdaptedBulkListSerializer
fields_mini = ['id', 'name']
fields_small = fields_mini + [
'resource_statistics',
'is_default', 'is_root',
'date_created',
'comment', 'created_by',
'is_default', 'is_root', 'comment',
'created_by', 'date_created', 'resource_statistics'
]
fields_m2m = ['users', 'admins', 'auditors']
@@ -81,12 +79,7 @@ class OrgMemberSerializer(BulkModelSerializer):
class Meta:
model = OrganizationMember
fields_mini = ['id']
fields_small = fields_mini + [
'role', 'role_display'
]
fields_fk = ['org', 'user', 'org_display', 'user_display',]
fields = fields_small + fields_fk
fields = ('id', 'org', 'user', 'role', 'org_display', 'user_display', 'role_display')
use_model_bulk_create = True
model_bulk_create_kwargs = {'ignore_conflicts': True}

View File

@@ -1,5 +1,5 @@
from django.db.models.signals import m2m_changed
from django.db.models.signals import post_save, pre_delete, pre_save
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from orgs.models import Organization, OrganizationMember
@@ -7,7 +7,6 @@ from assets.models import Node
from perms.models import (AssetPermission, ApplicationPermission)
from users.models import UserGroup, User
from applications.models import Application
from terminal.models import Session
from assets.models import Asset, AdminUser, SystemUser, Domain, Gateway
from common.const.signals import POST_PREFIX
from orgs.caches import OrgResourceStatisticsCache
@@ -18,7 +17,6 @@ def refresh_user_amount_on_user_create_or_delete(user_id):
for org in orgs:
org_cache = OrgResourceStatisticsCache(org)
org_cache.expire('users_amount')
OrgResourceStatisticsCache(Organization.root()).expire('users_amount')
@receiver(post_save, sender=User)
@@ -45,22 +43,20 @@ def on_org_user_changed_refresh_cache(sender, action, instance, reverse, pk_set,
for org in orgs:
org_cache = OrgResourceStatisticsCache(org)
org_cache.expire('users_amount')
OrgResourceStatisticsCache(Organization.root()).expire('users_amount')
class OrgResourceStatisticsRefreshUtil:
model_cache_field_mapper = {
ApplicationPermission: ['app_perms_amount'],
AssetPermission: ['asset_perms_amount'],
Application: ['applications_amount'],
Gateway: ['gateways_amount'],
Domain: ['domains_amount'],
SystemUser: ['system_users_amount'],
AdminUser: ['admin_users_amount'],
Node: ['nodes_amount'],
Asset: ['assets_amount'],
UserGroup: ['groups_amount'],
Session: ['total_count_online_users', 'total_count_online_sessions']
ApplicationPermission: 'app_perms_amount',
AssetPermission: 'asset_perms_amount',
Application: 'applications_amount',
Gateway: 'gateways_amount',
Domain: 'domains_amount',
SystemUser: 'system_users_amount',
AdminUser: 'admin_users_amount',
Node: 'nodes_amount',
Asset: 'assets_amount',
UserGroup: 'groups_amount',
}
@classmethod
@@ -68,13 +64,13 @@ class OrgResourceStatisticsRefreshUtil:
cache_field_name = cls.model_cache_field_mapper.get(type(instance))
if cache_field_name:
org_cache = OrgResourceStatisticsCache(instance.org)
org_cache.expire(*cache_field_name)
OrgResourceStatisticsCache(Organization.root()).expire(*cache_field_name)
org_cache.expire(cache_field_name)
@receiver(pre_save)
def on_post_save_refresh_org_resource_statistics_cache(sender, instance, **kwargs):
OrgResourceStatisticsRefreshUtil.refresh_if_need(instance)
@receiver(post_save)
def on_post_save_refresh_org_resource_statistics_cache(sender, instance, created, **kwargs):
if created:
OrgResourceStatisticsRefreshUtil.refresh_if_need(instance)
@receiver(pre_delete)

View File

@@ -8,6 +8,7 @@ from django.dispatch import receiver
from django.utils.functional import LazyObject
from django.db.models.signals import m2m_changed
from django.db.models.signals import post_save, post_delete, pre_delete
from django.utils.translation import ugettext as _
from orgs.utils import tmp_to_org
from orgs.models import Organization, OrganizationMember
@@ -18,6 +19,7 @@ from common.const.signals import PRE_REMOVE, POST_REMOVE
from common.signals import django_ready
from common.utils import get_logger
from common.utils.connection import RedisPubSub
from common.exceptions import JMSException
logger = get_logger(__file__)
@@ -44,19 +46,12 @@ def subscribe_orgs_mapping_expire(sender, **kwargs):
logger.debug("Start subscribe for expire orgs mapping from memory")
def keep_subscribe():
while True:
try:
subscribe = orgs_mapping_for_memory_pub_sub.subscribe()
for message in subscribe.listen():
if message['type'] != 'message':
continue
if message['data'] == b'error':
raise ValueError
Organization.expire_orgs_mapping()
logger.debug('Expire orgs mapping')
except Exception as e:
logger.exception(f'subscribe_orgs_mapping_expire: {e}')
Organization.expire_orgs_mapping()
subscribe = orgs_mapping_for_memory_pub_sub.subscribe()
for message in subscribe.listen():
if message['type'] != 'message':
continue
Organization.expire_orgs_mapping()
logger.debug('Expire orgs mapping')
t = threading.Thread(target=keep_subscribe)
t.daemon = True
@@ -167,3 +162,10 @@ def on_org_user_changed(action, instance, reverse, pk_set, **kwargs):
leaved_users = set(pk_set) - set(org.members.filter(id__in=user_pk_set).values_list('id', flat=True))
_clear_users_from_org(org, leaved_users)
@receiver(post_save, sender=User)
def on_user_create_refresh_cache(sender, instance, created, **kwargs):
if created:
default_org = Organization.default()
default_org.members.add(instance)

View File

@@ -1,11 +1,9 @@
# -*- coding: utf-8 -*-
#
import time
import uuid
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from rest_framework.views import APIView, Response
from rest_framework import status
from rest_framework.generics import (
ListAPIView, get_object_or_404
)
@@ -14,8 +12,7 @@ from orgs.utils import tmp_to_root_org
from applications.models import Application
from perms.utils.application.permission import (
has_application_system_permission,
get_application_system_user_ids,
validate_permission,
get_application_system_user_ids
)
from perms.api.asset.user_permission.mixin import RoleAdminMixin, RoleUserMixin
from common.permissions import IsOrgAdminOrAppUser
@@ -64,13 +61,18 @@ class ValidateUserApplicationPermissionApi(APIView):
application_id = request.query_params.get('application_id', '')
system_user_id = request.query_params.get('system_user_id', '')
if not all((user_id, application_id, system_user_id)):
return Response({'has_permission': False, 'expire_at': int(time.time())})
try:
user_id = uuid.UUID(user_id)
application_id = uuid.UUID(application_id)
system_user_id = uuid.UUID(system_user_id)
except ValueError:
return Response({'msg': False}, status=403)
user = User.objects.get(id=user_id)
application = Application.objects.get(id=application_id)
system_user = SystemUser.objects.get(id=system_user_id)
user = get_object_or_404(User, id=user_id)
application = get_object_or_404(Application, id=application_id)
system_user = get_object_or_404(SystemUser, id=system_user_id)
has_permission, expire_at = validate_permission(user, application, system_user)
status_code = status.HTTP_200_OK if has_permission else status.HTTP_403_FORBIDDEN
return Response({'has_permission': has_permission, 'expire_at': int(expire_at)}, status=status_code)
if has_application_system_permission(user, application, system_user):
return Response({'msg': True}, status=200)
return Response({'msg': False}, status=403)

View File

@@ -20,4 +20,3 @@ class AssetPermissionViewSet(OrgBulkModelViewSet):
model = AssetPermission
serializer_class = serializers.AssetPermissionSerializer
filterset_class = AssetPermissionFilter
search_fields = ('name',)

View File

@@ -1,23 +1,22 @@
# -*- coding: utf-8 -*-
#
import uuid
import time
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from rest_framework.views import APIView, Response
from rest_framework import status
from rest_framework.generics import (
ListAPIView, get_object_or_404, RetrieveAPIView, DestroyAPIView
)
from orgs.utils import tmp_to_root_org
from perms.utils.asset.permission import get_asset_system_user_ids_with_actions_by_user, validate_permission
from perms.utils.asset.permission import get_asset_system_user_ids_with_actions_by_user
from common.permissions import IsOrgAdminOrAppUser, IsOrgAdmin, IsValidUser
from common.utils import get_logger, lazyproperty
from perms.hands import User, Asset, SystemUser
from perms import serializers
from perms.models import Action
logger = get_logger(__name__)
@@ -66,22 +65,32 @@ class ValidateUserAssetPermissionApi(APIView):
def get_cache_policy(self):
return 0
def get(self, request, *args, **kwargs):
def get_user(self):
user_id = self.request.query_params.get('user_id', '')
user = get_object_or_404(User, id=user_id)
return user
def get(self, request, *args, **kwargs):
asset_id = request.query_params.get('asset_id', '')
system_id = request.query_params.get('system_user_id', '')
action_name = request.query_params.get('action_name', '')
if not all((user_id, asset_id, system_id, action_name)):
return Response({'has_permission': False, 'expire_at': int(time.time())})
try:
asset_id = uuid.UUID(asset_id)
system_id = uuid.UUID(system_id)
except ValueError:
return Response({'msg': False}, status=403)
user = User.objects.get(id=user_id)
asset = Asset.objects.valid().get(id=asset_id)
system_user = SystemUser.objects.get(id=system_id)
asset = get_object_or_404(Asset, id=asset_id, is_active=True)
system_user = get_object_or_404(SystemUser, id=system_id)
has_permission, expire_at = validate_permission(user, asset, system_user, action_name)
status_code = status.HTTP_200_OK if has_permission else status.HTTP_403_FORBIDDEN
return Response({'has_permission': has_permission, 'expire_at': int(expire_at)}, status=status_code)
system_users_actions = get_asset_system_user_ids_with_actions_by_user(self.get_user(), asset)
actions = system_users_actions.get(system_user.id)
if actions is None:
return Response({'msg': False}, status=403)
if action_name in Action.value_to_choices(actions):
return Response({'msg': True}, status=200)
return Response({'msg': False}, status=403)
# TODO 删除

Some files were not shown because too many files have changed in this diff Show More