admin: add metrics and charts
This commit is contained in:
parent
ae125dd1f0
commit
da9aaf69df
|
@ -0,0 +1,80 @@
|
||||||
|
"""passbook administration overview"""
|
||||||
|
import time
|
||||||
|
from collections import Counter
|
||||||
|
from datetime import timedelta
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
from django.db.models import Count, ExpressionWrapper, F
|
||||||
|
from django.db.models.fields import DurationField
|
||||||
|
from django.db.models.functions import ExtractHour
|
||||||
|
from django.http import response
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
|
from rest_framework.fields import SerializerMethodField
|
||||||
|
from rest_framework.permissions import IsAdminUser
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.serializers import Serializer
|
||||||
|
from rest_framework.viewsets import ViewSet
|
||||||
|
|
||||||
|
from passbook.audit.models import Event, EventAction
|
||||||
|
|
||||||
|
|
||||||
|
class AdministrationMetricsSerializer(Serializer):
|
||||||
|
"""Overview View"""
|
||||||
|
|
||||||
|
logins_per_1h = SerializerMethodField()
|
||||||
|
logins_failed_per_1h = SerializerMethodField()
|
||||||
|
|
||||||
|
def get_events_per_1h(self, action: str) -> List[Dict[str, int]]:
|
||||||
|
"""Get event count by hour in the last day, fill with zeros"""
|
||||||
|
date_from = now() - timedelta(days=1)
|
||||||
|
result = (
|
||||||
|
Event.objects.filter(action=action, created__gte=date_from)
|
||||||
|
.annotate(
|
||||||
|
age=ExpressionWrapper(
|
||||||
|
now() - F("created"), output_field=DurationField()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(age_hours=ExtractHour("age"))
|
||||||
|
.values("age_hours")
|
||||||
|
.annotate(count=Count("pk"))
|
||||||
|
.order_by("age_hours")
|
||||||
|
)
|
||||||
|
data = Counter({d["age_hours"]: d["count"] for d in result})
|
||||||
|
results = []
|
||||||
|
_now = now()
|
||||||
|
for hour in range(0, -24, -1):
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"x": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000,
|
||||||
|
"y": data[hour * -1],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def get_logins_per_1h(self, _):
|
||||||
|
"""Get successful logins per hour for the last 24 hours"""
|
||||||
|
return self.get_events_per_1h(EventAction.LOGIN)
|
||||||
|
|
||||||
|
def get_logins_failed_per_1h(self, _):
|
||||||
|
"""Get failed logins per hour for the last 24 hours"""
|
||||||
|
return self.get_events_per_1h(EventAction.LOGIN_FAILED)
|
||||||
|
|
||||||
|
def create(self, request: Request) -> response:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def update(self, request: Request) -> Response:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class AdministrationMetricsViewSet(ViewSet):
|
||||||
|
"""Return single instance of AdministrationMetricsSerializer"""
|
||||||
|
|
||||||
|
permission_classes = [IsAdminUser]
|
||||||
|
|
||||||
|
@swagger_auto_schema(responses={200: AdministrationMetricsSerializer(many=True)})
|
||||||
|
def list(self, request: Request) -> Response:
|
||||||
|
"""Return single instance of AdministrationMetricsSerializer"""
|
||||||
|
serializer = AdministrationMetricsSerializer(True)
|
||||||
|
return Response(serializer.data)
|
|
@ -1,6 +1,7 @@
|
||||||
{% extends "administration/base.html" %}
|
{% extends "administration/base.html" %}
|
||||||
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="pf-c-page__main-section pf-m-light">
|
<section class="pf-c-page__main-section pf-m-light">
|
||||||
|
@ -10,33 +11,51 @@
|
||||||
</section>
|
</section>
|
||||||
<section class="pf-c-page__main-section">
|
<section class="pf-c-page__main-section">
|
||||||
<div class="pf-l-gallery pf-m-gutter">
|
<div class="pf-l-gallery pf-m-gutter">
|
||||||
<a href="{% url 'passbook_admin:applications' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-4-col" style="grid-column-end: span 3;grid-row-end: span 2;">
|
||||||
<div class="pf-c-card__header">
|
<div class="pf-c-card__header">
|
||||||
<div class="pf-c-card__header-main">
|
<div class="pf-c-card__header-main">
|
||||||
<i class="pf-icon pf-icon-applications"></i> {% trans 'Applications' %}
|
<i class="pf-icon pf-icon-server"></i> {% trans 'Logins over the last 24 hours' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pf-c-card__body" style="position: relative; height:100%; width:100%">
|
||||||
|
<canvas id="logins-last-metrics"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-4-col" style="grid-column-end: span 2;grid-row-end: span 3;">
|
||||||
|
<div class="pf-c-card__header">
|
||||||
|
<div class="pf-c-card__header-main">
|
||||||
|
<i class="pf-icon pf-icon-server"></i> {% trans 'Apps with most usage' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-card__body">
|
<div class="pf-c-card__body">
|
||||||
<p class="aggregate-status">
|
<table class="pf-c-table pf-m-compact" role="grid">
|
||||||
<i class="fa fa-check-circle"></i> {{ application_count }}
|
<thead>
|
||||||
</p>
|
<tr role="row">
|
||||||
|
<th role="columnheader" scope="col">{% trans 'Application' %}</th>
|
||||||
|
<th role="columnheader" scope="col">{% trans 'Logins' %}</th>
|
||||||
|
<th role="columnheader" scope="col"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody role="rowgroup">
|
||||||
|
{% for app in most_used_applications %}
|
||||||
|
<tr role="row">
|
||||||
|
<td role="cell">
|
||||||
|
{{ app.application.name }}
|
||||||
|
</td>
|
||||||
|
<td role="cell">
|
||||||
|
{{ app.total_logins }}
|
||||||
|
</td>
|
||||||
|
<td role="cell">
|
||||||
|
<progress value="{{ app.total_logins }}" max="{{ most_used_applications.0.total_logins }}"></progress>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="{% url 'passbook_admin:sources' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
|
||||||
<div class="pf-c-card__header">
|
|
||||||
<div class="pf-c-card__header-main">
|
|
||||||
<i class="pf-icon pf-icon-middleware"></i> {% trans 'Sources' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pf-c-card__body">
|
|
||||||
<p class="aggregate-status">
|
|
||||||
<i class="fa fa-check-circle"></i> {{ source_count }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="{% url 'passbook_admin:providers' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
|
||||||
<div class="pf-c-card__header">
|
<div class="pf-c-card__header">
|
||||||
<div class="pf-c-card__header-main">
|
<div class="pf-c-card__header-main">
|
||||||
<i class="pf-icon pf-icon-plugged"></i> {% trans 'Providers' %}
|
<i class="pf-icon pf-icon-plugged"></i> {% trans 'Providers' %}
|
||||||
|
@ -54,42 +73,9 @@
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
|
|
||||||
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
|
||||||
<div class="pf-c-card__header">
|
|
||||||
<div class="pf-c-card__header-main">
|
|
||||||
<i class="pf-icon pf-icon-plugged"></i> {% trans 'Stages' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pf-c-card__body">
|
|
||||||
{% if stage_count < 1 %}
|
|
||||||
<p class="aggregate-status">
|
|
||||||
<i class="pficon-error-circle-o"></i> {{ stage_count }}
|
|
||||||
</p>
|
|
||||||
<p>{% trans 'No Stages configured. No Users will be able to login.' %}"></p>
|
|
||||||
{% else %}
|
|
||||||
<p class="aggregate-status">
|
|
||||||
<i class="fa fa-check-circle"></i> {{ stage_count }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
|
||||||
<div class="pf-c-card__header">
|
|
||||||
<div class="pf-c-card__header-main">
|
|
||||||
<i class="pf-icon pf-icon-topology"></i> {% trans 'Flows' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pf-c-card__body">
|
|
||||||
<p class="aggregate-status">
|
|
||||||
<i class="fa fa-check-circle"></i> {{ flow_count }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="{% url 'passbook_admin:policies' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
|
||||||
<div class="pf-c-card__header">
|
<div class="pf-c-card__header">
|
||||||
<div class="pf-c-card__header-main">
|
<div class="pf-c-card__header-main">
|
||||||
<i class="pf-icon pf-icon-infrastructure"></i> {% trans 'Policies' %}
|
<i class="pf-icon pf-icon-infrastructure"></i> {% trans 'Policies' %}
|
||||||
|
@ -107,22 +93,9 @@
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
|
|
||||||
<a href="{% url 'passbook_admin:stage-invitations' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
|
||||||
<div class="pf-c-card__header">
|
|
||||||
<div class="pf-c-card__header-main">
|
|
||||||
<i class="pf-icon pf-icon-migration"></i> {% trans 'Invitation' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pf-c-card__body">
|
|
||||||
<p class="aggregate-status">
|
|
||||||
<i class="fa fa-check-circle"></i> {{ invitation_count }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="{% url 'passbook_admin:users' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
|
||||||
<div class="pf-c-card__header">
|
<div class="pf-c-card__header">
|
||||||
<div class="pf-c-card__header-main">
|
<div class="pf-c-card__header-main">
|
||||||
<i class="pf-icon pf-icon-user"></i> {% trans 'Users' %}
|
<i class="pf-icon pf-icon-user"></i> {% trans 'Users' %}
|
||||||
|
@ -133,9 +106,9 @@
|
||||||
<i class="fa fa-check-circle"></i> {{ user_count }}
|
<i class="fa fa-check-circle"></i> {{ user_count }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
|
|
||||||
<div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
|
||||||
<div class="pf-c-card__header">
|
<div class="pf-c-card__header">
|
||||||
<div class="pf-c-card__header-main">
|
<div class="pf-c-card__header-main">
|
||||||
<i class="pf-icon pf-icon-bundle"></i> {% trans 'Version' %}
|
<i class="pf-icon pf-icon-bundle"></i> {% trans 'Version' %}
|
||||||
|
@ -161,7 +134,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
|
||||||
<div class="pf-c-card__header">
|
<div class="pf-c-card__header">
|
||||||
<div class="pf-c-card__header-main">
|
<div class="pf-c-card__header-main">
|
||||||
<i class="pf-icon pf-icon-server"></i> {% trans 'Workers' %}
|
<i class="pf-icon pf-icon-server"></i> {% trans 'Workers' %}
|
||||||
|
@ -189,7 +162,7 @@
|
||||||
</fetch-fill-slot>
|
</fetch-fill-slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact" data-target="modal" data-modal="clearCacheModalRoot">
|
<a class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-hoverable pf-m-compact" data-target="modal" data-modal="clearCacheModalRoot">
|
||||||
<div class="pf-c-card__header">
|
<div class="pf-c-card__header">
|
||||||
<div class="pf-c-card__header-main">
|
<div class="pf-c-card__header-main">
|
||||||
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Policies' %}
|
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Policies' %}
|
||||||
|
@ -209,7 +182,7 @@
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
|
||||||
<div class="pf-c-card__header">
|
<div class="pf-c-card__header">
|
||||||
<div class="pf-c-card__header-main">
|
<div class="pf-c-card__header-main">
|
||||||
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Flows' %}
|
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Flows' %}
|
||||||
|
@ -262,4 +235,65 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="{% static 'node_modules/chart.js/dist/Chart.bundle.min.js' %}"></script>
|
||||||
|
<script>
|
||||||
|
var ctx = document.getElementById('logins-last-metrics').getContext('2d');
|
||||||
|
fetch("{% url 'passbook_api:admin_metrics-list' %}").then(r => r.json()).then(r => {
|
||||||
|
var myChart = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Failed Logins',
|
||||||
|
backgroundColor: "rgba(201, 25, 11, .5)",
|
||||||
|
spanGaps: true,
|
||||||
|
data: r.logins_failed_per_1h,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Successful Logins',
|
||||||
|
backgroundColor: "rgba(189, 229, 184, .5)",
|
||||||
|
spanGaps: true,
|
||||||
|
data: r.logins_per_1h,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
spanGaps: true,
|
||||||
|
scales: {
|
||||||
|
xAxes: [{
|
||||||
|
stacked: true,
|
||||||
|
gridLines: {
|
||||||
|
color: "rgba(0, 0, 0, 0)",
|
||||||
|
},
|
||||||
|
type: 'time',
|
||||||
|
offset: true,
|
||||||
|
ticks: {
|
||||||
|
callback: function (value, index, values) {
|
||||||
|
const date = new Date();
|
||||||
|
const delta = (date - values[index].value);
|
||||||
|
const ago = Math.round(delta / 1000 / 3600);
|
||||||
|
console.log(ago);
|
||||||
|
return `${ago} Hours ago`;
|
||||||
|
},
|
||||||
|
autoSkip: true,
|
||||||
|
maxTicksLimit: 8
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
yAxes: [{
|
||||||
|
ticks: {
|
||||||
|
stepSize: 1
|
||||||
|
},
|
||||||
|
stacked: true,
|
||||||
|
gridLines: {
|
||||||
|
color: "rgba(0, 0, 0, 0)",
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -3,6 +3,8 @@ from typing import Union
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
from django.db.models import Count
|
||||||
|
from django.db.models.fields.json import KeyTextTransform
|
||||||
from django.shortcuts import redirect, reverse
|
from django.shortcuts import redirect, reverse
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from packaging.version import LegacyVersion, Version, parse
|
from packaging.version import LegacyVersion, Version, parse
|
||||||
|
@ -10,10 +12,9 @@ from packaging.version import LegacyVersion, Version, parse
|
||||||
from passbook import __version__
|
from passbook import __version__
|
||||||
from passbook.admin.mixins import AdminRequiredMixin
|
from passbook.admin.mixins import AdminRequiredMixin
|
||||||
from passbook.admin.tasks import VERSION_CACHE_KEY, update_latest_version
|
from passbook.admin.tasks import VERSION_CACHE_KEY, update_latest_version
|
||||||
from passbook.core.models import Application, Provider, Source, User
|
from passbook.audit.models import Event, EventAction
|
||||||
from passbook.flows.models import Flow, Stage
|
from passbook.core.models import Provider, User
|
||||||
from passbook.policies.models import Policy
|
from passbook.policies.models import Policy
|
||||||
from passbook.stages.invitation.models import Invitation
|
|
||||||
|
|
||||||
|
|
||||||
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
||||||
|
@ -37,17 +38,27 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
||||||
return parse(__version__)
|
return parse(__version__)
|
||||||
return parse(version_in_cache)
|
return parse(version_in_cache)
|
||||||
|
|
||||||
|
def get_most_used_applications(self):
|
||||||
|
"""Get Most used applications, total login counts and unique users that have used them."""
|
||||||
|
return (
|
||||||
|
Event.objects.filter(action=EventAction.AUTHORIZE_APPLICATION)
|
||||||
|
.exclude(context__authorized_application=None)
|
||||||
|
.annotate(application=KeyTextTransform("authorized_application", "context"))
|
||||||
|
.annotate(user_pk=KeyTextTransform("pk", "user"))
|
||||||
|
.values("application")
|
||||||
|
.annotate(total_logins=Count("application"))
|
||||||
|
.annotate(unique_users=Count("user_pk", distinct=True))
|
||||||
|
.values("unique_users", "application", "total_logins")
|
||||||
|
.order_by("-total_logins")[:15]
|
||||||
|
)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs["application_count"] = len(Application.objects.all())
|
|
||||||
kwargs["policy_count"] = len(Policy.objects.all())
|
kwargs["policy_count"] = len(Policy.objects.all())
|
||||||
kwargs["user_count"] = len(User.objects.all()) - 1 # Remove anonymous user
|
kwargs["user_count"] = len(User.objects.all()) - 1 # Remove anonymous user
|
||||||
kwargs["provider_count"] = len(Provider.objects.all())
|
kwargs["provider_count"] = len(Provider.objects.all())
|
||||||
kwargs["source_count"] = len(Source.objects.all())
|
|
||||||
kwargs["stage_count"] = len(Stage.objects.all())
|
|
||||||
kwargs["flow_count"] = len(Flow.objects.all())
|
|
||||||
kwargs["invitation_count"] = len(Invitation.objects.all())
|
|
||||||
kwargs["version"] = parse(__version__)
|
kwargs["version"] = parse(__version__)
|
||||||
kwargs["version_latest"] = self.get_latest_version()
|
kwargs["version_latest"] = self.get_latest_version()
|
||||||
|
kwargs["most_used_applications"] = self.get_most_used_applications()
|
||||||
kwargs["providers_without_application"] = Provider.objects.filter(
|
kwargs["providers_without_application"] = Provider.objects.filter(
|
||||||
application=None
|
application=None
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,6 +5,7 @@ from drf_yasg.views import get_schema_view
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
from passbook.admin.api.overview import AdministrationOverviewViewSet
|
from passbook.admin.api.overview import AdministrationOverviewViewSet
|
||||||
|
from passbook.admin.api.overview_metrics import AdministrationMetricsViewSet
|
||||||
from passbook.api.permissions import CustomObjectPermissions
|
from passbook.api.permissions import CustomObjectPermissions
|
||||||
from passbook.api.v2.messages import MessagesViewSet
|
from passbook.api.v2.messages import MessagesViewSet
|
||||||
from passbook.audit.api import EventViewSet
|
from passbook.audit.api import EventViewSet
|
||||||
|
@ -55,6 +56,7 @@ router.register("root/messages", MessagesViewSet, basename="messages")
|
||||||
router.register(
|
router.register(
|
||||||
"admin/overview", AdministrationOverviewViewSet, basename="admin_overview"
|
"admin/overview", AdministrationOverviewViewSet, basename="admin_overview"
|
||||||
)
|
)
|
||||||
|
router.register("admin/metrics", AdministrationMetricsViewSet, basename="admin_metrics")
|
||||||
|
|
||||||
router.register("core/applications", ApplicationViewSet)
|
router.register("core/applications", ApplicationViewSet)
|
||||||
router.register("core/groups", GroupViewSet)
|
router.register("core/groups", GroupViewSet)
|
||||||
|
|
|
@ -10,6 +10,7 @@ from django.utils import timezone
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
|
from passbook.audit.models import Event, EventAction
|
||||||
from passbook.core.models import Application
|
from passbook.core.models import Application
|
||||||
from passbook.flows.models import in_memory_stage
|
from passbook.flows.models import in_memory_stage
|
||||||
from passbook.flows.planner import (
|
from passbook.flows.planner import (
|
||||||
|
@ -226,6 +227,11 @@ class OAuthFulfillmentStage(StageView):
|
||||||
"consent_required",
|
"consent_required",
|
||||||
self.params.grant_type,
|
self.params.grant_type,
|
||||||
)
|
)
|
||||||
|
Event.new(
|
||||||
|
EventAction.AUTHORIZE_APPLICATION,
|
||||||
|
authorized_application=application,
|
||||||
|
flow=self.executor.plan.flow_pk,
|
||||||
|
).from_http(self.request)
|
||||||
return redirect(self.create_response_uri())
|
return redirect(self.create_response_uri())
|
||||||
except (ClientIdError, RedirectUriError) as error:
|
except (ClientIdError, RedirectUriError) as error:
|
||||||
self.executor.stage_invalid()
|
self.executor.stage_invalid()
|
||||||
|
|
|
@ -167,6 +167,15 @@
|
||||||
"supports-color": "^5.3.0"
|
"supports-color": "^5.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"chart.js": {
|
||||||
|
"version": "2.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.3.tgz",
|
||||||
|
"integrity": "sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw==",
|
||||||
|
"requires": {
|
||||||
|
"chartjs-color": "^2.1.0",
|
||||||
|
"moment": "^2.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"clean-css": {
|
"clean-css": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
|
||||||
|
@ -185,7 +194,6 @@
|
||||||
"version": "1.9.3",
|
"version": "1.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"color-name": "1.1.3"
|
"color-name": "1.1.3"
|
||||||
}
|
}
|
||||||
|
@ -193,8 +201,7 @@
|
||||||
"color-name": {
|
"color-name": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
|
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"commander": {
|
"commander": {
|
||||||
"version": "2.20.3",
|
"version": "2.20.3",
|
||||||
|
@ -349,6 +356,11 @@
|
||||||
"parse-literals": "^1.2.0"
|
"parse-literals": "^1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"moment": {
|
||||||
|
"version": "2.29.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.0.tgz",
|
||||||
|
"integrity": "sha512-z6IJ5HXYiuxvFTI6eiQ9dm77uE0gyy1yXNApVHqTcnIKfY9tIwEjlzsZ6u1LQXvVgKeTnv9Xm7NDvJ7lso3MtA=="
|
||||||
|
},
|
||||||
"no-case": {
|
"no-case": {
|
||||||
"version": "2.3.2",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^5.15.0",
|
"@fortawesome/fontawesome-free": "^5.15.0",
|
||||||
"@patternfly/patternfly": "^4.42.2",
|
"@patternfly/patternfly": "^4.42.2",
|
||||||
|
"chart.js": "^2.9.3",
|
||||||
"codemirror": "^5.58.1",
|
"codemirror": "^5.58.1",
|
||||||
"lit-element": "^2.4.0",
|
"lit-element": "^2.4.0",
|
||||||
"lit-html": "^1.3.0",
|
"lit-html": "^1.3.0",
|
||||||
|
|
26
swagger.yaml
26
swagger.yaml
|
@ -19,6 +19,21 @@ securityDefinitions:
|
||||||
security:
|
security:
|
||||||
- token: []
|
- token: []
|
||||||
paths:
|
paths:
|
||||||
|
/admin/metrics/:
|
||||||
|
get:
|
||||||
|
operationId: admin_metrics_list
|
||||||
|
description: Return single instance of AdministrationMetricsSerializer
|
||||||
|
parameters: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: ''
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/AdministrationMetrics'
|
||||||
|
tags:
|
||||||
|
- admin
|
||||||
|
parameters: []
|
||||||
/admin/overview/:
|
/admin/overview/:
|
||||||
get:
|
get:
|
||||||
operationId: admin_overview_list
|
operationId: admin_overview_list
|
||||||
|
@ -5958,6 +5973,17 @@ paths:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
definitions:
|
definitions:
|
||||||
|
AdministrationMetrics:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
logins_per_1h:
|
||||||
|
title: Logins per 1h
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
logins_failed_per_1h:
|
||||||
|
title: Logins failed per 1h
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
AdministrationOverview:
|
AdministrationOverview:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
Reference in New Issue