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.v2.config import ConfigView
from authentik.api.views import APIBrowserView from authentik.api.views import APIBrowserView
from authentik.core.api.applications import ApplicationViewSet 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.groups import GroupViewSet
from authentik.core.api.propertymappings import PropertyMappingViewSet from authentik.core.api.propertymappings import PropertyMappingViewSet
from authentik.core.api.providers import ProviderViewSet 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/system_tasks", TaskViewSet, basename="admin_system_tasks")
router.register("admin/apps", AppsViewSet, basename="apps") router.register("admin/apps", AppsViewSet, basename="apps")
router.register("core/authenticated_sessions", AuthenticatedSessionViewSet)
router.register("core/applications", ApplicationViewSet) router.register("core/applications", ApplicationViewSet)
router.register("core/groups", GroupViewSet) router.register("core/groups", GroupViewSet)
router.register("core/users", UserViewSet) 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.flows.models import Flow
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.models import CreatedUpdatedModel, SerializerModel from authentik.lib.models import CreatedUpdatedModel, SerializerModel
from authentik.lib.utils.http import get_client_ip
from authentik.managed.models import ManagedModel from authentik.managed.models import ManagedModel
from authentik.policies.models import PolicyBindingModel from authentik.policies.models import PolicyBindingModel
@ -452,3 +453,33 @@ class PropertyMapping(SerializerModel, ManagedModel):
verbose_name = _("Property Mapping") verbose_name = _("Property Mapping")
verbose_name_plural = _("Property Mappings") 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""" """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.cache import cache
from django.core.signals import Signal from django.core.signals import Signal
from django.db.models import Model from django.db.models import Model
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.http.request import HttpRequest
from prometheus_client import Gauge from prometheus_client import Gauge
# Arguments: user: User, password: str # Arguments: user: User, password: str
@ -13,6 +17,9 @@ GAUGE_MODELS = Gauge(
"authentik_models", "Count of various objects", ["model_name", "app"] "authentik_models", "Count of various objects", ["model_name", "app"]
) )
if TYPE_CHECKING:
from authentik.core.models import User
@receiver(post_save) @receiver(post_save)
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -33,3 +40,11 @@ def post_save_application(sender: type[Model], instance, created: bool, **_):
# Also delete user application cache # Also delete user application cache
keys = cache.keys(user_app_cache_key("*")) keys = cache.keys(user_app_cache_key("*"))
cache.delete_many(keys) 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 description: Bad request
'403': '403':
$ref: '#/components/schemas/GenericError' $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/: /api/v2beta/core/groups/:
get: get:
operationId: core_groups_list operationId: core_groups_list
@ -15454,6 +15562,30 @@ components:
If empty, user will not be able to configure this stage. If empty, user will not be able to configure this stage.
required: required:
- name - 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: AuthenticatorDuoChallenge:
type: object type: object
description: Duo Challenge description: Duo Challenge
@ -18894,6 +19026,41 @@ components:
required: required:
- pagination - pagination
- results - 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: PaginatedAuthenticatorDuoStageList:
type: object type: object
properties: properties: