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.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)
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
|
@ -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"
|
|
@ -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(),
|
||||||
|
}
|
|
@ -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,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")
|
|
@ -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()
|
|
@ -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
|
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:
|
||||||
|
|
|
@ -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.
|
Reference in New Issue