Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in
Toggle navigation
J
jumpserver
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ops
jumpserver
Commits
722bf786
Unverified
Commit
722bf786
authored
Jul 17, 2018
by
老广
Committed by
GitHub
Jul 17, 2018
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1542 from jumpserver/dev
Dev to master
parents
91b3b7ce
2cb5876d
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
50 changed files
with
652 additions
and
218 deletions
+652
-218
README.md
README.md
+5
-5
__init__.py
apps/__init__.py
+1
-1
asset.py
apps/assets/api/asset.py
+22
-1
asset.py
apps/assets/forms/asset.py
+2
-2
user.py
apps/assets/forms/user.py
+25
-4
asset.py
apps/assets/models/asset.py
+14
-0
base.py
apps/assets/models/base.py
+1
-1
user.py
apps/assets/models/user.py
+10
-0
asset.py
apps/assets/serializers/asset.py
+1
-1
system_user.py
apps/assets/serializers/system_user.py
+12
-2
_system_user.html
apps/assets/templates/assets/_system_user.html
+42
-4
admin_user_list.html
apps/assets/templates/assets/admin_user_list.html
+1
-4
asset_create.html
apps/assets/templates/assets/asset_create.html
+6
-5
asset_update.html
apps/assets/templates/assets/asset_update.html
+1
-0
domain_list.html
apps/assets/templates/assets/domain_list.html
+8
-3
gateway_create_update.html
apps/assets/templates/assets/gateway_create_update.html
+14
-5
system_user_detail.html
apps/assets/templates/assets/system_user_detail.html
+28
-13
system_user_list.html
apps/assets/templates/assets/system_user_list.html
+6
-5
system_user_update.html
apps/assets/templates/assets/system_user_update.html
+0
-1
api_urls.py
apps/assets/urls/api_urls.py
+2
-0
views_urls.py
apps/assets/urls/views_urls.py
+0
-1
utils.py
apps/assets/utils.py
+2
-1
domain.py
apps/assets/views/domain.py
+0
-5
forms.py
apps/common/forms.py
+20
-6
security_setting.html
apps/common/templates/common/security_setting.html
+2
-2
django.mo
apps/i18n/zh/LC_MESSAGES/django.mo
+0
-0
django.po
apps/i18n/zh/LC_MESSAGES/django.po
+0
-0
settings.py
apps/jumpserver/settings.py
+6
-2
inventory.py
apps/ops/inventory.py
+1
-1
api.py
apps/perms/api.py
+4
-4
views_urls.py
apps/perms/urls/views_urls.py
+7
-9
jumpserver.js
apps/static/js/jumpserver.js
+6
-5
_footer.html
apps/templates/_footer.html
+1
-1
api.py
apps/terminal/api.py
+64
-36
session_list.html
apps/terminal/templates/terminal/session_list.html
+2
-0
api.py
apps/users/api.py
+85
-24
authentication.py
apps/users/models/authentication.py
+28
-0
login.html
apps/users/templates/users/login.html
+5
-1
login_log_list.html
apps/users/templates/users/login_log_list.html
+6
-0
user_detail.html
apps/users/templates/users/user_detail.html
+46
-4
user_list.html
apps/users/templates/users/user_list.html
+1
-1
api_urls.py
apps/users/urls/api_urls.py
+2
-0
views_urls.py
apps/users/urls/views_urls.py
+20
-20
utils.py
apps/users/utils.py
+46
-6
login.py
apps/users/views/login.py
+72
-17
user.py
apps/users/views/user.py
+8
-2
config_example.py
config_example.py
+13
-11
deb_requirements.txt
requirements/deb_requirements.txt
+1
-1
requirements.txt
requirements/requirements.txt
+1
-1
make_migrations.sh
utils/make_migrations.sh
+2
-0
No files found.
README.md
View file @
722bf786
...
...
@@ -19,25 +19,25 @@ Jumpserver采纳分布式架构,支持多机房跨区域部署,中心节点
----
### 功能
!
[
Jumpserver功能
](
https://jumpserver-release.oss-cn-hangzhou.aliyuncs.com/Jumpserver13.jpg
"Jumpserver功能"
)
### 开始使用
快速开始文档
[
Docker安装
](
http://docs.jumpserver.org/zh/
latest/quickstart
.html
)
快速开始文档
[
Docker安装
](
http://docs.jumpserver.org/zh/
docs/dockerinstall
.html
)
一步一步安装文档
[
详细部署
](
http://docs.jumpserver.org/zh/
latest
/step_by_step.html
)
一步一步安装文档
[
详细部署
](
http://docs.jumpserver.org/zh/
docs
/step_by_step.html
)
也可以查看我们完整文档包括了使用和开发
[
文档
](
http://docs.jumpserver.org
)
### Demo 和 截图
### Demo 和 截图
我们提供了DEMO和截图可以让你快速了解Jumpserver
[
DEMO
](
http://demo.jumpserver.org
)
[
截图
](
http://docs.jumpserver.org/zh/docs/snapshot.html
)
### SDK
### SDK
我们还编写了一些SDK,供你其它系统快速和Jumpserver APi交互,
...
...
apps/__init__.py
View file @
722bf786
...
...
@@ -2,4 +2,4 @@
# -*- coding: utf-8 -*-
#
__version__
=
"1.3.
2
"
__version__
=
"1.3.
3
"
apps/assets/api/asset.py
View file @
722bf786
# -*- coding: utf-8 -*-
#
import
random
from
rest_framework
import
generics
from
rest_framework.response
import
Response
from
rest_framework_bulk
import
BulkModelViewSet
...
...
@@ -22,7 +24,8 @@ from ..utils import LabelFilter
logger
=
get_logger
(
__file__
)
__all__
=
[
'AssetViewSet'
,
'AssetListUpdateApi'
,
'AssetRefreshHardwareApi'
,
'AssetAdminUserTestApi'
'AssetRefreshHardwareApi'
,
'AssetAdminUserTestApi'
,
'AssetGatewayApi'
]
...
...
@@ -106,3 +109,20 @@ class AssetAdminUserTestApi(generics.RetrieveAPIView):
asset
=
get_object_or_404
(
Asset
,
pk
=
asset_id
)
task
=
test_asset_connectability_manual
.
delay
(
asset
)
return
Response
({
"task"
:
task
.
id
})
class
AssetGatewayApi
(
generics
.
RetrieveAPIView
):
queryset
=
Asset
.
objects
.
all
()
permission_classes
=
(
IsSuperUserOrAppUser
,)
def
retrieve
(
self
,
request
,
*
args
,
**
kwargs
):
asset_id
=
kwargs
.
get
(
'pk'
)
asset
=
get_object_or_404
(
Asset
,
pk
=
asset_id
)
if
asset
.
domain
and
\
asset
.
domain
.
gateways
.
filter
(
protocol
=
asset
.
protocol
)
.
exists
():
gateway
=
random
.
choice
(
asset
.
domain
.
gateways
.
filter
(
protocol
=
asset
.
protocol
))
serializer
=
serializers
.
GatewayWithAuthSerializer
(
instance
=
gateway
)
return
Response
(
serializer
.
data
)
else
:
return
Response
({
"msg"
:
"Not have gateway"
},
status
=
404
)
\ No newline at end of file
apps/assets/forms/asset.py
View file @
722bf786
...
...
@@ -16,7 +16,7 @@ class AssetCreateForm(forms.ModelForm):
fields
=
[
'hostname'
,
'ip'
,
'public_ip'
,
'port'
,
'comment'
,
'nodes'
,
'is_active'
,
'admin_user'
,
'labels'
,
'platform'
,
'domain'
,
'domain'
,
'protocol'
,
]
widgets
=
{
...
...
@@ -56,7 +56,7 @@ class AssetUpdateForm(forms.ModelForm):
fields
=
[
'hostname'
,
'ip'
,
'port'
,
'nodes'
,
'is_active'
,
'platform'
,
'public_ip'
,
'number'
,
'comment'
,
'admin_user'
,
'labels'
,
'domain'
,
'domain'
,
'protocol'
,
]
widgets
=
{
'nodes'
:
forms
.
SelectMultiple
(
attrs
=
{
...
...
apps/assets/forms/user.py
View file @
722bf786
...
...
@@ -93,14 +93,21 @@ class SystemUserForm(PasswordAndKeyAuthForm):
# Because we define custom field, so we need rewrite :method: `save`
system_user
=
super
()
.
save
()
password
=
self
.
cleaned_data
.
get
(
'password'
,
''
)
or
None
login_mode
=
self
.
cleaned_data
.
get
(
'login_mode'
,
''
)
or
None
protocol
=
self
.
cleaned_data
.
get
(
'protocol'
)
or
None
auto_generate_key
=
self
.
cleaned_data
.
get
(
'auto_generate_key'
,
False
)
private_key
,
public_key
=
super
()
.
gen_keys
()
if
login_mode
==
SystemUser
.
MANUAL_LOGIN
or
protocol
==
SystemUser
.
TELNET_PROTOCOL
:
system_user
.
auto_push
=
0
system_user
.
save
()
if
auto_generate_key
:
logger
.
info
(
'Auto generate key and set system user auth'
)
system_user
.
auto_gen_auth
()
else
:
system_user
.
set_auth
(
password
=
password
,
private_key
=
private_key
,
public_key
=
public_key
)
return
system_user
def
clean
(
self
):
...
...
@@ -109,12 +116,24 @@ class SystemUserForm(PasswordAndKeyAuthForm):
if
not
self
.
instance
and
not
auto_generate
:
super
()
.
validate_password_key
()
def
is_valid
(
self
):
validated
=
super
()
.
is_valid
()
username
=
self
.
cleaned_data
.
get
(
'username'
)
login_mode
=
self
.
cleaned_data
.
get
(
'login_mode'
)
if
login_mode
==
SystemUser
.
AUTO_LOGIN
and
not
username
:
self
.
add_error
(
"username"
,
_
(
'* Automatic login mode,'
' must fill in the username.'
)
)
return
False
return
validated
class
Meta
:
model
=
SystemUser
fields
=
[
'name'
,
'username'
,
'protocol'
,
'auto_generate_key'
,
'password'
,
'private_key_file'
,
'auto_push'
,
'sudo'
,
'comment'
,
'shell'
,
'priority'
,
'comment'
,
'shell'
,
'priority'
,
'login_mode'
,
]
widgets
=
{
'name'
:
forms
.
TextInput
(
attrs
=
{
'placeholder'
:
_
(
'Name'
)}),
...
...
@@ -124,5 +143,8 @@ class SystemUserForm(PasswordAndKeyAuthForm):
'name'
:
'* required'
,
'username'
:
'* required'
,
'auto_push'
:
_
(
'Auto push system user to asset'
),
'priority'
:
_
(
'High level will be using login asset as default, if user was granted more than 2 system user'
),
}
\ No newline at end of file
'priority'
:
_
(
'High level will be using login asset as default, '
'if user was granted more than 2 system user'
),
'login_mode'
:
_
(
'If you choose manual login mode, you do not '
'need to fill in the username and password.'
)
}
apps/assets/models/asset.py
View file @
722bf786
...
...
@@ -57,13 +57,27 @@ class Asset(models.Model):
(
'MacOS'
,
'MacOS'
),
(
'BSD'
,
'BSD'
),
(
'Windows'
,
'Windows'
),
(
'Windows2016'
,
'Windows(2016)'
),
(
'Other'
,
'Other'
),
)
SSH_PROTOCOL
=
'ssh'
RDP_PROTOCOL
=
'rdp'
TELNET_PROTOCOL
=
'telnet'
PROTOCOL_CHOICES
=
(
(
SSH_PROTOCOL
,
'ssh'
),
(
RDP_PROTOCOL
,
'rdp'
),
(
TELNET_PROTOCOL
,
'telnet (beta)'
),
)
id
=
models
.
UUIDField
(
default
=
uuid
.
uuid4
,
primary_key
=
True
)
ip
=
models
.
GenericIPAddressField
(
max_length
=
32
,
verbose_name
=
_
(
'IP'
),
db_index
=
True
)
hostname
=
models
.
CharField
(
max_length
=
128
,
unique
=
True
,
verbose_name
=
_
(
'Hostname'
))
protocol
=
models
.
CharField
(
max_length
=
128
,
default
=
SSH_PROTOCOL
,
choices
=
PROTOCOL_CHOICES
,
verbose_name
=
_
(
'Protocol'
))
port
=
models
.
IntegerField
(
default
=
22
,
verbose_name
=
_
(
'Port'
))
platform
=
models
.
CharField
(
max_length
=
128
,
choices
=
PLATFORM_CHOICES
,
default
=
'Linux'
,
verbose_name
=
_
(
'Platform'
))
...
...
apps/assets/models/base.py
View file @
722bf786
...
...
@@ -19,7 +19,7 @@ signer = get_signer()
class
AssetUser
(
models
.
Model
):
id
=
models
.
UUIDField
(
default
=
uuid
.
uuid4
,
primary_key
=
True
)
name
=
models
.
CharField
(
max_length
=
128
,
unique
=
True
,
verbose_name
=
_
(
'Name'
))
username
=
models
.
CharField
(
max_length
=
32
,
verbose_name
=
_
(
'Username'
),
validators
=
[
alphanumeric
])
username
=
models
.
CharField
(
max_length
=
32
,
blank
=
True
,
verbose_name
=
_
(
'Username'
),
validators
=
[
alphanumeric
])
_password
=
models
.
CharField
(
max_length
=
256
,
blank
=
True
,
null
=
True
,
verbose_name
=
_
(
'Password'
))
_private_key
=
models
.
TextField
(
max_length
=
4096
,
blank
=
True
,
null
=
True
,
verbose_name
=
_
(
'SSH private key'
),
validators
=
[
private_key_validator
,
])
_public_key
=
models
.
TextField
(
max_length
=
4096
,
blank
=
True
,
verbose_name
=
_
(
'SSH public key'
))
...
...
apps/assets/models/user.py
View file @
722bf786
...
...
@@ -95,9 +95,18 @@ class AdminUser(AssetUser):
class
SystemUser
(
AssetUser
):
SSH_PROTOCOL
=
'ssh'
RDP_PROTOCOL
=
'rdp'
TELNET_PROTOCOL
=
'telnet'
PROTOCOL_CHOICES
=
(
(
SSH_PROTOCOL
,
'ssh'
),
(
RDP_PROTOCOL
,
'rdp'
),
(
TELNET_PROTOCOL
,
'telnet (beta)'
),
)
AUTO_LOGIN
=
'auto'
MANUAL_LOGIN
=
'manual'
LOGIN_MODE_CHOICES
=
(
(
AUTO_LOGIN
,
_
(
'Automatic login'
)),
(
MANUAL_LOGIN
,
_
(
'Manually login'
))
)
nodes
=
models
.
ManyToManyField
(
'assets.Node'
,
blank
=
True
,
verbose_name
=
_
(
"Nodes"
))
...
...
@@ -107,6 +116,7 @@ class SystemUser(AssetUser):
auto_push
=
models
.
BooleanField
(
default
=
True
,
verbose_name
=
_
(
'Auto push'
))
sudo
=
models
.
TextField
(
default
=
'/bin/whoami'
,
verbose_name
=
_
(
'Sudo'
))
shell
=
models
.
CharField
(
max_length
=
64
,
default
=
'/bin/bash'
,
verbose_name
=
_
(
'Shell'
))
login_mode
=
models
.
CharField
(
choices
=
LOGIN_MODE_CHOICES
,
default
=
AUTO_LOGIN
,
max_length
=
10
,
verbose_name
=
_
(
'Login mode'
))
def
__str__
(
self
):
return
'{0.name}({0.username})'
.
format
(
self
)
...
...
apps/assets/serializers/asset.py
View file @
722bf786
...
...
@@ -43,7 +43,7 @@ class AssetGrantedSerializer(serializers.ModelSerializer):
fields
=
(
"id"
,
"hostname"
,
"ip"
,
"port"
,
"system_users_granted"
,
"is_active"
,
"system_users_join"
,
"os"
,
'domain'
,
"platform"
,
"comment"
"platform"
,
"comment"
,
"protocol"
,
)
@staticmethod
...
...
apps/assets/serializers/system_user.py
View file @
722bf786
...
...
@@ -18,6 +18,13 @@ class SystemUserSerializer(serializers.ModelSerializer):
model
=
SystemUser
exclude
=
(
'_password'
,
'_private_key'
,
'_public_key'
)
def
get_field_names
(
self
,
declared_fields
,
info
):
fields
=
super
(
SystemUserSerializer
,
self
)
.
get_field_names
(
declared_fields
,
info
)
fields
.
extend
([
'get_login_mode_display'
,
])
return
fields
@staticmethod
def
get_unreachable_assets
(
obj
):
return
obj
.
unreachable_assets
...
...
@@ -46,7 +53,7 @@ class SystemUserAuthSerializer(AuthSerializer):
model
=
SystemUser
fields
=
[
"id"
,
"name"
,
"username"
,
"protocol"
,
"password"
,
"private_key"
,
"
login_mode"
,
"
password"
,
"private_key"
,
]
...
...
@@ -56,7 +63,10 @@ class AssetSystemUserSerializer(serializers.ModelSerializer):
"""
class
Meta
:
model
=
SystemUser
fields
=
(
'id'
,
'name'
,
'username'
,
'priority'
,
'protocol'
,
'comment'
,)
fields
=
(
'id'
,
'name'
,
'username'
,
'priority'
,
'protocol'
,
'comment'
,
'login_mode'
)
class
SystemUserSimpleSerializer
(
serializers
.
ModelSerializer
):
...
...
apps/assets/templates/assets/_system_user.html
View file @
722bf786
...
...
@@ -36,12 +36,13 @@
{% endif %}
<h3>
{% trans 'Basic' %}
</h3>
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.login_mode layout="horizontal" %}
{% bootstrap_field form.username layout="horizontal" %}
{% bootstrap_field form.priority layout="horizontal" %}
{% bootstrap_field form.protocol layout="horizontal" %}
<h3
id=
"auth_title_id"
>
{% trans 'Auth' %}
</h3>
{% block auth %}
<h3>
{% trans 'Auth' %}
</h3>
<div
class=
"auto-generate"
>
<div
class=
"form-group"
>
<label
for=
"{{ form.auto_generate_key.id_for_label }}"
class=
"col-sm-2 control-label"
>
{% trans 'Auto generate key' %}
</label>
...
...
@@ -80,15 +81,22 @@
{% endblock %}
{% block custom_foot_js %}
<script>
var
auto_generate_key
=
'#'
+
'{{ form.auto_generate_key.id_for_label }}'
;
var
protocol_id
=
'#'
+
'{{ form.protocol.id_for_label }}'
;
var
login_mode_id
=
'#'
+
'{{ form.login_mode.id_for_label }}'
;
var
auto_generate_key
=
'#'
+
'{{ form.auto_generate_key.id_for_label }}'
;
var
password_id
=
'#'
+
'{{ form.password.id_for_label }}'
;
var
private_key_id
=
'#'
+
'{{ form.private_key_file.id_for_label }}'
;
var
auto_push_id
=
'#'
+
'{{ form.auto_push.id_for_label }}'
;
var
sudo_id
=
'#'
+
'{{ form.sudo.id_for_label }}'
;
var
shell_id
=
'#'
+
'{{ form.shell.id_for_label }}'
;
var
need_change_field
=
[
auto_generate_key
,
private_key_id
,
auto_push_id
,
sudo_id
,
shell_id
];
var
need_change_field_login_mode
=
[
auto_generate_key
,
private_key_id
,
auto_push_id
,
password_id
];
function
protocolChange
()
{
if
(
$
(
protocol_id
+
" option:selected"
).
text
()
===
'rdp'
)
{
...
...
@@ -96,7 +104,19 @@ function protocolChange() {
$
.
each
(
need_change_field
,
function
(
index
,
value
)
{
$
(
value
).
closest
(
'.form-group'
).
addClass
(
'hidden'
)
});
}
else
{
}
else
if
(
$
(
protocol_id
+
" option:selected"
).
text
()
===
'telnet (beta)'
)
{
$
(
'.auth-fields'
).
removeClass
(
'hidden'
);
$
.
each
(
need_change_field
,
function
(
index
,
value
)
{
$
(
value
).
closest
(
'.form-group'
).
addClass
(
'hidden'
)
});
}
else
{
if
(
$
(
login_mode_id
).
val
()
===
'manual'
){
$
(
sudo_id
).
closest
(
'.form-group'
).
removeClass
(
'hidden'
);
$
(
shell_id
).
closest
(
'.form-group'
).
removeClass
(
'hidden'
);
return
}
authFieldsDisplay
();
$
.
each
(
need_change_field
,
function
(
index
,
value
)
{
$
(
value
).
closest
(
'.form-group'
).
removeClass
(
'hidden'
)
...
...
@@ -111,18 +131,35 @@ function authFieldsDisplay() {
$
(
'.auth-fields'
).
removeClass
(
'hidden'
);
}
}
function
loginModeChange
(){
if
(
$
(
login_mode_id
).
val
()
===
'manual'
){
$
(
'#auth_title_id'
).
addClass
(
'hidden'
);
$
.
each
(
need_change_field_login_mode
,
function
(
index
,
value
){
$
(
value
).
closest
(
'.form-group'
).
addClass
(
'hidden'
)
})
}
else
if
(
$
(
login_mode_id
).
val
()
===
'auto'
){
$
(
'#auth_title_id'
).
removeClass
(
'hidden'
);
$
(
password_id
).
closest
(
'.form-group'
).
removeClass
(
'hidden'
)
protocolChange
();
}
}
$
(
document
).
ready
(
function
()
{
$
(
'.select2'
).
select2
();
authFieldsDisplay
();
protocolChange
();
loginModeChange
();
})
.
on
(
'change'
,
protocol_id
,
function
(){
protocolChange
();
})
.
on
(
'change'
,
auto_generate_key
,
function
(){
authFieldsDisplay
();
});
})
.
on
(
'change'
,
login_mode_id
,
function
(){
loginModeChange
();
})
</script>
{% endblock %}
\ No newline at end of file
apps/assets/templates/assets/admin_user_list.html
View file @
722bf786
...
...
@@ -5,7 +5,7 @@
{% block help_message %}
<div
class=
"alert alert-info help-message"
>
管理用户是
服务器
的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。
管理用户是
资产(被控服务器)上
的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。
Windows或其它硬件可以随意设置一个
</div>
{% endblock %}
...
...
@@ -107,6 +107,3 @@ $(document).ready(function(){
});
</script>
{% endblock %}
apps/assets/templates/assets/asset_create.html
View file @
722bf786
...
...
@@ -17,6 +17,7 @@
{% bootstrap_field form.hostname layout="horizontal" %}
{% bootstrap_field form.platform layout="horizontal" %}
{% bootstrap_field form.ip layout="horizontal" %}
{% bootstrap_field form.protocol layout="horizontal" %}
{% bootstrap_field form.port layout="horizontal" %}
{% bootstrap_field form.public_ip layout="horizontal" %}
{% bootstrap_field form.domain layout="horizontal" %}
...
...
@@ -85,14 +86,14 @@ $(document).ready(function () {
allowClear
:
true
,
templateSelection
:
format
});
$
(
"#id_p
latform
"
).
change
(
function
(){
var
p
latform
=
$
(
"#id_platform
option:selected"
).
text
();
$
(
"#id_p
rotocol
"
).
change
(
function
(){
var
p
rotocol
=
$
(
"#id_protocol
option:selected"
).
text
();
var
port
=
22
;
if
(
p
latform
===
'Windows
'
){
if
(
p
rotocol
===
'rdp
'
){
port
=
3389
;
}
if
(
p
latform
===
'Other
'
){
port
=
null
;
if
(
p
rotocol
===
'telnet (beta)
'
){
port
=
23
;
}
$
(
"#id_port"
).
val
(
port
);
});
...
...
apps/assets/templates/assets/asset_update.html
View file @
722bf786
...
...
@@ -21,6 +21,7 @@
<h3>
{% trans 'Basic' %}
</h3>
{% bootstrap_field form.hostname layout="horizontal" %}
{% bootstrap_field form.ip layout="horizontal" %}
{% bootstrap_field form.protocol layout="horizontal" %}
{% bootstrap_field form.port layout="horizontal" %}
{% bootstrap_field form.platform layout="horizontal" %}
{% bootstrap_field form.public_ip layout="horizontal" %}
...
...
apps/assets/templates/assets/domain_list.html
View file @
722bf786
{% extends '_base_list.html' %}
{% load i18n static %}
{% block table_search %}{% endblock %}
{% block help_message %}
<div
class=
"alert alert-info help-message"
>
网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通过网关服务器进行跳转登
录。
</div>
{% endblock %}
{% block table_container %}
<div
class=
"uc pull-left m-r-5"
>
<a
href=
"{% url 'assets:domain-create' %}"
class=
"btn btn-sm btn-primary"
>
{% trans "Create domain" %}
</a>
...
...
@@ -69,6 +77,3 @@ $(document).ready(function(){
});
</script>
{% endblock %}
apps/assets/templates/assets/gateway_create_update.html
View file @
722bf786
...
...
@@ -42,7 +42,7 @@
{% bootstrap_field form.domain layout="horizontal" %}
{% block auth %}
<h3>
{% trans 'Auth' %}
</h3>
<h3
id=
"auth_title"
>
{% trans 'Auth' %}
</h3>
<div
class=
"auth-fields"
>
{% bootstrap_field form.username layout="horizontal" %}
{% bootstrap_field form.password layout="horizontal" %}
...
...
@@ -72,14 +72,23 @@
var
protocol_id
=
'#'
+
'{{ form.protocol.id_for_label }}'
;
var
private_key_id
=
'#'
+
'{{ form.private_key_file.id_for_label }}'
;
var
port
=
'#'
+
'{{ form.port.id_for_label }}'
;
var
username
=
'#'
+
'{{ form.username.id_for_label }}'
;
var
password
=
'#'
+
'{{ form.password.id_for_label }}'
;
var
auth_title
=
'#auth_title'
;
function
protocolChange
()
{
if
(
$
(
protocol_id
+
" option:selected"
).
text
()
===
'rdp'
)
{
$
(
port
).
val
(
3389
);
$
(
private_key_id
).
closest
(
'.form-group'
).
addClass
(
'hidden'
)
{
#
$
(
port
).
val
(
3389
);
#
}
$
(
private_key_id
).
closest
(
'.form-group'
).
addClass
(
'hidden'
);
$
(
username
).
closest
(
'.form-group'
).
addClass
(
'hidden'
);
$
(
password
).
closest
(
'.form-group'
).
addClass
(
'hidden'
);
$
(
auth_title
).
addClass
(
'hidden'
);
}
else
{
$
(
port
).
val
(
22
);
$
(
private_key_id
).
closest
(
'.form-group'
).
removeClass
(
'hidden'
)
{
#
$
(
port
).
val
(
22
);
#
}
$
(
private_key_id
).
closest
(
'.form-group'
).
removeClass
(
'hidden'
);
$
(
username
).
closest
(
'.form-group'
).
removeClass
(
'hidden'
);
$
(
password
).
closest
(
'.form-group'
).
removeClass
(
'hidden'
);
$
(
auth_title
).
removeClass
(
'hidden'
);
}
}
...
...
apps/assets/templates/assets/system_user_detail.html
View file @
722bf786
...
...
@@ -62,6 +62,10 @@
<td>
{% trans 'Username' %}:
</td>
<td><b>
{{ system_user.username }}
</b></td>
</tr>
<tr>
<td>
{% trans 'Login mode' %}:
</td>
<td><b>
{{ system_user.get_login_mode_display }}
</b></td>
</tr>
<tr>
<td>
{% trans 'Protocol' %}:
</td>
<td><b
id=
"id_protocol_type"
>
{{ system_user.protocol }}
</b></td>
...
...
@@ -148,15 +152,14 @@
</span>
</td>
</tr>
<tr>
<td
width=
"50%"
>
{% trans 'Clear auth' %}:
</td>
<td>
<span
style=
"float: right"
>
<button
type=
"button"
class=
"btn btn-primary btn-xs btn-clear-auth"
style=
"width: 54px"
>
{% trans 'Clear' %}
</button>
</span>
</td>
</tr>
{#
<tr>
#}
{#
<td
width=
"50%"
>
{% trans 'Clear auth' %}:
</td>
#}
{#
<td>
#}
{#
<span
style=
"float: right"
>
#}
{#
<button
type=
"button"
class=
"btn btn-primary btn-xs btn-clear-auth"
style=
"width: 54px"
>
{% trans 'Clear' %}
</button>
#}
{#
</span>
#}
{#
</td>
#}
{#
</tr>
#}
{#
<tr>
#}
{#
<td
width=
"50%"
>
{% trans 'Change auth period' %}:
</td>
#}
...
...
@@ -333,10 +336,22 @@ $(document).ready(function () {
});
}).
on
(
'click'
,
'.btn-clear-auth'
,
function
()
{
var
the_url
=
'{% url "api-assets:system-user-auth-info" pk=system_user.id %}'
;
APIUpdateAttr
({
url
:
the_url
,
method
:
'DELETE'
,
success_message
:
"{% trans 'Clear auth' %}"
+
" {% trans 'success' %}"
var
name
=
'{{ system_user.name }}'
;
swal
({
title
:
'你确定清除该系统用户的认证信息吗 ?'
,
text
:
" ["
+
name
+
"] "
,
type
:
"warning"
,
showCancelButton
:
true
,
cancelButtonText
:
'取消'
,
confirmButtonColor
:
"#ed5565"
,
confirmButtonText
:
'确认'
,
closeOnConfirm
:
true
},
function
()
{
APIUpdateAttr
({
url
:
the_url
,
method
:
'DELETE'
,
success_message
:
"{% trans 'Clear auth' %}"
+
" {% trans 'success' %}"
});
});
})
</script>
...
...
apps/assets/templates/assets/system_user_list.html
View file @
722bf786
...
...
@@ -26,6 +26,7 @@
<th
class=
"text-center"
>
{% trans 'Name' %}
</th>
<th
class=
"text-center"
>
{% trans 'Username' %}
</th>
<th
class=
"text-center"
>
{% trans 'Protocol' %}
</th>
<th
class=
"text-center"
>
{% trans 'Login mode' %}
</th>
<th
class=
"text-center"
>
{% trans 'Asset' %}
</th>
<th
class=
"text-center"
>
{% trans 'Reachable' %}
</th>
<th
class=
"text-center"
>
{% trans 'Unreachable' %}
</th>
...
...
@@ -48,7 +49,7 @@ function initTable() {
var
detail_btn
=
'<a href="{% url "assets:system-user-detail" pk=DEFAULT_PK %}">'
+
cellData
+
'</a>'
;
$
(
td
).
html
(
detail_btn
.
replace
(
'{{ DEFAULT_PK }}'
,
rowData
.
id
));
}},
{
targets
:
5
,
createdCell
:
function
(
td
,
cellData
)
{
{
targets
:
6
,
createdCell
:
function
(
td
,
cellData
)
{
var
innerHtml
=
""
;
if
(
cellData
!==
0
)
{
innerHtml
=
"<span class='text-navy'>"
+
cellData
+
"</span>"
;
...
...
@@ -57,7 +58,7 @@ function initTable() {
}
$
(
td
).
html
(
'<span href="javascript:void(0);" data-toggle="tooltip" title="'
+
cellData
+
'">'
+
innerHtml
+
'</span>'
);
}},
{
targets
:
6
,
createdCell
:
function
(
td
,
cellData
)
{
{
targets
:
7
,
createdCell
:
function
(
td
,
cellData
)
{
var
innerHtml
=
""
;
if
(
cellData
!==
0
)
{
innerHtml
=
"<span class='text-danger'>"
+
cellData
+
"</span>"
;
...
...
@@ -66,7 +67,7 @@ function initTable() {
}
$
(
td
).
html
(
'<span href="javascript:void(0);" data-toggle="tooltip" title="'
+
cellData
+
'">'
+
innerHtml
+
'</span>'
);
}},
{
targets
:
7
,
createdCell
:
function
(
td
,
cellData
,
rowData
)
{
{
targets
:
8
,
createdCell
:
function
(
td
,
cellData
,
rowData
)
{
var
val
=
0
;
var
innerHtml
=
""
;
var
total
=
rowData
.
assets_amount
;
...
...
@@ -84,14 +85,14 @@ function initTable() {
$
(
td
).
html
(
'<span href="javascript:void(0);" data-toggle="tooltip" title="'
+
cellData
+
'">'
+
innerHtml
+
'</span>'
);
}},
{
targets
:
9
,
createdCell
:
function
(
td
,
cellData
,
rowData
)
{
{
targets
:
10
,
createdCell
:
function
(
td
,
cellData
,
rowData
)
{
var
update_btn
=
'<a href="{% url "assets:system-user-update" pk=DEFAULT_PK %}" class="btn btn-xs m-l-xs btn-info">{% trans "Update" %}</a>'
.
replace
(
'{{ DEFAULT_PK }}'
,
cellData
);
var
del_btn
=
'<a class="btn btn-xs btn-danger m-l-xs btn_admin_user_delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'
.
replace
(
'{{ DEFAULT_PK }}'
,
cellData
);
$
(
td
).
html
(
update_btn
+
del_btn
)
}}],
ajax_url
:
'{% url "api-assets:system-user-list" %}'
,
columns
:
[
{
data
:
"id"
},
{
data
:
"name"
},
{
data
:
"username"
},
{
data
:
"protocol"
},
{
data
:
"assets_amount"
},
{
data
:
"id"
},
{
data
:
"name"
},
{
data
:
"username"
},
{
data
:
"protocol"
},
{
data
:
"
get_login_mode_display"
},
{
data
:
"
assets_amount"
},
{
data
:
"reachable_amount"
},
{
data
:
"unreachable_amount"
},
{
data
:
"id"
},
{
data
:
"comment"
},
{
data
:
"id"
}
],
op_html
:
$
(
'#actions'
).
html
()
...
...
apps/assets/templates/assets/system_user_update.html
View file @
722bf786
...
...
@@ -4,7 +4,6 @@
{% load bootstrap3 %}
{% block auth %}
<h3>
{% trans 'Auth' %}
</h3>
{% bootstrap_field form.password layout="horizontal" %}
{% bootstrap_field form.private_key_file layout="horizontal" %}
<div
class=
"form-group"
>
...
...
apps/assets/urls/api_urls.py
View file @
722bf786
...
...
@@ -23,6 +23,8 @@ urlpatterns = [
api
.
AssetRefreshHardwareApi
.
as_view
(),
name
=
'asset-refresh'
),
url
(
r'^v1/assets/(?P<pk>[0-9a-zA-Z\-]{36})/alive/$'
,
api
.
AssetAdminUserTestApi
.
as_view
(),
name
=
'asset-alive-test'
),
url
(
r'^v1/assets/(?P<pk>[0-9a-zA-Z\-]{36})/gateway/$'
,
api
.
AssetGatewayApi
.
as_view
(),
name
=
'asset-gateway'
),
url
(
r'^v1/admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/nodes/$'
,
api
.
ReplaceNodesAdminUserApi
.
as_view
(),
name
=
'replace-nodes-admin-user'
),
url
(
r'^v1/admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/auth/$'
,
...
...
apps/assets/urls/views_urls.py
View file @
722bf786
...
...
@@ -50,4 +50,3 @@ urlpatterns = [
url
(
r'^domain/(?P<pk>[0-9a-zA-Z\-]{36})/gateway/create/$'
,
views
.
DomainGatewayCreateView
.
as_view
(),
name
=
'domain-gateway-create'
),
url
(
r'^domain/gateway/(?P<pk>[0-9a-zA-Z\-]{36})/update/$'
,
views
.
DomainGatewayUpdateView
.
as_view
(),
name
=
'domain-gateway-update'
),
]
apps/assets/utils.py
View file @
722bf786
...
...
@@ -54,7 +54,8 @@ def test_gateway_connectability(gateway):
proxy
.
set_missing_host_key_policy
(
paramiko
.
AutoAddPolicy
())
try
:
proxy
.
connect
(
gateway
.
ip
,
username
=
gateway
.
username
,
proxy
.
connect
(
gateway
.
ip
,
gateway
.
port
,
username
=
gateway
.
username
,
password
=
gateway
.
password
,
pkey
=
gateway
.
private_key_obj
)
except
(
paramiko
.
AuthenticationException
,
...
...
apps/assets/views/domain.py
View file @
722bf786
...
...
@@ -140,11 +140,6 @@ class DomainGatewayUpdateView(AdminUserRequiredMixin, UpdateView):
domain
=
self
.
object
.
domain
return
reverse
(
'assets:domain-gateway-list'
,
kwargs
=
{
"pk"
:
domain
.
id
})
def
form_valid
(
self
,
form
):
response
=
super
()
.
form_valid
(
form
)
print
(
form
.
cleaned_data
)
return
response
def
get_context_data
(
self
,
**
kwargs
):
context
=
{
'app'
:
_
(
'Assets'
),
...
...
apps/common/forms.py
View file @
722bf786
...
...
@@ -170,7 +170,7 @@ class TerminalSettingForm(BaseForm):
class
SecuritySettingForm
(
BaseForm
):
# MFA
全局设置
# MFA
global setting
SECURITY_MFA_AUTH
=
forms
.
BooleanField
(
initial
=
False
,
required
=
False
,
label
=
_
(
"MFA Secondary certification"
),
...
...
@@ -179,12 +179,26 @@ class SecuritySettingForm(BaseForm):
'authentication (valid for all users, including administrators)'
)
)
# 最小长度
# limit login count
SECURITY_LOGIN_LIMIT_COUNT
=
forms
.
IntegerField
(
initial
=
3
,
min_value
=
3
,
label
=
_
(
"Limit the number of login failures"
)
)
# limit login time
SECURITY_LOGIN_LIMIT_TIME
=
forms
.
IntegerField
(
initial
=
30
,
min_value
=
5
,
label
=
_
(
"No logon interval"
),
help_text
=
_
(
"Tip :(unit/minute) if the user has failed to log in for a limited "
"number of times, no login is allowed during this time interval."
)
)
# min length
SECURITY_PASSWORD_MIN_LENGTH
=
forms
.
IntegerField
(
initial
=
6
,
label
=
_
(
"Password minimum length"
),
min_value
=
6
)
#
大写字母
#
upper case
SECURITY_PASSWORD_UPPER_CASE
=
forms
.
BooleanField
(
initial
=
False
,
required
=
False
,
...
...
@@ -193,21 +207,21 @@ class SecuritySettingForm(BaseForm):
'After opening, the user password changes '
'and resets must contain uppercase letters'
)
)
#
小写字母
#
lower case
SECURITY_PASSWORD_LOWER_CASE
=
forms
.
BooleanField
(
initial
=
False
,
required
=
False
,
label
=
_
(
"Must contain lowercase letters"
),
help_text
=
_
(
'After opening, the user password changes '
'and resets must contain lowercase letters'
)
)
#
数字
#
number
SECURITY_PASSWORD_NUMBER
=
forms
.
BooleanField
(
initial
=
False
,
required
=
False
,
label
=
_
(
"Must contain numeric characters"
),
help_text
=
_
(
'After opening, the user password changes '
'and resets must contain numeric characters'
)
)
#
特殊字符
#
special char
SECURITY_PASSWORD_SPECIAL_CHAR
=
forms
.
BooleanField
(
initial
=
False
,
required
=
False
,
label
=
_
(
"Must contain special characters"
),
...
...
apps/common/templates/common/security_setting.html
View file @
722bf786
...
...
@@ -39,9 +39,9 @@
{% endif %}
{% csrf_token %}
<h3>
{% trans "
MFA setting
" %}
</h3>
<h3>
{% trans "
User login settings
" %}
</h3>
{% for field in form %}
{% if forloop.counter ==
2
%}
{% if forloop.counter ==
4
%}
<div
class=
"hr-line-dashed"
></div>
<h3>
{% trans "Password check rule" %}
</h3>
{% endif %}
...
...
apps/i18n/zh/LC_MESSAGES/django.mo
View file @
722bf786
No preview for this file type
apps/i18n/zh/LC_MESSAGES/django.po
View file @
722bf786
This diff is collapsed.
Click to expand it.
apps/jumpserver/settings.py
View file @
722bf786
...
...
@@ -343,10 +343,11 @@ if AUTH_LDAP:
AUTHENTICATION_BACKENDS
.
insert
(
0
,
AUTH_LDAP_BACKEND
)
# Celery using redis as broker
CELERY_BROKER_URL
=
'redis://:
%(password)
s@
%(host)
s:
%(port)
s/
3
'
%
{
CELERY_BROKER_URL
=
'redis://:
%(password)
s@
%(host)
s:
%(port)
s/
%(db)
s
'
%
{
'password'
:
CONFIG
.
REDIS_PASSWORD
if
CONFIG
.
REDIS_PASSWORD
else
''
,
'host'
:
CONFIG
.
REDIS_HOST
or
'127.0.0.1'
,
'port'
:
CONFIG
.
REDIS_PORT
or
6379
,
'db'
:
CONFIG
.
REDIS_DB_CELERY_BROKER
or
3
,
}
CELERY_TASK_SERIALIZER
=
'pickle'
CELERY_RESULT_SERIALIZER
=
'pickle'
...
...
@@ -367,10 +368,11 @@ CELERY_WORKER_HIJACK_ROOT_LOGGER = False
CACHES
=
{
'default'
:
{
'BACKEND'
:
'redis_cache.RedisCache'
,
'LOCATION'
:
'redis://:
%(password)
s@
%(host)
s:
%(port)
s/
4
'
%
{
'LOCATION'
:
'redis://:
%(password)
s@
%(host)
s:
%(port)
s/
%(db)
s
'
%
{
'password'
:
CONFIG
.
REDIS_PASSWORD
if
CONFIG
.
REDIS_PASSWORD
else
''
,
'host'
:
CONFIG
.
REDIS_HOST
or
'127.0.0.1'
,
'port'
:
CONFIG
.
REDIS_PORT
or
6379
,
'db'
:
CONFIG
.
REDIS_DB_CACHE
or
4
,
}
}
}
...
...
@@ -403,6 +405,8 @@ TERMINAL_REPLAY_STORAGE = {
DEFAULT_PASSWORD_MIN_LENGTH
=
6
DEFAULT_LOGIN_LIMIT_COUNT
=
3
DEFAULT_LOGIN_LIMIT_TIME
=
30
# Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html
BOOTSTRAP3
=
{
...
...
apps/ops/inventory.py
View file @
722bf786
...
...
@@ -93,7 +93,7 @@ class JMSInventory(BaseInventory):
if
gateway
.
password
:
proxy_command_list
.
insert
(
0
,
"sshpass -p
{}
"
.
format
(
gateway
.
password
)
0
,
"sshpass -p
'{}'
"
.
format
(
gateway
.
password
)
)
if
gateway
.
private_key
:
proxy_command_list
.
append
(
"-i {}"
.
format
(
gateway
.
private_key_file
))
...
...
apps/perms/api.py
View file @
722bf786
...
...
@@ -77,9 +77,9 @@ class UserGrantedAssetsApi(ListAPIView):
util
=
AssetPermissionUtil
(
user
)
for
k
,
v
in
util
.
get_assets
()
.
items
():
if
k
.
is_unixlike
():
system_users_granted
=
[
s
for
s
in
v
if
s
.
protocol
==
'ssh'
]
system_users_granted
=
[
s
for
s
in
v
if
s
.
protocol
in
[
'ssh'
,
'telnet'
]
]
else
:
system_users_granted
=
[
s
for
s
in
v
if
s
.
protocol
==
'rdp'
]
system_users_granted
=
[
s
for
s
in
v
if
s
.
protocol
in
[
'rdp'
,
'telnet'
]
]
k
.
system_users_granted
=
system_users_granted
queryset
.
append
(
k
)
return
queryset
...
...
@@ -128,9 +128,9 @@ class UserGrantedNodesWithAssetsApi(ListAPIView):
assets
=
_assets
.
keys
()
for
k
,
v
in
_assets
.
items
():
if
k
.
is_unixlike
():
system_users_granted
=
[
s
for
s
in
v
if
s
.
protocol
==
'ssh'
]
system_users_granted
=
[
s
for
s
in
v
if
s
.
protocol
in
[
'ssh'
,
'telnet'
]
]
else
:
system_users_granted
=
[
s
for
s
in
v
if
s
.
protocol
==
'rdp'
]
system_users_granted
=
[
s
for
s
in
v
if
s
.
protocol
in
[
'rdp'
,
'telnet'
]
]
k
.
system_users_granted
=
system_users_granted
node
.
assets_granted
=
assets
queryset
.
append
(
node
)
...
...
apps/perms/urls/views_urls.py
View file @
722bf786
...
...
@@ -6,13 +6,11 @@ from .. import views
app_name
=
'perms'
urlpatterns
=
[
url
(
r'^asset-permission$'
,
views
.
AssetPermissionListView
.
as_view
(),
name
=
'asset-permission-list'
),
url
(
r'^asset-permission/create$'
,
views
.
AssetPermissionCreateView
.
as_view
(),
name
=
'asset-permission-create'
),
url
(
r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/update$'
,
views
.
AssetPermissionUpdateView
.
as_view
(),
name
=
'asset-permission-update'
),
url
(
r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})$'
,
views
.
AssetPermissionDetailView
.
as_view
(),
name
=
'asset-permission-detail'
),
url
(
r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/delete$'
,
views
.
AssetPermissionDeleteView
.
as_view
(),
name
=
'asset-permission-delete'
),
url
(
r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/user$'
,
views
.
AssetPermissionUserView
.
as_view
(),
name
=
'asset-permission-user-list'
),
url
(
r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/asset$'
,
views
.
AssetPermissionAssetView
.
as_view
(),
name
=
'asset-permission-asset-list'
),
url
(
r'^asset-permission
/
$'
,
views
.
AssetPermissionListView
.
as_view
(),
name
=
'asset-permission-list'
),
url
(
r'^asset-permission/create
/
$'
,
views
.
AssetPermissionCreateView
.
as_view
(),
name
=
'asset-permission-create'
),
url
(
r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/update
/
$'
,
views
.
AssetPermissionUpdateView
.
as_view
(),
name
=
'asset-permission-update'
),
url
(
r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})
/
$'
,
views
.
AssetPermissionDetailView
.
as_view
(),
name
=
'asset-permission-detail'
),
url
(
r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/delete
/
$'
,
views
.
AssetPermissionDeleteView
.
as_view
(),
name
=
'asset-permission-delete'
),
url
(
r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/user
/
$'
,
views
.
AssetPermissionUserView
.
as_view
(),
name
=
'asset-permission-user-list'
),
url
(
r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/asset
/
$'
,
views
.
AssetPermissionAssetView
.
as_view
(),
name
=
'asset-permission-asset-list'
),
]
apps/static/js/jumpserver.js
View file @
722bf786
...
...
@@ -173,14 +173,14 @@ function APIUpdateAttr(props) {
}
if
(
typeof
props
.
success
===
'function'
)
{
return
props
.
success
(
data
);
}
}
}).
fail
(
function
(
jqXHR
,
textStatus
,
errorThrown
)
{
if
(
flash_message
)
{
toastr
.
error
(
fail_message
);
}
if
(
typeof
props
.
error
===
'function'
)
{
return
props
.
error
(
jqXHR
.
responseText
);
}
}
});
// return true;
}
...
...
@@ -198,7 +198,8 @@ function objectDelete(obj, name, url, redirectTo) {
}
};
var
fail
=
function
()
{
swal
(
"错误"
,
"删除"
+
"[ "
+
name
+
" ]"
+
"遇到错误"
,
"error"
);
// swal("错误", "删除"+"[ "+name+" ]"+"遇到错误", "error");
swal
(
"错误"
,
"[ "
+
name
+
" ]"
+
"正在被资产使用中,请先解除资产绑定"
,
"error"
);
};
APIUpdateAttr
({
url
:
url
,
...
...
@@ -219,7 +220,7 @@ function objectDelete(obj, name, url, redirectTo) {
confirmButtonText
:
'确认'
,
closeOnConfirm
:
true
,
},
function
()
{
doDelete
()
doDelete
()
});
}
...
...
@@ -272,7 +273,7 @@ jumpserver.initDataTable = function (options) {
$
(
td
).
html
(
'<input type="checkbox" class="text-center ipt_check" id=99991937>'
.
replace
(
'99991937'
,
cellData
));
}
},
{
className
:
'text-center'
,
targets
:
'_all'
}
{
className
:
'text-center'
,
render
:
$
.
fn
.
dataTable
.
render
.
text
(),
targets
:
'_all'
}
];
columnDefs
=
options
.
columnDefs
?
options
.
columnDefs
.
concat
(
columnDefs
)
:
columnDefs
;
var
select
=
{
...
...
apps/templates/_footer.html
View file @
722bf786
<div
class=
"footer fixed"
>
<div
class=
"pull-right"
>
Version
<strong>
1.3.
2
-{% include '_build.html' %}
</strong>
GPLv2.
Version
<strong>
1.3.
3
-{% include '_build.html' %}
</strong>
GPLv2.
<!--<img style="display: none" src="http://www.jumpserver.org/img/evaluate_avatar1.jpg">-->
</div>
<div>
...
...
apps/terminal/api.py
View file @
722bf786
...
...
@@ -259,10 +259,35 @@ class SessionReplayViewSet(viewsets.ViewSet):
serializer_class
=
ReplaySerializer
permission_classes
=
(
IsSuperUserOrAppUser
,)
session
=
None
def
gen_session_path
(
self
):
upload_to
=
'replay'
# 仅添加到本地存储中
def
get_session_path
(
self
,
version
=
2
):
"""
获取session日志的文件路径
:param version: 原来后缀是 .gz,为了统一新版本改为 .replay.gz
:return:
"""
suffix
=
'.replay.gz'
if
version
==
1
:
suffix
=
'.gz'
date
=
self
.
session
.
date_start
.
strftime
(
'
%
Y-
%
m-
%
d'
)
return
os
.
path
.
join
(
date
,
str
(
self
.
session
.
id
)
+
'.gz'
)
return
os
.
path
.
join
(
date
,
str
(
self
.
session
.
id
)
+
suffix
)
def
get_local_path
(
self
,
version
=
2
):
session_path
=
self
.
get_session_path
(
version
=
version
)
if
version
==
2
:
local_path
=
os
.
path
.
join
(
self
.
upload_to
,
session_path
)
else
:
local_path
=
session_path
return
local_path
def
save_to_storage
(
self
,
f
):
local_path
=
self
.
get_local_path
()
try
:
name
=
default_storage
.
save
(
local_path
,
f
)
return
name
,
None
except
OSError
as
e
:
return
None
,
e
def
create
(
self
,
request
,
*
args
,
**
kwargs
):
session_id
=
kwargs
.
get
(
'pk'
)
...
...
@@ -271,46 +296,49 @@ class SessionReplayViewSet(viewsets.ViewSet):
if
serializer
.
is_valid
():
file
=
serializer
.
validated_data
[
'file'
]
file_path
=
self
.
gen_session_path
(
)
try
:
default_storage
.
save
(
file_path
,
file
)
return
Response
({
'url'
:
default_storage
.
url
(
file_path
)},
status
=
201
)
except
IOError
:
return
Response
(
"Save error"
,
status
=
500
)
name
,
err
=
self
.
save_to_storage
(
file
)
if
not
name
:
msg
=
"Failed save replay `{}`: {}"
.
format
(
session_id
,
err
)
logger
.
error
(
msg
)
return
Response
({
'msg'
:
str
(
err
)},
status
=
400
)
url
=
default_storage
.
url
(
name
)
return
Response
({
'url'
:
url
},
status
=
201
)
else
:
logger
.
error
(
'Update load data invalid: {}'
.
format
(
serializer
.
errors
)
)
msg
=
'Upload data invalid: {}'
.
format
(
serializer
.
errors
)
logger
.
error
(
msg
)
return
Response
({
'msg'
:
serializer
.
errors
},
status
=
401
)
def
retrieve
(
self
,
request
,
*
args
,
**
kwargs
):
session_id
=
kwargs
.
get
(
'pk'
)
self
.
session
=
get_object_or_404
(
Session
,
id
=
session_id
)
path
=
self
.
gen_session_path
()
if
default_storage
.
exists
(
path
):
url
=
default_storage
.
url
(
path
)
return
redirect
(
url
)
else
:
config
=
settings
.
TERMINAL_REPLAY_STORAGE
configs
=
copy
.
deepcopy
(
config
)
for
cfg
in
config
:
if
config
[
cfg
][
'TYPE'
]
==
'server'
:
configs
.
__delitem__
(
cfg
)
if
not
configs
:
return
HttpResponseNotFound
()
date
=
self
.
session
.
date_start
.
strftime
(
'
%
Y-
%
m-
%
d'
)
file_path
=
os
.
path
.
join
(
date
,
str
(
self
.
session
.
id
)
+
'.replay.gz'
)
target_path
=
default_storage
.
base_location
+
'/'
+
path
storage
=
jms_storage
.
get_multi_object_storage
(
configs
)
ok
,
err
=
storage
.
download
(
file_path
,
target_path
)
if
ok
:
return
redirect
(
default_storage
.
url
(
path
))
else
:
logger
.
error
(
"Failed download replay file: {}"
.
format
(
err
))
return
HttpResponseNotFound
()
# 新版本和老版本的文件后缀不同
session_path
=
self
.
get_session_path
()
# 存在外部存储上的路径
local_path
=
self
.
get_local_path
()
local_path_v1
=
self
.
get_local_path
(
version
=
1
)
# 去default storage中查找
for
_local_path
in
(
local_path
,
local_path_v1
,
session_path
):
if
default_storage
.
exists
(
_local_path
):
url
=
default_storage
.
url
(
_local_path
)
return
redirect
(
url
)
# 去定义的外部storage查找
configs
=
settings
.
TERMINAL_REPLAY_STORAGE
configs
=
{
k
:
v
for
k
,
v
in
configs
.
items
()
if
v
[
'TYPE'
]
!=
'server'
}
if
not
configs
:
return
HttpResponseNotFound
()
target_path
=
os
.
path
.
join
(
default_storage
.
base_location
,
local_path
)
# 保存到storage的路径
target_dir
=
os
.
path
.
dirname
(
target_path
)
if
not
os
.
path
.
isdir
(
target_dir
):
os
.
makedirs
(
target_dir
,
exist_ok
=
True
)
storage
=
jms_storage
.
get_multi_object_storage
(
configs
)
ok
,
err
=
storage
.
download
(
session_path
,
target_path
)
if
not
ok
:
logger
.
error
(
"Failed download replay file: {}"
.
format
(
err
))
return
HttpResponseNotFound
()
return
redirect
(
default_storage
.
url
(
local_path
))
class
SessionReplayV2ViewSet
(
SessionReplayViewSet
):
...
...
apps/terminal/templates/terminal/session_list.html
View file @
722bf786
...
...
@@ -73,6 +73,7 @@
<th
class=
"text-center"
>
{% trans 'System user' %}
</th>
<th
class=
"text-center"
>
{% trans 'Remote addr' %}
</th>
<th
class=
"text-center"
>
{% trans 'Protocol' %}
</th>
<th
class=
"text-center"
>
{% trans 'Login from' %}
</th>
<th
class=
"text-center"
>
{% trans 'Command' %}
</th>
<th
class=
"text-center"
>
{% trans 'Date start' %}
</th>
{#
<th
class=
"text-center"
>
{% trans 'Date last active' %}
</th>
#}
...
...
@@ -92,6 +93,7 @@
<td
class=
"text-center"
>
{{ session.system_user }}
</td>
<td
class=
"text-center"
>
{{ session.remote_addr|default:"" }}
</td>
<td
class=
"text-center"
>
{{ session.protocol }}
</td>
<td
class=
"text-center"
>
{{ session.get_login_from_display }}
</td>
<td
class=
"text-center"
>
{{ session.id | get_session_command_amount }}
</td>
<td
class=
"text-center"
>
{{ session.date_start }}
</td>
...
...
apps/users/api.py
View file @
722bf786
...
...
@@ -3,6 +3,7 @@ import uuid
from
django.core.cache
import
cache
from
django.urls
import
reverse
from
django.utils.translation
import
ugettext
as
_
from
rest_framework
import
generics
from
rest_framework.permissions
import
AllowAny
,
IsAuthenticated
...
...
@@ -14,10 +15,11 @@ from .serializers import UserSerializer, UserGroupSerializer, \
UserGroupUpdateMemeberSerializer
,
UserPKUpdateSerializer
,
\
UserUpdateGroupSerializer
,
ChangeUserPasswordSerializer
from
.tasks
import
write_login_log_async
from
.models
import
User
,
UserGroup
from
.models
import
User
,
UserGroup
,
LoginLog
from
.permissions
import
IsSuperUser
,
IsValidUser
,
IsCurrentUserOrReadOnly
,
\
IsSuperUserOrAppUser
from
.utils
import
check_user_valid
,
generate_token
,
get_login_ip
,
check_otp_code
from
.utils
import
check_user_valid
,
generate_token
,
get_login_ip
,
\
check_otp_code
,
set_user_login_failed_count_to_cache
,
is_block_login
from
common.mixins
import
IDInFilterMixin
from
common.utils
import
get_logger
...
...
@@ -93,6 +95,22 @@ class UserUpdatePKApi(generics.UpdateAPIView):
user
.
save
()
class
UserUnblockPKApi
(
generics
.
UpdateAPIView
):
queryset
=
User
.
objects
.
all
()
permission_classes
=
(
IsSuperUser
,)
serializer_class
=
UserSerializer
key_prefix_limit
=
"_LOGIN_LIMIT_{}_{}"
key_prefix_block
=
"_LOGIN_BLOCK_{}"
def
perform_update
(
self
,
serializer
):
user
=
self
.
get_object
()
username
=
user
.
username
if
user
else
''
key_limit
=
self
.
key_prefix_limit
.
format
(
username
,
'*'
)
key_block
=
self
.
key_prefix_block
.
format
(
username
)
cache
.
delete_pattern
(
key_limit
)
cache
.
delete
(
key_block
)
class
UserGroupViewSet
(
IDInFilterMixin
,
BulkModelViewSet
):
queryset
=
UserGroup
.
objects
.
all
()
serializer_class
=
UserGroupSerializer
...
...
@@ -128,16 +146,12 @@ class UserToken(APIView):
return
Response
({
'error'
:
msg
},
status
=
406
)
class
UserProfile
(
APIView
):
permission_classes
=
(
Is
ValidUser
,)
class
UserProfile
(
generics
.
Retrieve
APIView
):
permission_classes
=
(
Is
Authenticated
,)
serializer_class
=
UserSerializer
def
get
(
self
,
request
):
# return Response(request.user.to_json())
return
Response
(
self
.
serializer_class
(
request
.
user
)
.
data
)
def
post
(
self
,
request
):
return
Response
(
self
.
serializer_class
(
request
.
user
)
.
data
)
def
get_object
(
self
):
return
self
.
request
.
user
class
UserOtpAuthApi
(
APIView
):
...
...
@@ -153,10 +167,23 @@ class UserOtpAuthApi(APIView):
return
Response
({
'msg'
:
'请先进行用户名和密码验证'
},
status
=
401
)
if
not
check_otp_code
(
user
.
otp_secret_key
,
otp_code
):
data
=
{
'username'
:
user
.
username
,
'mfa'
:
int
(
user
.
otp_enabled
),
'reason'
:
LoginLog
.
REASON_MFA
,
'status'
:
False
}
self
.
write_login_log
(
request
,
data
)
return
Response
({
'msg'
:
'MFA认证失败'
},
status
=
401
)
data
=
{
'username'
:
user
.
username
,
'mfa'
:
int
(
user
.
otp_enabled
),
'reason'
:
LoginLog
.
REASON_NOTHING
,
'status'
:
True
}
self
.
write_login_log
(
request
,
data
)
token
=
generate_token
(
request
,
user
)
self
.
write_login_log
(
request
,
user
)
return
Response
(
{
'token'
:
token
,
...
...
@@ -165,7 +192,7 @@ class UserOtpAuthApi(APIView):
)
@staticmethod
def
write_login_log
(
request
,
user
):
def
write_login_log
(
request
,
data
):
login_ip
=
request
.
data
.
get
(
'remote_addr'
,
None
)
login_type
=
request
.
data
.
get
(
'login_type'
,
''
)
user_agent
=
request
.
data
.
get
(
'HTTP_USER_AGENT'
,
''
)
...
...
@@ -173,25 +200,54 @@ class UserOtpAuthApi(APIView):
if
not
login_ip
:
login_ip
=
get_login_ip
(
request
)
write_login_log_async
.
delay
(
user
.
username
,
ip
=
login_ip
,
type
=
login_type
,
user_agent
=
user_agent
,
)
tmp_data
=
{
'ip'
:
login_ip
,
'type'
:
login_type
,
'user_agent'
:
user_agent
}
data
.
update
(
tmp_data
)
write_login_log_async
.
delay
(
**
data
)
class
UserAuthApi
(
APIView
):
permission_classes
=
(
AllowAny
,)
serializer_class
=
UserSerializer
key_prefix_limit
=
"_LOGIN_LIMIT_{}_{}"
key_prefix_block
=
"_LOGIN_BLOCK_{}"
def
post
(
self
,
request
):
user
,
msg
=
self
.
check_user_valid
(
request
)
# limit login
username
=
request
.
data
.
get
(
'username'
)
ip
=
request
.
data
.
get
(
'remote_addr'
,
None
)
ip
=
ip
if
ip
else
get_login_ip
(
request
)
key_limit
=
self
.
key_prefix_limit
.
format
(
username
,
ip
)
key_block
=
self
.
key_prefix_block
.
format
(
username
)
if
is_block_login
(
key_limit
):
msg
=
_
(
"Log in frequently and try again later"
)
return
Response
({
'msg'
:
msg
},
status
=
401
)
user
,
msg
=
self
.
check_user_valid
(
request
)
if
not
user
:
data
=
{
'username'
:
request
.
data
.
get
(
'username'
,
''
),
'mfa'
:
LoginLog
.
MFA_UNKNOWN
,
'reason'
:
LoginLog
.
REASON_PASSWORD
,
'status'
:
False
}
self
.
write_login_log
(
request
,
data
)
set_user_login_failed_count_to_cache
(
key_limit
,
key_block
)
return
Response
({
'msg'
:
msg
},
status
=
401
)
if
not
user
.
otp_enabled
:
data
=
{
'username'
:
user
.
username
,
'mfa'
:
int
(
user
.
otp_enabled
),
'reason'
:
LoginLog
.
REASON_NOTHING
,
'status'
:
True
}
self
.
write_login_log
(
request
,
data
)
token
=
generate_token
(
request
,
user
)
self
.
write_login_log
(
request
,
user
)
return
Response
(
{
'token'
:
token
,
...
...
@@ -208,7 +264,8 @@ class UserAuthApi(APIView):
'otp_url'
:
reverse
(
'api-users:user-otp-auth'
),
'seed'
:
seed
,
'user'
:
self
.
serializer_class
(
user
)
.
data
},
status
=
300
)
},
status
=
300
)
@staticmethod
def
check_user_valid
(
request
):
...
...
@@ -222,7 +279,7 @@ class UserAuthApi(APIView):
return
user
,
msg
@staticmethod
def
write_login_log
(
request
,
user
):
def
write_login_log
(
request
,
data
):
login_ip
=
request
.
data
.
get
(
'remote_addr'
,
None
)
login_type
=
request
.
data
.
get
(
'login_type'
,
''
)
user_agent
=
request
.
data
.
get
(
'HTTP_USER_AGENT'
,
''
)
...
...
@@ -230,10 +287,14 @@ class UserAuthApi(APIView):
if
not
login_ip
:
login_ip
=
get_login_ip
(
request
)
write_login_log_async
.
delay
(
user
.
username
,
ip
=
login_ip
,
type
=
login_type
,
user_agent
=
user_agent
,
)
tmp_data
=
{
'ip'
:
login_ip
,
'type'
:
login_type
,
'user_agent'
:
user_agent
,
}
data
.
update
(
tmp_data
)
write_login_log_async
.
delay
(
**
data
)
class
UserConnectionTokenApi
(
APIView
):
...
...
apps/users/models/authentication.py
View file @
722bf786
...
...
@@ -41,12 +41,40 @@ class LoginLog(models.Model):
(
'W'
,
'Web'
),
(
'T'
,
'Terminal'
),
)
MFA_DISABLED
=
0
MFA_ENABLED
=
1
MFA_UNKNOWN
=
2
MFA_CHOICE
=
(
(
MFA_DISABLED
,
_
(
'Disabled'
)),
(
MFA_ENABLED
,
_
(
'Enabled'
)),
(
MFA_UNKNOWN
,
_
(
'-'
)),
)
REASON_NOTHING
=
0
REASON_PASSWORD
=
1
REASON_MFA
=
2
REASON_CHOICE
=
(
(
REASON_NOTHING
,
_
(
'-'
)),
(
REASON_PASSWORD
,
_
(
'Username/password check failed'
)),
(
REASON_MFA
,
_
(
'MFA authentication failed'
)),
)
STATUS_CHOICE
=
(
(
True
,
_
(
'Success'
)),
(
False
,
_
(
'Failed'
))
)
id
=
models
.
UUIDField
(
default
=
uuid
.
uuid4
,
primary_key
=
True
)
username
=
models
.
CharField
(
max_length
=
20
,
verbose_name
=
_
(
'Username'
))
type
=
models
.
CharField
(
choices
=
LOGIN_TYPE_CHOICE
,
max_length
=
2
,
verbose_name
=
_
(
'Login type'
))
ip
=
models
.
GenericIPAddressField
(
verbose_name
=
_
(
'Login ip'
))
city
=
models
.
CharField
(
max_length
=
254
,
blank
=
True
,
null
=
True
,
verbose_name
=
_
(
'Login city'
))
user_agent
=
models
.
CharField
(
max_length
=
254
,
blank
=
True
,
null
=
True
,
verbose_name
=
_
(
'User agent'
))
mfa
=
models
.
SmallIntegerField
(
default
=
MFA_UNKNOWN
,
choices
=
MFA_CHOICE
,
verbose_name
=
_
(
'MFA'
))
reason
=
models
.
SmallIntegerField
(
default
=
REASON_NOTHING
,
choices
=
REASON_CHOICE
,
verbose_name
=
_
(
'Reason'
))
status
=
models
.
BooleanField
(
max_length
=
2
,
default
=
True
,
choices
=
STATUS_CHOICE
,
verbose_name
=
_
(
'Status'
))
datetime
=
models
.
DateTimeField
(
auto_now_add
=
True
,
verbose_name
=
_
(
'Date login'
))
class
Meta
:
...
...
apps/users/templates/users/login.html
View file @
722bf786
...
...
@@ -45,13 +45,17 @@
</div>
<form
class=
"m-t"
role=
"form"
method=
"post"
action=
""
>
{% csrf_token %}
{% if form.errors %}
{% if block_login %}
<p
class=
"red-fonts"
>
{% trans 'Log in frequently and try again later' %}
</p>
{% elif form.errors %}
{% if 'captcha' in form.errors %}
<p
class=
"red-fonts"
>
{% trans 'Captcha invalid' %}
</p>
{% else %}
<p
class=
"red-fonts"
>
{{ form.non_field_errors.as_text }}
</p>
{% endif %}
{% endif %}
<div
class=
"form-group"
>
<input
type=
"text"
class=
"form-control"
name=
"{{ form.username.html_name }}"
placeholder=
"{% trans 'Username' %}"
required=
""
value=
"{% if form.username.value %}{{ form.username.value }}{% endif %}"
>
</div>
...
...
apps/users/templates/users/login_log_list.html
View file @
722bf786
...
...
@@ -51,6 +51,9 @@
<th
class=
"text-center"
>
{% trans 'UA' %}
</th>
<th
class=
"text-center"
>
{% trans 'IP' %}
</th>
<th
class=
"text-center"
>
{% trans 'City' %}
</th>
<th
class=
"text-center"
>
{% trans 'MFA' %}
</th>
<th
class=
"text-center"
>
{% trans 'Reason' %}
</th>
<th
class=
"text-center"
>
{% trans 'Status' %}
</th>
<th
class=
"text-center"
>
{% trans 'Date' %}
</th>
{% endblock %}
...
...
@@ -65,6 +68,9 @@
</td>
<td
class=
"text-center"
>
{{ login_log.ip }}
</td>
<td
class=
"text-center"
>
{{ login_log.city }}
</td>
<td
class=
"text-center"
>
{{ login_log.get_mfa_display }}
</td>
<td
class=
"text-center"
>
{{ login_log.get_reason_display }}
</td>
<td
class=
"text-center"
>
{{ login_log.get_status_display }}
</td>
<td
class=
"text-center"
>
{{ login_log.datetime }}
</td>
</tr>
{% endfor %}
...
...
apps/users/templates/users/user_detail.html
View file @
722bf786
...
...
@@ -182,6 +182,14 @@
</span>
</td>
</tr>
<tr
style=
"{% if not unblock %}display:none{% endif %}"
>
<td>
{% trans 'Unblock user' %}
</td>
<td>
<span
class=
"pull-right"
>
<button
type=
"button"
class=
"btn btn-primary btn-xs"
id=
"btn-unblock-user"
style=
"width: 54px"
>
{% trans 'Unblock' %}
</button>
</span>
</td>
</tr>
</tbody>
</table>
</div>
...
...
@@ -275,7 +283,7 @@ $(document).ready(function() {
.
on
(
'select2:unselect'
,
function
(
evt
)
{
var
data
=
evt
.
params
.
data
;
delete
jumpserver
.
nodes_selected
[
data
.
id
];
})
})
;
})
.
on
(
'click'
,
'#is_active'
,
function
()
{
var
the_url
=
"{% url 'api-users:user-detail' pk=user_object.id %}"
;
...
...
@@ -293,7 +301,7 @@ $(document).ready(function() {
.
on
(
'click'
,
'#force_enable_otp'
,
function
()
{
{
%
if
request
.
user
==
user_object
%
}
toastr
.
error
(
"{% trans 'Goto profile page enable MFA' %}"
);
return
return
;
{
%
endif
%
}
var
the_url
=
"{% url 'api-users:user-detail' pk=user_object.id %}"
;
...
...
@@ -421,11 +429,45 @@ $(document).ready(function() {
APIUpdateAttr({ url: the_url, body: JSON.stringify(body), success: success, error: fail});
}).on('click', '.btn-delete-user', function () {
var $this = $(this);
var name = "{{ user.name }}";
var uid = "{{ user.id }}";
var name = "{{ user
_object
.name }}";
var uid = "{{ user
_object
.id }}";
var the_url = '{% url "api-users:user-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
var redirect_url = "{% url 'users:user-list' %}";
objectDelete($this, name, the_url, redirect_url);
}).on('click', '#btn-unblock-user', function () {
function doReset() {
{#var the_url = '{% url "api-users:user-reset-password" pk=user_object.id %}';#}
var the_url = '{% url "api-users:user-unblock" pk=user_object.id %}';
var body = {};
var success = function() {
var msg = "{% trans "Success" %}";
{#swal("{% trans 'Unblock user' %}", msg, "success");#}
swal({
title: "{% trans 'Unblock user' %}",
text: msg,
type: "success"
}, function() {
location.reload()
}
);
};
APIUpdateAttr({
url: the_url,
body: JSON.stringify(body),
success: success
});
}
swal({
title: "{% trans 'Are you sure?' %}",
text: "{% trans "After unlocking the user, the user can log in normally."%}",
type: "warning",
showCancelButton: true,
confirmButtonColor: "#DD6B55",
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false
}, function() {
doReset();
});
})
</script>
{% endblock %}
apps/users/templates/users/user_list.html
View file @
722bf786
...
...
@@ -59,7 +59,7 @@ function initTable() {
ele
:
$
(
'#user_list_table'
),
columnDefs
:
[
{
targets
:
1
,
createdCell
:
function
(
td
,
cellData
,
rowData
)
{
var
detail_btn
=
'<a href="{% url "users:user-detail" pk=DEFAULT_PK %}">'
+
cellData
+
'</a>'
;
var
detail_btn
=
'<a href="{% url "users:user-detail" pk=DEFAULT_PK %}">'
+
escape
(
cellData
)
+
'</a>'
;
$
(
td
).
html
(
detail_btn
.
replace
(
"{{ DEFAULT_PK }}"
,
rowData
.
id
));
}},
{
targets
:
4
,
createdCell
:
function
(
td
,
cellData
)
{
...
...
apps/users/urls/api_urls.py
View file @
722bf786
...
...
@@ -29,6 +29,8 @@ urlpatterns = [
api
.
UserResetPKApi
.
as_view
(),
name
=
'user-public-key-reset'
),
url
(
r'^v1/users/(?P<pk>[0-9a-zA-Z\-]{36})/pubkey/update/$'
,
api
.
UserUpdatePKApi
.
as_view
(),
name
=
'user-public-key-update'
),
url
(
r'^v1/users/(?P<pk>[0-9a-zA-Z\-]{36})/unblock/$'
,
api
.
UserUnblockPKApi
.
as_view
(),
name
=
'user-unblock'
),
url
(
r'^v1/users/(?P<pk>[0-9a-zA-Z\-]{36})/groups/$'
,
api
.
UserUpdateGroupApi
.
as_view
(),
name
=
'user-update-group'
),
url
(
r'^v1/groups/(?P<pk>[0-9a-zA-Z\-]{36})/users/$'
,
...
...
apps/users/urls/views_urls.py
View file @
722bf786
...
...
@@ -8,13 +8,13 @@ app_name = 'users'
urlpatterns
=
[
# Login view
url
(
r'^login$'
,
views
.
UserLoginView
.
as_view
(),
name
=
'login'
),
url
(
r'^logout$'
,
views
.
UserLogoutView
.
as_view
(),
name
=
'logout'
),
url
(
r'^login/otp$'
,
views
.
UserLoginOtpView
.
as_view
(),
name
=
'login-otp'
),
url
(
r'^password/forgot$'
,
views
.
UserForgotPasswordView
.
as_view
(),
name
=
'forgot-password'
),
url
(
r'^password/forgot/sendmail-success$'
,
views
.
UserForgotPasswordSendmailSuccessView
.
as_view
(),
name
=
'forgot-password-sendmail-success'
),
url
(
r'^password/reset$'
,
views
.
UserResetPasswordView
.
as_view
(),
name
=
'reset-password'
),
url
(
r'^password/reset/success$'
,
views
.
UserResetPasswordSuccessView
.
as_view
(),
name
=
'reset-password-success'
),
url
(
r'^login
/
$'
,
views
.
UserLoginView
.
as_view
(),
name
=
'login'
),
url
(
r'^logout
/
$'
,
views
.
UserLogoutView
.
as_view
(),
name
=
'logout'
),
url
(
r'^login/otp
/
$'
,
views
.
UserLoginOtpView
.
as_view
(),
name
=
'login-otp'
),
url
(
r'^password/forgot
/
$'
,
views
.
UserForgotPasswordView
.
as_view
(),
name
=
'forgot-password'
),
url
(
r'^password/forgot/sendmail-success
/
$'
,
views
.
UserForgotPasswordSendmailSuccessView
.
as_view
(),
name
=
'forgot-password-sendmail-success'
),
url
(
r'^password/reset
/
$'
,
views
.
UserResetPasswordView
.
as_view
(),
name
=
'reset-password'
),
url
(
r'^password/reset/success
/
$'
,
views
.
UserResetPasswordSuccessView
.
as_view
(),
name
=
'reset-password-success'
),
# Profile
url
(
r'^profile/$'
,
views
.
UserProfileView
.
as_view
(),
name
=
'user-profile'
),
...
...
@@ -29,23 +29,23 @@ urlpatterns = [
url
(
r'^profile/otp/settings-success/$'
,
views
.
UserOtpSettingsSuccessView
.
as_view
(),
name
=
'user-otp-settings-success'
),
# User view
url
(
r'^user$'
,
views
.
UserListView
.
as_view
(),
name
=
'user-list'
),
url
(
r'^user/export/'
,
views
.
UserExportView
.
as_view
(),
name
=
'user-export'
),
url
(
r'^user
/
$'
,
views
.
UserListView
.
as_view
(),
name
=
'user-list'
),
url
(
r'^user/export/
$
'
,
views
.
UserExportView
.
as_view
(),
name
=
'user-export'
),
url
(
r'^first-login/$'
,
views
.
UserFirstLoginView
.
as_view
(),
name
=
'user-first-login'
),
url
(
r'^user/import/$'
,
views
.
UserBulkImportView
.
as_view
(),
name
=
'user-import'
),
url
(
r'^user/create$'
,
views
.
UserCreateView
.
as_view
(),
name
=
'user-create'
),
url
(
r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/update$'
,
views
.
UserUpdateView
.
as_view
(),
name
=
'user-update'
),
url
(
r'^user/update$'
,
views
.
UserBulkUpdateView
.
as_view
(),
name
=
'user-bulk-update'
),
url
(
r'^user/(?P<pk>[0-9a-zA-Z\-]{36})$'
,
views
.
UserDetailView
.
as_view
(),
name
=
'user-detail'
),
url
(
r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/assets'
,
views
.
UserGrantedAssetView
.
as_view
(),
name
=
'user-granted-asset'
),
url
(
r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/login-history'
,
views
.
UserDetailView
.
as_view
(),
name
=
'user-login-history'
),
url
(
r'^user/create
/
$'
,
views
.
UserCreateView
.
as_view
(),
name
=
'user-create'
),
url
(
r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/update
/
$'
,
views
.
UserUpdateView
.
as_view
(),
name
=
'user-update'
),
url
(
r'^user/update
/
$'
,
views
.
UserBulkUpdateView
.
as_view
(),
name
=
'user-bulk-update'
),
url
(
r'^user/(?P<pk>[0-9a-zA-Z\-]{36})
/
$'
,
views
.
UserDetailView
.
as_view
(),
name
=
'user-detail'
),
url
(
r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/assets
/$
'
,
views
.
UserGrantedAssetView
.
as_view
(),
name
=
'user-granted-asset'
),
url
(
r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/login-history
/$
'
,
views
.
UserDetailView
.
as_view
(),
name
=
'user-login-history'
),
# User group view
url
(
r'^user-group$'
,
views
.
UserGroupListView
.
as_view
(),
name
=
'user-group-list'
),
url
(
r'^user-group/(?P<pk>[0-9a-zA-Z\-]{36})$'
,
views
.
UserGroupDetailView
.
as_view
(),
name
=
'user-group-detail'
),
url
(
r'^user-group/create$'
,
views
.
UserGroupCreateView
.
as_view
(),
name
=
'user-group-create'
),
url
(
r'^user-group/(?P<pk>[0-9a-zA-Z\-]{36})/update$'
,
views
.
UserGroupUpdateView
.
as_view
(),
name
=
'user-group-update'
),
url
(
r'^user-group/(?P<pk>[0-9a-zA-Z\-]{36})/assets'
,
views
.
UserGroupGrantedAssetView
.
as_view
(),
name
=
'user-group-granted-asset'
),
url
(
r'^user-group
/
$'
,
views
.
UserGroupListView
.
as_view
(),
name
=
'user-group-list'
),
url
(
r'^user-group/(?P<pk>[0-9a-zA-Z\-]{36})
/
$'
,
views
.
UserGroupDetailView
.
as_view
(),
name
=
'user-group-detail'
),
url
(
r'^user-group/create
/
$'
,
views
.
UserGroupCreateView
.
as_view
(),
name
=
'user-group-create'
),
url
(
r'^user-group/(?P<pk>[0-9a-zA-Z\-]{36})/update
/
$'
,
views
.
UserGroupUpdateView
.
as_view
(),
name
=
'user-group-update'
),
url
(
r'^user-group/(?P<pk>[0-9a-zA-Z\-]{36})/assets
/$
'
,
views
.
UserGroupGrantedAssetView
.
as_view
(),
name
=
'user-group-granted-asset'
),
# Login log
url
(
r'^login-log/$'
,
views
.
LoginLogListView
.
as_view
(),
name
=
'login-log-list'
),
...
...
apps/users/utils.py
View file @
722bf786
...
...
@@ -13,7 +13,7 @@ import ipaddress
from
django.http
import
Http404
from
django.conf
import
settings
from
django.contrib.auth.mixins
import
UserPassesTestMixin
from
django.contrib.auth
import
authenticate
,
login
as
auth_login
from
django.contrib.auth
import
authenticate
from
django.utils.translation
import
ugettext
as
_
from
django.core.cache
import
cache
...
...
@@ -200,16 +200,15 @@ def get_login_ip(request):
return
login_ip
def
write_login_log
(
username
,
type
=
''
,
ip
=
''
,
user_agent
=
''
):
def
write_login_log
(
*
args
,
**
kwargs
):
ip
=
kwargs
.
get
(
'ip'
,
''
)
if
not
(
ip
and
validate_ip
(
ip
)):
ip
=
ip
[:
15
]
city
=
"Unknown"
else
:
city
=
get_ip_city
(
ip
)
LoginLog
.
objects
.
create
(
username
=
username
,
type
=
type
,
ip
=
ip
,
city
=
city
,
user_agent
=
user_agent
)
kwargs
.
update
({
'ip'
:
ip
,
'city'
:
city
})
LoginLog
.
objects
.
create
(
**
kwargs
)
def
get_ip_city
(
ip
,
timeout
=
10
):
...
...
@@ -332,3 +331,44 @@ def check_password_rules(password):
match_obj
=
re
.
match
(
pattern
,
password
)
return
bool
(
match_obj
)
def
set_user_login_failed_count_to_cache
(
key_limit
,
key_block
):
count
=
cache
.
get
(
key_limit
)
count
=
count
+
1
if
count
else
1
setting_limit_time
=
Setting
.
objects
.
filter
(
name
=
'SECURITY_LOGIN_LIMIT_TIME'
)
.
first
()
limit_time
=
setting_limit_time
.
cleaned_value
if
setting_limit_time
\
else
settings
.
DEFAULT_LOGIN_LIMIT_TIME
setting_limit_count
=
Setting
.
objects
.
filter
(
name
=
'SECURITY_LOGIN_LIMIT_COUNT'
)
.
first
()
limit_count
=
setting_limit_count
.
cleaned_value
if
setting_limit_count
\
else
settings
.
DEFAULT_LOGIN_LIMIT_COUNT
if
count
>=
limit_count
:
cache
.
set
(
key_block
,
1
,
int
(
limit_time
)
*
60
)
cache
.
set
(
key_limit
,
count
,
int
(
limit_time
)
*
60
)
def
is_block_login
(
key_limit
):
count
=
cache
.
get
(
key_limit
)
setting_limit_count
=
Setting
.
objects
.
filter
(
name
=
'SECURITY_LOGIN_LIMIT_COUNT'
)
.
first
()
limit_count
=
setting_limit_count
.
cleaned_value
if
setting_limit_count
\
else
settings
.
DEFAULT_LOGIN_LIMIT_COUNT
if
count
and
count
>=
limit_count
:
return
True
def
is_need_unblock
(
key_block
):
if
not
cache
.
get
(
key_block
):
return
False
return
True
apps/users/views/login.py
View file @
722bf786
...
...
@@ -25,8 +25,10 @@ from common.utils import get_object_or_none
from
common.mixins
import
DatetimeSearchMixin
,
AdminUserRequiredMixin
from
common.models
import
Setting
from
..models
import
User
,
LoginLog
from
..utils
import
send_reset_password_mail
,
check_otp_code
,
get_login_ip
,
redirect_user_first_login_or_index
,
\
get_user_or_tmp_user
,
set_tmp_user_to_cache
,
get_password_check_rules
,
check_password_rules
from
..utils
import
send_reset_password_mail
,
check_otp_code
,
get_login_ip
,
\
redirect_user_first_login_or_index
,
get_user_or_tmp_user
,
\
set_tmp_user_to_cache
,
get_password_check_rules
,
check_password_rules
,
\
is_block_login
,
set_user_login_failed_count_to_cache
from
..tasks
import
write_login_log_async
from
..
import
forms
...
...
@@ -47,7 +49,9 @@ class UserLoginView(FormView):
form_class
=
forms
.
UserLoginForm
form_class_captcha
=
forms
.
UserLoginCaptchaForm
redirect_field_name
=
'next'
key_prefix
=
"_LOGIN_INVALID_{}"
key_prefix_captcha
=
"_LOGIN_INVALID_{}"
key_prefix_limit
=
"_LOGIN_LIMIT_{}_{}"
key_prefix_block
=
"_LOGIN_BLOCK_{}"
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
if
request
.
user
.
is_staff
:
...
...
@@ -57,6 +61,16 @@ class UserLoginView(FormView):
request
.
session
.
set_test_cookie
()
return
super
()
.
get
(
request
,
*
args
,
**
kwargs
)
def
post
(
self
,
request
,
*
args
,
**
kwargs
):
# limit login authentication
ip
=
get_login_ip
(
request
)
username
=
self
.
request
.
POST
.
get
(
'username'
)
key_limit
=
self
.
key_prefix_limit
.
format
(
username
,
ip
)
if
is_block_login
(
key_limit
):
return
self
.
render_to_response
(
self
.
get_context_data
(
block_login
=
True
))
return
super
()
.
post
(
request
,
*
args
,
**
kwargs
)
def
form_valid
(
self
,
form
):
if
not
self
.
request
.
session
.
test_cookie_worked
():
return
HttpResponse
(
_
(
"Please enable cookies and try again."
))
...
...
@@ -65,8 +79,24 @@ class UserLoginView(FormView):
return
redirect
(
self
.
get_success_url
())
def
form_invalid
(
self
,
form
):
# write login failed log
username
=
form
.
cleaned_data
.
get
(
'username'
)
data
=
{
'username'
:
username
,
'mfa'
:
LoginLog
.
MFA_UNKNOWN
,
'reason'
:
LoginLog
.
REASON_PASSWORD
,
'status'
:
False
}
self
.
write_login_log
(
data
)
# limit user login failed count
ip
=
get_login_ip
(
self
.
request
)
cache
.
set
(
self
.
key_prefix
.
format
(
ip
),
1
,
3600
)
key_limit
=
self
.
key_prefix_limit
.
format
(
username
,
ip
)
key_block
=
self
.
key_prefix_block
.
format
(
username
)
set_user_login_failed_count_to_cache
(
key_limit
,
key_block
)
# show captcha
cache
.
set
(
self
.
key_prefix_captcha
.
format
(
ip
),
1
,
3600
)
old_form
=
form
form
=
self
.
form_class_captcha
(
data
=
form
.
data
)
form
.
_errors
=
old_form
.
errors
...
...
@@ -74,7 +104,7 @@ class UserLoginView(FormView):
def
get_form_class
(
self
):
ip
=
get_login_ip
(
self
.
request
)
if
cache
.
get
(
self
.
key_prefix
.
format
(
ip
)):
if
cache
.
get
(
self
.
key_prefix
_captcha
.
format
(
ip
)):
return
self
.
form_class_captcha
else
:
return
self
.
form_class
...
...
@@ -91,7 +121,13 @@ class UserLoginView(FormView):
elif
not
user
.
otp_enabled
:
# 0 & T,F
auth_login
(
self
.
request
,
user
)
self
.
write_login_log
()
data
=
{
'username'
:
self
.
request
.
user
.
username
,
'mfa'
:
int
(
self
.
request
.
user
.
otp_enabled
),
'reason'
:
LoginLog
.
REASON_NOTHING
,
'status'
:
True
}
self
.
write_login_log
(
data
)
return
redirect_user_first_login_or_index
(
self
.
request
,
self
.
redirect_field_name
)
def
get_context_data
(
self
,
**
kwargs
):
...
...
@@ -101,13 +137,16 @@ class UserLoginView(FormView):
kwargs
.
update
(
context
)
return
super
()
.
get_context_data
(
**
kwargs
)
def
write_login_log
(
self
):
def
write_login_log
(
self
,
data
):
login_ip
=
get_login_ip
(
self
.
request
)
user_agent
=
self
.
request
.
META
.
get
(
'HTTP_USER_AGENT'
,
''
)
write_login_log_async
.
delay
(
self
.
request
.
user
.
username
,
type
=
'W'
,
ip
=
login_ip
,
user_agent
=
user_agent
)
tmp_data
=
{
'ip'
:
login_ip
,
'type'
:
'W'
,
'user_agent'
:
user_agent
}
data
.
update
(
tmp_data
)
write_login_log_async
.
delay
(
**
data
)
class
UserLoginOtpView
(
FormView
):
...
...
@@ -122,22 +161,38 @@ class UserLoginOtpView(FormView):
if
check_otp_code
(
otp_secret_key
,
otp_code
):
auth_login
(
self
.
request
,
user
)
self
.
write_login_log
()
data
=
{
'username'
:
self
.
request
.
user
.
username
,
'mfa'
:
int
(
self
.
request
.
user
.
otp_enabled
),
'reason'
:
LoginLog
.
REASON_NOTHING
,
'status'
:
True
}
self
.
write_login_log
(
data
)
return
redirect
(
self
.
get_success_url
())
else
:
data
=
{
'username'
:
user
.
username
,
'mfa'
:
int
(
user
.
otp_enabled
),
'reason'
:
LoginLog
.
REASON_MFA
,
'status'
:
False
}
self
.
write_login_log
(
data
)
form
.
add_error
(
'otp_code'
,
_
(
'MFA code invalid'
))
return
super
()
.
form_invalid
(
form
)
def
get_success_url
(
self
):
return
redirect_user_first_login_or_index
(
self
.
request
,
self
.
redirect_field_name
)
def
write_login_log
(
self
):
def
write_login_log
(
self
,
data
):
login_ip
=
get_login_ip
(
self
.
request
)
user_agent
=
self
.
request
.
META
.
get
(
'HTTP_USER_AGENT'
,
''
)
write_login_log_async
.
delay
(
self
.
request
.
user
.
username
,
type
=
'W'
,
ip
=
login_ip
,
user_agent
=
user_agent
)
tmp_data
=
{
'ip'
:
login_ip
,
'type'
:
'W'
,
'user_agent'
:
user_agent
}
data
.
update
(
tmp_data
)
write_login_log_async
.
delay
(
**
data
)
@method_decorator
(
never_cache
,
name
=
'dispatch'
)
...
...
apps/users/views/user.py
View file @
722bf786
...
...
@@ -36,7 +36,9 @@ from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen
from
common.models
import
Setting
from
..
import
forms
from
..models
import
User
,
UserGroup
from
..utils
import
AdminUserRequiredMixin
,
generate_otp_uri
,
check_otp_code
,
get_user_or_tmp_user
,
get_password_check_rules
,
check_password_rules
from
..utils
import
AdminUserRequiredMixin
,
generate_otp_uri
,
check_otp_code
,
\
get_user_or_tmp_user
,
get_password_check_rules
,
check_password_rules
,
\
is_need_unblock
from
..signals
import
post_user_create
from
..tasks
import
write_login_log_async
...
...
@@ -168,13 +170,17 @@ class UserDetailView(AdminUserRequiredMixin, DetailView):
model
=
User
template_name
=
'users/user_detail.html'
context_object_name
=
"user_object"
key_prefix_block
=
"_LOGIN_BLOCK_{}"
def
get_context_data
(
self
,
**
kwargs
):
user
=
self
.
get_object
()
key_block
=
self
.
key_prefix_block
.
format
(
user
.
username
)
groups
=
UserGroup
.
objects
.
exclude
(
id__in
=
self
.
object
.
groups
.
all
())
context
=
{
'app'
:
_
(
'Users'
),
'action'
:
_
(
'User detail'
),
'groups'
:
groups
'groups'
:
groups
,
'unblock'
:
is_need_unblock
(
key_block
),
}
kwargs
.
update
(
context
)
return
super
()
.
get_context_data
(
**
kwargs
)
...
...
config_example.py
View file @
722bf786
...
...
@@ -21,10 +21,10 @@ class Config:
ALLOWED_HOSTS
=
[
'*'
]
# Development env open this, when error occur display the full process track, Production disable it
DEBUG
=
True
DEBUG
=
os
.
environ
.
get
(
"DEBUG"
)
or
True
# DEBUG, INFO, WARNING, ERROR, CRITICAL can set. See https://docs.djangoproject.com/en/1.10/topics/logging/
LOG_LEVEL
=
'DEBUG'
LOG_LEVEL
=
os
.
environ
.
get
(
"LOG_LEVEL"
)
or
'DEBUG'
LOG_DIR
=
os
.
path
.
join
(
BASE_DIR
,
'logs'
)
# Database setting, Support sqlite3, mysql, postgres ....
...
...
@@ -35,12 +35,12 @@ class Config:
DB_NAME
=
os
.
path
.
join
(
BASE_DIR
,
'data'
,
'db.sqlite3'
)
# MySQL or postgres setting like:
# DB_ENGINE = 'mysql'
# DB_HOST = '127.0.0.1'
# DB_PORT = 3306
# DB_USER =
'root
'
# DB_PASSWORD =
'
'
# DB_NAME = 'jumpserver'
# DB_ENGINE =
os.environ.get("DB_ENGINE") or
'mysql'
# DB_HOST =
os.environ.get("DB_HOST") or
'127.0.0.1'
# DB_PORT =
os.environ.get("DB_PORT") or
3306
# DB_USER =
os.environ.get("DB_USER") or 'jumpserver
'
# DB_PASSWORD =
os.environ.get("DB_PASSWORD") or 'weakPassword
'
# DB_NAME =
os.environ.get("DB_NAME") or
'jumpserver'
# When Django start it will bind this host and port
# ./manage.py runserver 127.0.0.1:8080
...
...
@@ -48,9 +48,11 @@ class Config:
HTTP_LISTEN_PORT
=
8080
# Use Redis as broker for celery and web socket
REDIS_HOST
=
'127.0.0.1'
REDIS_PORT
=
6379
REDIS_PASSWORD
=
''
REDIS_HOST
=
os
.
environ
.
get
(
"REDIS_HOST"
)
or
'127.0.0.1'
REDIS_PORT
=
os
.
environ
.
get
(
"REDIS_PORT"
)
or
6379
REDIS_PASSWORD
=
os
.
environ
.
get
(
"REDIS_PASSWORD"
)
or
''
REDIS_DB_CELERY
=
os
.
environ
.
get
(
'REDIS_DB'
)
or
3
REDIS_DB_CACHE
=
os
.
environ
.
get
(
'REDIS_DB'
)
or
4
def
__init__
(
self
):
pass
...
...
requirements/deb_requirements.txt
View file @
722bf786
libtiff5-dev libjpeg8-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev python-tk python-dev openssl libssl-dev libldap2-dev libsasl2-dev sqlite gcc automake
libtiff5-dev libjpeg8-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev python-tk python-dev openssl libssl-dev libldap2-dev libsasl2-dev sqlite gcc automake
libkrb5-dev
requirements/requirements.txt
View file @
722bf786
...
...
@@ -61,7 +61,7 @@ pytz==2018.3
PyYAML==3.12
redis==2.10.6
requests==2.18.4
jms-storage==0.0.1
7
jms-storage==0.0.1
8
s3transfer==0.1.13
simplejson==3.13.2
six==1.11.0
...
...
utils/make_migrations.sh
View file @
722bf786
...
...
@@ -4,3 +4,5 @@
python3 ../apps/manage.py makemigrations
python3 ../apps/manage.py migrate
python3 ../apps/manage.py makemigrations
--merge
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment