diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 4c12f4df4..bd6009b9f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,7 +4,7 @@ Please check the [Contributing guidelines](https://github.com/goauthentik/authentik/blob/main/CONTRIBUTING.md#how-can-i-contribute). --> -# Details +## Details - **Does this resolve an issue?** Resolves # diff --git a/authentik/stages/identification/api.py b/authentik/stages/identification/api.py index d55f94911..0c1983f54 100644 --- a/authentik/stages/identification/api.py +++ b/authentik/stages/identification/api.py @@ -1,4 +1,6 @@ """Identification Stage API Views""" +from django.utils.translation import gettext_lazy as _ +from rest_framework.exceptions import ValidationError from rest_framework.viewsets import ModelViewSet from authentik.core.api.used_by import UsedByMixin @@ -9,6 +11,16 @@ from authentik.stages.identification.models import IdentificationStage class IdentificationStageSerializer(StageSerializer): """IdentificationStage Serializer""" + def validate(self, attrs: dict) -> dict: + # Check that at least 1 source is selected when no user fields are selected. + sources = attrs.get("sources", []) + user_fields = attrs.get("user_fields", []) + if len(user_fields) < 1 and len(sources) < 1: + raise ValidationError( + _("When no user fields are selected, at least one source must be selected") + ) + return super().validate(attrs) + class Meta: model = IdentificationStage fields = StageSerializer.Meta.fields + [ diff --git a/authentik/stages/identification/tests.py b/authentik/stages/identification/tests.py index 2b6481c88..cdd7bf0e9 100644 --- a/authentik/stages/identification/tests.py +++ b/authentik/stages/identification/tests.py @@ -1,11 +1,14 @@ """identification tests""" from django.urls import reverse +from rest_framework.exceptions import ValidationError from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.flows.challenge import ChallengeTypes from authentik.flows.models import FlowDesignation, FlowStageBinding from authentik.flows.tests import FlowTestCase +from authentik.lib.generators import generate_id from authentik.sources.oauth.models import OAuthSource +from authentik.stages.identification.api import IdentificationStageSerializer from authentik.stages.identification.models import IdentificationStage, UserFields from authentik.stages.password import BACKEND_INBUILT from authentik.stages.password.models import PasswordStage @@ -222,3 +225,22 @@ class TestIdentificationStage(FlowTestCase): } ], ) + + def test_api_validate(self): + """Test API validation""" + self.assertTrue( + IdentificationStageSerializer( + data={ + "name": generate_id(), + "user_fields": [UserFields.E_MAIL, UserFields.USERNAME], + } + ).is_valid(raise_exception=True) + ) + with self.assertRaises(ValidationError): + IdentificationStageSerializer( + data={ + "name": generate_id(), + "user_fields": [], + "sources": [], + } + ).is_valid(raise_exception=True) diff --git a/web/src/admin/events/RuleForm.ts b/web/src/admin/events/RuleForm.ts index 57e059af0..52cb1f1cc 100644 --- a/web/src/admin/events/RuleForm.ts +++ b/web/src/admin/events/RuleForm.ts @@ -1,4 +1,4 @@ -import { SeverityToLabel } from "@goauthentik/admin/events/RuleListPage"; +import { SeverityToLabel } from "@goauthentik/admin/events/utils"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import "@goauthentik/elements/forms/HorizontalFormElement"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; diff --git a/web/src/admin/events/RuleListPage.ts b/web/src/admin/events/RuleListPage.ts index 95c5047af..475cdb04f 100644 --- a/web/src/admin/events/RuleListPage.ts +++ b/web/src/admin/events/RuleListPage.ts @@ -1,4 +1,5 @@ import "@goauthentik/admin/events/RuleForm"; +import { SeverityToLabel } from "@goauthentik/admin/events/utils"; import "@goauthentik/admin/policies/BoundPoliciesList"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { uiConfig } from "@goauthentik/common/ui/config"; @@ -14,20 +15,7 @@ import { t } from "@lingui/macro"; import { TemplateResult, html } from "lit"; import { customElement, property } from "lit/decorators.js"; -import { EventsApi, NotificationRule, SeverityEnum } from "@goauthentik/api"; - -export function SeverityToLabel(severity: SeverityEnum | null | undefined): string { - if (!severity) return t`Unknown severity`; - switch (severity) { - case SeverityEnum.Alert: - return t`Alert`; - case SeverityEnum.Notice: - return t`Notice`; - case SeverityEnum.Warning: - return t`Warning`; - } - return t`Unknown severity`; -} +import { EventsApi, NotificationRule } from "@goauthentik/api"; @customElement("ak-event-rule-list") export class RuleListPage extends TablePage { diff --git a/web/src/admin/events/utils.ts b/web/src/admin/events/utils.ts index 27e3f921e..2dd5f59bf 100644 --- a/web/src/admin/events/utils.ts +++ b/web/src/admin/events/utils.ts @@ -5,7 +5,7 @@ import { t } from "@lingui/macro"; import { TemplateResult, html } from "lit"; -import { EventActions } from "@goauthentik/api"; +import { EventActions, SeverityEnum } from "@goauthentik/api"; export function EventGeo(event: EventWithContext): TemplateResult { let geo: KeyUnknown | undefined = undefined; @@ -78,3 +78,16 @@ export function ActionToLabel(action?: EventActions): string { return action; } } + +export function SeverityToLabel(severity: SeverityEnum | null | undefined): string { + if (!severity) return t`Unknown severity`; + switch (severity) { + case SeverityEnum.Alert: + return t`Alert`; + case SeverityEnum.Notice: + return t`Notice`; + case SeverityEnum.Warning: + return t`Warning`; + } + return t`Unknown severity`; +} diff --git a/web/src/flow/stages/identification/IdentificationStage.ts b/web/src/flow/stages/identification/IdentificationStage.ts index 1cb8de5c5..11e729293 100644 --- a/web/src/flow/stages/identification/IdentificationStage.ts +++ b/web/src/flow/stages/identification/IdentificationStage.ts @@ -66,6 +66,23 @@ export class IdentificationStage extends BaseStage< } firstUpdated(): void { + this.autoRedirect(); + this.createHelperForm(); + } + + autoRedirect(): void { + if (!this.challenge) return; + // we only want to auto-redirect to a source if there's only one source + if (this.challenge.sources?.length !== 1) return; + // and we also only do an auto-redirect if no user fields are select + // meaning that without the auto-redirect the user would only have the option + // to manually click on the source button + if ((this.challenge.userFields || []).length !== 0) return; + const source = this.challenge.sources[0]; + this.host.challenge = source.challenge; + } + + createHelperForm(): void { this.form = document.createElement("form"); document.documentElement.appendChild(this.form); // Only add the additional username input if we're in a shadow dom @@ -206,7 +223,6 @@ export class IdentificationStage extends BaseStage< class="pf-c-form__group" .errors=${(this.challenge.responseErrors || {})["uid_field"]} > -