[Update] Merge fromo dev to lina

pull/3986/head
Bai 2020-05-11 14:03:24 +08:00
commit 0cff6ab29b
41 changed files with 1944 additions and 1513 deletions

View File

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

View File

@ -1,6 +1,5 @@
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
# #
import time
from treelib import Tree from treelib import Tree
from treelib.exceptions import NodeIDAbsentError from treelib.exceptions import NodeIDAbsentError
from collections import defaultdict from collections import defaultdict
@ -77,21 +76,9 @@ class TreeService(Tree):
ancestor_ids.pop(0) ancestor_ids.pop(0)
return ancestor_ids return ancestor_ids
def ancestors(self, nid, with_self=False, deep=False, with_assets=True): def ancestors(self, nid, with_self=False, deep=False):
# now = time.time()
# print("Start get ancestor_ids")
ancestor_ids = self.ancestors_ids(nid, with_self=with_self) ancestor_ids = self.ancestors_ids(nid, with_self=with_self)
# now2 = time.time()
# interval = (now2 - now) * 1000
# print("Start get ancestor_ids using: {}".format(interval))
ancestors = [self.get_node(i, deep=deep) for i in ancestor_ids] ancestors = [self.get_node(i, deep=deep) for i in ancestor_ids]
# interval = (time.time() - now2) * 1000
# print("Get ancestors done: {}".format(interval))
if with_assets:
return ancestors
for n in ancestors:
n.data['assets'] = set()
n.data['all_assets'] = None
return ancestors return ancestors
def get_node_full_tag(self, nid): def get_node_full_tag(self, nid):
@ -116,6 +103,7 @@ class TreeService(Tree):
node = super().get_node(nid) node = super().get_node(nid)
if deep: if deep:
node = self.copy_node(node) node = self.copy_node(node)
node.data = {}
return node return node
def parent(self, nid, deep=False): def parent(self, nid, deep=False):

View File

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

View File

@ -0,0 +1,4 @@
"""
使用下面的工程进行jumpserver oidc 认证
https://github.com/BaiJiangJie/jumpserver-django-oidc-rp
"""

View File

@ -1,7 +0,0 @@
# -*- coding: utf-8 -*-
#
from .backends import *
from .middleware import *
from .utils import *
from .decorator import *

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

295
apps/jumpserver/api.py Normal file
View File

