stages/deny: add deny stage
This commit is contained in:
parent
ed8b78600e
commit
2ae5a81c15
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
0
authentik/stages/deny/__init__.py
Normal file
0
authentik/stages/deny/__init__.py
Normal file
21
authentik/stages/deny/api.py
Normal file
21
authentik/stages/deny/api.py
Normal 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
|
10
authentik/stages/deny/apps.py
Normal file
10
authentik/stages/deny/apps.py
Normal 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"
|
16
authentik/stages/deny/forms.py
Normal file
16
authentik/stages/deny/forms.py
Normal 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(),
|
||||
}
|
37
authentik/stages/deny/migrations/0001_initial.py
Normal file
37
authentik/stages/deny/migrations/0001_initial.py
Normal 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",),
|
||||
),
|
||||
]
|
0
authentik/stages/deny/migrations/__init__.py
Normal file
0
authentik/stages/deny/migrations/__init__.py
Normal file
36
authentik/stages/deny/models.py
Normal file
36
authentik/stages/deny/models.py
Normal 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")
|
15
authentik/stages/deny/stage.py
Normal file
15
authentik/stages/deny/stage.py
Normal 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()
|
50
authentik/stages/deny/tests.py
Normal file
50
authentik/stages/deny/tests.py
Normal 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)
|
160
swagger.yaml
160
swagger.yaml
|
@ -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:
|
||||
|
|
|
@ -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.
|
Reference in a new issue