mirror of https://github.com/jumpserver/jumpserver
commit
d289960ff2
|
@ -10,7 +10,7 @@ RUN yum -y install epel-release && \
|
||||||
echo -e "[mysql]\nname=mysql\nbaseurl=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo
|
echo -e "[mysql]\nname=mysql\nbaseurl=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo
|
||||||
RUN cd /tmp/requirements && yum -y install $(cat rpm_requirements.txt)
|
RUN cd /tmp/requirements && yum -y install $(cat rpm_requirements.txt)
|
||||||
RUN cd /tmp/requirements && pip install --upgrade pip setuptools && pip install wheel && \
|
RUN cd /tmp/requirements && pip install --upgrade pip setuptools && pip install wheel && \
|
||||||
pip install -i https://mirrors.aliyun.com/pypi/simple/ -r requirements.txt || pip install -r requirements.txt
|
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt || pip install -r requirements.txt
|
||||||
RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
|
RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
|
||||||
|
|
||||||
COPY . /opt/jumpserver
|
COPY . /opt/jumpserver
|
||||||
|
|
47
README.md
47
README.md
|
@ -1,21 +1,28 @@
|
||||||
# JumpServer 多云环境下更好用的堡垒机
|
# JumpServer 多云环境下更好用的堡垒机
|
||||||
|
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
[](https://www.djangoproject.com/)
|
[](https://www.djangoproject.com/)
|
||||||
[](https://www.ansible.com/)
|
|
||||||
[](http://www.paramiko.org/)
|
|
||||||
|
|
||||||
JumpServer 是全球首款开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 机制的运维安全审计系统。
|
JumpServer 是全球首款开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 规范的运维安全审计系统。
|
||||||
|
|
||||||
JumpServer 使用 Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
|
JumpServer 使用 Python / Django 为主进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
|
||||||
|
|
||||||
JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。
|
JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。
|
||||||
|
|
||||||
改变世界,从一点点开始。
|
改变世界,从一点点开始。
|
||||||
|
|
||||||
注: [KubeOperator](https://github.com/KubeOperator/KubeOperator) 是 JumpServer 团队在 Kubernetes 领域的的又一全新力作,欢迎关注和使用。
|
> 注: [KubeOperator](https://github.com/KubeOperator/KubeOperator) 是 JumpServer 团队在 Kubernetes 领域的的又一全新力作,欢迎关注和使用。
|
||||||
|
|
||||||
## 核心功能列表
|
## 特色优势
|
||||||
|
|
||||||
|
- 开源: 零门槛,线上快速获取和安装;
|
||||||
|
- 分布式: 轻松支持大规模并发访问;
|
||||||
|
- 无插件: 仅需浏览器,极致的 Web Terminal 使用体验;
|
||||||
|
- 多云支持: 一套系统,同时管理不同云上面的资产;
|
||||||
|
- 云端存储: 审计录像云端存储,永不丢失;
|
||||||
|
- 多租户: 一套系统,多个子公司和部门同时使用。
|
||||||
|
|
||||||
|
## 功能列表
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -172,22 +179,26 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
## 安装及使用指南
|
## 快速开始
|
||||||
|
|
||||||
- [Docker 快速安装文档](http://docs.jumpserver.org/zh/docs/dockerinstall.html)
|
|
||||||
- [Step by Step 安装文档](http://docs.jumpserver.org/zh/docs/step_by_step.html)
|
|
||||||
- [完整文档](http://docs.jumpserver.org)
|
|
||||||
|
|
||||||
## 演示视频和截屏
|
|
||||||
|
|
||||||
我们提供了演示视频和系统截图可以让你快速了解 JumpServer:
|
|
||||||
|
|
||||||
|
- [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/)
|
||||||
|
- [完整文档](https://docs.jumpserver.org)
|
||||||
- [演示视频](https://jumpserver.oss-cn-hangzhou.aliyuncs.com/jms-media/%E3%80%90%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91%E3%80%91Jumpserver%20%E5%A0%A1%E5%9E%92%E6%9C%BA%20V1.5.0%20%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91%20-%20final.mp4)
|
- [演示视频](https://jumpserver.oss-cn-hangzhou.aliyuncs.com/jms-media/%E3%80%90%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91%E3%80%91Jumpserver%20%E5%A0%A1%E5%9E%92%E6%9C%BA%20V1.5.0%20%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91%20-%20final.mp4)
|
||||||
- [系统截图](http://docs.JumpServer.org/zh/docs/snapshot.html)
|
|
||||||
|
## 案例研究
|
||||||
|
|
||||||
|
- [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)。
|
||||||
|
|
||||||
## License & Copyright
|
## License & Copyright
|
||||||
|
|
||||||
Copyright (c) 2014-2019 飞致云 FIT2CLOUD, All rights reserved.
|
Copyright (c) 2014-2020 飞致云 FIT2CLOUD, All rights reserved.
|
||||||
|
|
||||||
Licensed under The GNU General Public License version 2 (GPLv2) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
Licensed under The GNU General Public License version 2 (GPLv2) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,9 @@ class AssetByNodeFilterBackend(filters.BaseFilterBackend):
|
||||||
if query_all:
|
if query_all:
|
||||||
pattern = node.get_all_children_pattern(with_self=True)
|
pattern = node.get_all_children_pattern(with_self=True)
|
||||||
else:
|
else:
|
||||||
pattern = node.get_children_key_pattern(with_self=True)
|
# pattern = node.get_children_key_pattern(with_self=True)
|
||||||
|
# 只显示当前节点下资产
|
||||||
|
pattern = r"^{}$".format(node.key)
|
||||||
return self.perform_query(pattern, queryset)
|
return self.perform_query(pattern, queryset)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -41,11 +41,10 @@ def default_node():
|
||||||
|
|
||||||
|
|
||||||
class AssetManager(OrgManager):
|
class AssetManager(OrgManager):
|
||||||
# def get_queryset(self):
|
def get_queryset(self):
|
||||||
# return super().get_queryset().annotate(
|
return super().get_queryset().annotate(
|
||||||
# platform_base=models.F('platform__base')
|
platform_base=models.F('platform__base')
|
||||||
# )
|
)
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AssetQuerySet(models.QuerySet):
|
class AssetQuerySet(models.QuerySet):
|
||||||
|
|
|
@ -76,8 +76,9 @@ $(document).ready(function () {
|
||||||
'ip': "{% trans 'IP' %}",
|
'ip': "{% trans 'IP' %}",
|
||||||
'protocols': "{% trans 'Protocols' %}",
|
'protocols': "{% trans 'Protocols' %}",
|
||||||
'platform': "{% trans 'Platform' %}",
|
'platform': "{% trans 'Platform' %}",
|
||||||
'system_users_join': "{% trans 'System user' %}",
|
{#'system_users_join': "{% trans 'System user' %}",#}
|
||||||
'domain': "{% trans 'Domain' %}",
|
'domain': "{% trans 'Domain' %}",
|
||||||
|
'comment': "{% trans 'Comment' %}",
|
||||||
};
|
};
|
||||||
var value;
|
var value;
|
||||||
for (var i = 0; i < data.results.length; i++) {
|
for (var i = 0; i < data.results.length; i++) {
|
||||||
|
|
|
@ -136,8 +136,8 @@ def on_user_auth_success(sender, user, request, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_auth_failed)
|
@receiver(post_auth_failed)
|
||||||
def on_user_auth_failed(sender, username, request, reason, **kwargs):
|
def on_user_auth_failed(sender, username, request, reason='', **kwargs):
|
||||||
logger.debug('User login failed: {}'.format(username))
|
logger.debug('User login failed: {}'.format(username))
|
||||||
data = generate_data(username, request)
|
data = generate_data(username, request)
|
||||||
data.update({'reason': reason, 'status': False})
|
data.update({'reason': reason[:128], 'status': False})
|
||||||
write_login_log(**data)
|
write_login_log(**data)
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
"""
|
||||||
|
使用下面的工程,进行jumpserver 的 oidc 认证
|
||||||
|
https://github.com/BaiJiangJie/jumpserver-django-oidc-rp
|
||||||
|
"""
|
|
@ -1,7 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
|
|
||||||
from .backends import *
|
|
||||||
from .middleware import *
|
|
||||||
from .utils import *
|
|
||||||
from .decorator import *
|
|
|
@ -1,82 +0,0 @@
|
||||||
# coding:utf-8
|
|
||||||
#
|
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from common.utils import get_logger
|
|
||||||
from .utils import new_client
|
|
||||||
from .models import OIDT_ACCESS_TOKEN
|
|
||||||
|
|
||||||
UserModel = get_user_model()
|
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
|
||||||
client = new_client()
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'OpenIDAuthorizationCodeBackend', 'OpenIDAuthorizationPasswordBackend',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class BaseOpenIDAuthorizationBackend(object):
|
|
||||||
@staticmethod
|
|
||||||
def user_can_authenticate(user):
|
|
||||||
"""
|
|
||||||
Reject users with is_active=False. Custom user models that don't have
|
|
||||||
that attribute are allowed.
|
|
||||||
"""
|
|
||||||
is_valid = getattr(user, 'is_valid', None)
|
|
||||||
return is_valid or is_valid is None
|
|
||||||
|
|
||||||
def get_user(self, user_id):
|
|
||||||
try:
|
|
||||||
user = UserModel._default_manager.get(pk=user_id)
|
|
||||||
except UserModel.DoesNotExist:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return user if self.user_can_authenticate(user) else None
|
|
||||||
|
|
||||||
|
|
||||||
class OpenIDAuthorizationCodeBackend(BaseOpenIDAuthorizationBackend):
|
|
||||||
def authenticate(self, request, **kwargs):
|
|
||||||
logger.info('Authentication OpenID code backend')
|
|
||||||
code = kwargs.get('code')
|
|
||||||
redirect_uri = kwargs.get('redirect_uri')
|
|
||||||
if not code or not redirect_uri:
|
|
||||||
logger.info('Authenticate failed: No code or No redirect uri')
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
oidt_profile = client.update_or_create_from_code(
|
|
||||||
code=code, redirect_uri=redirect_uri
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.info('Authenticate failed: get oidt_profile: {}'.format(e))
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
# Check openid user single logout or not with access_token
|
|
||||||
request.session[OIDT_ACCESS_TOKEN] = oidt_profile.access_token
|
|
||||||
user = oidt_profile.user
|
|
||||||
logger.info('Authenticate success: user -> {}'.format(user))
|
|
||||||
return user if self.user_can_authenticate(user) else None
|
|
||||||
|
|
||||||
|
|
||||||
class OpenIDAuthorizationPasswordBackend(BaseOpenIDAuthorizationBackend):
|
|
||||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
|
||||||
logger.info('Authentication OpenID password backend')
|
|
||||||
if not username:
|
|
||||||
logger.info('Authenticate failed: Not username')
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
oidt_profile = client.update_or_create_from_password(
|
|
||||||
username=username, password=password
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e, exc_info=True)
|
|
||||||
logger.info('Authenticate failed: get oidt_profile: {}'.format(e))
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
user = oidt_profile.user
|
|
||||||
logger.info('Authenticate success: user -> {}'.format(user))
|
|
||||||
return user if self.user_can_authenticate(user) else None
|
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
# coding: utf-8
|
|
||||||
#
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
import contextlib
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from urllib3.exceptions import InsecureRequestWarning
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'ssl_verification',
|
|
||||||
]
|
|
||||||
|
|
||||||
old_merge_environment_settings = requests.Session.merge_environment_settings
|
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def no_ssl_verification():
|
|
||||||
"""
|
|
||||||
https://stackoverflow.com/questions/15445981/
|
|
||||||
how-do-i-disable-the-security-certificate-check-in-python-requests
|
|
||||||
"""
|
|
||||||
opened_adapters = set()
|
|
||||||
|
|
||||||
def merge_environment_settings(self, url, proxies, stream, verify, cert):
|
|
||||||
# Verification happens only once per connection so we need to close
|
|
||||||
# all the opened adapters once we're done. Otherwise, the effects of
|
|
||||||
# verify=False persist beyond the end of this context manager.
|
|
||||||
opened_adapters.add(self.get_adapter(url))
|
|
||||||
_settings = old_merge_environment_settings(
|
|
||||||
self, url, proxies, stream, verify, cert
|
|
||||||
)
|
|
||||||
_settings['verify'] = False
|
|
||||||
return _settings
|
|
||||||
|
|
||||||
requests.Session.merge_environment_settings = merge_environment_settings
|
|
||||||
try:
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
warnings.simplefilter('ignore', InsecureRequestWarning)
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
requests.Session.merge_environment_settings = old_merge_environment_settings
|
|
||||||
for adapter in opened_adapters:
|
|
||||||
try:
|
|
||||||
adapter.close()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def ssl_verification(func):
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
if not settings.AUTH_OPENID_IGNORE_SSL_VERIFICATION:
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
with no_ssl_verification():
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
return wrapper
|
|
|
@ -1,41 +0,0 @@
|
||||||
# coding:utf-8
|
|
||||||
#
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth import logout
|
|
||||||
from django.utils.deprecation import MiddlewareMixin
|
|
||||||
from django.contrib.auth import BACKEND_SESSION_KEY
|
|
||||||
|
|
||||||
from common.utils import get_logger
|
|
||||||
from .utils import new_client
|
|
||||||
from .models import OIDT_ACCESS_TOKEN
|
|
||||||
|
|
||||||
BACKEND_OPENID_AUTH_CODE = 'OpenIDAuthorizationCodeBackend'
|
|
||||||
logger = get_logger(__file__)
|
|
||||||
__all__ = ['OpenIDAuthenticationMiddleware']
|
|
||||||
|
|
||||||
|
|
||||||
class OpenIDAuthenticationMiddleware(MiddlewareMixin):
|
|
||||||
"""
|
|
||||||
Check openid user single logout (with access_token)
|
|
||||||
"""
|
|
||||||
def process_request(self, request):
|
|
||||||
# Don't need openid auth if AUTH_OPENID is False
|
|
||||||
if not settings.AUTH_OPENID:
|
|
||||||
return
|
|
||||||
# Don't need openid auth if no shared session enabled
|
|
||||||
if not settings.AUTH_OPENID_SHARE_SESSION:
|
|
||||||
return
|
|
||||||
# Don't need check single logout if user not authenticated
|
|
||||||
if not request.user.is_authenticated:
|
|
||||||
return
|
|
||||||
elif not request.session[BACKEND_SESSION_KEY].endswith(
|
|
||||||
BACKEND_OPENID_AUTH_CODE):
|
|
||||||
return
|
|
||||||
# Check openid user single logout or not with access_token
|
|
||||||
try:
|
|
||||||
client = new_client()
|
|
||||||
client.get_userinfo(token=request.session.get(OIDT_ACCESS_TOKEN))
|
|
||||||
except Exception as e:
|
|
||||||
logout(request)
|
|
||||||
logger.error(e)
|
|
|
@ -1,185 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
|
|
||||||
from django.db import transaction
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from keycloak.realm import KeycloakRealm
|
|
||||||
from keycloak.keycloak_openid import KeycloakOpenID
|
|
||||||
from users.utils import construct_user_email
|
|
||||||
|
|
||||||
from .signals import post_create_or_update_openid_user
|
|
||||||
from .decorator import ssl_verification
|
|
||||||
|
|
||||||
OIDT_ACCESS_TOKEN = 'oidt_access_token'
|
|
||||||
|
|
||||||
|
|
||||||
class Nonce(object):
|
|
||||||
"""
|
|
||||||
The openid-login is stored in cache as a temporary object, recording the
|
|
||||||
user's redirect_uri and next_pat
|
|
||||||
"""
|
|
||||||
def __init__(self, redirect_uri, next_path):
|
|
||||||
import uuid
|
|
||||||
self.state = uuid.uuid4()
|
|
||||||
self.redirect_uri = redirect_uri
|
|
||||||
self.next_path = next_path
|
|
||||||
|
|
||||||
|
|
||||||
class OpenIDTokenProfile(object):
|
|
||||||
def __init__(self, user, access_token, refresh_token):
|
|
||||||
"""
|
|
||||||
:param user: User object
|
|
||||||
:param access_token:
|
|
||||||
:param refresh_token:
|
|
||||||
"""
|
|
||||||
self.user = user
|
|
||||||
self.access_token = access_token
|
|
||||||
self.refresh_token = refresh_token
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "{}'s OpenID token profile".format(self.user.username)
|
|
||||||
|
|
||||||
|
|
||||||
class Client(object):
|
|
||||||
def __init__(self, server_url, realm_name, client_id, client_secret):
|
|
||||||
self.server_url = server_url
|
|
||||||
self.realm_name = realm_name
|
|
||||||
self.client_id = client_id
|
|
||||||
self.client_secret = client_secret
|
|
||||||
self._openid_client = None
|
|
||||||
self._realm = None
|
|
||||||
self._openid_connect_client = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def realm(self):
|
|
||||||
if self._realm is None:
|
|
||||||
self._realm = KeycloakRealm(
|
|
||||||
server_url=self.server_url,
|
|
||||||
realm_name=self.realm_name,
|
|
||||||
headers={}
|
|
||||||
)
|
|
||||||
return self._realm
|
|
||||||
|
|
||||||
@property
|
|
||||||
def openid_connect_client(self):
|
|
||||||
"""
|
|
||||||
:rtype: keycloak.openid_connect.KeycloakOpenidConnect
|
|
||||||
"""
|
|
||||||
if self._openid_connect_client is None:
|
|
||||||
self._openid_connect_client = self.realm.open_id_connect(
|
|
||||||
client_id=self.client_id,
|
|
||||||
client_secret=self.client_secret
|
|
||||||
)
|
|
||||||
return self._openid_connect_client
|
|
||||||
|
|
||||||
@property
|
|
||||||
def openid_client(self):
|
|
||||||
"""
|
|
||||||
:rtype: keycloak.keycloak_openid.KeycloakOpenID
|
|
||||||
"""
|
|
||||||
if self._openid_client is None:
|
|
||||||
self._openid_client = KeycloakOpenID(
|
|
||||||
server_url='%sauth/' % self.server_url,
|
|
||||||
realm_name=self.realm_name,
|
|
||||||
client_id=self.client_id,
|
|
||||||
client_secret_key=self.client_secret,
|
|
||||||
)
|
|
||||||
return self._openid_client
|
|
||||||
|
|
||||||
@ssl_verification
|
|
||||||
def get_url(self, name):
|
|
||||||
return self.openid_connect_client.get_url(name=name)
|
|
||||||
|
|
||||||
def get_url_end_session_endpoint(self):
|
|
||||||
return self.get_url(name='end_session_endpoint')
|
|
||||||
|
|
||||||
@ssl_verification
|
|
||||||
def get_authorization_url(self, redirect_uri, scope, state):
|
|
||||||
url = self.openid_connect_client.authorization_url(
|
|
||||||
redirect_uri=redirect_uri, scope=scope, state=state
|
|
||||||
)
|
|
||||||
return url
|
|
||||||
|
|
||||||
@ssl_verification
|
|
||||||
def get_userinfo(self, token):
|
|
||||||
user_info = self.openid_connect_client.userinfo(token=token)
|
|
||||||
return user_info
|
|
||||||
|
|
||||||
@ssl_verification
|
|
||||||
def authorization_code(self, code, redirect_uri):
|
|
||||||
token_response = self.openid_connect_client.authorization_code(
|
|
||||||
code=code, redirect_uri=redirect_uri
|
|
||||||
)
|
|
||||||
return token_response
|
|
||||||
|
|
||||||
@ssl_verification
|
|
||||||
def authorization_password(self, username, password):
|
|
||||||
token_response = self.openid_client.token(
|
|
||||||
username=username, password=password
|
|
||||||
)
|
|
||||||
return token_response
|
|
||||||
|
|
||||||
def update_or_create_from_code(self, code, redirect_uri):
|
|
||||||
"""
|
|
||||||
Update or create an user based on an authentication code.
|
|
||||||
Response as specified in:
|
|
||||||
https://tools.ietf.org/html/rfc6749#section-4.1.4
|
|
||||||
:param str code: authentication code
|
|
||||||
:param str redirect_uri:
|
|
||||||
:rtype: OpenIDTokenProfile
|
|
||||||
"""
|
|
||||||
token_response = self.authorization_code(code, redirect_uri)
|
|
||||||
return self._update_or_create(token_response=token_response)
|
|
||||||
|
|
||||||
def update_or_create_from_password(self, username, password):
|
|
||||||
"""
|
|
||||||
Update or create an user based on an authentication username and password.
|
|
||||||
:param str username: authentication username
|
|
||||||
:param str password: authentication password
|
|
||||||
:return: OpenIDTokenProfile
|
|
||||||
"""
|
|
||||||
token_response = self.authorization_password(username, password)
|
|
||||||
return self._update_or_create(token_response=token_response)
|
|
||||||
|
|
||||||
def _update_or_create(self, token_response):
|
|
||||||
"""
|
|
||||||
Update or create an user based on a token response.
|
|
||||||
`token_response` contains the items returned by the OpenIDConnect Token API
|
|
||||||
end-point:
|
|
||||||
- id_token
|
|
||||||
- access_token
|
|
||||||
- expires_in
|
|
||||||
- refresh_token
|
|
||||||
- refresh_expires_in
|
|
||||||
:param dict token_response:
|
|
||||||
:rtype: OpenIDTokenProfile
|
|
||||||
"""
|
|
||||||
userinfo = self.get_userinfo(token=token_response['access_token'])
|
|
||||||
with transaction.atomic():
|
|
||||||
name = userinfo.get('name', '')
|
|
||||||
username = userinfo.get('preferred_username', '')
|
|
||||||
email = userinfo.get('email', '')
|
|
||||||
email = construct_user_email(username, email)
|
|
||||||
|
|
||||||
user, created = get_user_model().objects.update_or_create(
|
|
||||||
username=username,
|
|
||||||
defaults={
|
|
||||||
'name': name, 'email': email,
|
|
||||||
'first_name': userinfo.get('given_name', ''),
|
|
||||||
'last_name': userinfo.get('family_name', ''),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
oidt_profile = OpenIDTokenProfile(
|
|
||||||
user=user,
|
|
||||||
access_token=token_response['access_token'],
|
|
||||||
refresh_token=token_response['refresh_token'],
|
|
||||||
)
|
|
||||||
if user:
|
|
||||||
post_create_or_update_openid_user.send(
|
|
||||||
sender=user.__class__, user=user, created=created
|
|
||||||
)
|
|
||||||
|
|
||||||
return oidt_profile
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.client_id
|
|
|
@ -1,5 +0,0 @@
|
||||||
from django.dispatch import Signal
|
|
||||||
|
|
||||||
|
|
||||||
post_create_or_update_openid_user = Signal(providing_args=('user',))
|
|
||||||
post_openid_login_success = Signal(providing_args=('user', 'request'))
|
|
|
@ -1,11 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path('login/', views.OpenIDLoginView.as_view(), name='openid-login'),
|
|
||||||
path('login/complete/', views.OpenIDLoginCompleteView.as_view(),
|
|
||||||
name='openid-login-complete'),
|
|
||||||
]
|
|
|
@ -1,19 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from .models import Client
|
|
||||||
|
|
||||||
__all__ = ['new_client']
|
|
||||||
|
|
||||||
|
|
||||||
def new_client():
|
|
||||||
"""
|
|
||||||
:return: authentication.models.Client
|
|
||||||
"""
|
|
||||||
return Client(
|
|
||||||
server_url=settings.AUTH_OPENID_SERVER_URL,
|
|
||||||
realm_name=settings.AUTH_OPENID_REALM_NAME,
|
|
||||||
client_id=settings.AUTH_OPENID_CLIENT_ID,
|
|
||||||
client_secret=settings.AUTH_OPENID_CLIENT_SECRET
|
|
||||||
)
|
|
|
@ -1,71 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.cache import cache
|
|
||||||
from django.views.generic.base import RedirectView
|
|
||||||
from django.contrib.auth import authenticate, login
|
|
||||||
from django.http.response import (
|
|
||||||
HttpResponseBadRequest,
|
|
||||||
HttpResponseServerError,
|
|
||||||
HttpResponseRedirect
|
|
||||||
)
|
|
||||||
|
|
||||||
from .utils import new_client
|
|
||||||
from .models import Nonce
|
|
||||||
from .signals import post_openid_login_success
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
client = new_client()
|
|
||||||
|
|
||||||
__all__ = ['OpenIDLoginView', 'OpenIDLoginCompleteView']
|
|
||||||
|
|
||||||
|
|
||||||
class OpenIDLoginView(RedirectView):
|
|
||||||
def get_redirect_url(self, *args, **kwargs):
|
|
||||||
redirect_uri = settings.BASE_SITE_URL + \
|
|
||||||
str(settings.AUTH_OPENID_LOGIN_COMPLETE_URL)
|
|
||||||
nonce = Nonce(
|
|
||||||
redirect_uri=redirect_uri,
|
|
||||||
next_path=self.request.GET.get('next')
|
|
||||||
)
|
|
||||||
cache.set(str(nonce.state), nonce, 24*3600)
|
|
||||||
|
|
||||||
self.request.session['openid_state'] = str(nonce.state)
|
|
||||||
authorization_url = client.get_authorization_url(
|
|
||||||
redirect_uri=nonce.redirect_uri,
|
|
||||||
scope='code',
|
|
||||||
state=str(nonce.state)
|
|
||||||
)
|
|
||||||
return authorization_url
|
|
||||||
|
|
||||||
|
|
||||||
class OpenIDLoginCompleteView(RedirectView):
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
if 'error' in request.GET:
|
|
||||||
return HttpResponseServerError(self.request.GET['error'])
|
|
||||||
if 'code' not in self.request.GET and 'state' not in self.request.GET:
|
|
||||||
return HttpResponseBadRequest(content='Code or State is empty')
|
|
||||||
if self.request.GET['state'] != self.request.session['openid_state']:
|
|
||||||
return HttpResponseBadRequest(content='State invalid')
|
|
||||||
nonce = cache.get(self.request.GET['state'])
|
|
||||||
if not nonce:
|
|
||||||
return HttpResponseBadRequest(content='State failure')
|
|
||||||
|
|
||||||
user = authenticate(
|
|
||||||
request=self.request,
|
|
||||||
code=self.request.GET['code'],
|
|
||||||
redirect_uri=nonce.redirect_uri
|
|
||||||
)
|
|
||||||
cache.delete(str(nonce.state))
|
|
||||||
if not user:
|
|
||||||
return HttpResponseBadRequest(content='Authenticate user failed')
|
|
||||||
|
|
||||||
login(self.request, user)
|
|
||||||
post_openid_login_success.send(
|
|
||||||
sender=self.__class__, user=user, request=self.request
|
|
||||||
)
|
|
||||||
return HttpResponseRedirect(nonce.next_path or '/')
|
|
||||||
|
|
|
@ -93,6 +93,9 @@ class AuthFailedError(Exception):
|
||||||
'msg': self.msg,
|
'msg': self.msg,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.msg)
|
||||||
|
|
||||||
|
|
||||||
class CredentialError(AuthFailedNeedLogMixin, AuthFailedNeedBlockMixin, AuthFailedError):
|
class CredentialError(AuthFailedNeedLogMixin, AuthFailedNeedBlockMixin, AuthFailedError):
|
||||||
def __init__(self, error, username, ip, request):
|
def __init__(self, error, username, ip, request):
|
||||||
|
@ -168,7 +171,7 @@ class MFARequiredError(NeedMoreInfoError):
|
||||||
'error': self.error,
|
'error': self.error,
|
||||||
'msg': self.msg,
|
'msg': self.msg,
|
||||||
'data': {
|
'data': {
|
||||||
'choices': ['otp'],
|
'choices': ['code'],
|
||||||
'url': reverse('api-auth:mfa-challenge')
|
'url': reverse('api-auth:mfa-challenge')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,8 +62,7 @@ class AuthMixin:
|
||||||
password = request.POST.get('password', '')
|
password = request.POST.get('password', '')
|
||||||
public_key = request.POST.get('public_key', '')
|
public_key = request.POST.get('public_key', '')
|
||||||
user, error = check_user_valid(
|
user, error = check_user_valid(
|
||||||
username=username, password=password,
|
request=request, username=username, password=password, public_key=public_key
|
||||||
public_key=public_key
|
|
||||||
)
|
)
|
||||||
ip = self.get_request_ip()
|
ip = self.get_request_ip()
|
||||||
if not user:
|
if not user:
|
||||||
|
|
|
@ -1,54 +1,15 @@
|
||||||
from django.http.request import QueryDict
|
|
||||||
from django.conf import settings
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.contrib.auth.signals import user_logged_out
|
|
||||||
from django_auth_ldap.backend import populate_user
|
|
||||||
|
|
||||||
from users.models import User
|
from jms_oidc_rp.signals import openid_user_login_failed, openid_user_login_success
|
||||||
from .backends.openid import new_client
|
|
||||||
from .backends.openid.signals import (
|
from .signals import post_auth_success, post_auth_failed
|
||||||
post_create_or_update_openid_user, post_openid_login_success
|
|
||||||
)
|
|
||||||
from .signals import post_auth_success
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(user_logged_out)
|
@receiver(openid_user_login_success)
|
||||||
def on_user_logged_out(sender, request, user, **kwargs):
|
def on_oidc_user_login_success(sender, request, user, **kwargs):
|
||||||
if not settings.AUTH_OPENID:
|
post_auth_success.send(sender, user=user, request=request)
|
||||||
return
|
|
||||||
if not settings.AUTH_OPENID_SHARE_SESSION:
|
|
||||||
return
|
|
||||||
query = QueryDict('', mutable=True)
|
|
||||||
query.update({
|
|
||||||
'redirect_uri': settings.BASE_SITE_URL
|
|
||||||
})
|
|
||||||
client = new_client()
|
|
||||||
openid_logout_url = "%s?%s" % (
|
|
||||||
client.get_url_end_session_endpoint(),
|
|
||||||
query.urlencode()
|
|
||||||
)
|
|
||||||
request.COOKIES['next'] = openid_logout_url
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_create_or_update_openid_user)
|
|
||||||
def on_post_create_or_update_openid_user(sender, user=None, created=True, **kwargs):
|
|
||||||
if created and user and user.username != 'admin':
|
|
||||||
user.source = user.SOURCE_OPENID
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_openid_login_success)
|
|
||||||
def on_openid_login_success(sender, user=None, request=None, **kwargs):
|
|
||||||
post_auth_success.send(sender=sender, user=user, request=request)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(populate_user)
|
|
||||||
def on_ldap_create_user(sender, user, ldap_user, **kwargs):
|
|
||||||
if user and user.username not in ['admin']:
|
|
||||||
exists = User.objects.filter(username=user.username).exists()
|
|
||||||
if not exists:
|
|
||||||
user.source = user.SOURCE_LDAP
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(openid_user_login_failed)
|
||||||
|
def on_oidc_user_login_failed(sender, username, request, reason, **kwargs):
|
||||||
|
post_auth_failed.send(sender, username=username, request=request, reason=reason)
|
||||||
|
|
|
@ -135,6 +135,19 @@ $(document).ready(function () {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
requestApi(data)
|
requestApi(data)
|
||||||
|
}).on('click', '.btn-api-keydel', function (){
|
||||||
|
var url = "{% url "api-auth:access-key-detail" pk=DEFAULT_PK %}";
|
||||||
|
url = url.replace("{{ DEFAULT_PK }}", $(this).data("id")) ;
|
||||||
|
var data = {
|
||||||
|
url: url,
|
||||||
|
method: "DELETE",
|
||||||
|
success: function () {
|
||||||
|
ak_table.ajax.reload();
|
||||||
|
},
|
||||||
|
success_message: "{% trans 'Delete success' %}"
|
||||||
|
|
||||||
|
};
|
||||||
|
requestApi(data)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -56,9 +56,9 @@
|
||||||
<div class="hr-line-dashed"></div>
|
<div class="hr-line-dashed"></div>
|
||||||
<p class="text-muted text-center">{% trans "More login options" %}</p>
|
<p class="text-muted text-center">{% trans "More login options" %}</p>
|
||||||
<div>
|
<div>
|
||||||
<button type="button" class="btn btn-default btn-sm btn-block" onclick="location.href='{% url 'authentication:openid:openid-login' %}'">
|
<button type="button" class="btn btn-default btn-sm btn-block" onclick="location.href='{% url 'authentication:openid:login' %}'">
|
||||||
<i class="fa fa-openid"></i>
|
<i class="fa fa-openid"></i>
|
||||||
{% trans 'Keycloak' %}
|
{% trans 'OpenID' %}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -16,6 +16,6 @@ urlpatterns = [
|
||||||
path('logout/', views.UserLogoutView.as_view(), name='logout'),
|
path('logout/', views.UserLogoutView.as_view(), name='logout'),
|
||||||
|
|
||||||
# openid
|
# openid
|
||||||
path('openid/', include(('authentication.backends.openid.urls', 'authentication'), namespace='openid')),
|
|
||||||
path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')),
|
path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')),
|
||||||
|
path('openid/', include(('jms_oidc_rp.urls', 'authentication'), namespace='openid')),
|
||||||
]
|
]
|
||||||
|
|
|
@ -58,7 +58,7 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||||
if self.request.GET.get("admin", 0):
|
if self.request.GET.get("admin", 0):
|
||||||
return None
|
return None
|
||||||
if settings.AUTH_OPENID:
|
if settings.AUTH_OPENID:
|
||||||
redirect_url = reverse("authentication:openid:openid-login")
|
redirect_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME)
|
||||||
elif settings.AUTH_CAS:
|
elif settings.AUTH_CAS:
|
||||||
redirect_url = reverse(settings.CAS_LOGIN_URL_NAME)
|
redirect_url = reverse(settings.CAS_LOGIN_URL_NAME)
|
||||||
|
|
||||||
|
@ -133,7 +133,7 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView):
|
||||||
user = self.check_user_auth_if_need()
|
user = self.check_user_auth_if_need()
|
||||||
self.check_user_mfa_if_need(user)
|
self.check_user_mfa_if_need(user)
|
||||||
self.check_user_login_confirm_if_need(user)
|
self.check_user_login_confirm_if_need(user)
|
||||||
except errors.CredentialError:
|
except (errors.CredentialError, errors.SessionEmptyError):
|
||||||
return self.format_redirect_url(self.login_url)
|
return self.format_redirect_url(self.login_url)
|
||||||
except errors.MFARequiredError:
|
except errors.MFARequiredError:
|
||||||
return self.format_redirect_url(self.login_otp_url)
|
return self.format_redirect_url(self.login_otp_url)
|
||||||
|
@ -185,18 +185,18 @@ class UserLogoutView(TemplateView):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_backend_logout_url():
|
def get_backend_logout_url():
|
||||||
|
if settings.AUTH_OPENID:
|
||||||
|
return settings.AUTH_OPENID_AUTH_LOGOUT_URL_NAME
|
||||||
# if settings.AUTH_CAS:
|
# if settings.AUTH_CAS:
|
||||||
# return settings.CAS_LOGOUT_URL_NAME
|
# return settings.CAS_LOGOUT_URL_NAME
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
auth_logout(request)
|
|
||||||
backend_logout_url = self.get_backend_logout_url()
|
backend_logout_url = self.get_backend_logout_url()
|
||||||
if backend_logout_url:
|
if backend_logout_url:
|
||||||
return redirect(backend_logout_url)
|
return redirect(backend_logout_url)
|
||||||
next_uri = request.COOKIES.get("next")
|
|
||||||
if next_uri:
|
auth_logout(request)
|
||||||
return redirect(next_uri)
|
|
||||||
response = super().get(request, *args, **kwargs)
|
response = super().get(request, *args, **kwargs)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,9 @@ from django.views.generic.edit import FormView
|
||||||
from .. import forms, errors, mixins
|
from .. import forms, errors, mixins
|
||||||
from .utils import redirect_to_guard_view
|
from .utils import redirect_to_guard_view
|
||||||
|
|
||||||
|
from common.utils import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
__all__ = ['UserLoginOtpView']
|
__all__ = ['UserLoginOtpView']
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,4 +25,7 @@ class UserLoginOtpView(mixins.AuthMixin, FormView):
|
||||||
except errors.MFAFailedError as e:
|
except errors.MFAFailedError as e:
|
||||||
form.add_error('otp_code', e.msg)
|
form.add_error('otp_code', e.msg)
|
||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return redirect_to_guard_view()
|
||||||
|
|
||||||
|
|
|
@ -219,7 +219,7 @@ class TotalCountMixin:
|
||||||
return count
|
return count
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_total_count_online_assets():
|
def get_total_count_online_sessions():
|
||||||
return Session.objects.filter(is_finished=False).count()
|
return Session.objects.filter(is_finished=False).count()
|
||||||
|
|
||||||
|
|
||||||
|
@ -239,7 +239,7 @@ class IndexApi(TotalCountMixin, WeekSessionMetricMixin, MonthLoginMetricMixin, A
|
||||||
'total_count_assets': self.get_total_count_assets(),
|
'total_count_assets': self.get_total_count_assets(),
|
||||||
'total_count_users': self.get_total_count_users(),
|
'total_count_users': self.get_total_count_users(),
|
||||||
'total_count_online_users': self.get_total_count_online_users(),
|
'total_count_online_users': self.get_total_count_online_users(),
|
||||||
'total_count_online_assets': self.get_total_count_online_assets(),
|
'total_count_online_sessions': self.get_total_count_online_sessions(),
|
||||||
})
|
})
|
||||||
|
|
||||||
if _all or query_params.get('month_metrics'):
|
if _all or query_params.get('month_metrics'):
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
3. 程序需要, 用户需要更改的写到本config中
|
3. 程序需要, 用户需要更改的写到本config中
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
import errno
|
import errno
|
||||||
|
@ -15,6 +16,7 @@ import json
|
||||||
import yaml
|
import yaml
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
from urllib.parse import urljoin, urlparse
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
PROJECT_DIR = os.path.dirname(BASE_DIR)
|
PROJECT_DIR = os.path.dirname(BASE_DIR)
|
||||||
|
@ -38,6 +40,38 @@ def import_string(dotted_path):
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
|
|
||||||
|
def is_absolute_uri(uri):
|
||||||
|
""" 判断一个uri是否是绝对地址 """
|
||||||
|
if not isinstance(uri, str):
|
||||||
|
return False
|
||||||
|
|
||||||
|
result = re.match(r'^http[s]?://.*', uri)
|
||||||
|
if result is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def build_absolute_uri(base, uri):
|
||||||
|
""" 构建绝对uri地址 """
|
||||||
|
if uri is None:
|
||||||
|
return base
|
||||||
|
|
||||||
|
if isinstance(uri, int):
|
||||||
|
uri = str(uri)
|
||||||
|
|
||||||
|
if not isinstance(uri, str):
|
||||||
|
return base
|
||||||
|
|
||||||
|
if is_absolute_uri(uri):
|
||||||
|
return uri
|
||||||
|
|
||||||
|
parsed_base = urlparse(base)
|
||||||
|
url = "{}://{}".format(parsed_base.scheme, parsed_base.netloc)
|
||||||
|
path = '{}/{}/'.format(parsed_base.path.strip('/'), uri.strip('/'))
|
||||||
|
return urljoin(url, path)
|
||||||
|
|
||||||
|
|
||||||
class DoesNotExist(Exception):
|
class DoesNotExist(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -136,14 +170,32 @@ class Config(dict):
|
||||||
'AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS': False,
|
'AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS': False,
|
||||||
'AUTH_LDAP_OPTIONS_OPT_REFERRALS': -1,
|
'AUTH_LDAP_OPTIONS_OPT_REFERRALS': -1,
|
||||||
|
|
||||||
|
# OpenID 配置参数
|
||||||
|
# OpenID 公有配置参数 (version <= 1.5.8 或 version >= 1.5.8)
|
||||||
'AUTH_OPENID': False,
|
'AUTH_OPENID': False,
|
||||||
'BASE_SITE_URL': 'http://localhost:8080',
|
'BASE_SITE_URL': None,
|
||||||
'AUTH_OPENID_SERVER_URL': 'http://openid',
|
'AUTH_OPENID_CLIENT_ID': 'client-id',
|
||||||
'AUTH_OPENID_REALM_NAME': 'jumpserver',
|
'AUTH_OPENID_CLIENT_SECRET': 'client-secret',
|
||||||
'AUTH_OPENID_CLIENT_ID': 'jumpserver',
|
|
||||||
'AUTH_OPENID_CLIENT_SECRET': '',
|
|
||||||
'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True,
|
|
||||||
'AUTH_OPENID_SHARE_SESSION': True,
|
'AUTH_OPENID_SHARE_SESSION': True,
|
||||||
|
'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True,
|
||||||
|
# OpenID 新配置参数 (version >= 1.5.9)
|
||||||
|
'AUTH_OPENID_PROVIDER_ENDPOINT': 'https://op-example.com/',
|
||||||
|
'AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT': 'https://op-example.com/authorize',
|
||||||
|
'AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT': 'https://op-example.com/token',
|
||||||
|
'AUTH_OPENID_PROVIDER_JWKS_ENDPOINT': 'https://op-example.com/jwks',
|
||||||
|
'AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT': 'https://op-example.com/userinfo',
|
||||||
|
'AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT': 'https://op-example.com/logout',
|
||||||
|
'AUTH_OPENID_PROVIDER_SIGNATURE_ALG': 'HS256',
|
||||||
|
'AUTH_OPENID_PROVIDER_SIGNATURE_KEY': None,
|
||||||
|
'AUTH_OPENID_SCOPES': 'openid profile email',
|
||||||
|
'AUTH_OPENID_ID_TOKEN_MAX_AGE': 60,
|
||||||
|
'AUTH_OPENID_ID_TOKEN_INCLUDE_CLAIMS': True,
|
||||||
|
'AUTH_OPENID_USE_STATE': True,
|
||||||
|
'AUTH_OPENID_USE_NONCE': True,
|
||||||
|
'AUTH_OPENID_ALWAYS_UPDATE_USER': True,
|
||||||
|
# OpenID 旧配置参数 (version <= 1.5.8 (discarded))
|
||||||
|
'AUTH_OPENID_SERVER_URL': 'http://openid',
|
||||||
|
'AUTH_OPENID_REALM_NAME': None,
|
||||||
|
|
||||||
'AUTH_RADIUS': False,
|
'AUTH_RADIUS': False,
|
||||||
'RADIUS_SERVER': 'localhost',
|
'RADIUS_SERVER': 'localhost',
|
||||||
|
@ -207,6 +259,88 @@ class Config(dict):
|
||||||
'ORG_CHANGE_TO_URL': ''
|
'ORG_CHANGE_TO_URL': ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def compatible_auth_openid_of_key(self):
|
||||||
|
"""
|
||||||
|
兼容OpenID旧配置 (即 version <= 1.5.8)
|
||||||
|
因为旧配置只支持OpenID协议的Keycloak实现,
|
||||||
|
所以只需要根据旧配置和Keycloak的Endpoint说明文档,
|
||||||
|
构造出新配置中标准OpenID协议中所需的Endpoint即可
|
||||||
|
(Keycloak说明文档参考: https://www.keycloak.org/docs/latest/securing_apps/)
|
||||||
|
"""
|
||||||
|
if not self.AUTH_OPENID:
|
||||||
|
return
|
||||||
|
|
||||||
|
realm_name = self.AUTH_OPENID_REALM_NAME
|
||||||
|
if realm_name is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
compatible_keycloak_config = [
|
||||||
|
(
|
||||||
|
'AUTH_OPENID_PROVIDER_ENDPOINT',
|
||||||
|
self.AUTH_OPENID_SERVER_URL
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT',
|
||||||
|
'/realms/{}/protocol/openid-connect/auth'.format(realm_name)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT',
|
||||||
|
'/realms/{}/protocol/openid-connect/token'.format(realm_name)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'AUTH_OPENID_PROVIDER_JWKS_ENDPOINT',
|
||||||
|
'/realms/{}/protocol/openid-connect/certs'.format(realm_name)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT',
|
||||||
|
'/realms/{}/protocol/openid-connect/userinfo'.format(realm_name)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT',
|
||||||
|
'/realms/{}/protocol/openid-connect/logout'.format(realm_name)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
for key, value in compatible_keycloak_config:
|
||||||
|
self[key] = value
|
||||||
|
|
||||||
|
def compatible_auth_openid_of_value(self):
|
||||||
|
"""
|
||||||
|
兼容值的绝对路径、相对路径
|
||||||
|
(key 为 AUTH_OPENID_PROVIDER_*_ENDPOINT 的配置)
|
||||||
|
"""
|
||||||
|
if not self.AUTH_OPENID:
|
||||||
|
return
|
||||||
|
|
||||||
|
base = self.AUTH_OPENID_PROVIDER_ENDPOINT
|
||||||
|
config = list(self.items())
|
||||||
|
for key, value in config:
|
||||||
|
result = re.match(r'^AUTH_OPENID_PROVIDER_.*_ENDPOINT$', key)
|
||||||
|
if result is None:
|
||||||
|
continue
|
||||||
|
if value is None:
|
||||||
|
# None 在 url 中有特殊含义 (比如对于: end_session_endpoint)
|
||||||
|
continue
|
||||||
|
value = build_absolute_uri(base, value)
|
||||||
|
self[key] = value
|
||||||
|
|
||||||
|
def compatible(self):
|
||||||
|
"""
|
||||||
|
对配置做兼容处理
|
||||||
|
1. 对`key`的兼容 (例如:版本升级)
|
||||||
|
2. 对`value`做兼容 (例如:True、true、1 => True)
|
||||||
|
|
||||||
|
处理顺序要保持先对key做处理, 再对value做处理,
|
||||||
|
因为处理value的时候,只根据最新版本支持的key进行
|
||||||
|
"""
|
||||||
|
parts = ['key', 'value']
|
||||||
|
targets = ['auth_openid']
|
||||||
|
for part in parts:
|
||||||
|
for target in targets:
|
||||||
|
method_name = 'compatible_{}_of_{}'.format(target, part)
|
||||||
|
method = getattr(self, method_name, None)
|
||||||
|
if method is not None:
|
||||||
|
method()
|
||||||
|
|
||||||
def convert_type(self, k, v):
|
def convert_type(self, k, v):
|
||||||
default_value = self.defaults.get(k)
|
default_value = self.defaults.get(k)
|
||||||
if default_value is None:
|
if default_value is None:
|
||||||
|
@ -283,9 +417,6 @@ class DynamicConfig:
|
||||||
return lambda: self.get(item)
|
return lambda: self.get(item)
|
||||||
|
|
||||||
def LOGIN_URL(self):
|
def LOGIN_URL(self):
|
||||||
auth_openid = self.get('AUTH_OPENID')
|
|
||||||
if auth_openid:
|
|
||||||
return reverse_lazy("authentication:openid:openid-login")
|
|
||||||
return self.get('LOGIN_URL')
|
return self.get('LOGIN_URL')
|
||||||
|
|
||||||
def AUTHENTICATION_BACKENDS(self):
|
def AUTHENTICATION_BACKENDS(self):
|
||||||
|
@ -298,8 +429,8 @@ class DynamicConfig:
|
||||||
if self.static_config.get('AUTH_CAS'):
|
if self.static_config.get('AUTH_CAS'):
|
||||||
backends.insert(0, 'authentication.backends.cas.CASBackend')
|
backends.insert(0, 'authentication.backends.cas.CASBackend')
|
||||||
if self.static_config.get('AUTH_OPENID'):
|
if self.static_config.get('AUTH_OPENID'):
|
||||||
backends.insert(0, 'authentication.backends.openid.backends.OpenIDAuthorizationPasswordBackend')
|
backends.insert(0, 'jms_oidc_rp.backends.OIDCAuthPasswordBackend')
|
||||||
backends.insert(0, 'authentication.backends.openid.backends.OpenIDAuthorizationCodeBackend')
|
backends.insert(0, 'jms_oidc_rp.backends.OIDCAuthCodeBackend')
|
||||||
if self.static_config.get('AUTH_RADIUS'):
|
if self.static_config.get('AUTH_RADIUS'):
|
||||||
backends.insert(0, 'authentication.backends.radius.RadiusBackend')
|
backends.insert(0, 'authentication.backends.radius.RadiusBackend')
|
||||||
return backends
|
return backends
|
||||||
|
@ -480,9 +611,9 @@ class ConfigManager:
|
||||||
|
|
||||||
manager = cls(root_path=root_path)
|
manager = cls(root_path=root_path)
|
||||||
if manager.load_from_object():
|
if manager.load_from_object():
|
||||||
return manager.config
|
config = manager.config
|
||||||
elif manager.load_from_yml():
|
elif manager.load_from_yml():
|
||||||
return manager.config
|
config = manager.config
|
||||||
else:
|
else:
|
||||||
msg = """
|
msg = """
|
||||||
|
|
||||||
|
@ -492,6 +623,10 @@ class ConfigManager:
|
||||||
"""
|
"""
|
||||||
raise ImportError(msg)
|
raise ImportError(msg)
|
||||||
|
|
||||||
|
# 对config进行兼容处理
|
||||||
|
config.compatible()
|
||||||
|
return config
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_dynamic_config(cls, config):
|
def get_dynamic_config(cls, config):
|
||||||
return DynamicConfig(config)
|
return DynamicConfig(config)
|
||||||
|
|
|
@ -7,6 +7,6 @@ __all__ = ['BASE_DIR', 'PROJECT_DIR', 'VERSION', 'CONFIG', 'DYNAMIC']
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
PROJECT_DIR = os.path.dirname(BASE_DIR)
|
PROJECT_DIR = os.path.dirname(BASE_DIR)
|
||||||
VERSION = '1.5.8'
|
VERSION = '1.5.9'
|
||||||
CONFIG = ConfigManager.load_user_config()
|
CONFIG = ConfigManager.load_user_config()
|
||||||
DYNAMIC = ConfigManager.get_dynamic_config(CONFIG)
|
DYNAMIC = ConfigManager.get_dynamic_config(CONFIG)
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
#
|
#
|
||||||
import os
|
import os
|
||||||
import ldap
|
import ldap
|
||||||
from django.urls import reverse_lazy
|
|
||||||
|
|
||||||
from ..const import CONFIG, DYNAMIC, PROJECT_DIR
|
from ..const import CONFIG, DYNAMIC, PROJECT_DIR
|
||||||
|
|
||||||
|
@ -43,19 +42,36 @@ AUTH_LDAP_SYNC_INTERVAL = CONFIG.AUTH_LDAP_SYNC_INTERVAL
|
||||||
AUTH_LDAP_SYNC_CRONTAB = CONFIG.AUTH_LDAP_SYNC_CRONTAB
|
AUTH_LDAP_SYNC_CRONTAB = CONFIG.AUTH_LDAP_SYNC_CRONTAB
|
||||||
AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS = CONFIG.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS
|
AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS = CONFIG.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS
|
||||||
|
|
||||||
# openid
|
|
||||||
# Auth OpenID settings
|
# ==============================================================================
|
||||||
BASE_SITE_URL = CONFIG.BASE_SITE_URL
|
# 认证 OpenID 配置参数
|
||||||
|
# 参考: https://django-oidc-rp.readthedocs.io/en/stable/settings.html
|
||||||
|
# ==============================================================================
|
||||||
AUTH_OPENID = CONFIG.AUTH_OPENID
|
AUTH_OPENID = CONFIG.AUTH_OPENID
|
||||||
AUTH_OPENID_SERVER_URL = CONFIG.AUTH_OPENID_SERVER_URL
|
BASE_SITE_URL = CONFIG.BASE_SITE_URL
|
||||||
AUTH_OPENID_REALM_NAME = CONFIG.AUTH_OPENID_REALM_NAME
|
|
||||||
AUTH_OPENID_CLIENT_ID = CONFIG.AUTH_OPENID_CLIENT_ID
|
AUTH_OPENID_CLIENT_ID = CONFIG.AUTH_OPENID_CLIENT_ID
|
||||||
AUTH_OPENID_CLIENT_SECRET = CONFIG.AUTH_OPENID_CLIENT_SECRET
|
AUTH_OPENID_CLIENT_SECRET = CONFIG.AUTH_OPENID_CLIENT_SECRET
|
||||||
AUTH_OPENID_IGNORE_SSL_VERIFICATION = CONFIG.AUTH_OPENID_IGNORE_SSL_VERIFICATION
|
AUTH_OPENID_PROVIDER_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_ENDPOINT
|
||||||
AUTH_OPENID_SHARE_SESSION = CONFIG.AUTH_OPENID_SHARE_SESSION
|
AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT
|
||||||
AUTH_OPENID_LOGIN_URL = reverse_lazy("authentication:openid:openid-login")
|
AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT
|
||||||
AUTH_OPENID_LOGIN_COMPLETE_URL = reverse_lazy("authentication:openid:openid-login-complete")
|
AUTH_OPENID_PROVIDER_JWKS_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_JWKS_ENDPOINT
|
||||||
|
AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT
|
||||||
|
AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT
|
||||||
|
AUTH_OPENID_PROVIDER_SIGNATURE_ALG = CONFIG.AUTH_OPENID_PROVIDER_SIGNATURE_ALG
|
||||||
|
AUTH_OPENID_PROVIDER_SIGNATURE_KEY = CONFIG.AUTH_OPENID_PROVIDER_SIGNATURE_KEY
|
||||||
|
AUTH_OPENID_SCOPES = CONFIG.AUTH_OPENID_SCOPES
|
||||||
|
AUTH_OPENID_ID_TOKEN_MAX_AGE = CONFIG.AUTH_OPENID_ID_TOKEN_MAX_AGE
|
||||||
|
AUTH_OPENID_ID_TOKEN_INCLUDE_CLAIMS = CONFIG.AUTH_OPENID_ID_TOKEN_INCLUDE_CLAIMS
|
||||||
|
AUTH_OPENID_USE_STATE = CONFIG.AUTH_OPENID_USE_STATE
|
||||||
|
AUTH_OPENID_USE_NONCE = CONFIG.AUTH_OPENID_USE_NONCE
|
||||||
|
|
||||||
|
AUTH_OPENID_SHARE_SESSION = CONFIG.AUTH_OPENID_SHARE_SESSION
|
||||||
|
AUTH_OPENID_IGNORE_SSL_VERIFICATION = CONFIG.AUTH_OPENID_IGNORE_SSL_VERIFICATION
|
||||||
|
AUTH_OPENID_ALWAYS_UPDATE_USER = CONFIG.AUTH_OPENID_ALWAYS_UPDATE_USER
|
||||||
|
AUTH_OPENID_AUTH_LOGIN_URL_NAME = 'authentication:openid:login'
|
||||||
|
AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME = 'authentication:openid:login-callback'
|
||||||
|
AUTH_OPENID_AUTH_LOGOUT_URL_NAME = 'authentication:openid:logout'
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
# Radius Auth
|
# Radius Auth
|
||||||
AUTH_RADIUS = CONFIG.AUTH_RADIUS
|
AUTH_RADIUS = CONFIG.AUTH_RADIUS
|
||||||
|
|
|
@ -48,6 +48,7 @@ INSTALLED_APPS = [
|
||||||
'authentication.apps.AuthenticationConfig', # authentication
|
'authentication.apps.AuthenticationConfig', # authentication
|
||||||
'applications.apps.ApplicationsConfig',
|
'applications.apps.ApplicationsConfig',
|
||||||
'tickets.apps.TicketsConfig',
|
'tickets.apps.TicketsConfig',
|
||||||
|
'jms_oidc_rp',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'rest_framework_swagger',
|
'rest_framework_swagger',
|
||||||
'drf_yasg',
|
'drf_yasg',
|
||||||
|
@ -75,7 +76,7 @@ MIDDLEWARE = [
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
'authentication.backends.openid.middleware.OpenIDAuthenticationMiddleware',
|
'jms_oidc_rp.middleware.OIDCRefreshIDTokenMiddleware',
|
||||||
'django_cas_ng.middleware.CASMiddleware',
|
'django_cas_ng.middleware.CASMiddleware',
|
||||||
'jumpserver.middleware.TimezoneMiddleware',
|
'jumpserver.middleware.TimezoneMiddleware',
|
||||||
'jumpserver.middleware.DemoMiddleware',
|
'jumpserver.middleware.DemoMiddleware',
|
||||||
|
@ -103,6 +104,7 @@ TEMPLATES = [
|
||||||
'django.template.context_processors.media',
|
'django.template.context_processors.media',
|
||||||
'jumpserver.context_processor.jumpserver_processor',
|
'jumpserver.context_processor.jumpserver_processor',
|
||||||
'orgs.context_processor.org_processor',
|
'orgs.context_processor.org_processor',
|
||||||
|
'jms_oidc_rp.context_processors.oidc',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -18,4 +18,3 @@ def get_current_request():
|
||||||
|
|
||||||
|
|
||||||
current_request = LocalProxy(partial(_find, 'current_request'))
|
current_request = LocalProxy(partial(_find, 'current_request'))
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: JumpServer 0.3.3\n"
|
"Project-Id-Version: JumpServer 0.3.3\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2020-05-11 16:53+0800\n"
|
"POT-Creation-Date: 2020-05-20 19:25+0800\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: ibuler <ibuler@qq.com>\n"
|
"Last-Translator: ibuler <ibuler@qq.com>\n"
|
||||||
"Language-Team: JumpServer team<ibuler@qq.com>\n"
|
"Language-Team: JumpServer team<ibuler@qq.com>\n"
|
||||||
|
@ -26,7 +26,7 @@ msgstr "自定义"
|
||||||
#: applications/templates/applications/remote_app_list.html:27
|
#: applications/templates/applications/remote_app_list.html:27
|
||||||
#: applications/templates/applications/user_remote_app_list.html:18
|
#: applications/templates/applications/user_remote_app_list.html:18
|
||||||
#: assets/forms/domain.py:15 assets/forms/label.py:13
|
#: assets/forms/domain.py:15 assets/forms/label.py:13
|
||||||
#: assets/models/asset.py:353 assets/models/authbook.py:27
|
#: assets/models/asset.py:352 assets/models/authbook.py:27
|
||||||
#: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:32
|
#: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:32
|
||||||
#: assets/serializers/asset_user.py:47 assets/serializers/asset_user.py:84
|
#: assets/serializers/asset_user.py:47 assets/serializers/asset_user.py:84
|
||||||
#: assets/serializers/system_user.py:44 assets/serializers/system_user.py:176
|
#: assets/serializers/system_user.py:44 assets/serializers/system_user.py:176
|
||||||
|
@ -112,7 +112,7 @@ msgstr "运行参数"
|
||||||
#: applications/templates/applications/user_database_app_list.html:16
|
#: applications/templates/applications/user_database_app_list.html:16
|
||||||
#: applications/templates/applications/user_remote_app_list.html:16
|
#: applications/templates/applications/user_remote_app_list.html:16
|
||||||
#: assets/forms/asset.py:21 assets/forms/domain.py:77 assets/forms/user.py:74
|
#: assets/forms/asset.py:21 assets/forms/domain.py:77 assets/forms/user.py:74
|
||||||
#: assets/forms/user.py:96 assets/models/asset.py:146 assets/models/base.py:232
|
#: assets/forms/user.py:96 assets/models/asset.py:145 assets/models/base.py:232
|
||||||
#: assets/models/cluster.py:18 assets/models/cmd_filter.py:21
|
#: assets/models/cluster.py:18 assets/models/cmd_filter.py:21
|
||||||
#: assets/models/domain.py:20 assets/models/group.py:20
|
#: assets/models/domain.py:20 assets/models/group.py:20
|
||||||
#: assets/models/label.py:18 assets/templates/assets/_node_detail_modal.html:27
|
#: assets/models/label.py:18 assets/templates/assets/_node_detail_modal.html:27
|
||||||
|
@ -204,7 +204,7 @@ msgstr "主机"
|
||||||
#: applications/models/database_app.py:27
|
#: applications/models/database_app.py:27
|
||||||
#: applications/templates/applications/database_app_detail.html:60
|
#: applications/templates/applications/database_app_detail.html:60
|
||||||
#: applications/templates/applications/database_app_list.html:26
|
#: applications/templates/applications/database_app_list.html:26
|
||||||
#: assets/forms/asset.py:25 assets/models/asset.py:192
|
#: assets/forms/asset.py:25 assets/models/asset.py:191
|
||||||
#: assets/models/domain.py:50
|
#: assets/models/domain.py:50
|
||||||
#: assets/templates/assets/domain_gateway_list.html:64
|
#: assets/templates/assets/domain_gateway_list.html:64
|
||||||
msgid "Port"
|
msgid "Port"
|
||||||
|
@ -227,7 +227,7 @@ msgstr "数据库"
|
||||||
#: applications/templates/applications/remote_app_list.html:28
|
#: applications/templates/applications/remote_app_list.html:28
|
||||||
#: applications/templates/applications/user_database_app_list.html:20
|
#: applications/templates/applications/user_database_app_list.html:20
|
||||||
#: applications/templates/applications/user_remote_app_list.html:19
|
#: applications/templates/applications/user_remote_app_list.html:19
|
||||||
#: assets/models/asset.py:151 assets/models/asset.py:227
|
#: assets/models/asset.py:150 assets/models/asset.py:226
|
||||||
#: assets/models/base.py:237 assets/models/cluster.py:29
|
#: assets/models/base.py:237 assets/models/cluster.py:29
|
||||||
#: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:56
|
#: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:56
|
||||||
#: assets/models/domain.py:21 assets/models/domain.py:53
|
#: assets/models/domain.py:21 assets/models/domain.py:53
|
||||||
|
@ -244,7 +244,8 @@ msgstr "数据库"
|
||||||
#: assets/templates/assets/platform_detail.html:64
|
#: assets/templates/assets/platform_detail.html:64
|
||||||
#: assets/templates/assets/platform_list.html:18
|
#: assets/templates/assets/platform_list.html:18
|
||||||
#: assets/templates/assets/system_user_detail.html:112
|
#: assets/templates/assets/system_user_detail.html:112
|
||||||
#: assets/templates/assets/system_user_list.html:29 ops/models/adhoc.py:37
|
#: assets/templates/assets/system_user_list.html:29
|
||||||
|
#: assets/templates/assets/user_asset_list.html:81 ops/models/adhoc.py:37
|
||||||
#: orgs/models.py:18 perms/models/base.py:56
|
#: orgs/models.py:18 perms/models/base.py:56
|
||||||
#: perms/templates/perms/asset_permission_detail.html:97
|
#: perms/templates/perms/asset_permission_detail.html:97
|
||||||
#: perms/templates/perms/database_app_permission_detail.html:93
|
#: perms/templates/perms/database_app_permission_detail.html:93
|
||||||
|
@ -309,7 +310,7 @@ msgstr "参数"
|
||||||
#: applications/models/remote_app.py:39
|
#: applications/models/remote_app.py:39
|
||||||
#: applications/templates/applications/database_app_detail.html:72
|
#: applications/templates/applications/database_app_detail.html:72
|
||||||
#: applications/templates/applications/remote_app_detail.html:68
|
#: applications/templates/applications/remote_app_detail.html:68
|
||||||
#: assets/models/asset.py:225 assets/models/base.py:240
|
#: assets/models/asset.py:224 assets/models/base.py:240
|
||||||
#: assets/models/cluster.py:28 assets/models/cmd_filter.py:26
|
#: assets/models/cluster.py:28 assets/models/cmd_filter.py:26
|
||||||
#: assets/models/cmd_filter.py:59 assets/models/group.py:21
|
#: assets/models/cmd_filter.py:59 assets/models/group.py:21
|
||||||
#: assets/templates/assets/admin_user_detail.html:63
|
#: assets/templates/assets/admin_user_detail.html:63
|
||||||
|
@ -335,7 +336,7 @@ msgstr "创建者"
|
||||||
#: applications/models/remote_app.py:42
|
#: applications/models/remote_app.py:42
|
||||||
#: applications/templates/applications/database_app_detail.html:68
|
#: applications/templates/applications/database_app_detail.html:68
|
||||||
#: applications/templates/applications/remote_app_detail.html:64
|
#: applications/templates/applications/remote_app_detail.html:64
|
||||||
#: assets/models/asset.py:226 assets/models/base.py:238
|
#: assets/models/asset.py:225 assets/models/base.py:238
|
||||||
#: assets/models/cluster.py:26 assets/models/domain.py:23
|
#: assets/models/cluster.py:26 assets/models/domain.py:23
|
||||||
#: assets/models/gathered_user.py:19 assets/models/group.py:22
|
#: assets/models/gathered_user.py:19 assets/models/group.py:22
|
||||||
#: assets/models/label.py:25 assets/templates/assets/admin_user_detail.html:59
|
#: assets/models/label.py:25 assets/templates/assets/admin_user_detail.html:59
|
||||||
|
@ -737,7 +738,7 @@ msgstr "不能移除资产的管理用户账号"
|
||||||
msgid "Latest version could not be delete"
|
msgid "Latest version could not be delete"
|
||||||
msgstr "最新版本的不能被删除"
|
msgstr "最新版本的不能被删除"
|
||||||
|
|
||||||
#: assets/forms/asset.py:83 assets/models/asset.py:196
|
#: assets/forms/asset.py:83 assets/models/asset.py:195
|
||||||
#: assets/models/user.py:109 assets/templates/assets/asset_detail.html:186
|
#: assets/models/user.py:109 assets/templates/assets/asset_detail.html:186
|
||||||
#: assets/templates/assets/asset_detail.html:194
|
#: assets/templates/assets/asset_detail.html:194
|
||||||
#: assets/templates/assets/system_user_assets.html:118
|
#: assets/templates/assets/system_user_assets.html:118
|
||||||
|
@ -748,7 +749,7 @@ msgstr "最新版本的不能被删除"
|
||||||
msgid "Nodes"
|
msgid "Nodes"
|
||||||
msgstr "节点"
|
msgstr "节点"
|
||||||
|
|
||||||
#: assets/forms/asset.py:86 assets/models/asset.py:200
|
#: assets/forms/asset.py:86 assets/models/asset.py:199
|
||||||
#: assets/models/cluster.py:19 assets/models/user.py:65
|
#: assets/models/cluster.py:19 assets/models/user.py:65
|
||||||
#: assets/templates/assets/admin_user_list.html:62
|
#: assets/templates/assets/admin_user_list.html:62
|
||||||
#: assets/templates/assets/asset_detail.html:72 templates/_nav.html:44
|
#: assets/templates/assets/asset_detail.html:72 templates/_nav.html:44
|
||||||
|
@ -766,7 +767,7 @@ msgstr "管理用户"
|
||||||
msgid "Label"
|
msgid "Label"
|
||||||
msgstr "标签"
|
msgstr "标签"
|
||||||
|
|
||||||
#: assets/forms/asset.py:92 assets/models/asset.py:195
|
#: assets/forms/asset.py:92 assets/models/asset.py:194
|
||||||
#: assets/models/domain.py:26 assets/models/domain.py:52
|
#: assets/models/domain.py:26 assets/models/domain.py:52
|
||||||
#: assets/templates/assets/asset_detail.html:76
|
#: assets/templates/assets/asset_detail.html:76
|
||||||
#: assets/templates/assets/user_asset_list.html:80
|
#: assets/templates/assets/user_asset_list.html:80
|
||||||
|
@ -774,8 +775,8 @@ msgstr "标签"
|
||||||
msgid "Domain"
|
msgid "Domain"
|
||||||
msgstr "网域"
|
msgstr "网域"
|
||||||
|
|
||||||
#: assets/forms/asset.py:95 assets/models/asset.py:170
|
#: assets/forms/asset.py:95 assets/models/asset.py:169
|
||||||
#: assets/models/asset.py:194 assets/serializers/asset.py:67
|
#: assets/models/asset.py:193 assets/serializers/asset.py:67
|
||||||
#: assets/templates/assets/asset_detail.html:100
|
#: assets/templates/assets/asset_detail.html:100
|
||||||
#: assets/templates/assets/user_asset_list.html:78
|
#: assets/templates/assets/user_asset_list.html:78
|
||||||
msgid "Platform"
|
msgid "Platform"
|
||||||
|
@ -980,24 +981,24 @@ msgstr "SFTP的起始路径,tmp目录, 用户home目录或者自定义"
|
||||||
msgid "Username is dynamic, When connect asset, using current user's username"
|
msgid "Username is dynamic, When connect asset, using current user's username"
|
||||||
msgstr "用户名是动态的,登录资产时使用当前用户的用户名登录"
|
msgstr "用户名是动态的,登录资产时使用当前用户的用户名登录"
|
||||||
|
|
||||||
#: assets/models/asset.py:147 xpack/plugins/cloud/providers/base.py:16
|
#: assets/models/asset.py:146 xpack/plugins/cloud/providers/base.py:16
|
||||||
msgid "Base"
|
msgid "Base"
|
||||||
msgstr "基础"
|
msgstr "基础"
|
||||||
|
|
||||||
#: assets/models/asset.py:148 assets/templates/assets/platform_detail.html:56
|
#: assets/models/asset.py:147 assets/templates/assets/platform_detail.html:56
|
||||||
msgid "Charset"
|
msgid "Charset"
|
||||||
msgstr "编码"
|
msgstr "编码"
|
||||||
|
|
||||||
#: assets/models/asset.py:149 assets/templates/assets/platform_detail.html:60
|
#: assets/models/asset.py:148 assets/templates/assets/platform_detail.html:60
|
||||||
#: tickets/models/ticket.py:38
|
#: tickets/models/ticket.py:38
|
||||||
msgid "Meta"
|
msgid "Meta"
|
||||||
msgstr "元数据"
|
msgstr "元数据"
|
||||||
|
|
||||||
#: assets/models/asset.py:150
|
#: assets/models/asset.py:149
|
||||||
msgid "Internal"
|
msgid "Internal"
|
||||||
msgstr "内部的"
|
msgstr "内部的"
|
||||||
|
|
||||||
#: assets/models/asset.py:187 assets/models/domain.py:49
|
#: assets/models/asset.py:186 assets/models/domain.py:49
|
||||||
#: assets/serializers/asset_user.py:46
|
#: assets/serializers/asset_user.py:46
|
||||||
#: assets/templates/assets/_asset_list_modal.html:47
|
#: assets/templates/assets/_asset_list_modal.html:47
|
||||||
#: assets/templates/assets/_asset_user_list.html:20
|
#: assets/templates/assets/_asset_user_list.html:20
|
||||||
|
@ -1014,7 +1015,7 @@ msgstr "内部的"
|
||||||
msgid "IP"
|
msgid "IP"
|
||||||
msgstr "IP"
|
msgstr "IP"
|
||||||
|
|
||||||
#: assets/models/asset.py:188 assets/serializers/asset_user.py:45
|
#: assets/models/asset.py:187 assets/serializers/asset_user.py:45
|
||||||
#: assets/serializers/gathered_user.py:20
|
#: assets/serializers/gathered_user.py:20
|
||||||
#: assets/templates/assets/_asset_list_modal.html:46
|
#: assets/templates/assets/_asset_list_modal.html:46
|
||||||
#: assets/templates/assets/_asset_user_auth_update_modal.html:9
|
#: assets/templates/assets/_asset_user_auth_update_modal.html:9
|
||||||
|
@ -1031,7 +1032,7 @@ msgstr "IP"
|
||||||
msgid "Hostname"
|
msgid "Hostname"
|
||||||
msgstr "主机名"
|
msgstr "主机名"
|
||||||
|
|
||||||
#: assets/models/asset.py:191 assets/models/domain.py:51
|
#: assets/models/asset.py:190 assets/models/domain.py:51
|
||||||
#: assets/models/user.py:114 assets/templates/assets/asset_detail.html:68
|
#: assets/models/user.py:114 assets/templates/assets/asset_detail.html:68
|
||||||
#: assets/templates/assets/domain_gateway_list.html:65
|
#: assets/templates/assets/domain_gateway_list.html:65
|
||||||
#: assets/templates/assets/system_user_detail.html:78
|
#: assets/templates/assets/system_user_detail.html:78
|
||||||
|
@ -1043,84 +1044,84 @@ msgstr "主机名"
|
||||||
msgid "Protocol"
|
msgid "Protocol"
|
||||||
msgstr "协议"
|
msgstr "协议"
|
||||||
|
|
||||||
#: assets/models/asset.py:193 assets/serializers/asset.py:69
|
#: assets/models/asset.py:192 assets/serializers/asset.py:69
|
||||||
#: assets/templates/assets/asset_create.html:24
|
#: assets/templates/assets/asset_create.html:24
|
||||||
#: assets/templates/assets/user_asset_list.html:77
|
#: assets/templates/assets/user_asset_list.html:77
|
||||||
#: perms/serializers/user_permission.py:60
|
#: perms/serializers/user_permission.py:60
|
||||||
msgid "Protocols"
|
msgid "Protocols"
|
||||||
msgstr "协议组"
|
msgstr "协议组"
|
||||||
|
|
||||||
#: assets/models/asset.py:197 assets/models/cmd_filter.py:22
|
#: assets/models/asset.py:196 assets/models/cmd_filter.py:22
|
||||||
#: assets/models/domain.py:54 assets/models/label.py:22
|
#: assets/models/domain.py:54 assets/models/label.py:22
|
||||||
#: assets/templates/assets/asset_detail.html:108 authentication/models.py:45
|
#: assets/templates/assets/asset_detail.html:108 authentication/models.py:45
|
||||||
msgid "Is active"
|
msgid "Is active"
|
||||||
msgstr "激活"
|
msgstr "激活"
|
||||||
|
|
||||||
#: assets/models/asset.py:203 assets/templates/assets/asset_detail.html:64
|
#: assets/models/asset.py:202 assets/templates/assets/asset_detail.html:64
|
||||||
msgid "Public IP"
|
msgid "Public IP"
|
||||||
msgstr "公网IP"
|
msgstr "公网IP"
|
||||||
|
|
||||||
#: assets/models/asset.py:204 assets/templates/assets/asset_detail.html:116
|
#: assets/models/asset.py:203 assets/templates/assets/asset_detail.html:116
|
||||||
msgid "Asset number"
|
msgid "Asset number"
|
||||||
msgstr "资产编号"
|
msgstr "资产编号"
|
||||||
|
|
||||||
#: assets/models/asset.py:207 assets/templates/assets/asset_detail.html:80
|
#: assets/models/asset.py:206 assets/templates/assets/asset_detail.html:80
|
||||||
msgid "Vendor"
|
msgid "Vendor"
|
||||||
msgstr "制造商"
|
msgstr "制造商"
|
||||||
|
|
||||||
#: assets/models/asset.py:208 assets/templates/assets/asset_detail.html:84
|
#: assets/models/asset.py:207 assets/templates/assets/asset_detail.html:84
|
||||||
msgid "Model"
|
msgid "Model"
|
||||||
msgstr "型号"
|
msgstr "型号"
|
||||||
|
|
||||||
#: assets/models/asset.py:209 assets/templates/assets/asset_detail.html:112
|
#: assets/models/asset.py:208 assets/templates/assets/asset_detail.html:112
|
||||||
msgid "Serial number"
|
msgid "Serial number"
|
||||||
msgstr "序列号"
|
msgstr "序列号"
|
||||||
|
|
||||||
#: assets/models/asset.py:211
|
#: assets/models/asset.py:210
|
||||||
msgid "CPU model"
|
msgid "CPU model"
|
||||||
msgstr "CPU型号"
|
msgstr "CPU型号"
|
||||||
|
|
||||||
#: assets/models/asset.py:212
|
#: assets/models/asset.py:211
|
||||||
msgid "CPU count"
|
msgid "CPU count"
|
||||||
msgstr "CPU数量"
|
msgstr "CPU数量"
|
||||||
|
|
||||||
#: assets/models/asset.py:213
|
#: assets/models/asset.py:212
|
||||||
msgid "CPU cores"
|
msgid "CPU cores"
|
||||||
msgstr "CPU核数"
|
msgstr "CPU核数"
|
||||||
|
|
||||||
#: assets/models/asset.py:214
|
#: assets/models/asset.py:213
|
||||||
msgid "CPU vcpus"
|
msgid "CPU vcpus"
|
||||||
msgstr "CPU总数"
|
msgstr "CPU总数"
|
||||||
|
|
||||||
#: assets/models/asset.py:215 assets/templates/assets/asset_detail.html:92
|
#: assets/models/asset.py:214 assets/templates/assets/asset_detail.html:92
|
||||||
msgid "Memory"
|
msgid "Memory"
|
||||||
msgstr "内存"
|
msgstr "内存"
|
||||||
|
|
||||||
#: assets/models/asset.py:216
|
#: assets/models/asset.py:215
|
||||||
msgid "Disk total"
|
msgid "Disk total"
|
||||||
msgstr "硬盘大小"
|
msgstr "硬盘大小"
|
||||||
|
|
||||||
#: assets/models/asset.py:217
|
#: assets/models/asset.py:216
|
||||||
msgid "Disk info"
|
msgid "Disk info"
|
||||||
msgstr "硬盘信息"
|
msgstr "硬盘信息"
|
||||||
|
|
||||||
#: assets/models/asset.py:219 assets/templates/assets/asset_detail.html:104
|
#: assets/models/asset.py:218 assets/templates/assets/asset_detail.html:104
|
||||||
msgid "OS"
|
msgid "OS"
|
||||||
msgstr "操作系统"
|
msgstr "操作系统"
|
||||||
|
|
||||||
#: assets/models/asset.py:220
|
#: assets/models/asset.py:219
|
||||||
msgid "OS version"
|
msgid "OS version"
|
||||||
msgstr "系统版本"
|
msgstr "系统版本"
|
||||||
|
|
||||||
#: assets/models/asset.py:221
|
#: assets/models/asset.py:220
|
||||||
msgid "OS arch"
|
msgid "OS arch"
|
||||||
msgstr "系统架构"
|
msgstr "系统架构"
|
||||||
|
|
||||||
#: assets/models/asset.py:222
|
#: assets/models/asset.py:221
|
||||||
msgid "Hostname raw"
|
msgid "Hostname raw"
|
||||||
msgstr "主机名原始"
|
msgstr "主机名原始"
|
||||||
|
|
||||||
#: assets/models/asset.py:224 assets/templates/assets/asset_create.html:46
|
#: assets/models/asset.py:223 assets/templates/assets/asset_create.html:46
|
||||||
#: assets/templates/assets/asset_detail.html:220 templates/_nav.html:46
|
#: assets/templates/assets/asset_detail.html:220 templates/_nav.html:46
|
||||||
msgid "Labels"
|
msgid "Labels"
|
||||||
msgstr "标签管理"
|
msgstr "标签管理"
|
||||||
|
@ -1449,8 +1450,7 @@ msgid "SFTP Root"
|
||||||
msgstr "SFTP根路径"
|
msgstr "SFTP根路径"
|
||||||
|
|
||||||
#: assets/models/user.py:195 assets/templates/assets/system_user_list.html:73
|
#: assets/models/user.py:195 assets/templates/assets/system_user_list.html:73
|
||||||
#: assets/templates/assets/user_asset_list.html:79 audits/models.py:21
|
#: audits/models.py:21 audits/templates/audits/ftp_log_list.html:53
|
||||||
#: audits/templates/audits/ftp_log_list.html:53
|
|
||||||
#: audits/templates/audits/ftp_log_list.html:76
|
#: audits/templates/audits/ftp_log_list.html:76
|
||||||
#: perms/forms/asset_permission.py:95 perms/forms/remote_app_permission.py:49
|
#: perms/forms/asset_permission.py:95 perms/forms/remote_app_permission.py:49
|
||||||
#: perms/models/asset_permission.py:82
|
#: perms/models/asset_permission.py:82
|
||||||
|
@ -1671,7 +1671,7 @@ msgstr "定期测试系统用户可连接性: {}"
|
||||||
|
|
||||||
#: assets/tasks/utils.py:17
|
#: assets/tasks/utils.py:17
|
||||||
msgid "Asset has been disabled, skipped: {}"
|
msgid "Asset has been disabled, skipped: {}"
|
||||||
msgstr "资产或许不支持ansible, 跳过: {}"
|
msgstr "资产已经被禁用, 跳过: {}"
|
||||||
|
|
||||||
#: assets/tasks/utils.py:21
|
#: assets/tasks/utils.py:21
|
||||||
msgid "Asset may not be support ansible, skipped: {}"
|
msgid "Asset may not be support ansible, skipped: {}"
|
||||||
|
@ -1771,7 +1771,7 @@ msgstr "获取认证信息错误"
|
||||||
#: assets/templates/assets/_asset_user_auth_view_modal.html:101
|
#: assets/templates/assets/_asset_user_auth_view_modal.html:101
|
||||||
#: assets/templates/assets/_node_detail_modal.html:67
|
#: assets/templates/assets/_node_detail_modal.html:67
|
||||||
#: assets/templates/assets/_user_asset_detail_modal.html:23
|
#: assets/templates/assets/_user_asset_detail_modal.html:23
|
||||||
#: authentication/templates/authentication/_access_key_modal.html:142
|
#: authentication/templates/authentication/_access_key_modal.html:155
|
||||||
#: authentication/templates/authentication/_mfa_confirm_modal.html:53
|
#: authentication/templates/authentication/_mfa_confirm_modal.html:53
|
||||||
#: settings/templates/settings/_ldap_list_users_modal.html:171
|
#: settings/templates/settings/_ldap_list_users_modal.html:171
|
||||||
#: templates/_modal.html:22 tickets/models/ticket.py:68
|
#: templates/_modal.html:22 tickets/models/ticket.py:68
|
||||||
|
@ -1817,6 +1817,7 @@ msgid "Push"
|
||||||
msgstr "推送"
|
msgstr "推送"
|
||||||
|
|
||||||
#: assets/templates/assets/_asset_user_list.html:167
|
#: assets/templates/assets/_asset_user_list.html:167
|
||||||
|
#: authentication/templates/authentication/_access_key_modal.html:147
|
||||||
msgid "Delete success"
|
msgid "Delete success"
|
||||||
msgstr "删除成功"
|
msgstr "删除成功"
|
||||||
|
|
||||||
|
@ -2769,7 +2770,7 @@ msgid ""
|
||||||
msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)"
|
msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)"
|
||||||
|
|
||||||
#: authentication/errors.py:48 users/views/profile/otp.py:63
|
#: authentication/errors.py:48 users/views/profile/otp.py:63
|
||||||
#: users/views/profile/otp.py:100 users/views/profile/otp.py:116
|
#: users/views/profile/otp.py:102 users/views/profile/otp.py:121
|
||||||
msgid "MFA code invalid, or ntp sync server time"
|
msgid "MFA code invalid, or ntp sync server time"
|
||||||
msgstr "MFA验证码不正确,或者服务器端时间不对"
|
msgstr "MFA验证码不正确,或者服务器端时间不对"
|
||||||
|
|
||||||
|
@ -2881,8 +2882,8 @@ msgid "More login options"
|
||||||
msgstr "更多登录方式"
|
msgstr "更多登录方式"
|
||||||
|
|
||||||
#: authentication/templates/authentication/login.html:61
|
#: authentication/templates/authentication/login.html:61
|
||||||
msgid "Keycloak"
|
msgid "OpenID"
|
||||||
msgstr ""
|
msgstr "OpenID"
|
||||||
|
|
||||||
#: authentication/templates/authentication/login_otp.html:17
|
#: authentication/templates/authentication/login_otp.html:17
|
||||||
msgid "One-time password"
|
msgid "One-time password"
|
||||||
|
@ -5598,7 +5599,7 @@ msgstr "上一步"
|
||||||
|
|
||||||
#: users/templates/users/first_login_done.html:31
|
#: users/templates/users/first_login_done.html:31
|
||||||
msgid "Welcome to use jumpserver, visit "
|
msgid "Welcome to use jumpserver, visit "
|
||||||
msgstr "欢迎使用JumpServer开源跳板机系统"
|
msgstr "欢迎使用 JumpServer 堡垒机"
|
||||||
|
|
||||||
#: users/templates/users/first_login_done.html:32
|
#: users/templates/users/first_login_done.html:32
|
||||||
msgid "Use guide"
|
msgid "Use guide"
|
||||||
|
@ -6183,19 +6184,19 @@ msgstr "首次登录"
|
||||||
msgid "Profile setting"
|
msgid "Profile setting"
|
||||||
msgstr "个人信息设置"
|
msgstr "个人信息设置"
|
||||||
|
|
||||||
#: users/views/profile/otp.py:144
|
#: users/views/profile/otp.py:145
|
||||||
msgid "MFA enable success"
|
msgid "MFA enable success"
|
||||||
msgstr "多因子认证启用成功"
|
msgstr "多因子认证启用成功"
|
||||||
|
|
||||||
#: users/views/profile/otp.py:145
|
#: users/views/profile/otp.py:146
|
||||||
msgid "MFA enable success, return login page"
|
msgid "MFA enable success, return login page"
|
||||||
msgstr "多因子认证启用成功,返回到登录页面"
|
msgstr "多因子认证启用成功,返回到登录页面"
|
||||||
|
|
||||||
#: users/views/profile/otp.py:147
|
#: users/views/profile/otp.py:148
|
||||||
msgid "MFA disable success"
|
msgid "MFA disable success"
|
||||||
msgstr "多因子认证禁用成功"
|
msgstr "多因子认证禁用成功"
|
||||||
|
|
||||||
#: users/views/profile/otp.py:148
|
#: users/views/profile/otp.py:149
|
||||||
msgid "MFA disable success, return login page"
|
msgid "MFA disable success, return login page"
|
||||||
msgstr "多因子认证禁用成功,返回登录页面"
|
msgstr "多因子认证禁用成功,返回登录页面"
|
||||||
|
|
||||||
|
@ -6587,11 +6588,15 @@ msgstr "华南-广州"
|
||||||
|
|
||||||
#: xpack/plugins/cloud/providers/huaweicloud.py:30
|
#: xpack/plugins/cloud/providers/huaweicloud.py:30
|
||||||
msgid "CN Southwest-Guiyang1"
|
msgid "CN Southwest-Guiyang1"
|
||||||
msgstr "西南-贵阳1"
|
msgstr "西南-贵阳一"
|
||||||
|
|
||||||
#: xpack/plugins/cloud/providers/huaweicloud.py:31
|
#: xpack/plugins/cloud/providers/huaweicloud.py:31
|
||||||
msgid "EU-Paris"
|
msgid "EU-Paris"
|
||||||
msgstr ""
|
msgstr "欧洲-巴黎"
|
||||||
|
|
||||||
|
#: xpack/plugins/cloud/providers/huaweicloud.py:32
|
||||||
|
msgid "LA-Santiago"
|
||||||
|
msgstr "拉美-圣地亚哥"
|
||||||
|
|
||||||
#: xpack/plugins/cloud/providers/qcloud.py:17
|
#: xpack/plugins/cloud/providers/qcloud.py:17
|
||||||
msgid "Tencent Cloud"
|
msgid "Tencent Cloud"
|
||||||
|
@ -6989,9 +6994,6 @@ msgstr "密码匣子"
|
||||||
msgid "vault create"
|
msgid "vault create"
|
||||||
msgstr "创建"
|
msgstr "创建"
|
||||||
|
|
||||||
#~ msgid "LA-Santiago"
|
|
||||||
#~ msgstr "拉美-圣地亚哥"
|
|
||||||
|
|
||||||
#~ msgid "Total hosts"
|
#~ msgid "Total hosts"
|
||||||
#~ msgstr "主机总数"
|
#~ msgstr "主机总数"
|
||||||
|
|
||||||
|
|
|
@ -19,10 +19,13 @@ class AdHocExecutionSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_stat(obj):
|
def get_stat(obj):
|
||||||
|
count_failed_hosts = len(obj.failed_hosts)
|
||||||
|
count_success_hosts = len(obj.success_hosts)
|
||||||
|
count_total = count_success_hosts + count_failed_hosts
|
||||||
return {
|
return {
|
||||||
"total": obj.hosts_amount,
|
"total": count_total,
|
||||||
"success": len(obj.summary.get("contacted", [])),
|
"success": count_success_hosts,
|
||||||
"failed": len(obj.summary.get("dark", [])),
|
"failed": count_failed_hosts
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_field_names(self, declared_fields, info):
|
def get_field_names(self, declared_fields, info):
|
||||||
|
|
|
@ -5,3 +5,7 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
class PermsConfig(AppConfig):
|
class PermsConfig(AppConfig):
|
||||||
name = 'perms'
|
name = 'perms'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
super().ready()
|
||||||
|
from . import signals_handler
|
||||||
|
|
|
@ -218,7 +218,7 @@ function addGroups(groupsId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeGroup(groupId) {
|
function removeGroup(groupId) {
|
||||||
var theUrl = "{% url 'api-perms:asset-permissions-user-groups-relation-list' %}?assetpermission={{ object.id }}";
|
var theUrl = "{% url 'api-perms:asset-permissions-user-groups-relation-list' %}?assetpermission={{ object.id }}&usergroup=groupId";
|
||||||
theUrl = theUrl.replace("groupId", groupId);
|
theUrl = theUrl.replace("groupId", groupId);
|
||||||
var success = function(data) {
|
var success = function(data) {
|
||||||
location.reload();
|
location.reload();
|
||||||
|
|
|
@ -218,7 +218,7 @@ function addGroups(groupsId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeGroup(groupId) {
|
function removeGroup(groupId) {
|
||||||
var theUrl = "{% url 'api-perms:database-app-permissions-user-groups-relation-list' %}?databaseapppermission={{ object.id }}";
|
var theUrl = "{% url 'api-perms:database-app-permissions-user-groups-relation-list' %}?databaseapppermission={{ object.id }}&usergroup=groupId";
|
||||||
theUrl = theUrl.replace("groupId", groupId);
|
theUrl = theUrl.replace("groupId", groupId);
|
||||||
var success = function(data) {
|
var success = function(data) {
|
||||||
location.reload();
|
location.reload();
|
||||||
|
|
|
@ -179,9 +179,13 @@
|
||||||
var body = {
|
var body = {
|
||||||
user_groups: groups
|
user_groups: groups
|
||||||
};
|
};
|
||||||
|
var success = function(data) {
|
||||||
|
location.reload();
|
||||||
|
};
|
||||||
requestApi({
|
requestApi({
|
||||||
url: the_url,
|
url: the_url,
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body),
|
||||||
|
success: success
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
|
|
|
@ -477,6 +477,7 @@ class ParserNode:
|
||||||
'platform': asset.platform_base,
|
'platform': asset.platform_base,
|
||||||
'domain': asset.domain_id,
|
'domain': asset.domain_id,
|
||||||
'org_name': asset.org_name,
|
'org_name': asset.org_name,
|
||||||
|
'org_id': asset.org_id
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="ibox-content">
|
<div class="ibox-content">
|
||||||
<h1 class="no-margins"><a href="{% url 'terminal:session-online-list' %}"> <span id="total_count_online_assets"></span></a></h1>
|
<h1 class="no-margins"><a href="{% url 'terminal:session-online-list' %}"> <span id="total_count_online_sessions"></span></a></h1>
|
||||||
<small>Online sessions</small>
|
<small>Online sessions</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -422,7 +422,7 @@ function renderTotalCount(){
|
||||||
$('#total_count_assets').html(data['total_count_assets']);
|
$('#total_count_assets').html(data['total_count_assets']);
|
||||||
$('#total_count_users').html(data['total_count_users']);
|
$('#total_count_users').html(data['total_count_users']);
|
||||||
$('#total_count_online_users').html(data['total_count_online_users']);
|
$('#total_count_online_users').html(data['total_count_online_users']);
|
||||||
$('#total_count_online_assets').html(data['total_count_online_assets']);
|
$('#total_count_online_sessions').html(data['total_count_online_sessions']);
|
||||||
};
|
};
|
||||||
renderRequestApi('total_count=1', success);
|
renderRequestApi('total_count=1', success);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ class TerminalSerializer(serializers.ModelSerializer):
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'remote_addr', 'http_port', 'ssh_port',
|
'id', 'name', 'remote_addr', 'http_port', 'ssh_port',
|
||||||
'comment', 'is_accepted', "is_active", 'session_online',
|
'comment', 'is_accepted', "is_active", 'session_online',
|
||||||
'is_alive'
|
'is_alive', 'date_created', 'command_storage', 'replay_storage'
|
||||||
]
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
@ -3,12 +3,17 @@
|
||||||
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.db.models.signals import m2m_changed
|
from django.db.models.signals import m2m_changed
|
||||||
|
from django_auth_ldap.backend import populate_user
|
||||||
|
from django.conf import settings
|
||||||
from django_cas_ng.signals import cas_user_authenticated
|
from django_cas_ng.signals import cas_user_authenticated
|
||||||
|
|
||||||
|
from jms_oidc_rp.signals import openid_create_or_update_user
|
||||||
|
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from .signals import post_user_create
|
from .signals import post_user_create
|
||||||
from .models import User
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,3 +42,33 @@ def on_cas_user_authenticated(sender, user, created, **kwargs):
|
||||||
if created:
|
if created:
|
||||||
user.source = user.SOURCE_CAS
|
user.source = user.SOURCE_CAS
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(populate_user)
|
||||||
|
def on_ldap_create_user(sender, user, ldap_user, **kwargs):
|
||||||
|
if user and user.username not in ['admin']:
|
||||||
|
exists = User.objects.filter(username=user.username).exists()
|
||||||
|
if not exists:
|
||||||
|
user.source = user.SOURCE_LDAP
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(openid_create_or_update_user)
|
||||||
|
def on_openid_create_or_update_user(sender, request, user, created, name, username, email, **kwargs):
|
||||||
|
if created:
|
||||||
|
logger.debug(
|
||||||
|
"Receive OpenID user created signal: {}, "
|
||||||
|
"Set user source is: {}".format(user, User.SOURCE_OPENID)
|
||||||
|
)
|
||||||
|
user.source = User.SOURCE_OPENID
|
||||||
|
user.save()
|
||||||
|
elif not created and settings.AUTH_OPENID_ALWAYS_UPDATE_USER:
|
||||||
|
logger.debug(
|
||||||
|
"Receive OpenID user updated signal: {}, "
|
||||||
|
"Update user info: {}"
|
||||||
|
"".format(user, "name: {}|username: {}|email: {}".format(name, username, email))
|
||||||
|
)
|
||||||
|
user.name = name
|
||||||
|
user.username = username
|
||||||
|
user.email = email
|
||||||
|
user.save()
|
||||||
|
|
|
@ -83,26 +83,12 @@ class UserOtpEnableBindView(TemplateView, FormView):
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class UserVerifyMFAView(FormView):
|
class UserDisableMFAView(FormView):
|
||||||
template_name = 'users/user_verify_mfa.html'
|
template_name = 'users/user_verify_mfa.html'
|
||||||
form_class = forms.UserCheckOtpCodeForm
|
form_class = forms.UserCheckOtpCodeForm
|
||||||
success_url = reverse_lazy('users:user-otp-settings-success')
|
success_url = reverse_lazy('users:user-otp-settings-success')
|
||||||
permission_classes = [IsValidUser]
|
permission_classes = [IsValidUser]
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
user = self.request.user
|
|
||||||
otp_code = form.cleaned_data.get('otp_code')
|
|
||||||
|
|
||||||
valid = user.check_mfa(otp_code)
|
|
||||||
if valid:
|
|
||||||
return super().form_valid(form)
|
|
||||||
else:
|
|
||||||
error = _('MFA code invalid, or ntp sync server time')
|
|
||||||
form.add_error('otp_code', error)
|
|
||||||
return super().form_invalid(form)
|
|
||||||
|
|
||||||
|
|
||||||
class UserDisableMFAView(UserVerifyMFAView):
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
otp_code = form.cleaned_data.get('otp_code')
|
otp_code = form.cleaned_data.get('otp_code')
|
||||||
|
@ -118,8 +104,23 @@ class UserDisableMFAView(UserVerifyMFAView):
|
||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
||||||
|
|
||||||
class UserOtpUpdateView(UserVerifyMFAView):
|
class UserOtpUpdateView(FormView):
|
||||||
|
template_name = 'users/user_verify_mfa.html'
|
||||||
|
form_class = forms.UserCheckOtpCodeForm
|
||||||
success_url = reverse_lazy('users:user-otp-enable-bind')
|
success_url = reverse_lazy('users:user-otp-enable-bind')
|
||||||
|
permission_classes = [IsValidUser]
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
user = self.request.user
|
||||||
|
otp_code = form.cleaned_data.get('otp_code')
|
||||||
|
|
||||||
|
valid = user.check_mfa(otp_code)
|
||||||
|
if valid:
|
||||||
|
return super().form_valid(form)
|
||||||
|
else:
|
||||||
|
error = _('MFA code invalid, or ntp sync server time')
|
||||||
|
form.add_error('otp_code', error)
|
||||||
|
return super().form_invalid(form)
|
||||||
|
|
||||||
|
|
||||||
class UserOtpSettingsSuccessView(TemplateView):
|
class UserOtpSettingsSuccessView(TemplateView):
|
||||||
|
|
|
@ -67,7 +67,7 @@ class UserVerifyPasswordView(FormView):
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
user = get_user_or_pre_auth_user(self.request)
|
user = get_user_or_pre_auth_user(self.request)
|
||||||
password = form.cleaned_data.get('password')
|
password = form.cleaned_data.get('password')
|
||||||
user = authenticate(username=user.username, password=password)
|
user = authenticate(request=self.request, username=user.username, password=password)
|
||||||
if not user:
|
if not user:
|
||||||
form.add_error("password", _("Password invalid"))
|
form.add_error("password", _("Password invalid"))
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
|
@ -29,7 +29,6 @@ BOOTSTRAP_TOKEN:
|
||||||
# 使用单文件sqlite数据库
|
# 使用单文件sqlite数据库
|
||||||
# DB_ENGINE: sqlite3
|
# DB_ENGINE: sqlite3
|
||||||
# DB_NAME:
|
# DB_NAME:
|
||||||
|
|
||||||
# MySQL or postgres setting like:
|
# MySQL or postgres setting like:
|
||||||
# 使用Mysql作为数据库
|
# 使用Mysql作为数据库
|
||||||
DB_ENGINE: mysql
|
DB_ENGINE: mysql
|
||||||
|
@ -54,16 +53,28 @@ REDIS_PORT: 6379
|
||||||
# REDIS_DB_CELERY: 3
|
# REDIS_DB_CELERY: 3
|
||||||
# REDIS_DB_CACHE: 4
|
# REDIS_DB_CACHE: 4
|
||||||
|
|
||||||
# Use OpenID authorization
|
# Use OpenID Authorization
|
||||||
# 使用OpenID 来进行认证设置
|
# 使用 OpenID 进行认证设置
|
||||||
# BASE_SITE_URL: http://localhost:8080
|
# AUTH_OPENID: False # True or False
|
||||||
# AUTH_OPENID: false # True or False
|
# BASE_SITE_URL: None
|
||||||
# AUTH_OPENID_SERVER_URL: https://openid-auth-server.com/
|
|
||||||
# AUTH_OPENID_REALM_NAME: realm-name
|
|
||||||
# AUTH_OPENID_CLIENT_ID: client-id
|
# AUTH_OPENID_CLIENT_ID: client-id
|
||||||
# AUTH_OPENID_CLIENT_SECRET: client-secret
|
# AUTH_OPENID_CLIENT_SECRET: client-secret
|
||||||
# AUTH_OPENID_IGNORE_SSL_VERIFICATION: True
|
# AUTH_OPENID_PROVIDER_ENDPOINT: https://op-example.com/
|
||||||
|
# AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT: https://op-example.com/authorize
|
||||||
|
# AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT: https://op-example.com/token
|
||||||
|
# AUTH_OPENID_PROVIDER_JWKS_ENDPOINT: https://op-example.com/jwks
|
||||||
|
# AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT: https://op-example.com/userinfo
|
||||||
|
# AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT: https://op-example.com/logout
|
||||||
|
# AUTH_OPENID_PROVIDER_SIGNATURE_ALG: HS256
|
||||||
|
# AUTH_OPENID_PROVIDER_SIGNATURE_KEY: None
|
||||||
|
# AUTH_OPENID_SCOPES: "openid profile email"
|
||||||
|
# AUTH_OPENID_ID_TOKEN_MAX_AGE: 60
|
||||||
|
# AUTH_OPENID_ID_TOKEN_INCLUDE_CLAIMS: True
|
||||||
|
# AUTH_OPENID_USE_STATE: True
|
||||||
|
# AUTH_OPENID_USE_NONCE: True
|
||||||
# AUTH_OPENID_SHARE_SESSION: True
|
# AUTH_OPENID_SHARE_SESSION: True
|
||||||
|
# AUTH_OPENID_IGNORE_SSL_VERIFICATION: True
|
||||||
|
# AUTH_OPENID_ALWAYS_UPDATE_USER: True
|
||||||
|
|
||||||
# Use Radius authorization
|
# Use Radius authorization
|
||||||
# 使用Radius来认证
|
# 使用Radius来认证
|
||||||
|
|
|
@ -74,8 +74,6 @@ Werkzeug==0.15.3
|
||||||
drf-nested-routers==0.91
|
drf-nested-routers==0.91
|
||||||
aliyun-python-sdk-core-v3==2.9.1
|
aliyun-python-sdk-core-v3==2.9.1
|
||||||
aliyun-python-sdk-ecs==4.10.1
|
aliyun-python-sdk-ecs==4.10.1
|
||||||
python-keycloak==0.13.3
|
|
||||||
python-keycloak-client==0.1.3
|
|
||||||
rest_condition==1.0.3
|
rest_condition==1.0.3
|
||||||
python-ldap==3.1.0
|
python-ldap==3.1.0
|
||||||
tencentcloud-sdk-python==3.0.40
|
tencentcloud-sdk-python==3.0.40
|
||||||
|
@ -98,3 +96,4 @@ ipython
|
||||||
huaweicloud-sdk-python==1.0.21
|
huaweicloud-sdk-python==1.0.21
|
||||||
django-redis==4.11.0
|
django-redis==4.11.0
|
||||||
python-redis-lock==3.5.0
|
python-redis-lock==3.5.0
|
||||||
|
jumpserver-django-oidc-rp==0.3.7.4
|
||||||
|
|
Loading…
Reference in New Issue