@ -0,0 +1,295 @@
from django.core.cache import cache
from django.utils import timezone
from django.utils.timesince import timesince
from django.db.models import Count, Max
from django.http.response import JsonResponse
from rest_framework.views import APIView
from collections import Counter
from users.models import User
from assets.models import Asset
from terminal.models import Session
from orgs.utils import current_org
from common.permissions import IsOrgAdmin
from common.utils import lazyproperty
__all__ = ['IndexApi']
class MonthLoginMetricMixin:
@lazyproperty
def session_month(self):
month_ago = timezone.now() - timezone.timedelta(days=30)
session_month = Session.objects.filter(date_start__gt=month_ago)
return session_month
@lazyproperty
def session_month_dates(self):
dates = self.session_month.dates('date_start', 'day')
return dates
def get_month_metrics_date(self):
month_metrics_date = [d.strftime('%m-%d') for d in self.session_month_dates] or ['0']
return month_metrics_date
@staticmethod
def get_cache_key(date, tp):
date_str = date.strftime("%Y%m%d")
key = "SESSION_MONTH_{}_{}".format(tp, date_str)
return key
def __get_data_from_cache(self, date, tp):
if date == timezone.now().date():
return None
cache_key = self.get_cache_key(date, tp)
count = cache.get(cache_key)
return count
def __set_data_to_cache(self, date, tp, count):
cache_key = self.get_cache_key(date, tp)
cache.set(cache_key, count, 3600*24*7)
@staticmethod
def get_date_start_2_end(d):
time_min = timezone.datetime.min.time()
time_max = timezone.datetime.max.time()
tz = timezone.get_current_timezone()
ds = timezone.datetime.combine(d, time_min).replace(tzinfo=tz)
de = timezone.datetime.combine(d, time_max).replace(tzinfo=tz)
return ds, de
def get_date_login_count(self, date):
tp = "LOGIN"
count = self.__get_data_from_cache(date, tp)
if count is not None:
return count
ds, de = self.get_date_start_2_end(date)
count = Session.objects.filter(date_start__range=(ds, de)).count()
self.__set_data_to_cache(date, tp, count)
return count
def get_month_metrics_total_count_login(self):
data = []
for d in self.session_month_dates:
count = self.get_date_login_count(d)
data.append(count)
if len(data) == 0:
data = [0]
return data
def get_date_user_count(self, date):
tp = "USER"
count = self.__get_data_from_cache(date, tp)
if count is not None:
return count
ds, de = self.get_date_start_2_end(date)
count = len(set(Session.objects.filter(date_start__range=(ds, de)).values_list('user', flat=True)))
self.__set_data_to_cache(date, tp, count)
return count
def get_month_metrics_total_count_active_users(self):
data = []
for d in self.session_month_dates:
count = self.get_date_user_count(d)
data.append(count)
return data
def get_date_asset_count(self, date):
tp = "ASSET"
count = self.__get_data_from_cache(date, tp)
if count is not None:
return count
ds, de = self.get_date_start_2_end(date)
count = len(set(Session.objects.filter(date_start__range=(ds, de)).values_list('asset', flat=True)))
self.__set_data_to_cache(date, tp, count)
return count
def get_month_metrics_total_count_active_assets(self):
data = []
for d in self.session_month_dates:
count = self.get_date_asset_count(d)
data.append(count)
return data
@lazyproperty
def month_total_count_active_users(self):
count = len(set(self.session_month.values_list('user', flat=True)))
return count
@lazyproperty
def month_total_count_inactive_users(self):
total = current_org.get_org_members().count()
active = self.month_total_count_active_users
count = total - active
if count < 0:
count = 0
return count
@lazyproperty
def month_total_count_disabled_users(self):
return current_org.get_org_members().filter(is_active=False).count()
@lazyproperty
def month_total_count_active_assets(self):
return len(set(self.session_month.values_list('asset', flat=True)))
@lazyproperty
def month_total_count_inactive_assets(self):
total = Asset.objects.all().count()
active = self.month_total_count_active_assets
count = total - active
if count < 0:
count = 0
return count
@lazyproperty
def month_total_count_disabled_assets(self):
return Asset.objects.filter(is_active=False).count()
class WeekSessionMetricMixin:
session_week = None
@lazyproperty
def session_week(self):
week_ago = timezone.now() - timezone.timedelta(weeks=1)
session_week = Session.objects.filter(date_start__gt=week_ago)
return session_week
def get_week_login_times_top5_users(self):
users = self.session_week.values_list('user', flat=True)
users = [
{'user': user, 'total': total}
for user, total in Counter(users).most_common(5)
]
return users
def get_week_total_count_login_users(self):
return len(set(self.session_week.values_list('user', flat=True)))
def get_week_total_count_login_times(self):
return self.session_week.count()
def get_week_login_times_top10_assets(self):
assets = self.session_week.values("asset")\
.annotate(total=Count("asset"))\
.annotate(last=Max("date_start")).order_by("-total")[:10]
for asset in assets:
asset['last'] = str(asset['last'])
return list(assets)
def get_week_login_times_top10_users(self):
users = self.session_week.values("user") \
.annotate(total=Count("user")) \
.annotate(last=Max("date_start")).order_by("-total")[:10]
for user in users:
user['last'] = str(user['last'])
return list(users)
def get_week_login_record_top10_sessions(self):
sessions = self.session_week.order_by('-date_start')[:10]
for session in sessions:
session.avatar_url = User.get_avatar_url("")
sessions = [
{
'user': session.user,
'asset': session.asset,
'is_finished': session.is_finished,
'date_start': str(session.date_start),
'timesince': timesince(session.date_start)
}
for session in sessions
]
return sessions
class TotalCountMixin:
@staticmethod
def get_total_count_users():
return current_org.get_org_members().count()
@staticmethod
def get_total_count_assets():
return Asset.objects.all().count()
@staticmethod
def get_total_count_online_users():
count = len(set(Session.objects.filter(is_finished=False).values_list('user', flat=True)))
return count
@staticmethod
def get_total_count_online_sessions():
return Session.objects.filter(is_finished=False).count()
class IndexApi(TotalCountMixin, WeekSessionMetricMixin, MonthLoginMetricMixin, APIView):
permission_classes = (IsOrgAdmin,)
http_method_names = ['get']
def get(self, request, *args, **kwargs):
data = {}
query_params = self.request.query_params
_all = query_params.get('all')
if _all or query_params.get('total_count'):
data.update({
'total_count_assets': self.get_total_count_assets(),
'total_count_users': self.get_total_count_users(),
'total_count_online_users': self.get_total_count_online_users(),
'total_count_online_sessions': self.get_total_count_online_sessions(),
})
if _all or query_params.get('month_metrics'):
data.update({
'month_metrics_date': self.get_month_metrics_date(),
'month_metrics_total_count_login': self.get_month_metrics_total_count_login(),
'month_metrics_total_count_active_users': self.get_month_metrics_total_count_active_users(),
'month_metrics_total_count_active_assets': self.get_month_metrics_total_count_active_assets(),
})
if _all or query_params.get('month_total_count_users'):
data.update({
'month_total_count_active_users': self.month_total_count_active_users,
'month_total_count_inactive_users': self.month_total_count_inactive_users,
'month_total_count_disabled_users': self.month_total_count_disabled_users,
})
if _all or query_params.get('month_total_count_assets'):
data.update({
'month_total_count_active_assets': self.month_total_count_active_assets,
'month_total_count_inactive_assets': self.month_total_count_inactive_assets,
'month_total_count_disabled_assets': self.month_total_count_disabled_assets,
})
if _all or query_params.get('week_total_count'):
data.update({
'week_total_count_login_users': self.get_week_total_count_login_users(),
'week_total_count_login_times': self.get_week_total_count_login_times(),
})
if _all or query_params.get('week_login_times_top5_users'):
data.update({
'week_login_times_top5_users': self.get_week_login_times_top5_users(),
})
if _all or query_params.get('week_login_times_top10_assets'):
data.update({
'week_login_times_top10_assets': self.get_week_login_times_top10_assets(),
})
if _all or query_params.get('week_login_times_top10_users'):
data.update({
'week_login_times_top10_users': self.get_week_login_times_top10_users(),
})
if _all or query_params.get('week_login_record_top10_sessions'):
data.update({
'week_login_record_top10_sessions': self.get_week_login_record_top10_sessions()
})
return JsonResponse(data, status=200)

View File

