Merge branch 'version-2021.5' into next

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

# Conflicts:
#	authentik/stages/authenticator_static/api.py
#	swagger.yaml
This commit is contained in:
Jens Langhammer 2021-05-21 21:33:18 +02:00
commit 1a0f72d0a8
23 changed files with 176 additions and 227 deletions

View File

@ -43,7 +43,7 @@ def token_from_header(raw_header: bytes) -> Optional[Token]:
return tokens.first()
class AuthentikTokenAuthentication(BaseAuthentication):
class TokenAuthentication(BaseAuthentication):
"""Token-based authentication using HTTP Bearer authentication"""
def authenticate(self, request: Request) -> Union[tuple[User, Any], None]:
@ -61,7 +61,7 @@ class AuthentikTokenAuthentication(BaseAuthentication):
class TokenSchema(OpenApiAuthenticationExtension):
"""Auth schema"""
target_class = AuthentikTokenAuthentication
target_class = TokenAuthentication
name = "authentik"
def get_security_definition(self, auto_schema):

View File

@ -0,0 +1,35 @@
"""API Authorization"""
from django.db.models import Model
from django.db.models.query import QuerySet
from rest_framework.filters import BaseFilterBackend
from rest_framework.permissions import BasePermission
from rest_framework.request import Request
class OwnerFilter(BaseFilterBackend):
"""Filter objects by their owner"""
owner_key = "user"
def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
return queryset.filter(**{self.owner_key: request.user})
class OwnerPermissions(BasePermission):
"""Authorize requests by an object's owner matching the requesting user"""
owner_key = "user"
def has_permission(self, request: Request, view) -> bool:
"""If the user is authenticated, we allow all requests here. For listing, the
object-level permissions are done by the filter backend"""
return request.user.is_authenticated
def has_object_permission(self, request: Request, view, obj: Model) -> bool:
"""Check if the object's owner matches the currently logged in user"""
if not hasattr(obj, self.owner_key):
return False
owner = getattr(obj, self.owner_key)
if owner != request.user:
return False
return True

View File

@ -5,7 +5,7 @@ from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
from rest_framework.exceptions import AuthenticationFailed
from authentik.api.auth import token_from_header
from authentik.api.authentication import token_from_header
from authentik.core.models import Token, TokenIntents

View File

@ -161,9 +161,19 @@ router.register("propertymappings/scope", ScopeMappingViewSet)
router.register("authenticators/static", StaticDeviceViewSet)
router.register("authenticators/totp", TOTPDeviceViewSet)
router.register("authenticators/webauthn", WebAuthnDeviceViewSet)
router.register("authenticators/admin/static", StaticAdminDeviceViewSet)
router.register("authenticators/admin/totp", TOTPAdminDeviceViewSet)
router.register("authenticators/admin/webauthn", WebAuthnAdminDeviceViewSet)
router.register(
"authenticators/admin/static",
StaticAdminDeviceViewSet,
basename="admin-staticdevice",
)
router.register(
"authenticators/admin/totp", TOTPAdminDeviceViewSet, basename="admin-totpdevice"
)
router.register(
"authenticators/admin/webauthn",
WebAuthnAdminDeviceViewSet,
basename="admin-webauthndevice",
)
router.register("stages/all", StageViewSet)
router.register("stages/authenticator/static", AuthenticatorStaticStageViewSet)

View File

