core: initial authenticated sessions

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-05-30 00:10:50 +02:00
parent f51ab7a878
commit 133fc38c05
6 changed files with 310 additions and 0 deletions

View File

@ -11,6 +11,7 @@ from authentik.admin.api.workers import WorkerView
from authentik.api.v2.config import ConfigView
from authentik.api.views import APIBrowserView
from authentik.core.api.applications import ApplicationViewSet
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
from authentik.core.api.groups import GroupViewSet
from authentik.core.api.propertymappings import PropertyMappingViewSet
from authentik.core.api.providers import ProviderViewSet
@ -107,6 +108,7 @@ router = routers.DefaultRouter()
router.register("admin/system_tasks", TaskViewSet, basename="admin_system_tasks")
router.register("admin/apps", AppsViewSet, basename="apps")
router.register("core/authenticated_sessions", AuthenticatedSessionViewSet)
router.register("core/applications", ApplicationViewSet)
router.register("core/groups", GroupViewSet)
router.register("core/users", UserViewSet)

View File

@ -0,0 +1,44 @@
"""AuthenticatedSessions API Viewset"""
from guardian.utils import get_anonymous_user
from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.core.models import AuthenticatedSession
class AuthenticatedSessionSerializer(ModelSerializer):
"""AuthenticatedSession Serializer"""
class Meta:
model = AuthenticatedSession
fields = [
"uuid",
"user",
"last_ip",
"last_user_agent",
"last_used",
"expires",
]
class AuthenticatedSessionViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""AuthenticatedSession Viewset"""
queryset = AuthenticatedSession.objects.all()
serializer_class = AuthenticatedSessionSerializer
search_fields = ["user__username", "last_ip", "last_user_agent"]
filterset_fields = ["user__username", "last_ip", "last_user_agent"]
ordering = ["user__username"]
def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(user=user.pk)

View File

@ -0,0 +1,51 @@
# Generated by Django 3.2.3 on 2021-05-29 22:14
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import authentik.core.models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0021_alter_application_slug"),
]
operations = [
migrations.CreateModel(
name="AuthenticatedSession",
fields=[
(
"expires",
models.DateTimeField(
default=authentik.core.models.default_token_duration
),
),
("expiring", models.BooleanField(default=True)),
(
"uuid",
models.UUIDField(
default=uuid.uuid4, primary_key=True, serialize=False
),
),
("session_key", models.CharField(max_length=40)),
("last_ip", models.TextField()),
("last_user_agent", models.TextField(blank=True)),
("last_used", models.DateTimeField(auto_now=True)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
),
]

View File

@ -28,6 +28,7 @@ from authentik.flows.challenge import Challenge
from authentik.flows.models import Flow
from authentik.lib.config import CONFIG
from authentik.lib.models import CreatedUpdatedModel, SerializerModel
from authentik.lib.utils.http import get_client_ip
from authentik.managed.models import ManagedModel
from authentik.policies.models import PolicyBindingModel
@ -452,3 +453,33 @@ class PropertyMapping(SerializerModel, ManagedModel):
verbose_name = _("Property Mapping")
verbose_name_plural = _("Property Mappings")
class AuthenticatedSession(ExpiringModel):
"""Additional session class for authenticated users. Augments the standard django session
to achieve the following:
- Make it queryable by user
- Have a direct connection to user objects
- Allow users to view their own sessions and terminate them
- Save structured and well-defined information.
"""
uuid = models.UUIDField(default=uuid4, primary_key=True)
session_key = models.CharField(max_length=40)
user = models.ForeignKey(User, on_delete=models.CASCADE)
last_ip = models.TextField()
last_user_agent = models.TextField(blank=True)
last_used = models.DateTimeField(auto_now=True)
@staticmethod
def from_request(request: HttpRequest, user: User) -> "AuthenticatedSession":
"""Create a new session from a http request"""
return AuthenticatedSession(
session_key=request.session.session_key,
user=user,
last_ip=get_client_ip(request),
last_user_agent=request.META.get("HTTP_USER_AGENT", ""),
expires=request.session.get_expiry_date(),
)

View File

@ -1,9 +1,13 @@
"""authentik core signals"""
from typing import TYPE_CHECKING
from django.contrib.auth.signals import user_logged_in
from django.core.cache import cache
from django.core.signals import Signal
from django.db.models import Model
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.http.request import HttpRequest
from prometheus_client import Gauge
# Arguments: user: User, password: str
@ -13,6 +17,9 @@ GAUGE_MODELS = Gauge(
"authentik_models", "Count of various objects", ["model_name", "app"]
)
if TYPE_CHECKING:
from authentik.core.models import User
@receiver(post_save)
# pylint: disable=unused-argument
@ -33,3 +40,11 @@ def post_save_application(sender: type[Model], instance, created: bool, **_):
# Also delete user application cache
keys = cache.keys(user_app_cache_key("*"))
cache.delete_many(keys)
@receiver(user_logged_in)
def user_logged_in_session(sender, request: HttpRequest, user: "User", **_):
"""Create an AuthenticatedSession from request"""
from authentik.core.models import AuthenticatedSession
AuthenticatedSession.from_request(request, user).save()

View File

@ -1500,6 +1500,114 @@ paths:
description: Bad request
'403':
$ref: '#/components/schemas/GenericError'
/api/v2beta/core/authenticated_sessions/:
get:
operationId: core_authenticated_sessions_list
description: AuthenticatedSession Viewset
parameters:
- in: query
name: last_ip
schema:
type: string
- in: query
name: last_user_agent
schema:
type: string
- name: ordering
required: false
in: query
description: Which field to use when ordering the results.
schema:
type: string
- name: page
required: false
in: query
description: A page number within the paginated result set.
schema:
type: integer
- name: page_size
required: false
in: query
description: Number of results to return per page.
schema:
type: integer
- name: search
required: false
in: query
description: A search term.
schema:
type: string
- in: query
name: user__username
schema:
type: string
tags:
- core
security:
- authentik: []
- cookieAuth: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedAuthenticatedSessionList'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/api/v2beta/core/authenticated_sessions/{uuid}/:
get:
operationId: core_authenticated_sessions_retrieve
description: AuthenticatedSession Viewset
parameters:
- in: path
name: uuid
schema:
type: string
format: uuid
description: A UUID string identifying this authenticated session.
required: true
tags:
- core
security:
- authentik: []
- cookieAuth: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/AuthenticatedSession'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
delete:
operationId: core_authenticated_sessions_destroy
description: AuthenticatedSession Viewset
parameters:
- in: path
name: uuid
schema:
type: string
format: uuid
description: A UUID string identifying this authenticated session.
required: true
tags:
- core
security:
- authentik: []
- cookieAuth: []
responses:
'204':
description: No response body
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/api/v2beta/core/groups/:
get:
operationId: core_groups_list
@ -15454,6 +15562,30 @@ components:
If empty, user will not be able to configure this stage.
required:
- name
AuthenticatedSession:
type: object
description: AuthenticatedSession Serializer
properties:
uuid:
type: string
format: uuid
user:
type: integer
last_ip:
type: string
last_user_agent:
type: string
last_used:
type: string
format: date-time
readOnly: true
expires:
type: string
format: date-time
required:
- last_ip
- last_used
- user
AuthenticatorDuoChallenge:
type: object
description: Duo Challenge
@ -18894,6 +19026,41 @@ components:
required:
- pagination
- results
PaginatedAuthenticatedSessionList:
type: object
properties:
pagination:
type: object
properties:
next:
type: number
previous:
type: number
count:
type: number
current:
type: number
total_pages:
type: number
start_index:
type: number
end_index:
type: number
required:
- next
- previous
- count
- current
- total_pages
- start_index
- end_index
results:
type: array
items:
$ref: '#/components/schemas/AuthenticatedSession'
required:
- pagination
- results
PaginatedAuthenticatorDuoStageList:
type: object
properties: