core: initial authenticated sessions
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
f51ab7a878
commit
133fc38c05
|
@ -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)
|
||||||
|
|
44
authentik/core/api/authenticated_sessions.py
Normal file
44
authentik/core/api/authenticated_sessions.py
Normal 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)
|
51
authentik/core/migrations/0022_authenticatedsession.py
Normal file
51
authentik/core/migrations/0022_authenticatedsession.py
Normal 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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -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(),
|
||||||
|
)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
167
schema.yml
167
schema.yml
|
@ -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:
|
||||||
|
|
Reference in a new issue