stages/deny: add deny stage

This commit is contained in:
Jens Langhammer 2021-03-01 20:16:54 +01:00
parent ed8b78600e
commit 2ae5a81c15
13 changed files with 356 additions and 1 deletions

View File

@ -59,6 +59,7 @@ from authentik.stages.authenticator_validate.api import (
from authentik.stages.authenticator_webauthn.api import AuthenticateWebAuthnStageViewSet
from authentik.stages.captcha.api import CaptchaStageViewSet
from authentik.stages.consent.api import ConsentStageViewSet
from authentik.stages.deny.api import DenyStageViewSet
from authentik.stages.dummy.api import DummyStageViewSet
from authentik.stages.email.api import EmailStageViewSet
from authentik.stages.identification.api import IdentificationStageViewSet
@ -135,6 +136,7 @@ router.register("stages/authenticator/validate", AuthenticatorValidateStageViewS
router.register("stages/authenticator/webauthn", AuthenticateWebAuthnStageViewSet)
router.register("stages/captcha", CaptchaStageViewSet)
router.register("stages/consent", ConsentStageViewSet)
router.register("stages/deny", DenyStageViewSet)
router.register("stages/email", EmailStageViewSet)
router.register("stages/identification", IdentificationStageViewSet)
router.register("stages/invitation/invitations", InvitationViewSet)

View File

@ -114,6 +114,7 @@ INSTALLED_APPS = [
"authentik.stages.authenticator_webauthn.apps.AuthentikStageAuthenticatorWebAuthnConfig",
"authentik.stages.captcha.apps.AuthentikStageCaptchaConfig",
"authentik.stages.consent.apps.AuthentikStageConsentConfig",
"authentik.stages.deny.apps.AuthentikStageDenyConfig",
"authentik.stages.dummy.apps.AuthentikStageDummyConfig",
"authentik.stages.email.apps.AuthentikStageEmailConfig",
"authentik.stages.identification.apps.AuthentikStageIdentificationConfig",

View File

View File

@ -0,0 +1,21 @@
"""deny Stage API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.flows.api.stages import StageSerializer
from authentik.stages.deny.models import DenyStage
class DenyStageSerializer(StageSerializer):
"""DenyStage Serializer"""
class Meta:
model = DenyStage
fields = StageSerializer.Meta.fields
class DenyStageViewSet(ModelViewSet):
"""DenyStage Viewset"""
queryset = DenyStage.objects.all()
serializer_class = DenyStageSerializer

View File

@ -0,0 +1,10 @@
"""authentik deny stage app config"""
from django.apps import AppConfig
class AuthentikStageDenyConfig(AppConfig):
"""authentik deny stage config"""
name = "authentik.stages.deny"
label = "authentik_stages_deny"
verbose_name = "authentik Stages.Deny"

View File

@ -0,0 +1,16 @@
"""authentik flows deny forms"""
from django import forms
from authentik.stages.deny.models import DenyStage
class DenyStageForm(forms.ModelForm):
"""Form to create/edit DenyStage instances"""
class Meta:
model = DenyStage
fields = ["name"]
widgets = {
"name": forms.TextInput(),
}

View File

@ -0,0 +1,37 @@
# Generated by Django 3.1.7 on 2021-03-01 18:59
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("authentik_flows", "0016_auto_20201202_1307"),
]
operations = [
migrations.CreateModel(
name="DenyStage",
fields=[
(
"stage_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_flows.stage",
),
),
],
options={
"verbose_name": "Deny Stage",
"verbose_name_plural": "Deny Stages",
},
bases=("authentik_flows.stage",),
),
]

View File

@ -0,0 +1,36 @@
"""deny stage models"""
from typing import Type
from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.serializers import BaseSerializer
from authentik.flows.models import Stage
class DenyStage(Stage):
"""Cancells the current flow."""
@property
def serializer(self) -> BaseSerializer:
from authentik.stages.deny.api import DenyStageSerializer
return DenyStageSerializer
@property
def type(self) -> Type[View]:
from authentik.stages.deny.stage import DenyStageView
return DenyStageView
@property
def form(self) -> Type[ModelForm]:
from authentik.stages.deny.forms import DenyStageForm
return DenyStageForm
class Meta:
verbose_name = _("Deny Stage")
verbose_name_plural = _("Deny Stages")

View File

@ -0,0 +1,15 @@
"""Deny stage logic"""
from django.http import HttpRequest, HttpResponse
from structlog.stdlib import get_logger
from authentik.flows.stage import StageView
LOGGER = get_logger()
class DenyStageView(StageView):
"""Cancells the current flow"""
def get(self, request: HttpRequest) -> HttpResponse:
"""Cancells the current flow"""
return self.executor.stage_invalid()

View File

@ -0,0 +1,50 @@
"""deny tests"""
from django.test import Client, TestCase
from django.urls import reverse
from django.utils.encoding import force_str
from authentik.core.models import User
from authentik.flows.markers import StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import FlowPlan
from authentik.flows.views import SESSION_KEY_PLAN
from authentik.stages.deny.forms import DenyStageForm
from authentik.stages.deny.models import DenyStage
class TestUserDenyStage(TestCase):
"""Deny tests"""
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-logout",
slug="test-logout",
designation=FlowDesignation.AUTHENTICATION,
)
self.stage = DenyStage.objects.create(name="logout")
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
def test_valid_password(self):
"""Test with a valid pending user and backend"""
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
)
self.assertEqual(response.status_code, 200)
self.assertIn("Permission denied", force_str(response.content))
def test_form(self):
"""Test Form"""
data = {"name": "test"}
self.assertEqual(DenyStageForm(data).is_valid(), True)

View File

@ -6469,6 +6469,133 @@ paths:
required: true
type: string
format: uuid
/stages/deny/:
get:
operationId: stages_deny_list
description: DenyStage Viewset
parameters:
- name: ordering
in: query
description: Which field to use when ordering the results.
required: false
type: string
- name: search
in: query
description: A search term.
required: false
type: string
- name: page
in: query
description: A page number within the paginated result set.
required: false
type: integer
- name: page_size
in: query
description: Number of results to return per page.
required: false
type: integer
responses:
'200':
description: ''
schema:
required:
- count
- results
type: object
properties:
count:
type: integer
next:
type: string
format: uri
x-nullable: true
previous:
type: string
format: uri
x-nullable: true
results:
type: array
items:
$ref: '#/definitions/DenyStage'
tags:
- stages
post:
operationId: stages_deny_create
description: DenyStage Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/DenyStage'
responses:
'201':
description: ''
schema:
$ref: '#/definitions/DenyStage'
tags:
- stages
parameters: []
/stages/deny/{stage_uuid}/:
get:
operationId: stages_deny_read
description: DenyStage Viewset
parameters: []
responses:
'200':
description: ''
schema:
$ref: '#/definitions/DenyStage'
tags:
- stages
put:
operationId: stages_deny_update
description: DenyStage Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/DenyStage'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/DenyStage'
tags:
- stages
patch:
operationId: stages_deny_partial_update
description: DenyStage Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/DenyStage'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/DenyStage'
tags:
- stages
delete:
operationId: stages_deny_delete
description: DenyStage Viewset
parameters: []
responses:
'204':
description: ''
tags:
- stages
parameters:
- name: stage_uuid
in: path
description: A UUID string identifying this Deny Stage.
required: true
type: string
format: uuid
/stages/dummy/:
get:
operationId: stages_dummy_list
@ -9684,6 +9811,7 @@ definitions:
- authentik.stages.authenticator_webauthn
- authentik.stages.captcha
- authentik.stages.consent
- authentik.stages.deny
- authentik.stages.dummy
- authentik.stages.email
- authentik.stages.identification
@ -11206,6 +11334,38 @@ definitions:
description: 'Offset after which consent expires. (Format: hours=1;minutes=2;seconds=3).'
type: string
minLength: 1
DenyStage:
description: DenyStage Serializer
required:
- name
type: object
properties:
pk:
title: Stage uuid
type: string
format: uuid
readOnly: true
name:
title: Name
type: string
minLength: 1
object_type:
title: Object type
type: string
readOnly: true
verbose_name:
title: Verbose name
type: string
readOnly: true
verbose_name_plural:
title: Verbose name plural
type: string
readOnly: true
flow_set:
description: ''
type: array
items:
$ref: '#/definitions/Flow'
DummyStage:
description: DummyStage Serializer
required:

View File

@ -1,5 +1,5 @@
---
title: Next release
title: Release 2021.1.3
---
## Headline Changes
@ -32,6 +32,13 @@ title: Next release
It also allows other services to use the flow executor via an API, which will be used by the outpost further down the road.
- Deny stage
A new stage which simply denies access. This can be used to conditionally deny access to users during a flow. Authorization flows for example required an authenticated user, but there was no previous way to block access for un-authenticated users.
If you conditionally include this stage in a flow, make sure to disable "Evaluate on plan", as that will always include the stage in the flow, irregardless of the inputs.
## Upgrading
This release does not introduce any new requirements.