diff --git a/.github/workflows/gha-cache-cleanup.yml b/.github/workflows/gha-cache-cleanup.yml index 178d00cac..473625d1c 100644 --- a/.github/workflows/gha-cache-cleanup.yml +++ b/.github/workflows/gha-cache-cleanup.yml @@ -6,6 +6,10 @@ on: types: - closed +permissions: + # Permission to delete cache + actions: write + jobs: cleanup: runs-on: ubuntu-latest diff --git a/.github/workflows/translation-advice.yml b/.github/workflows/translation-advice.yml index 4de916bf0..ad76424fa 100644 --- a/.github/workflows/translation-advice.yml +++ b/.github/workflows/translation-advice.yml @@ -10,6 +10,10 @@ on: - "!locale/en/**" - "web/xliff/**" +permissions: + # Permission to write comment + pull-requests: write + jobs: post-comment: runs-on: ubuntu-latest diff --git a/.github/workflows/translation-rename.yml b/.github/workflows/translation-rename.yml index b2c947bda..7fe0a7ab5 100644 --- a/.github/workflows/translation-rename.yml +++ b/.github/workflows/translation-rename.yml @@ -6,6 +6,10 @@ on: pull_request: types: [opened, reopened] +permissions: + # Permission to rename PR + pull-requests: write + jobs: rename_pr: runs-on: ubuntu-latest diff --git a/authentik/flows/tests/test_executor.py b/authentik/flows/tests/test_executor.py index 4d5fb5c8b..dfca80517 100644 --- a/authentik/flows/tests/test_executor.py +++ b/authentik/flows/tests/test_executor.py @@ -472,6 +472,7 @@ class TestFlowExecutor(FlowTestCase): ident_stage = IdentificationStage.objects.create( name="ident", user_fields=[UserFields.E_MAIL], + pretend_user_exists=False, ) FlowStageBinding.objects.create( target=flow, diff --git a/authentik/lib/avatars.py b/authentik/lib/avatars.py index 8a6e2b9c1..3faa10376 100644 --- a/authentik/lib/avatars.py +++ b/authentik/lib/avatars.py @@ -154,7 +154,15 @@ def generate_avatar_from_name( def avatar_mode_generated(user: "User", mode: str) -> Optional[str]: """Wrapper that converts generated avatar to base64 svg""" - svg = generate_avatar_from_name(user.name if user.name.strip() != "" else "a k") + # By default generate based off of user's display name + name = user.name.strip() + if name == "": + # Fallback to username + name = user.username.strip() + # If we still don't have anything, fallback to `a k` + if name == "": + name = "a k" + svg = generate_avatar_from_name(name) return f"data:image/svg+xml;base64,{b64encode(svg.encode('utf-8')).decode('utf-8')}" diff --git a/authentik/stages/authenticator_totp/migrations/0010_alter_totpdevice_key.py b/authentik/stages/authenticator_totp/migrations/0010_alter_totpdevice_key.py index af007e4df..4ea982d87 100644 --- a/authentik/stages/authenticator_totp/migrations/0010_alter_totpdevice_key.py +++ b/authentik/stages/authenticator_totp/migrations/0010_alter_totpdevice_key.py @@ -29,4 +29,14 @@ class Migration(migrations.Migration): name="totpdevice", options={"verbose_name": "TOTP Device", "verbose_name_plural": "TOTP Devices"}, ), + migrations.AlterField( + model_name="authenticatortotpstage", + name="digits", + field=models.IntegerField( + choices=[ + ("6", "6 digits, widely compatible"), + ("8", "8 digits, not compatible with apps like Google Authenticator"), + ] + ), + ), ] diff --git a/authentik/stages/identification/api.py b/authentik/stages/identification/api.py index 0c1983f54..1f2ab5057 100644 --- a/authentik/stages/identification/api.py +++ b/authentik/stages/identification/api.py @@ -33,6 +33,7 @@ class IdentificationStageSerializer(StageSerializer): "passwordless_flow", "sources", "show_source_labels", + "pretend_user_exists", ] diff --git a/authentik/stages/identification/migrations/0014_identificationstage_pretend.py b/authentik/stages/identification/migrations/0014_identificationstage_pretend.py new file mode 100644 index 000000000..da6eab2e4 --- /dev/null +++ b/authentik/stages/identification/migrations/0014_identificationstage_pretend.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.7 on 2023-11-17 16:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "authentik_stages_identification", + "0002_auto_20200530_2204_squashed_0013_identificationstage_passwordless_flow", + ), + ] + + operations = [ + migrations.AddField( + model_name="identificationstage", + name="pretend_user_exists", + field=models.BooleanField( + default=True, + help_text="When enabled, the stage will succeed and continue even when incorrect user info is entered.", + ), + ), + ] diff --git a/authentik/stages/identification/models.py b/authentik/stages/identification/models.py index a9f65b878..8b8b3d1fd 100644 --- a/authentik/stages/identification/models.py +++ b/authentik/stages/identification/models.py @@ -54,6 +54,13 @@ class IdentificationStage(Stage): "entered will be shown" ), ) + pretend_user_exists = models.BooleanField( + default=True, + help_text=_( + "When enabled, the stage will succeed and continue even when incorrect user info " + "is entered." + ), + ) enrollment_flow = models.ForeignKey( Flow, diff --git a/authentik/stages/identification/stage.py b/authentik/stages/identification/stage.py index 3a6a5bd25..568030af8 100644 --- a/authentik/stages/identification/stage.py +++ b/authentik/stages/identification/stage.py @@ -121,8 +121,8 @@ class IdentificationChallengeResponse(ChallengeResponse): self.pre_user = self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER] if not current_stage.show_matched_user: self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER_IDENTIFIER] = uid_field - if self.stage.executor.flow.designation == FlowDesignation.RECOVERY: - # When used in a recovery flow, always continue to not disclose if a user exists + # when `pretend` is enabled, continue regardless + if current_stage.pretend_user_exists: return attrs raise ValidationError("Failed to authenticate.") self.pre_user = pre_user diff --git a/authentik/stages/identification/tests.py b/authentik/stages/identification/tests.py index dabdea050..375a9d04d 100644 --- a/authentik/stages/identification/tests.py +++ b/authentik/stages/identification/tests.py @@ -28,6 +28,7 @@ class TestIdentificationStage(FlowTestCase): self.stage = IdentificationStage.objects.create( name="identification", user_fields=[UserFields.E_MAIL], + pretend_user_exists=False, ) self.stage.sources.set([source]) self.stage.save() @@ -106,6 +107,26 @@ class TestIdentificationStage(FlowTestCase): form_data, ) self.assertEqual(response.status_code, 200) + self.assertStageResponse( + response, + self.flow, + component="ak-stage-identification", + response_errors={ + "non_field_errors": [{"string": "Failed to authenticate.", "code": "invalid"}] + }, + ) + + def test_invalid_with_username_pretend(self): + """Test invalid with username (user exists but stage only allows email)""" + self.stage.pretend_user_exists = True + self.stage.save() + form_data = {"uid_field": self.user.username} + response = self.client.post( + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), + form_data, + ) + self.assertEqual(response.status_code, 200) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) def test_invalid_no_fields(self): """Test invalid with username (no user fields are enabled)""" diff --git a/blueprints/schema.json b/blueprints/schema.json index bab793b70..423094b60 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -7425,6 +7425,11 @@ "show_source_labels": { "type": "boolean", "title": "Show source labels" + }, + "pretend_user_exists": { + "type": "boolean", + "title": "Pretend user exists", + "description": "When enabled, the stage will succeed and continue even when incorrect user info is entered." } }, "required": [] diff --git a/schema.yml b/schema.yml index d7c59dfec..c83edbc15 100644 --- a/schema.yml +++ b/schema.yml @@ -32013,6 +32013,10 @@ components: description: Specify which sources should be shown. show_source_labels: type: boolean + pretend_user_exists: + type: boolean + description: When enabled, the stage will succeed and continue even when + incorrect user info is entered. required: - component - meta_model_name @@ -32077,6 +32081,10 @@ components: description: Specify which sources should be shown. show_source_labels: type: boolean + pretend_user_exists: + type: boolean + description: When enabled, the stage will succeed and continue even when + incorrect user info is entered. required: - name InstallID: @@ -36560,6 +36568,10 @@ components: description: Specify which sources should be shown. show_source_labels: type: boolean + pretend_user_exists: + type: boolean + description: When enabled, the stage will succeed and continue even when + incorrect user info is entered. PatchedInvitationRequest: type: object description: Invitation Serializer diff --git a/web/src/admin/stages/identification/IdentificationStageForm.ts b/web/src/admin/stages/identification/IdentificationStageForm.ts index 769caaa76..6fad5fbb2 100644 --- a/web/src/admin/stages/identification/IdentificationStageForm.ts +++ b/web/src/admin/stages/identification/IdentificationStageForm.ts @@ -68,7 +68,7 @@ export class IdentificationStageForm extends ModelForm + return html` ${msg("Let the user identify themselves with their username or Email address.")} @@ -169,6 +169,26 @@ export class IdentificationStageForm extends ModelForm + + +

+ ${msg( + "When enabled, the stage will always accept the given user identifier and continue.", + )} +

+