You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
jumpserver/apps/jumpserver/api.py

527 lines
18 KiB

import time
from collections import defaultdict
from django.core.cache import cache
from django.db.models import Count, Max, F, CharField
from django.db.models.functions import Cast
from django.http.response import JsonResponse, HttpResponse
from django.utils import timezone
from django.utils.timesince import timesince
from rest_framework.permissions import AllowAny
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from assets.const import AllTypes
from assets.models import Asset
from audits.api import OperateLogViewSet
from audits.const import LoginStatusChoices
from audits.models import UserLoginLog, PasswordChangeLog, OperateLog, FTPLog, JobLog
from audits.utils import construct_userlogin_usernames
from common.utils import lazyproperty
from common.utils.timezone import local_now, local_zero_hour
from ops.const import JobStatus
from orgs.caches import OrgResourceStatisticsCache
from orgs.utils import current_org
from terminal.const import RiskLevelChoices
from terminal.models import Session, Command
from terminal.utils import ComponentsPrometheusMetricsUtil
from users.models import User
__all__ = ['IndexApi']
class DateTimeMixin:
request: Request
@property
def org(self):
return current_org
@lazyproperty
def days(self):
query_params = self.request.query_params
count = query_params.get('days')
count = int(count) if count else 1
return count
@property
def days_to_datetime(self):
days = self.days
if days == 1:
t = local_zero_hour()
else:
t = local_now() - timezone.timedelta(days=days)
return t
@lazyproperty
def date_start_end(self):
return self.days_to_datetime.date(), local_now().date() + timezone.timedelta(days=1)
@lazyproperty
def dates_list(self):
return [
(local_now() - timezone.timedelta(days=i)).date()
for i in range(self.days - 1, -1, -1)
]
def get_dates_metrics_date(self):
return [d.strftime('%m-%d') for d in self.dates_list] or ['0']
def get_logs_queryset_filter(self, qs, query_field, is_timestamp=False):
dt = self.days_to_datetime
t = dt.timestamp() if is_timestamp else dt
query = {f'{query_field}__gte': t}
return qs.filter(**query)
@lazyproperty
def users(self):
return self.org.get_members()
def get_logs_queryset(self, queryset, query_params):
query = {}
users = self.users
if not self.org.is_root():
if query_params == 'username':
query = {
f'{query_params}__in': construct_userlogin_usernames(users)
}
else:
query = {
f'{query_params}__in': [str(user) for user in users]
}
queryset = queryset.filter(**query)
return queryset
@lazyproperty
def sessions_queryset(self):
return self.get_logs_queryset_filter(Session.objects, 'date_start')
@lazyproperty
def login_logs_queryset(self):
qs = UserLoginLog.objects.all()
qs = self.get_logs_queryset_filter(qs, 'datetime')
queryset = self.get_logs_queryset(qs, 'username')
return queryset
@lazyproperty
def user_login_logs_on_the_system_queryset(self):
qs = UserLoginLog.objects.filter(status=LoginStatusChoices.success)
qs = self.get_logs_queryset_filter(qs, 'datetime')
queryset = qs.filter(username__in=construct_userlogin_usernames(self.users))
return queryset
@lazyproperty
def password_change_logs_queryset(self):
qs = PasswordChangeLog.objects.all()
qs = self.get_logs_queryset_filter(qs, 'datetime')
queryset = self.get_logs_queryset(qs, 'user')
return queryset
@lazyproperty
def operate_logs_queryset(self):
qs = OperateLogViewSet().get_queryset()
return self.get_logs_queryset_filter(qs, 'datetime')
@lazyproperty
def ftp_logs_queryset(self):
qs = FTPLog.objects.all()
return self.get_logs_queryset_filter(qs, 'date_start')
@lazyproperty
def command_type_queryset_tuple(self):
type_queryset_tuple = Command.get_all_type_queryset_tuple()
return (
(tp, self.get_logs_queryset_filter(
qs, 'timestamp', is_timestamp=True
))
for tp, qs in type_queryset_tuple
)
@lazyproperty
def job_logs_queryset(self):
qs = JobLog.objects.all()
return self.get_logs_queryset_filter(qs, 'date_start')
class DatesLoginMetricMixin:
dates_list: list
date_start_end: tuple
command_type_queryset_tuple: tuple
sessions_queryset: Session.objects
ftp_logs_queryset: FTPLog.objects
job_logs_queryset: JobLog.objects
login_logs_queryset: UserLoginLog.objects
user_login_logs_on_the_system_queryset: UserLoginLog.objects
operate_logs_queryset: OperateLog.objects
password_change_logs_queryset: PasswordChangeLog.objects
@lazyproperty
def get_type_to_assets(self):
result = Asset.objects.annotate(type=F('platform__type')). \
values('type').order_by('type').annotate(total=Count(1))
all_types_dict = dict(AllTypes.choices())
result = list(result)
for i in result:
tp = i['type']
i['label'] = all_types_dict.get(tp, tp)
return result
def filter_date_start_end(self, queryset, field_name):
query = {f'{field_name}__range': self.date_start_end}
return queryset.filter(**query)
def get_date_metrics(self, queryset, field_name, count_fields):
queryset = self.filter_date_start_end(queryset, field_name)
if not isinstance(count_fields, (list, tuple)):
count_fields = [count_fields]
values_list = [field_name] + list(count_fields)
queryset = queryset.values_list(*values_list)
date_group_map = defaultdict(lambda: defaultdict(set))
for row in queryset:
datetime = row[0]
date_str = str(datetime.date())
for idx, count_field in enumerate(count_fields):
date_group_map[date_str][count_field].add(row[idx + 1])
date_metrics_dict = defaultdict(list)
for field in count_fields:
for date_str in self.dates_list:
count = len(date_group_map.get(str(date_str), {}).get(field, set()))
date_metrics_dict[field].append(count)
return date_metrics_dict
def get_dates_metrics_total_count_active_users_and_assets(self):
date_metrics_dict = self.get_date_metrics(
Session.objects, 'date_start', ('user_id', 'asset_id')
)
return date_metrics_dict.get('user_id', []), date_metrics_dict.get('asset_id', [])
def get_dates_metrics_total_count_login(self):
date_metrics_dict = self.get_date_metrics(
UserLoginLog.objects, 'datetime', 'id'
)
return date_metrics_dict.get('id', [])
def get_dates_metrics_total_count_sessions(self):
date_metrics_dict = self.get_date_metrics(
Session.objects, 'date_start', 'id'
)
return date_metrics_dict.get('id', [])
def get_dates_login_times_assets(self):
assets = self.sessions_queryset.values("asset") \
.annotate(total=Count("asset")) \
.annotate(last=Cast(Max("date_start"), output_field=CharField())) \
.order_by("-total")
return list(assets[:10])
def get_dates_login_times_users(self):
users = self.sessions_queryset.values("user_id") \
.annotate(total=Count("user_id")) \
.annotate(user=Max('user')) \
.annotate(last=Cast(Max("date_start"), output_field=CharField())) \
.order_by("-total")
return list(users[:10])
def get_dates_login_record_sessions(self):
sessions = self.sessions_queryset.order_by('-date_start')
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[:10]
]
return sessions
@lazyproperty
def user_login_logs_amount(self):
return self.login_logs_queryset.count()
@lazyproperty
def user_login_success_logs_amount(self):
return self.login_logs_queryset.filter(status=LoginStatusChoices.success).count()
@lazyproperty
def user_login_amount(self):
return self.user_login_logs_on_the_system_queryset.values('username').distinct().count()
@lazyproperty
def operate_logs_amount(self):
return self.operate_logs_queryset.count()
@lazyproperty
def change_password_logs_amount(self):
return self.password_change_logs_queryset.count()
@lazyproperty
def command_statistics(self):
from terminal.const import CommandStorageType
total_amount = 0
danger_amount = 0
for tp, qs in self.command_type_queryset_tuple:
if tp == CommandStorageType.es:
total_amount += qs.count(limit_to_max_result_window=False)
danger_amount += qs.filter(risk_level=RiskLevelChoices.reject).count(limit_to_max_result_window=False)
else:
total_amount += qs.count()
danger_amount += qs.filter(risk_level=RiskLevelChoices.reject).count()
return total_amount, danger_amount
@lazyproperty
def commands_amount(self):
total_amount, __ = self.command_statistics
return total_amount
@lazyproperty
def commands_danger_amount(self):
__, danger_amount = self.command_statistics
return danger_amount
@lazyproperty
def job_logs_running_amount(self):
return self.job_logs_queryset.filter(status=JobStatus.running).count()
@lazyproperty
def job_logs_failed_amount(self):
return self.job_logs_queryset.filter(
status__in=[JobStatus.failed, JobStatus.timeout]
).count()
@lazyproperty
def job_logs_amount(self):
return self.job_logs_queryset.count()
@lazyproperty
def sessions_amount(self):
return self.sessions_queryset.count()
@lazyproperty
def online_sessions_amount(self):
return self.sessions_queryset.filter(is_finished=False).count()
@lazyproperty
def ftp_logs_amount(self):
return self.ftp_logs_queryset.count()
class IndexApi(DateTimeMixin, DatesLoginMetricMixin, APIView):
http_method_names = ['get']
rbac_perms = {
'GET': ['rbac.view_audit | rbac.view_console'],
}
def get(self, request, *args, **kwargs):
data = {}
query_params = self.request.query_params
caches = OrgResourceStatisticsCache(self.org)
_all = query_params.get('all')
if _all or query_params.get('total_count') or query_params.get('total_count_users'):
data.update({
'total_count_users': caches.users_amount,
})
if _all or query_params.get('total_count') or query_params.get('total_count_assets'):
data.update({
'total_count_assets': caches.assets_amount,
})
if _all or query_params.get('total_count') or query_params.get('total_count_users_this_week'):
data.update({
'total_count_users_this_week': caches.new_users_amount_this_week,
})
if _all or query_params.get('total_count') or query_params.get('total_count_assets_this_week'):
data.update({
'total_count_assets_this_week': caches.new_assets_amount_this_week,
})
if _all or query_params.get('total_count') or query_params.get('total_count_login_users'):
data.update({
'total_count_login_users': self.user_login_amount
})
if _all or query_params.get('total_count') or query_params.get('total_count_today_active_assets'):
data.update({
'total_count_today_active_assets': caches.total_count_today_active_assets,
})
if _all or query_params.get('total_count') or query_params.get('total_count_online_users'):
data.update({
'total_count_online_users': caches.total_count_online_users,
})
if _all or query_params.get('total_count') or query_params.get('total_count_online_sessions'):
data.update({
'total_count_online_sessions': caches.total_count_online_sessions,
})
if _all or query_params.get('total_count') or query_params.get('total_count_today_failed_sessions'):
data.update({
'total_count_today_failed_sessions': caches.total_count_today_failed_sessions,
})
if _all or query_params.get('total_count') or query_params.get('total_count_user_login_logs'):
data.update({
'total_count_user_login_logs': self.user_login_logs_amount,
})
if _all or query_params.get('total_count') or query_params.get('total_count_user_login_success_logs'):
data.update({
'total_count_user_login_success_logs': self.user_login_success_logs_amount,
})
if _all or query_params.get('total_count') or query_params.get('total_count_operate_logs'):
data.update({
'total_count_operate_logs': self.operate_logs_amount,
})
if _all or query_params.get('total_count') or query_params.get('total_count_change_password_logs'):
data.update({
'total_count_change_password_logs': self.change_password_logs_amount,
})
if _all or query_params.get('total_count') or query_params.get('total_count_commands'):
data.update({
'total_count_commands': self.commands_amount,
})
if _all or query_params.get('total_count') or query_params.get('total_count_commands_danger'):
data.update({
'total_count_commands_danger': self.commands_danger_amount,
})
if _all or query_params.get('total_count') or query_params.get('total_count_history_sessions'):
data.update({
'total_count_history_sessions': self.sessions_amount - self.online_sessions_amount,
})
if _all or query_params.get('total_count') or query_params.get('total_count_ftp_logs'):
data.update({
'total_count_ftp_logs': self.ftp_logs_amount,
})
if _all or query_params.get('total_count') or query_params.get('total_count_job_logs'):
data.update({
'total_count_job_logs': self.job_logs_amount,
})
if _all or query_params.get('total_count') or query_params.get('total_count_job_logs_running'):
data.update({
'total_count_job_logs_running': self.job_logs_running_amount,
})
if _all or query_params.get('total_count') or query_params.get('total_count_job_logs_failed'):
data.update({
'total_count_job_logs_failed': self.job_logs_failed_amount,
})
if _all or query_params.get('total_count') or query_params.get('total_count_type_to_assets_amount'):
data.update({
'total_count_type_to_assets_amount': self.get_type_to_assets,
})
if _all or query_params.get('session_dates_metrics'):
data.update({
'dates_metrics_date': self.get_dates_metrics_date(),
'dates_metrics_total_count_session': self.get_dates_metrics_total_count_sessions(),
})
if _all or query_params.get('dates_metrics'):
user_data, asset_data = self.get_dates_metrics_total_count_active_users_and_assets()
login_data = self.get_dates_metrics_total_count_login()
data.update({
'dates_metrics_date': self.get_dates_metrics_date(),
'dates_metrics_total_count_login': login_data,
'dates_metrics_total_count_active_users': user_data,
'dates_metrics_total_count_active_assets': asset_data,
})
if _all or query_params.get('dates_login_times_top10_assets'):
data.update({
'dates_login_times_top10_assets': self.get_dates_login_times_assets(),
})
if _all or query_params.get('dates_login_times_top10_users'):
data.update({
'dates_login_times_top10_users': self.get_dates_login_times_users(),
})
if _all or query_params.get('dates_login_record_top10_sessions'):
data.update({
'dates_login_record_top10_sessions': self.get_dates_login_record_sessions()
})
return JsonResponse(data, status=200)
class HealthApiMixin(APIView):
pass
class HealthCheckView(HealthApiMixin):
permission_classes = (AllowAny,)
@staticmethod
def get_db_status():
t1 = time.time()
try:
ok = User.objects.first() is not None
t2 = time.time()
return ok, t2 - t1
except Exception as e:
return False, str(e)
@staticmethod
def get_redis_status():
key = 'HEALTH_CHECK'
t1 = time.time()
try:
value = '1'
cache.set(key, '1', 10)
got = cache.get(key)
t2 = time.time()
if value == got:
return True, t2 - t1
return False, 'Value not match'
except Exception as e:
return False, str(e)
def get(self, request):
redis_status, redis_time = self.get_redis_status()
db_status, db_time = self.get_db_status()
status = all([redis_status, db_status])
data = {
'status': status,
'db_status': db_status,
'db_time': db_time,
'redis_status': redis_status,
'redis_time': redis_time,
'time': int(time.time()),
}
return Response(data)
class PrometheusMetricsApi(HealthApiMixin):
permission_classes = (AllowAny,)
def get(self, request, *args, **kwargs):
util = ComponentsPrometheusMetricsUtil()
metrics_text = util.get_prometheus_metrics_text()
return HttpResponse(metrics_text, content_type='text/plain; version=0.0.4; charset=utf-8')