@ -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,9 +16,12 @@ 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)
XPACK_DIR = os.path.join(BASE_DIR, 'xpack')
HAS_XPACK = os.path.isdir(XPACK_DIR)
def import_string(dotted_path): def import_string(dotted_path):
@ -36,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
@ -134,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,
'AUTH_OPENID_CLIENT_ID': 'client-id',
'AUTH_OPENID_CLIENT_SECRET': 'client-secret',
'AUTH_OPENID_SHARE_SESSION': True,
'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True,
# OpenID 新配置参数 (version >= 1.5.8)
'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))
'BASE_SITE_URL': 'http://localhost:8080', 'BASE_SITE_URL': 'http://localhost:8080',
'AUTH_OPENID_SERVER_URL': 'http://openid', 'AUTH_OPENID_SERVER_URL': 'http://openid',
'AUTH_OPENID_REALM_NAME': 'jumpserver', 'AUTH_OPENID_REALM_NAME': None,
'AUTH_OPENID_CLIENT_ID': 'jumpserver',
'AUTH_OPENID_CLIENT_SECRET': '',
'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True,
'AUTH_OPENID_SHARE_SESSION': True,
'AUTH_RADIUS': False, 'AUTH_RADIUS': False,
'RADIUS_SERVER': 'localhost', 'RADIUS_SERVER': 'localhost',
@ -190,7 +244,7 @@ class Config(dict):
'TASK_LOG_KEEP_DAYS': 10, 'TASK_LOG_KEEP_DAYS': 10,
'ASSETS_PERM_CACHE_TIME': 3600 * 24, 'ASSETS_PERM_CACHE_TIME': 3600 * 24,
'SECURITY_MFA_VERIFY_TTL': 3600, 'SECURITY_MFA_VERIFY_TTL': 3600,
'ASSETS_PERM_CACHE_ENABLE': False, 'ASSETS_PERM_CACHE_ENABLE': HAS_XPACK,
'SYSLOG_ADDR': '', # '192.168.0.1:514' 'SYSLOG_ADDR': '', # '192.168.0.1:514'
'SYSLOG_FACILITY': 'user', 'SYSLOG_FACILITY': 'user',
'SYSLOG_SOCKTYPE': 2, 'SYSLOG_SOCKTYPE': 2,
@ -207,6 +261,88 @@ class Config(dict):
'TIME_ZONE': 'Asia/Shanghai' 'TIME_ZONE': 'Asia/Shanghai'
} }
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`做兼容 (例如Truetrue1 => 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 +419,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 +431,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 +613,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 +625,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)

View File

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

View File

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

View File

@ -7,9 +7,10 @@ from django.conf.urls.static import static
from django.conf.urls.i18n import i18n_patterns from django.conf.urls.i18n import i18n_patterns
from django.views.i18n import JavaScriptCatalog from django.views.i18n import JavaScriptCatalog
from . import views from . import views, api
api_v1 = [ api_v1 = [
path('index/', api.IndexApi.as_view()),
path('users/', include('users.urls.api_urls', namespace='api-users')), path('users/', include('users.urls.api_urls', namespace='api-users')),
path('assets/', include('assets.urls.api_urls', namespace='api-assets')), path('assets/', include('assets.urls.api_urls', namespace='api-assets')),
path('perms/', include('perms.urls.api_urls', namespace='api-perms')), path('perms/', include('perms.urls.api_urls', namespace='api-perms')),

View File

@ -18,4 +18,3 @@ def get_current_request():
current_request = LocalProxy(partial(_find, 'current_request')) current_request = LocalProxy(partial(_find, 'current_request'))

View File

@ -1,224 +1,12 @@
from django.core.cache import cache
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db.models import Count, Max
from django.shortcuts import redirect from django.shortcuts import redirect
from users.models import User
from assets.models import Asset
from terminal.models import Session
from orgs.utils import current_org
from common.permissions import PermissionsMixin, IsValidUser from common.permissions import PermissionsMixin, IsValidUser
from common.utils import timeit, lazyproperty
__all__ = ['IndexView'] __all__ = ['IndexView']
class MonthLoginMetricMixin: class IndexView(PermissionsMixin, TemplateView):
@lazyproperty
def session_month(self):
month_ago = timezone.now() - timezone.timedelta(days=30)
session_month = Session.objects.filter(date_start__gt=month_ago)
return session_month
@lazyproperty
def session_month_dates(self):
dates = self.session_month.dates('date_start', 'day')
return dates
def get_month_day_metrics(self):
month_str = [
d.strftime('%m-%d') for d in self.session_month_dates
] or ['0']
return month_str
@staticmethod
def get_cache_key(date, tp):
date_str = date.strftime("%Y%m%d")
key = "SESSION_MONTH_{}_{}".format(tp, date_str)
return key
def __get_data_from_cache(self, date, tp):
if date == timezone.now().date():
return None
cache_key = self.get_cache_key(date, tp)
count = cache.get(cache_key)
return count
def __set_data_to_cache(self, date, tp, count):
cache_key = self.get_cache_key(date, tp)
cache.set(cache_key, count, 3600*24*7)
@lazyproperty
def user_disabled_total(self):
return current_org.get_org_members().filter(is_active=False).count()
@lazyproperty
def asset_disabled_total(self):
return Asset.objects.filter(is_active=False).count()
@staticmethod
def get_date_start_2_end(d):
time_min = timezone.datetime.min.time()
time_max = timezone.datetime.max.time()
tz = timezone.get_current_timezone()
ds = timezone.datetime.combine(d, time_min).replace(tzinfo=tz)
de = timezone.datetime.combine(d, time_max).replace(tzinfo=tz)
return ds, de
def get_date_login_count(self, date):
tp = "LOGIN"
count = self.__get_data_from_cache(date, tp)
if count is not None:
return count
ds, de = self.get_date_start_2_end(date)
count = Session.objects.filter(date_start__range=(ds, de)).count()
self.__set_data_to_cache(date, tp, count)
return count
def get_month_login_metrics(self):
data = []
for d in self.session_month_dates:
count = self.get_date_login_count(d)
data.append(count)
if len(data) == 0:
data = [0]
return data
def get_date_user_count(self, date):
tp = "USER"
count = self.__get_data_from_cache(date, tp)
if count is not None:
return count
ds, de = self.get_date_start_2_end(date)
count = Session.objects.filter(date_start__range=(ds, de))\
.values('user').distinct().count()
self.__set_data_to_cache(date, tp, count)
return count
def get_month_active_user_metrics(self):
data = []
for d in self.session_month_dates:
count = self.get_date_user_count(d)
data.append(count)
return data
def get_date_asset_count(self, date):
tp = "ASSET"
count = self.__get_data_from_cache(date, tp)
if count is not None:
return count
ds, de = self.get_date_start_2_end(date)
count = Session.objects.filter(date_start__range=(ds, de)) \
.values('asset').distinct().count()
self.__set_data_to_cache(date, tp, count)
return count
def get_month_active_asset_metrics(self):
data = []
for d in self.session_month_dates:
count = self.get_date_asset_count(d)
data.append(count)
return data
@lazyproperty
def month_active_user_total(self):
count = self.session_month.values('user').distinct().count()
return count
@lazyproperty
def month_inactive_user_total(self):
total = current_org.get_org_members().count()
active = self.month_active_user_total
count = total - active
if count < 0:
count = 0
return count
@lazyproperty
def month_active_asset_total(self):
return self.session_month.values('asset').distinct().count()
@lazyproperty
def month_inactive_asset_total(self):
total = Asset.objects.all().count()
active = self.month_active_asset_total
count = total - active
if count < 0:
count = 0
return count
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'month_str': self.get_month_day_metrics(),
'month_total_visit_count': self.get_month_login_metrics(),
'month_user': self.get_month_active_user_metrics(),
'mouth_asset': self.get_month_active_asset_metrics(),
'month_user_active': self.month_active_user_total,
'month_user_inactive': self.month_inactive_user_total,
'month_user_disabled': self.user_disabled_total,
'month_asset_active': self.month_active_asset_total,
'month_asset_inactive': self.month_inactive_asset_total,
'month_asset_disabled': self.asset_disabled_total,
})
return context
class WeekSessionMetricMixin:
session_week = None
@lazyproperty
def session_week(self):
week_ago = timezone.now() - timezone.timedelta(weeks=1)
session_week = Session.objects.filter(date_start__gt=week_ago)
return session_week
def get_top5_user_a_week(self):
users = self.session_week.values('user') \
.annotate(total=Count('user')) \
.order_by('-total')[:5]
return users
def get_week_login_user_count(self):
return self.session_week.values('user').distinct().count()
def get_week_login_asset_count(self):
return self.session_week.count()
def get_week_top10_assets(self):
assets = self.session_week.values("asset")\
.annotate(total=Count("asset"))\
.annotate(last=Max("date_start")).order_by("-total")[:10]
return assets
def get_week_top10_users(self):
users = self.session_week.values("user") \
.annotate(total=Count("user")) \
.annotate(last=Max("date_start")).order_by("-total")[:10]
return users
def get_last10_sessions(self):
sessions = self.session_week.order_by('-date_start')[:10]
for session in sessions:
session.avatar_url = User.get_avatar_url("")
return sessions
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'user_visit_count_weekly': self.get_week_login_user_count(),
'asset_visit_count_weekly': self.get_week_login_asset_count(),
'user_visit_count_top_five': self.get_top5_user_a_week(),
'last_login_ten': self.get_last10_sessions(),
'week_asset_hot_ten': self.get_week_top10_assets(),
'week_user_hot_ten': self.get_week_top10_users(),
})
return context
class IndexView(PermissionsMixin, MonthLoginMetricMixin, WeekSessionMetricMixin, TemplateView):
template_name = 'index.html' template_name = 'index.html'
permission_classes = [IsValidUser] permission_classes = [IsValidUser]
@ -229,31 +17,9 @@ class IndexView(PermissionsMixin, MonthLoginMetricMixin, WeekSessionMetricMixin,
return redirect('assets:user-asset-list') return redirect('assets:user-asset-list')
return super(IndexView, self).dispatch(request, *args, **kwargs) return super(IndexView, self).dispatch(request, *args, **kwargs)
@staticmethod
def get_user_count():
return current_org.get_org_members().count()
@staticmethod
def get_asset_count():
return Asset.objects.all().count()
@staticmethod
def get_online_user_count():
count = Session.objects.filter(is_finished=False)\
.values_list('user', flat=True).distinct().count()
return count
@staticmethod
def get_online_session_count():
return Session.objects.filter(is_finished=False).count()
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context.update({ context.update({
'assets_count': self.get_asset_count(),
'users_count': self.get_user_count(),
'online_user_count': self.get_online_user_count(),
'online_asset_count': self.get_online_session_count(),
'app': _("Dashboard"), 'app': _("Dashboard"),
}) })
return context return context

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,16 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from rest_framework import status from django.shortcuts import get_object_or_404
from rest_framework import status, generics
from rest_framework.views import Response from rest_framework.views import Response
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
from common.permissions import IsSuperUserOrAppUser from common.permissions import IsSuperUserOrAppUser
from .models import Organization from .models import Organization
from .serializers import OrgSerializer, OrgReadSerializer, \ from .serializers import OrgSerializer, OrgReadSerializer, \
OrgMembershipUserSerializer, OrgMembershipAdminSerializer OrgMembershipUserSerializer, OrgMembershipAdminSerializer, \
OrgAllUserSerializer
from users.models import User, UserGroup from users.models import User, UserGroup
from assets.models import Asset, Domain, AdminUser, SystemUser, Label from assets.models import Asset, Domain, AdminUser, SystemUser, Label
from perms.models import AssetPermission from perms.models import AssetPermission
@ -67,3 +69,15 @@ class OrgMembershipUsersViewSet(OrgMembershipModelViewSetMixin, BulkModelViewSet
membership_class = Organization.users.through membership_class = Organization.users.through
permission_classes = (IsSuperUserOrAppUser, ) permission_classes = (IsSuperUserOrAppUser, )
class OrgAllUserListApi(generics.ListAPIView):
permission_classes = (IsSuperUserOrAppUser,)
serializer_class = OrgAllUserSerializer
filter_fields = ("username", "name")
search_fields = filter_fields
def get_queryset(self):
pk = self.kwargs.get("pk")
org = get_object_or_404(Organization, pk=pk)
users = org.get_org_users().only(*self.serializer_class.Meta.only_fields)
return users

View File

@ -80,3 +80,15 @@ class OrgMembershipUserSerializer(OrgMembershipSerializerMixin, ModelSerializer)
model = Organization.users.through model = Organization.users.through
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
fields = '__all__' fields = '__all__'
class OrgAllUserSerializer(serializers.Serializer):
user = serializers.UUIDField(read_only=True, source='id')
user_display = serializers.SerializerMethodField()
class Meta:
only_fields = ['id', 'username', 'name']
@staticmethod
def get_user_display(obj):
return str(obj)

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.urls import re_path from django.urls import re_path, path
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from common import api as capi from common import api as capi
@ -24,6 +24,7 @@ old_version_urlpatterns = [
] ]
urlpatterns = [ urlpatterns = [
path('<uuid:pk>/users/all/', api.OrgAllUserListApi.as_view(), name='org-all-users'),
] ]
urlpatterns += router.urls + old_version_urlpatterns urlpatterns += router.urls + old_version_urlpatterns

View File

@ -1,14 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf import settings
from django.apps import AppConfig from django.apps import AppConfig
class PermsConfig(AppConfig): class PermsConfig(AppConfig):
name = 'perms' name = 'perms'
def ready(self):
from . import signals_handler
if not settings.XPACK_ENABLED:
settings.ASSETS_PERM_CACHE_ENABLE = False
return super().ready()

View File

@ -302,7 +302,6 @@ class AssetPermissionUtil(AssetPermissionUtilCacheMixin):
continue continue
ancestors = self.full_tree.ancestors( ancestors = self.full_tree.ancestors(
child.identifier, with_self=False, deep=True, child.identifier, with_self=False, deep=True,
with_assets=False,
) )
# print("Get ancestors: {}".format(len(ancestors))) # print("Get ancestors: {}".format(len(ancestors)))
if not ancestors: if not ancestors:

View File

@ -11,7 +11,7 @@
<h5>{% trans 'Total users' %}</h5> <h5>{% trans 'Total users' %}</h5>
</div> </div>
<div class="ibox-content"> <div class="ibox-content">
<h1 class="no-margins"><a href="{% url 'users:user-list' %}">{{ users_count }}</a></h1> <h1 class="no-margins"><a href="{% url 'users:user-list' %}"><span id="total_count_users"></span></a></h1>
<small>All users</small> <small>All users</small>
</div> </div>
</div> </div>
@ -19,12 +19,12 @@
<div class="col-sm-3"> <div class="col-sm-3">
<div class="ibox float-e-margins"> <div class="ibox float-e-margins">
<div class="ibox-title"> <div class="ibox-title">
<span class="label label-info pull-right">Hosts</span> <span class="label label-info pull-right">Assets</span>
<h5>{% trans 'Total hosts' %}</h5> <h5>{% trans 'Total assets' %}</h5>
</div> </div>
<div class="ibox-content"> <div class="ibox-content">
<h1 class="no-margins"><a href="{% url 'assets:asset-list' %}">{{ assets_count }}</a></h1> <h1 class="no-margins"><a href="{% url 'assets:asset-list' %}"><span id="total_count_assets"></span></a></h1>
<small>All hosts</small> <small>All assets</small>
</div> </div>
</div> </div>
</div> </div>
@ -36,7 +36,7 @@
<h5>{% trans 'Online users' %}</h5> <h5>{% trans 'Online users' %}</h5>
</div> </div>
<div class="ibox-content"> <div class="ibox-content">
<h1 class="no-margins"><a href="{% url 'terminal:session-online-list' %}"> <span id="online_users"></span>{{ online_user_count }}</a></h1> <h1 class="no-margins"><a href="{% url 'terminal:session-online-list' %}"> <span id="total_count_online_users"></span></a></h1>
<small>Online users</small> <small>Online users</small>
</div> </div>
</div> </div>
@ -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="online_hosts"></span>{{ online_asset_count }}</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>
@ -58,19 +58,11 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-2 border-bottom white-bg dashboard-header" style="margin-left:15px;height: 346px"> <div class="col-sm-2 border-bottom white-bg dashboard-header" style="margin-left:15px;height: 346px">
<small>{% trans 'In the past week, a total of ' %}<span class="text-info">{{ user_visit_count_weekly }}</span>{% trans ' users have logged in ' %}<span class="text-success">{{ asset_visit_count_weekly }}</span>{% trans ' times asset.' %}</small> <small>{% trans 'In the past week, a total of ' %}<span class="text-info" id="week_total_count_login_users"></span>{% trans ' users have logged in ' %}<span class="text-success" id="week_total_count_login_times"></span>{% trans ' times asset.' %}</small>
<ul class="list-group clear-list m-t"> <ul class="list-group clear-list m-t" id="week_login_times_top5_users">
{% for data in user_visit_count_top_five %}
<li class="list-group-item fist-item">
<span class="pull-right">
{{ data.total }}{% trans ' times/week' %}
</span>
<span class="label ">{{ forloop.counter }}</span> {{ data.user }}
</li>
{% endfor %}
</ul> </ul>
</div> </div>
<div class="col-sm-7" id="top10" style="margin-left: -15px;height: 346px;padding: 15px 0 15px 0;"></div> <div class="col-sm-7" id="month_metrics_echarts" style="margin-left: -15px;height: 346px;padding: 15px 0 15px 0;"></div>
<div class="col-sm-3 white-bg" id="top1" style="margin-left: -15px;height: 346px"> <div class="col-sm-3 white-bg" id="top1" style="margin-left: -15px;height: 346px">
<div class="statistic-box"> <div class="statistic-box">
<h4> <h4>
@ -81,13 +73,13 @@
</p> </p>
<div class="row text-center"> <div class="row text-center">
<div class="col-sm-6"> <div class="col-sm-6">
<div id="activeUser" style="width: 140px; height: 140px;"> <div id="month_total_count_users_pie" style="width: 140px; height: 140px;">
</div> </div>
<h5>{% trans 'User' %}</h5> <h5>{% trans 'User' %}</h5>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div id="activeAsset" style="width: 140px; height: 140px;"></div> <div id="month_total_count_assets_pie" style="width: 140px; height: 140px;"></div>
<h5>{% trans 'Host' %}</h5> <h5>{% trans 'Asset' %}</h5>
</div> </div>
</div> </div>
<div class="m-t"> <div class="m-t">
@ -120,27 +112,7 @@
<h3><i class="fa fa-inbox"></i>{% trans 'Top 10 assets in a week'%}</h3> <h3><i class="fa fa-inbox"></i>{% trans 'Top 10 assets in a week'%}</h3>
<small><i class="fa fa-map-marker"></i>{% trans 'Login frequency and last login record.' %}</small> <small><i class="fa fa-map-marker"></i>{% trans 'Login frequency and last login record.' %}</small>
</div> </div>
<div class="ibox-content inspinia-timeline"> <div class="ibox-content inspinia-timeline" id="week_login_times_top10_assets">
{% if week_asset_hot_ten %}
{% for data in week_asset_hot_ten %}
<div class="timeline-item">
<div class="row">
<div class="col-xs-5 date ellipsis">
<i class="fa fa-info-circle"></i>
<strong data-toggle="tooltip" title="{{ data.asset }}">{{ data.asset }}</strong>
<br/>
<small class="text-navy">{{ data.total }}{% trans ' times' %}</small>
</div>
<div class="col-xs-7 content no-top-border">
<p class="m-b-xs">{% trans 'The time last logged in' %}</p>
<p>{% trans 'At' %} {{ data.last|date:"Y-m-d H:i:s" }}</p>
</div>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-center">{% trans '(No)' %}</p>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -158,27 +130,7 @@
</div> </div>
<div class="ibox-content"> <div class="ibox-content">
<div> <div>
<div class="feed-activity-list"> <div class="feed-activity-list" id="week_login_record_top10_sessions">
{% if last_login_ten %}
{% for login in last_login_ten %}
<div class="feed-element">
<a href="#" class="pull-left">
<img alt="image" class="img-circle" src="{% static 'img/avatar/user.png' %}">
</a>
<div class="media-body ">
{% ifequal login.is_finished 0 %}
<small class="pull-right text-navy">{{ login.date_start|timesince }} {% trans 'Before' %}</small>
{% else %}
<small class="pull-right">{{ login.date_start|timesince }} {% trans 'Before' %}</small>
{% endifequal %}
<strong>{{ login.user }}</strong> {% trans 'Login in ' %}{{ login.asset }} <br>
<small class="text-muted">{{ login.date_start }}</small>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-center">{% trans '(No)' %}</p>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -206,27 +158,7 @@
<h3><i class="fa fa-user"></i>{% trans 'Top 10 users in a week' %}</h3> <h3><i class="fa fa-user"></i>{% trans 'Top 10 users in a week' %}</h3>
<small><i class="fa fa-map-marker"></i>{% trans 'User login frequency and last login record.' %}</small> <small><i class="fa fa-map-marker"></i>{% trans 'User login frequency and last login record.' %}</small>
</div> </div>
<div class="ibox-content inspinia-timeline"> <div class="ibox-content inspinia-timeline" id="week_login_times_top10_users">
{% if week_user_hot_ten %}
{% for data in week_user_hot_ten %}
<div class="timeline-item">
<div class="row">
<div class="col-xs-5 date ellipsis">
<i class="fa fa-info-circle"></i>
<strong data-toggle="tooltip" title="{{ data.user }}">{{ data.user }}</strong>
<br/>
<small class="text-navy">{{ data.total }}{% trans ' times' %}</small>
</div>
<div class="col-xs-7 content no-top-border">
<p class="m-b-xs">{% trans 'The time last logged in' %}</p>
<p>{% trans 'At' %} {{ data.last|date:"Y-m-d H:i:s" }}</p>
</div>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-center">{% trans '(No)' %}</p>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -238,27 +170,15 @@
{% block custom_foot_js %} {% block custom_foot_js %}
<script src="{% static 'js/plugins/echarts/echarts.js' %}"></script> <script src="{% static 'js/plugins/echarts/echarts.js' %}"></script>
<script> <script>
$(document).ready(function(){
$('#show').click(function(){ function requireMonthMetricsECharts(data){
$('#show').css('display', 'none');
$('#more').css('display', 'block');
});
$("[data-toggle='tooltip']").tooltip();
});
require.config({
paths: {
'echarts': '/static/js/plugins/echarts/chart/',
'echarts/chart/line': '/static/js/plugins/echarts/chart/line',
'echarts/chart/pie': '/static/js/plugins/echarts/chart/pie'
}
});
require( require(
[ [
'echarts', 'echarts',
'echarts/chart/line' 'echarts/chart/line'
], ],
function (ec) { function (ec) {
var top10Chart = ec.init(document.getElementById('top10')); var monthMetricsECharts = ec.init(document.getElementById('month_metrics_echarts'));
var option = { var option = {
title : { title : {
text: "{% trans 'Monthly data overview' %}", text: "{% trans 'Monthly data overview' %}",
@ -284,7 +204,7 @@ $(document).ready(function(){
{ {
type : 'category', type : 'category',
boundaryGap : false, boundaryGap : false,
data : {{ month_str|safe}} data : data['month_metrics_date'],
} }
], ],
yAxis : [ yAxis : [
@ -298,38 +218,42 @@ $(document).ready(function(){
type:'line', type:'line',
smooth: true, smooth: true,
itemStyle: {normal: {areaStyle: {type: 'default'}}}, itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: {{ month_total_visit_count|safe}} data: data['month_metrics_total_count_login']
}, },
{ {
name: "{% trans 'Active users' %}", name: "{% trans 'Active users' %}",
type: 'line', type: 'line',
smooth: true, smooth: true,
itemStyle: {normal: {areaStyle: {type: 'default'}}}, itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: {{ month_user|safe }} data: data['month_metrics_total_count_active_users']
}, },
{ {
name:"{% trans 'Active assets' %}", name:"{% trans 'Active assets' %}",
type:'line', type:'line',
smooth:true, smooth:true,
itemStyle: {normal: {areaStyle: {type: 'default'}}}, itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: {{ mouth_asset|safe }} data: data['month_metrics_total_count_active_assets']
} }
] ]
}; };
top10Chart.setOption(option); monthMetricsECharts.setOption(option);
} }
); );
}
function requireMonthTotalCountUsersPie(data){
require( require(
[ [
'echarts', 'echarts',
'echarts/chart/pie' 'echarts/chart/pie'
], ],
function (ec) { function (ec) {
var auChart = ec.init(document.getElementById('activeUser')); var monthTotalCountUsersPie = ec.init(document.getElementById('month_total_count_users_pie'));
var option = { var option = {
tooltip : { tooltip : {
trigger: 'item', trigger: 'item',
formatter: "{b} <br> {c} ({d}%)" formatter: "{b} <br> {c} <br> ({d}%)"
}, },
legend: { legend: {
show: false, show: false,
@ -364,6 +288,7 @@ $(document).ready(function(){
name:"{% trans 'Access to the source' %}", name:"{% trans 'Access to the source' %}",
type:'pie', type:'pie',
radius : ['50%', '70%'], radius : ['50%', '70%'],
avoidLabelOverlap: false,
itemStyle : { itemStyle : {
normal : { normal : {
label : { label : {
@ -385,33 +310,36 @@ $(document).ready(function(){
} }
}, },
data:[ data:[
{value:{{ month_user_active }}, name:"{% trans 'Monthly active users' %}"}, {value:data['month_total_count_active_users'], name:"{% trans 'Monthly active users' %}"},
{value:{{ month_user_disabled }}, name:"{% trans 'Disable user' %}"}, {value:data['month_total_count_disabled_users'], name:"{% trans 'Disable user' %}"},
{value:{{ month_user_inactive }}, name:"{% trans 'Month not logged in user' %}"} {value:data['month_total_count_inactive_users'], name:"{% trans 'Month not logged in user' %}"}
] ]
} }
] ]
}; };
auChart.setOption(option); monthTotalCountUsersPie.setOption(option);
} }
); );
}
function requireMonthTotalCountAssetsPie(data){
require( require(
[ [
'echarts', 'echarts',
'echarts/chart/pie' 'echarts/chart/pie'
], ],
function (ec) { function (ec) {
var aaChart = ec.init(document.getElementById('activeAsset')); var monthTotalCountAssetsPie = ec.init(document.getElementById('month_total_count_assets_pie'));
var option = { var option = {
tooltip : { tooltip : {
trigger: 'item', trigger: 'item',
formatter: "{b} <br> {c} ({d}%)" formatter: "{b} <br> {c} <br> ({d}%)"
}, },
legend: { legend: {
show: false, show: false,
orient : 'vertical', orient : 'vertical',
x : 'left', x : 'left',
data:["{% trans 'Month is logged into the host' %}", "{% trans 'Disable host' %}", "{% trans 'Month not logged on host' %}"] data:["{% trans 'Month is logged into the asset' %}", "{% trans 'Disable host' %}", "{% trans 'Month not logged on host' %}"]
}, },
toolbox: { toolbox: {
show : false, show : false,
@ -461,16 +389,230 @@ $(document).ready(function(){
} }
}, },
data:[ data:[
{value:{{ month_asset_active }}, name:"{% trans 'Month is logged into the host' %}"}, {value:data['month_total_count_active_assets'], name:"{% trans 'Month is logged into the host' %}"},
{value:{{ month_asset_disabled }}, name:"{% trans 'Disable host' %}"}, {value:data['month_total_count_disabled_assets'], name:"{% trans 'Disable host' %}"},
{value:{{ month_asset_inactive }}, name:"{% trans 'Month not logged on host' %}"} {value:data['month_total_count_inactive_assets'], name:"{% trans 'Month not logged on host' %}"}
] ]
} }
] ]
}; };
aaChart.setOption(option); monthTotalCountAssetsPie.setOption(option);
} }
); );
}
var indexUrl = "/api/v1/index/";
function renderRequestApi(query, success, error){
var url = indexUrl + "?" + query;
if (!error){
error = function (){console.log("Request url error: " + url)}
}
requestApi({
url: url,
method: "GET",
success: success,
error: error,
flash_message: false,
})
}
function renderTotalCount(){
var success = function (data) {
$('#total_count_assets').html(data['total_count_assets']);
$('#total_count_users').html(data['total_count_users']);
$('#total_count_online_users').html(data['total_count_online_users']);
$('#total_count_online_sessions').html(data['total_count_online_sessions']);
};
renderRequestApi('total_count=1', success);
}
function renderMonthMetricsECharts(){
var success = function (data) {
requireMonthMetricsECharts(data)
};
renderRequestApi('month_metrics=1', success)
}
function renderMonthTotalCountUsersPie(){
var success = function (data) {
requireMonthTotalCountUsersPie(data)
};
renderRequestApi('month_total_count_users=1', success)
}
function renderMonthTotalCountAssetsPie(){
var success = function (data) {
requireMonthTotalCountAssetsPie(data)
};
renderRequestApi('month_total_count_assets=1', success)
}
function renderWeekTotalCount(){
var success = function (data) {
$('#week_total_count_login_users').html(data['week_total_count_login_users']);
$('#week_total_count_login_times').html(data['week_total_count_login_times'])
};
renderRequestApi('week_total_count=1', success)
}
function renderWeekLoginTimesTop5Users(){
var success = function (data){
var html = "";
var html_cell = "" +
"<li class=\"list-group-item fist-item\">" +
"<span class=\"pull-right\">" +
"{TOTAL} {% trans ' times/week' %}" +
"</span>" +
"<span class=\"label \">{INDEX}</span> {USER}" +
"</li>";
$.each(data['week_login_times_top5_users'], function(index, value){
html += html_cell.replace('{TOTAL}', value['total'])
.replace('{USER}', value['user'])
.replace('{INDEX}', index+1)
});
$('#week_login_times_top5_users').html(html)
};
renderRequestApi('week_login_times_top5_users=1', success)
}
function renderWeekLoginTimesTop10Assets(){
var success = function (data){
var html = "";
var html_cell = "" +
"<div class=\"timeline-item\">" +
"<div class=\"row\">" +
"<div class=\"col-xs-5 date ellipsis\">" +
"<i class=\"fa fa-info-circle\"></i>" +
"<strong data-toggle=\"tooltip\" title=\"{ASSET}\">{ASSET}</strong>" +
"<br/>" +
"<small class=\"text-navy\">{TOTAL}{% trans ' times' %}</small>" +
"</div>" +
"<div class=\"col-xs-7 content no-top-border\">" +
"<p class=\"m-b-xs\">{% trans 'The time last logged in' %}</p>" +
"<p>{% trans 'At' %} {DATE_LAST}</p>" +
"</div>" +
"</div>" +
"</div>";
var assets = data['week_login_times_top10_assets'];
if (assets.length !== 0){
$.each(assets, function(index, value){
html += html_cell
.replaceAll('{ASSET}', value['asset'])
.replace('{TOTAL}', value['total'])
.replace('{DATE_LAST}', toSafeLocalDateStr(value['last']))
});
}
else{
html += "<p class=\"text-center\">{% trans '(No)' %}</p>"
}
$('#week_login_times_top10_assets').html(html)
};
renderRequestApi('week_login_times_top10_assets=1', success)
}
function renderWeekLoginTimesTop10Users(){
var success = function (data){
var html = "";
var html_cell = "" +
"<div class=\"timeline-item\">" +
"<div class=\"row\">" +
"<div class=\"col-xs-5 date ellipsis\">" +
"<i class=\"fa fa-info-circle\"></i>" +
"<strong data-toggle=\"tooltip\" title=\"{USER}\">{USER}</strong>" +
"<br/>" +
"<small class=\"text-navy\">{TOTAL}{% trans ' times' %}</small>" +
"</div>" +
"<div class=\"col-xs-7 content no-top-border\">" +
"<p class=\"m-b-xs\">{% trans 'The time last logged in' %}</p>" +
"<p>{% trans 'At' %} {DATE_LAST}</p>" +
"</div>" +
"</div>" +
"</div>";
var users = data['week_login_times_top10_users'];
if (users.length !== 0){
$.each(users, function(index, value){
html += html_cell.replaceAll('{USER}', value['user'])
.replace('{TOTAL}', value['total'])
.replace('{DATE_LAST}', toSafeLocalDateStr(value['last']))
});
}
else{
html += "<p class=\"text-center\">{% trans '(No)' %}</p>"
}
$('#week_login_times_top10_users').html(html)
};
renderRequestApi('week_login_times_top10_users=1', success)
}
function renderWeekLoginRecordTop10Sessions(){
var success = function (data){
var html = "";
var html_cell = "" +
"<div class=\"feed-element\">" +
"<a href=\"#\" class=\"pull-left\">" +
"<img alt=\"image\" class=\"img-circle\" src=\"{% static 'img/avatar/user.png' %}\">" +
"</a>" +
"<div class=\"media-body \">" +
"<small class=\"pull-right {TEXT_NAVY}\">{TIMESINCE} {% trans 'Before' %}</small>" +
"<strong>{USER}</strong> {% trans 'Login in ' %}{ASSET} <br>" +
"<small class=\"text-muted\">{DATE_START}</small>" +
"</div>" +
"</div>";
var users = data['week_login_record_top10_sessions'];
if (users.length !== 0){
$.each(users, function(index, value){
console.log(value['is_finished'])
html += html_cell.replaceAll('{USER}', value['user'])
.replace('{ASSET}', value['asset'])
.replace('{DATE_START}', toSafeLocalDateStr(value['date_start']))
.replace('{TEXT_NAVY}', value['is_finished']?'':'text-navy')
.replace('{TIMESINCE}', value['timesince'])
});
}
else{
html += "<p class=\"text-center\">{% trans '(No)' %}</p>"
}
$('#week_login_record_top10_sessions').html(html)
};
renderRequestApi('week_login_record_top10_sessions=1', success)
}
function renderData(){
renderTotalCount();
renderMonthMetricsECharts();
renderMonthTotalCountUsersPie();
renderMonthTotalCountAssetsPie();
renderWeekTotalCount();
renderWeekLoginTimesTop5Users();
renderWeekLoginTimesTop10Assets();
renderWeekLoginRecordTop10Sessions();
renderWeekLoginTimesTop10Users();
}
require.config({
paths: {
'echarts': '/static/js/plugins/echarts/chart/',
'echarts/chart/line': '/static/js/plugins/echarts/chart/line',
'echarts/chart/pie': '/static/js/plugins/echarts/chart/pie'
}
});
$(document).ready(function(){
$('#show').click(function(){
$('#show').css('display', 'none');
$('#more').css('display', 'block');
});
$("[data-toggle='tooltip']").tooltip();
renderData()
});
</script> </script>
{% endblock %} {% endblock %}

View File

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

View File

@ -1,9 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .. import utils from .. import utils
from users.models import User
class UserQuerysetMixin: class UserQuerysetMixin:
def get_queryset(self): def get_queryset(self):
if self.request.query_params.get('all'):
queryset = User.objects.exclude(role=User.ROLE_APP)
else:
queryset = utils.get_current_org_members() queryset = utils.get_current_org_members()
return queryset return queryset

View File

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

View File

@ -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,27 @@ 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
# 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来认证

View File

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