@ -78,7 +78,7 @@ class PropertyMappingViewSet(
filterset_fields = {"managed": ["isnull"]}
ordering = ["name"]
def get_queryset(self):
def get_queryset(self): # pragma: no cover
return PropertyMapping.objects.select_subclasses()
@extend_schema(responses={200: TypeCreateSerializer(many=True)})

View File

@ -63,7 +63,7 @@ class ProviderViewSet(
"application__name",
]
def get_queryset(self):
def get_queryset(self): # pragma: no cover
return Provider.objects.select_subclasses()
@extend_schema(responses={200: TypeCreateSerializer(many=True)})

View File

@ -61,7 +61,7 @@ class SourceViewSet(
serializer_class = SourceSerializer
lookup_field = "slug"
def get_queryset(self):
def get_queryset(self): # pragma: no cover
return Source.objects.select_subclasses()
@extend_schema(responses={200: TypeCreateSerializer(many=True)})

View File

@ -139,7 +139,7 @@ class UserViewSet(ModelViewSet):
search_fields = ["username", "name", "is_active"]
filterset_class = UsersFilter
def get_queryset(self):
def get_queryset(self): # pragma: no cover
return User.objects.all().exclude(pk=get_anonymous_user().pk)
@extend_schema(responses={200: SessionUserSerializer(many=False)})

View File

@ -4,7 +4,7 @@ from channels.generic.websocket import JsonWebsocketConsumer
from rest_framework.exceptions import AuthenticationFailed
from structlog.stdlib import get_logger
from authentik.api.auth import token_from_header
from authentik.api.authentication import token_from_header
from authentik.core.models import User
LOGGER = get_logger()

View File

@ -1,12 +1,12 @@
"""Notification API Views"""
from django_filters.rest_framework import DjangoFilterBackend
from guardian.utils import get_anonymous_user
from rest_framework import mixins
from rest_framework.fields import ReadOnlyField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.events.api.event import EventSerializer
from authentik.events.models import Notification
@ -49,12 +49,5 @@ class NotificationViewSet(
"event",
"seen",
]
filter_backends = [
DjangoFilterBackend,
OrderingFilter,
SearchFilter,
]
def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
return Notification.objects.filter(user=user.pk)
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]

View File

@ -65,7 +65,7 @@ class StageViewSet(
search_fields = ["name"]
filterset_fields = ["name"]
def get_queryset(self):
def get_queryset(self): # pragma: no cover
return Stage.objects.select_subclasses()
@extend_schema(responses={200: TypeCreateSerializer(many=True)})

View File

@ -2,28 +2,6 @@
from django.http import HttpRequest
from django.template.response import TemplateResponse
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView
from guardian.shortcuts import assign_perm
class CreateAssignPermView(CreateView):
"""Assign permissions to object after creation"""
permissions = [
"%s.view_%s",
"%s.change_%s",
"%s.delete_%s",
]
def form_valid(self, form):
response = super().form_valid(form)
for permission in self.permissions:
full_permission = permission % (
self.object._meta.app_label,
self.object._meta.model_name,
)
assign_perm(full_permission, self.request.user, self.object)
return response
def bad_request_message(

View File

@ -92,7 +92,7 @@ class PolicyViewSet(
}
search_fields = ["name"]
def get_queryset(self):
def get_queryset(self): # pragma: no cover
return Policy.objects.select_subclasses().prefetch_related(
"bindings", "promptstage_set"
)

View File

@ -174,7 +174,7 @@ REST_FRAMEWORK = {
"rest_framework.permissions.DjangoObjectPermissions",
),
"DEFAULT_AUTHENTICATION_CLASSES": (
"authentik.api.auth.AuthentikTokenAuthentication",
"authentik.api.authentication.TokenAuthentication",
"rest_framework.authentication.SessionAuthentication",
),
"DEFAULT_RENDERER_CLASSES": [

View File

@ -1,9 +1,10 @@
"""OAuth Source Serializer"""
from django_filters.rest_framework import DjangoFilterBackend
from guardian.utils import get_anonymous_user
from rest_framework import mixins
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.viewsets import ModelViewSet
from rest_framework.viewsets import GenericViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.sources import SourceSerializer
from authentik.sources.oauth.models import UserOAuthSourceConnection
@ -21,20 +22,17 @@ class UserOAuthSourceConnectionSerializer(SourceSerializer):
]
class UserOAuthSourceConnectionViewSet(ModelViewSet):
class UserOAuthSourceConnectionViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""Source Viewset"""
queryset = UserOAuthSourceConnection.objects.all()
serializer_class = UserOAuthSourceConnectionSerializer
filterset_fields = ["source__slug"]
filter_backends = [
DjangoFilterBackend,
OrderingFilter,
SearchFilter,
]
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)
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]

View File

@ -1,12 +1,13 @@
"""AuthenticatorStaticStage API Views"""
from django_filters.rest_framework import DjangoFilterBackend
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
from guardian.utils import get_anonymous_user
from rest_framework import mixins
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import IsAdminUser
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.flows.api.stages import StageSerializer
from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
@ -47,23 +48,22 @@ class StaticDeviceSerializer(ModelSerializer):
fields = ["name", "token_set", "pk"]
class StaticDeviceViewSet(ModelViewSet):
class StaticDeviceViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""Viewset for static authenticator devices"""
queryset = StaticDevice.objects.none()
queryset = StaticDevice.objects.all()
serializer_class = StaticDeviceSerializer
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
search_fields = ["name"]
filterset_fields = ["name"]
ordering = ["name"]
filter_backends = [
DjangoFilterBackend,
OrderingFilter,
SearchFilter,
]
def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
return StaticDevice.objects.filter(user=user.pk)
class StaticAdminDeviceViewSet(ReadOnlyModelViewSet):

View File

@ -0,0 +1,20 @@
"""Test Static API"""
from django.urls import reverse
from django_otp.plugins.otp_static.models import StaticDevice
from rest_framework.test import APITestCase
from authentik.core.models import User
class AuthenticatorStaticStage(APITestCase):
"""Test Static API"""
def test_api_delete(self):
"""Test api delete"""
user = User.objects.create(username="foo")
self.client.force_login(user)
dev = StaticDevice.objects.create(user=user)
response = self.client.delete(
reverse("authentik_api:staticdevice-detail", kwargs={"pk": dev.pk})
)
self.assertEqual(response.status_code, 204)

View File

@ -1,12 +1,13 @@
"""AuthenticatorTOTPStage API Views"""
from django_filters.rest_framework import DjangoFilterBackend
from django_filters.rest_framework.backends import DjangoFilterBackend
from django_otp.plugins.otp_totp.models import TOTPDevice
from guardian.utils import get_anonymous_user
from rest_framework import mixins
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import IsAdminUser
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.flows.api.stages import StageSerializer
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
@ -40,23 +41,22 @@ class TOTPDeviceSerializer(ModelSerializer):
depth = 2
class TOTPDeviceViewSet(ModelViewSet):
class TOTPDeviceViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""Viewset for totp authenticator devices"""
queryset = TOTPDevice.objects.none()
queryset = TOTPDevice.objects.all()
serializer_class = TOTPDeviceSerializer
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
search_fields = ["name"]
filterset_fields = ["name"]
ordering = ["name"]
filter_backends = [
DjangoFilterBackend,
OrderingFilter,
SearchFilter,
]
def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
return TOTPDevice.objects.filter(user=user.pk)
class TOTPAdminDeviceViewSet(ReadOnlyModelViewSet):

View File

@ -0,0 +1,20 @@
"""Test TOTP API"""
from django.urls import reverse
from django_otp.plugins.otp_totp.models import TOTPDevice
from rest_framework.test import APITestCase
from authentik.core.models import User
class AuthenticatorTOTPStage(APITestCase):
"""Test TOTP API"""
def test_api_delete(self):
"""Test api delete"""
user = User.objects.create(username="foo")
self.client.force_login(user)
dev = TOTPDevice.objects.create(user=user)
response = self.client.delete(
reverse("authentik_api:totpdevice-detail", kwargs={"pk": dev.pk})
)
self.assertEqual(response.status_code, 204)

View File

@ -1,11 +1,12 @@
"""AuthenticateWebAuthnStage API Views"""
from django_filters.rest_framework import DjangoFilterBackend
from guardian.utils import get_anonymous_user
from django_filters.rest_framework.backends import DjangoFilterBackend
from rest_framework import mixins
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import IsAdminUser
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.flows.api.stages import StageSerializer
from authentik.stages.authenticator_webauthn.models import (
AuthenticateWebAuthnStage,
@ -39,23 +40,22 @@ class WebAuthnDeviceSerializer(ModelSerializer):
depth = 2
class WebAuthnDeviceViewSet(ModelViewSet):
class WebAuthnDeviceViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""Viewset for WebAuthn authenticator devices"""
queryset = WebAuthnDevice.objects.none()
queryset = WebAuthnDevice.objects.all()
serializer_class = WebAuthnDeviceSerializer
search_fields = ["name"]
filterset_fields = ["name"]
ordering = ["name"]
filter_backends = [
DjangoFilterBackend,
OrderingFilter,
SearchFilter,
]
def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
return WebAuthnDevice.objects.filter(user=user.pk)
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
class WebAuthnAdminDeviceViewSet(ReadOnlyModelViewSet):

View File

@ -0,0 +1,20 @@
"""Test WebAuthn API"""
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import User
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
class AuthenticatorWebAuthnStage(APITestCase):
"""Test WebAuthn API"""
def test_api_delete(self):
"""Test api delete"""
user = User.objects.create(username="foo")
self.client.force_login(user)
dev = WebAuthnDevice.objects.create(user=user)
response = self.client.delete(
reverse("authentik_api:webauthndevice-detail", kwargs={"pk": dev.pk})
)
self.assertEqual(response.status_code, 204)

View File

@ -1,5 +1,5 @@
"""dummy tests"""
from django.test import Client, TestCase
from django.test import TestCase
from django.urls import reverse
from django.utils.encoding import force_str
@ -14,7 +14,6 @@ class TestDummyStage(TestCase):
def setUp(self):
super().setUp()
self.user = User.objects.create(username="unittest", email="test@beryju.org")
self.client = Client()
self.flow = Flow.objects.create(
name="test-dummy",

View File

@ -444,37 +444,6 @@ paths:
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
post:
operationId: authenticators_static_create
description: Viewset for static authenticator devices
tags:
- authenticators
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/StaticDeviceRequest'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/StaticDeviceRequest'
multipart/form-data:
schema:
$ref: '#/components/schemas/StaticDeviceRequest'
required: true
security:
- authentik: []
- cookieAuth: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/StaticDevice'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/api/v2beta/authenticators/static/{id}/:
get:
operationId: authenticators_static_retrieve
@ -648,37 +617,6 @@ paths:
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
post:
operationId: authenticators_totp_create
description: Viewset for totp authenticator devices
tags:
- authenticators
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TOTPDeviceRequest'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/TOTPDeviceRequest'
multipart/form-data:
schema:
$ref: '#/components/schemas/TOTPDeviceRequest'
required: true
security:
- authentik: []
- cookieAuth: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/TOTPDevice'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/api/v2beta/authenticators/totp/{id}/:
get:
operationId: authenticators_totp_retrieve
@ -852,37 +790,6 @@ paths:
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
post:
operationId: authenticators_webauthn_create
description: Viewset for WebAuthn authenticator devices
tags:
- authenticators
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/WebAuthnDeviceRequest'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/WebAuthnDeviceRequest'
multipart/form-data:
schema:
$ref: '#/components/schemas/WebAuthnDeviceRequest'
required: true
security:
- authentik: []
- cookieAuth: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/WebAuthnDevice'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/api/v2beta/authenticators/webauthn/{id}/:
get:
operationId: authenticators_webauthn_retrieve
@ -10117,37 +10024,6 @@ paths:
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
post:
operationId: sources_oauth_user_connections_create
description: Source Viewset
tags:
- sources
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UserOAuthSourceConnectionRequest'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/UserOAuthSourceConnectionRequest'
multipart/form-data:
schema:
$ref: '#/components/schemas/UserOAuthSourceConnectionRequest'
required: true
security:
- authentik: []
- cookieAuth: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/UserOAuthSourceConnection'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/api/v2beta/sources/oauth_user_connections/{id}/:
get:
operationId: sources_oauth_user_connections_retrieve