mirror of https://github.com/jumpserver/jumpserver
feat: 用户在线session控制
parent
5cbbf9e737
commit
3abc8bddfa
|
@ -26,7 +26,10 @@ from terminal.models import default_storage
|
||||||
from users.models import User
|
from users.models import User
|
||||||
from .backends import TYPE_ENGINE_MAPPING
|
from .backends import TYPE_ENGINE_MAPPING
|
||||||
from .const import ActivityChoices
|
from .const import ActivityChoices
|
||||||
from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog, ActivityLog, JobLog
|
from .models import (
|
||||||
|
FTPLog, UserLoginLog, OperateLog, PasswordChangeLog,
|
||||||
|
ActivityLog, JobLog,
|
||||||
|
)
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
FTPLogSerializer, UserLoginLogSerializer, JobLogSerializer,
|
FTPLogSerializer, UserLoginLogSerializer, JobLogSerializer,
|
||||||
OperateLogSerializer, OperateLogActionDetailSerializer,
|
OperateLogSerializer, OperateLogActionDetailSerializer,
|
||||||
|
|
|
@ -1,17 +1,23 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
from datetime import timedelta
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import BACKEND_SESSION_KEY
|
from django.contrib.auth import BACKEND_SESSION_KEY
|
||||||
|
from django.core.cache import caches
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils import timezone, translation
|
from django.utils import timezone, translation
|
||||||
from django.utils.functional import LazyObject
|
from django.utils.functional import LazyObject
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
|
||||||
|
from audits.models import UserLoginLog
|
||||||
from authentication.signals import post_auth_failed, post_auth_success
|
from authentication.signals import post_auth_failed, post_auth_success
|
||||||
from authentication.utils import check_different_city_login_if_need
|
from authentication.utils import check_different_city_login_if_need
|
||||||
from common.utils import get_request_ip, get_logger
|
from common.utils import get_request_ip, get_logger
|
||||||
from users.models import User
|
from users.models import User, UserSession
|
||||||
|
from ..const import LoginTypeChoices
|
||||||
from ..utils import write_login_log
|
from ..utils import write_login_log
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
@ -75,6 +81,26 @@ def generate_data(username, request, login_type=None):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def create_user_session(session_key, user_id, instance: UserLoginLog):
|
||||||
|
session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore
|
||||||
|
session_store = session_store_cls(session_key=session_key)
|
||||||
|
cache_key = session_store.cache_key
|
||||||
|
ttl = caches[settings.SESSION_CACHE_ALIAS].ttl(cache_key)
|
||||||
|
|
||||||
|
online_session_data = {
|
||||||
|
'user_id': user_id,
|
||||||
|
'ip': instance.ip,
|
||||||
|
'key': session_key,
|
||||||
|
'city': instance.city,
|
||||||
|
'type': instance.type,
|
||||||
|
'backend': instance.backend,
|
||||||
|
'user_agent': instance.user_agent,
|
||||||
|
'date_created': instance.datetime,
|
||||||
|
'date_expired': instance.datetime + timedelta(seconds=ttl),
|
||||||
|
}
|
||||||
|
UserSession.objects.create(**online_session_data)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_auth_success)
|
@receiver(post_auth_success)
|
||||||
def on_user_auth_success(sender, user, request, login_type=None, **kwargs):
|
def on_user_auth_success(sender, user, request, login_type=None, **kwargs):
|
||||||
logger.debug('User login success: {}'.format(user.username))
|
logger.debug('User login success: {}'.format(user.username))
|
||||||
|
@ -84,7 +110,12 @@ def on_user_auth_success(sender, user, request, login_type=None, **kwargs):
|
||||||
)
|
)
|
||||||
request.session['login_time'] = data['datetime'].strftime("%Y-%m-%d %H:%M:%S")
|
request.session['login_time'] = data['datetime'].strftime("%Y-%m-%d %H:%M:%S")
|
||||||
data.update({'mfa': int(user.mfa_enabled), 'status': True})
|
data.update({'mfa': int(user.mfa_enabled), 'status': True})
|
||||||
write_login_log(**data)
|
instance = write_login_log(**data)
|
||||||
|
session_key = request.session.session_key
|
||||||
|
# TODO 目前只记录 web 登录的 session
|
||||||
|
if not session_key or instance.type != LoginTypeChoices.web:
|
||||||
|
return
|
||||||
|
create_user_session(session_key, user.id, instance)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_auth_failed)
|
@receiver(post_auth_failed)
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import copy
|
import copy
|
||||||
from itertools import chain
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from common.utils.timezone import as_current_tz
|
|
||||||
from common.utils import validate_ip, get_ip_city, get_logger
|
|
||||||
from common.db.fields import RelatedManager
|
from common.db.fields import RelatedManager
|
||||||
|
from common.utils import validate_ip, get_ip_city, get_logger
|
||||||
|
from common.utils.timezone import as_current_tz
|
||||||
from .const import DEFAULT_CITY
|
from .const import DEFAULT_CITY
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
@ -22,7 +22,7 @@ def write_login_log(*args, **kwargs):
|
||||||
else:
|
else:
|
||||||
city = get_ip_city(ip) or DEFAULT_CITY
|
city = get_ip_city(ip) or DEFAULT_CITY
|
||||||
kwargs.update({'ip': ip, 'city': city})
|
kwargs.update({'ip': ip, 'city': city})
|
||||||
UserLoginLog.objects.create(**kwargs)
|
return UserLoginLog.objects.create(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
def _get_instance_field_value(
|
def _get_instance_field_value(
|
||||||
|
|
|
@ -36,6 +36,7 @@ system_user_perms += (user_perms + _view_all_joined_org_perms)
|
||||||
|
|
||||||
_auditor_perms = (
|
_auditor_perms = (
|
||||||
('rbac', 'menupermission', 'view', 'audit'),
|
('rbac', 'menupermission', 'view', 'audit'),
|
||||||
|
('users', 'usersession', '*', '*'),
|
||||||
('audits', '*', '*', '*'),
|
('audits', '*', '*', '*'),
|
||||||
('audits', 'joblog', '*', '*'),
|
('audits', 'joblog', '*', '*'),
|
||||||
('terminal', 'commandstorage', 'view', 'commandstorage'),
|
('terminal', 'commandstorage', 'view', 'commandstorage'),
|
||||||
|
|
|
@ -28,6 +28,7 @@ exclude_permissions = (
|
||||||
('authentication', 'superconnectiontoken', 'change,delete', 'superconnectiontoken'),
|
('authentication', 'superconnectiontoken', 'change,delete', 'superconnectiontoken'),
|
||||||
('authentication', 'temptoken', 'delete', 'temptoken'),
|
('authentication', 'temptoken', 'delete', 'temptoken'),
|
||||||
('users', 'userpasswordhistory', '*', '*'),
|
('users', 'userpasswordhistory', '*', '*'),
|
||||||
|
('users', 'usersession', 'add,delete,change', 'usersession'),
|
||||||
('assets', 'adminuser', '*', '*'),
|
('assets', 'adminuser', '*', '*'),
|
||||||
('assets', 'assetgroup', '*', '*'),
|
('assets', 'assetgroup', '*', '*'),
|
||||||
('assets', 'cluster', '*', '*'),
|
('assets', 'cluster', '*', '*'),
|
||||||
|
|
|
@ -98,6 +98,7 @@ special_pid_mapper = {
|
||||||
'terminal.endpoint': 'terminal_node',
|
'terminal.endpoint': 'terminal_node',
|
||||||
'terminal.endpointrule': 'terminal_node',
|
'terminal.endpointrule': 'terminal_node',
|
||||||
'audits.ftplog': 'terminal',
|
'audits.ftplog': 'terminal',
|
||||||
|
'users.usersession': 'terminal',
|
||||||
'perms.view_myassets': 'my_assets',
|
'perms.view_myassets': 'my_assets',
|
||||||
'ops.celerytask': 'task_center',
|
'ops.celerytask': 'task_center',
|
||||||
'ops.view_celerytaskexecution': 'task_center',
|
'ops.view_celerytaskexecution': 'task_center',
|
||||||
|
|
|
@ -38,7 +38,7 @@ from users.models import User
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'SessionViewSet', 'SessionReplayViewSet',
|
'SessionViewSet', 'SessionReplayViewSet',
|
||||||
'SessionJoinValidateAPI', 'MySessionAPIView',
|
'SessionJoinValidateAPI', 'MySessionAPIView'
|
||||||
]
|
]
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from .command import *
|
from .command import *
|
||||||
from .session import *
|
|
||||||
from .replay import *
|
from .replay import *
|
||||||
|
from .session import *
|
||||||
from .sharing import *
|
from .sharing import *
|
||||||
|
|
|
@ -16,3 +16,4 @@ def on_session_finished(sender, instance: Session, created, **kwargs):
|
||||||
return
|
return
|
||||||
# 清理一次可能因 task 未执行的缓存数据
|
# 清理一次可能因 task 未执行的缓存数据
|
||||||
Session.unlock_session(instance.id)
|
Session.unlock_session(instance.id)
|
||||||
|
|
||||||
|
|
|
@ -6,4 +6,5 @@ from .preference import *
|
||||||
from .profile import *
|
from .profile import *
|
||||||
from .relation import *
|
from .relation import *
|
||||||
from .service import *
|
from .service import *
|
||||||
|
from .session import *
|
||||||
from .user import *
|
from .user import *
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from common.api import CommonApiMixin
|
||||||
|
from orgs.utils import current_org
|
||||||
|
from users import serializers
|
||||||
|
from ..models import UserSession
|
||||||
|
|
||||||
|
|
||||||
|
class UserSessionViewSet(CommonApiMixin, viewsets.ModelViewSet):
|
||||||
|
http_method_names = ('get', 'post', 'head', 'options', 'trace')
|
||||||
|
serializer_class = serializers.UserSessionSerializer
|
||||||
|
filterset_fields = ['id', 'ip', 'city', 'type']
|
||||||
|
search_fields = ['id', 'ip', 'city']
|
||||||
|
|
||||||
|
rbac_perms = {
|
||||||
|
'offline': ['users.offline_usersession']
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def org_user_ids(self):
|
||||||
|
user_ids = current_org.get_members().values_list('id', flat=True)
|
||||||
|
return user_ids
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = UserSession.objects.filter(date_expired__gt=timezone.now())
|
||||||
|
if current_org.is_root():
|
||||||
|
return queryset
|
||||||
|
user_ids = self.org_user_ids
|
||||||
|
queryset = queryset.filter(user_id__in=user_ids)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
@action(['POST'], detail=False, url_path='offline')
|
||||||
|
def offline(self, request, *args, **kwargs):
|
||||||
|
ids = request.data.get('ids', [])
|
||||||
|
queryset = self.get_queryset().exclude(key=request.session.session_key).filter(id__in=ids)
|
||||||
|
if not queryset.exists():
|
||||||
|
return Response(status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
keys = queryset.values_list('key', flat=True)
|
||||||
|
session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore
|
||||||
|
for key in keys:
|
||||||
|
session_store_cls(key).delete()
|
||||||
|
queryset.delete()
|
||||||
|
return Response(status=status.HTTP_200_OK)
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Generated by Django 4.1.10 on 2023-09-14 07:23
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0043_remove_user_secret_key_preference'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserSession',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||||
|
('ip', models.GenericIPAddressField(verbose_name='Login IP')),
|
||||||
|
('key', models.CharField(max_length=128, verbose_name='Session key')),
|
||||||
|
('city', models.CharField(blank=True, max_length=254, null=True, verbose_name='Login city')),
|
||||||
|
('user_agent', models.CharField(blank=True, max_length=254, null=True, verbose_name='User agent')),
|
||||||
|
('type', models.CharField(choices=[('W', 'Web'), ('T', 'Terminal'), ('U', 'Unknown')], max_length=2, verbose_name='Login type')),
|
||||||
|
('backend', models.CharField(default='', max_length=32, verbose_name='Authentication backend')),
|
||||||
|
('date_created', models.DateTimeField(blank=True, null=True, verbose_name='Date created')),
|
||||||
|
('date_expired', models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Date expired')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sessions', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'User session',
|
||||||
|
'ordering': ['-date_created'],
|
||||||
|
'permissions': [('offline_usersession', 'Offline ussr session')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -4,5 +4,6 @@
|
||||||
|
|
||||||
from .group import *
|
from .group import *
|
||||||
from .preference import *
|
from .preference import *
|
||||||
|
from .session import *
|
||||||
from .user import *
|
from .user import *
|
||||||
from .utils import *
|
from .utils import *
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext, gettext_lazy as _
|
||||||
|
|
||||||
|
from audits.const import LoginTypeChoices
|
||||||
|
|
||||||
|
|
||||||
|
class UserSession(models.Model):
|
||||||
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||||
|
ip = models.GenericIPAddressField(verbose_name=_("Login IP"))
|
||||||
|
key = models.CharField(max_length=128, verbose_name=_("Session key"))
|
||||||
|
city = models.CharField(max_length=254, blank=True, null=True, verbose_name=_("Login city"))
|
||||||
|
user_agent = models.CharField(max_length=254, blank=True, null=True, verbose_name=_("User agent"))
|
||||||
|
type = models.CharField(choices=LoginTypeChoices.choices, max_length=2, verbose_name=_("Login type"))
|
||||||
|
backend = models.CharField(max_length=32, default="", verbose_name=_("Authentication backend"))
|
||||||
|
date_created = models.DateTimeField(null=True, blank=True, verbose_name=_('Date created'))
|
||||||
|
date_expired = models.DateTimeField(null=True, blank=True, verbose_name=_("Date expired"), db_index=True)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
'users.User', verbose_name=_('User'), related_name='sessions', on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '%s(%s)' % (self.user, self.ip)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def backend_display(self):
|
||||||
|
return gettext(self.backend)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def clear_expired_sessions(cls):
|
||||||
|
cls.objects.filter(date_expired__lt=timezone.now()).delete()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-date_created']
|
||||||
|
verbose_name = _('User session')
|
||||||
|
permissions = [
|
||||||
|
('offline_usersession', _('Offline ussr session')),
|
||||||
|
]
|
|
@ -4,4 +4,5 @@ from .group import *
|
||||||
from .preference import *
|
from .preference import *
|
||||||
from .profile import *
|
from .profile import *
|
||||||
from .realtion import *
|
from .realtion import *
|
||||||
|
from .session import *
|
||||||
from .user import *
|
from .user import *
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from audits.const import LoginTypeChoices
|
||||||
|
from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
|
||||||
|
from users.models import User
|
||||||
|
from ..models import UserSession
|
||||||
|
|
||||||
|
|
||||||
|
class UserSessionSerializer(serializers.ModelSerializer):
|
||||||
|
type = LabeledChoiceField(choices=LoginTypeChoices.choices, label=_("Type"))
|
||||||
|
user = ObjectRelatedField(required=False, queryset=User.objects, label=_('User'))
|
||||||
|
is_current_user_session = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = UserSession
|
||||||
|
fields_mini = ['id']
|
||||||
|
fields_small = fields_mini + [
|
||||||
|
'type', 'ip', 'city', 'user_agent', 'user', 'is_current_user_session',
|
||||||
|
'backend', 'backend_display', 'date_created', 'date_expired'
|
||||||
|
]
|
||||||
|
fields = fields_small
|
||||||
|
extra_kwargs = {
|
||||||
|
"backend_display": {"label": _("Authentication backend")},
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_is_current_user_session(self, obj):
|
||||||
|
request = self.context.get('request')
|
||||||
|
if not request:
|
||||||
|
return False
|
||||||
|
return request.session.session_key == obj.key
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
from celery import shared_task
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.signals import user_logged_out
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
@ -10,10 +12,12 @@ from django_cas_ng.signals import cas_user_authenticated
|
||||||
from authentication.backends.oauth2.signals import oauth2_create_or_update_user
|
from authentication.backends.oauth2.signals import oauth2_create_or_update_user
|
||||||
from authentication.backends.oidc.signals import openid_create_or_update_user
|
from authentication.backends.oidc.signals import openid_create_or_update_user
|
||||||
from authentication.backends.saml2.signals import saml2_create_or_update_user
|
from authentication.backends.saml2.signals import saml2_create_or_update_user
|
||||||
|
from common.const.crontab import CRONTAB_AT_PM_TWO
|
||||||
from common.decorators import on_transaction_commit
|
from common.decorators import on_transaction_commit
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from jumpserver.utils import get_current_request
|
from jumpserver.utils import get_current_request
|
||||||
from .models import User, UserPasswordHistory
|
from ops.celery.decorator import register_as_period_task
|
||||||
|
from .models import User, UserPasswordHistory, UserSession
|
||||||
from .signals import post_user_create
|
from .signals import post_user_create
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
@ -156,3 +160,15 @@ def on_openid_create_or_update_user(sender, request, user, created, name, userna
|
||||||
user.username = username
|
user.username = username
|
||||||
user.email = email
|
user.email = email
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(verbose_name=_('Clean audits session task log'))
|
||||||
|
@register_as_period_task(crontab=CRONTAB_AT_PM_TWO)
|
||||||
|
def clean_audits_log_period():
|
||||||
|
UserSession.clear_expired_sessions()
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(user_logged_out)
|
||||||
|
def user_logged_out_callback(sender, request, user, **kwargs):
|
||||||
|
session_key = request.session.session_key
|
||||||
|
UserSession.objects.filter(key=session_key).delete()
|
||||||
|
|
|
@ -17,6 +17,7 @@ router.register(r'groups', api.UserGroupViewSet, 'user-group')
|
||||||
router.register(r'users-groups-relations', api.UserUserGroupRelationViewSet, 'users-groups-relation')
|
router.register(r'users-groups-relations', api.UserUserGroupRelationViewSet, 'users-groups-relation')
|
||||||
router.register(r'service-account-registrations', api.ServiceAccountRegistrationViewSet, 'service-account-registration')
|
router.register(r'service-account-registrations', api.ServiceAccountRegistrationViewSet, 'service-account-registration')
|
||||||
router.register(r'connection-token', auth_api.ConnectionTokenViewSet, 'connection-token')
|
router.register(r'connection-token', auth_api.ConnectionTokenViewSet, 'connection-token')
|
||||||
|
router.register(r'user-sessions', api.UserSessionViewSet, 'user-session')
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
Loading…
Reference in New Issue