From aecd4b52ef05bdb53b3e01dc9b1443ab7eb79f4e Mon Sep 17 00:00:00 2001 From: Tana M Berry Date: Fri, 17 Nov 2023 12:23:05 -0600 Subject: [PATCH 01/21] website/docs: fixed formatting on tabs (#7609) * fixed formatting on tabs * sync tabs and use same code blocks for both Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer Co-authored-by: Tana Berry Co-authored-by: Jens Langhammer --- website/docs/installation/configuration.mdx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/website/docs/installation/configuration.mdx b/website/docs/installation/configuration.mdx index f2004d316..28d8e5f50 100644 --- a/website/docs/installation/configuration.mdx +++ b/website/docs/installation/configuration.mdx @@ -20,18 +20,22 @@ All of these variables can be set to values, but you can also use a URI-like for import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; - + If you are using Docker Compose, edit your .env file to append any keys that you want to add, and then run the following command to apply them: + ``` docker-compose up -d + ``` If you are running in Kubernetes, edit your values.yaml file to append any keys that you want to add, and then run the following commands to apply: + ``` helm repo update helm upgrade --install authentik authentik/authentik -f values.yaml + ``` @@ -41,15 +45,19 @@ import TabItem from "@theme/TabItem"; To check if your config has been applied correctly, you can run the following command to output the full config: - + + ``` docker-compose run --rm worker dump_config + ``` + ``` kubectl exec -it deployment/authentik-worker -c authentik -- ak dump_config + ``` From 98a07cd0ef14f0179477d009429d1298af46f6d2 Mon Sep 17 00:00:00 2001 From: Jens L Date: Sat, 18 Nov 2023 01:46:16 +0100 Subject: [PATCH 02/21] events: stop spam (#7611) * events: don't log updates to internal service accounts Signed-off-by: Jens Langhammer * dont log reputation updates Signed-off-by: Jens Langhammer * don't actually ignore things, stop updating outpost user when not required Signed-off-by: Jens Langhammer * prevent updating internal service account users Signed-off-by: Jens Langhammer * fix setattr call Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer --- authentik/core/api/users.py | 5 +++++ authentik/events/middleware.py | 3 +++ authentik/outposts/models.py | 20 +++++++++++++++----- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index d4adacc97..5ee249729 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -171,6 +171,11 @@ class UserSerializer(ModelSerializer): raise ValidationError("Setting a user to internal service account is not allowed.") return user_type + def validate(self, attrs: dict) -> dict: + if self.instance and self.instance.type == UserTypes.INTERNAL_SERVICE_ACCOUNT: + raise ValidationError("Can't modify internal service account users") + return super().validate(attrs) + class Meta: model = User fields = [ diff --git a/authentik/events/middleware.py b/authentik/events/middleware.py index d482bb21e..7834bae5e 100644 --- a/authentik/events/middleware.py +++ b/authentik/events/middleware.py @@ -27,6 +27,7 @@ from authentik.lib.sentry import before_send from authentik.lib.utils.errors import exception_to_string from authentik.outposts.models import OutpostServiceConnection from authentik.policies.models import Policy, PolicyBindingModel +from authentik.policies.reputation.models import Reputation from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken from authentik.providers.scim.models import SCIMGroup, SCIMUser from authentik.stages.authenticator_static.models import StaticToken @@ -52,11 +53,13 @@ IGNORED_MODELS = ( RefreshToken, SCIMUser, SCIMGroup, + Reputation, ) def should_log_model(model: Model) -> bool: """Return true if operation on `model` should be logged""" + # Check for silk by string so this comparison doesn't fail when silk isn't installed if model.__module__.startswith("silk"): return False return model.__class__ not in IGNORED_MODELS diff --git a/authentik/outposts/models.py b/authentik/outposts/models.py index f876a0cf3..14f896c35 100644 --- a/authentik/outposts/models.py +++ b/authentik/outposts/models.py @@ -344,12 +344,22 @@ class Outpost(SerializerModel, ManagedModel): user_created = False if not user: user: User = User.objects.create(username=self.user_identifier) - user.set_unusable_password() user_created = True - user.type = UserTypes.INTERNAL_SERVICE_ACCOUNT - user.name = f"Outpost {self.name} Service-Account" - user.path = USER_PATH_OUTPOSTS - user.save() + attrs = { + "type": UserTypes.INTERNAL_SERVICE_ACCOUNT, + "name": f"Outpost {self.name} Service-Account", + "path": USER_PATH_OUTPOSTS, + } + dirty = False + for key, value in attrs.items(): + if getattr(user, key) != value: + dirty = True + setattr(user, key, value) + if user.has_usable_password(): + user.set_unusable_password() + dirty = True + if dirty: + user.save() if user_created: self.build_user_permissions(user) return user From 44fc9ee80c51ffcfa061c7ce0d6f61536bf28ec9 Mon Sep 17 00:00:00 2001 From: Jens L Date: Sat, 18 Nov 2023 01:55:48 +0100 Subject: [PATCH 03/21] stages/identification: add option to pretend user exists (#7610) * stages/identification: add option to pretend user exists Signed-off-by: Jens Langhammer * fix tests? Signed-off-by: Jens Langhammer * update docs Signed-off-by: Jens Langhammer * test CI permission fix Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer --- .github/workflows/gha-cache-cleanup.yml | 4 ++ .github/workflows/translation-advice.yml | 4 ++ .github/workflows/translation-rename.yml | 4 ++ authentik/flows/tests/test_executor.py | 1 + authentik/lib/avatars.py | 10 +++- .../migrations/0010_alter_totpdevice_key.py | 10 ++++ authentik/stages/identification/api.py | 1 + .../0014_identificationstage_pretend.py | 23 ++++++++ authentik/stages/identification/models.py | 7 +++ authentik/stages/identification/stage.py | 4 +- authentik/stages/identification/tests.py | 21 ++++++++ blueprints/schema.json | 5 ++ schema.yml | 12 +++++ .../identification/IdentificationStageForm.ts | 22 +++++++- web/xliff/de.xlf | 6 +++ web/xliff/en.xlf | 6 +++ web/xliff/es.xlf | 6 +++ web/xliff/fr.xlf | 6 +++ web/xliff/pl.xlf | 6 +++ web/xliff/pseudo-LOCALE.xlf | 6 +++ web/xliff/tr.xlf | 6 +++ web/xliff/zh-Hans.xlf | 54 ++++++++++--------- web/xliff/zh-Hant.xlf | 6 +++ web/xliff/zh_TW.xlf | 6 +++ .../docs/flow/stages/identification/index.md | 8 +++ website/docs/releases/2024/v2024.1.md | 4 ++ 26 files changed, 220 insertions(+), 28 deletions(-) create mode 100644 authentik/stages/identification/migrations/0014_identificationstage_pretend.py 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.", + )} +

+