feat: 用户在线session控制

pull/11567/head
feng 2023-09-13 16:52:11 +08:00 committed by Bryan
parent 5cbbf9e737
commit 3abc8bddfa
18 changed files with 227 additions and 10 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

51
apps/users/api/session.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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