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.authenticator_webauthn.api import AuthenticateWebAuthnStageViewSet
from authentik.stages.captcha.api import CaptchaStageViewSet from authentik.stages.captcha.api import CaptchaStageViewSet
from authentik.stages.consent.api import ConsentStageViewSet from authentik.stages.consent.api import ConsentStageViewSet
from authentik.stages.deny.api import DenyStageViewSet
from authentik.stages.dummy.api import DummyStageViewSet from authentik.stages.dummy.api import DummyStageViewSet
from authentik.stages.email.api import EmailStageViewSet from authentik.stages.email.api import EmailStageViewSet
from authentik.stages.identification.api import IdentificationStageViewSet 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/authenticator/webauthn", AuthenticateWebAuthnStageViewSet)
router.register("stages/captcha", CaptchaStageViewSet) router.register("stages/captcha", CaptchaStageViewSet)
router.register("stages/consent", ConsentStageViewSet) router.register("stages/consent", ConsentStageViewSet)
router.register("stages/deny", DenyStageViewSet)
router.register("stages/email", EmailStageViewSet) router.register("stages/email", EmailStageViewSet)
router.register("stages/identification", IdentificationStageViewSet) router.register("stages/identification", IdentificationStageViewSet)
router.register("stages/invitation/invitations", InvitationViewSet) router.register("stages/invitation/invitations", InvitationViewSet)

View File

@ -114,6 +114,7 @@ INSTALLED_APPS = [
"authentik.stages.authenticator_webauthn.apps.AuthentikStageAuthenticatorWebAuthnConfig", "authentik.stages.authenticator_webauthn.apps.AuthentikStageAuthenticatorWebAuthnConfig",
"authentik.stages.captcha.apps.AuthentikStageCaptchaConfig", "authentik.stages.captcha.apps.AuthentikStageCaptchaConfig",
"authentik.stages.consent.apps.AuthentikStageConsentConfig", "authentik.stages.consent.apps.AuthentikStageConsentConfig",
"authentik.stages.deny.apps.AuthentikStageDenyConfig",
"authentik.stages.dummy.apps.AuthentikStageDummyConfig", "authentik.stages.dummy.apps.AuthentikStageDummyConfig",
"authentik.stages.email.apps.AuthentikStageEmailConfig", "authentik.stages.email.apps.AuthentikStageEmailConfig",
"authentik.stages.identification.apps.AuthentikStageIdentificationConfig", "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 required: true
type: string type: string
format: uuid 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/: /stages/dummy/:
get: get:
operationId: stages_dummy_list operationId: stages_dummy_list
@ -9684,6 +9811,7 @@ definitions:
- authentik.stages.authenticator_webauthn - authentik.stages.authenticator_webauthn
- authentik.stages.captcha - authentik.stages.captcha
- authentik.stages.consent - authentik.stages.consent
- authentik.stages.deny
- authentik.stages.dummy - authentik.stages.dummy
- authentik.stages.email - authentik.stages.email
- authentik.stages.identification - authentik.stages.identification
@ -11206,6 +11334,38 @@ definitions:
description: 'Offset after which consent expires. (Format: hours=1;minutes=2;seconds=3).' description: 'Offset after which consent expires. (Format: hours=1;minutes=2;seconds=3).'
type: string type: string
minLength: 1 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: DummyStage:
description: DummyStage Serializer description: DummyStage Serializer
required: required:

View File

@ -1,5 +1,5 @@
--- ---
title: Next release title: Release 2021.1.3
--- ---
## Headline Changes ## 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. 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 ## Upgrading
This release does not introduce any new requirements. This release does not introduce any new requirements.