diff --git a/authentik/core/api/applications.py b/authentik/core/api/applications.py index 846b88886a2b..3903a9b01d6c 100644 --- a/authentik/core/api/applications.py +++ b/authentik/core/api/applications.py @@ -4,7 +4,7 @@ from copy import copy from django.core.cache import cache -from django.db.models import Case, Q, QuerySet +from django.db.models import Case, QuerySet from django.db.models.expressions import When from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ @@ -120,6 +120,7 @@ class Meta: "meta_publisher", "policy_engine_mode", "group", + "meta_hide", ] extra_kwargs = { "backchannel_providers": {"required": False}, @@ -283,14 +284,12 @@ def list(self, request: Request) -> Response: ) == "true" queryset = self._filter_queryset_for_list(self.get_queryset()) + queryset = queryset.exclude(meta_hide=True) if only_with_launch_url: # Pre-filter at DB level to skip expensive per-app policy evaluation - # for apps that can never appear in the launcher: - # - No meta_launch_url AND no provider: no possible launch URL - # - meta_launch_url="blank://blank": documented convention to hide from launcher - queryset = queryset.exclude( - Q(meta_launch_url="", provider__isnull=True) | Q(meta_launch_url="blank://blank") - ) + # for apps that can never appear in the launcher (no meta_launch_url + # and no provider, so no possible launch URL). + queryset = queryset.exclude(meta_launch_url="", provider__isnull=True) paginator: Pagination = self.paginator paginated_apps = paginator.paginate_queryset(queryset, request) diff --git a/authentik/core/migrations/0059_add_application_meta_hide.py b/authentik/core/migrations/0059_add_application_meta_hide.py new file mode 100644 index 000000000000..331639f2bf9a --- /dev/null +++ b/authentik/core/migrations/0059_add_application_meta_hide.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.12 on 2026-04-09 18:04 + +from django.apps.registry import Apps +from django.db import migrations, models +from django.db.backends.base.schema import BaseDatabaseSchemaEditor + + +def migrate_blank_launch_url(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): + db_alias = schema_editor.connection.alias + Application = apps.get_model("authentik_core", "Application") + + Application.objects.using(db_alias).filter(meta_launch_url="blank://blank").update( + meta_hide=True, meta_launch_url="" + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_core", "0058_setup"), + ] + + operations = [ + migrations.AddField( + model_name="application", + name="meta_hide", + field=models.BooleanField( + default=False, + help_text="Hide this application from the user's My applications page.", + ), + ), + migrations.RunPython(migrate_blank_launch_url, migrations.RunPython.noop), + ] diff --git a/authentik/core/models.py b/authentik/core/models.py index be8421a6d28c..96133530fd90 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -735,6 +735,9 @@ class Application(SerializerModel, PolicyBindingModel): meta_icon = FileField(default="", blank=True) meta_description = models.TextField(default="", blank=True) meta_publisher = models.TextField(default="", blank=True) + meta_hide = models.BooleanField( + default=False, help_text=_("Hide this application from the user's My applications page.") + ) objects = ApplicationQuerySet.as_manager() diff --git a/authentik/core/tests/test_applications_api.py b/authentik/core/tests/test_applications_api.py index 29f972fbe343..a753a731f52a 100644 --- a/authentik/core/tests/test_applications_api.py +++ b/authentik/core/tests/test_applications_api.py @@ -129,6 +129,7 @@ def test_list(self): "meta_icon_url": None, "meta_icon_themed_urls": None, "meta_description": "", + "meta_hide": False, "meta_publisher": "", "policy_engine_mode": "any", }, @@ -187,12 +188,14 @@ def test_list_superuser_full_list(self): "meta_icon_url": None, "meta_icon_themed_urls": None, "meta_description": "", + "meta_hide": False, "meta_publisher": "", "policy_engine_mode": "any", }, { "launch_url": None, "meta_description": "", + "meta_hide": False, "meta_icon": "", "meta_icon_url": None, "meta_icon_themed_urls": None, diff --git a/authentik/stages/identification/stage.py b/authentik/stages/identification/stage.py index 28780d278c27..15b8617270a5 100644 --- a/authentik/stages/identification/stage.py +++ b/authentik/stages/identification/stage.py @@ -353,7 +353,7 @@ def get_challenge(self) -> Challenge: PLAN_CONTEXT_APPLICATION, Application() ) challenge.initial_data["application_pre"] = app.name - if launch_url := app.get_launch_url(): + if not app.meta_hide and (launch_url := app.get_launch_url()): challenge.initial_data["application_pre_launch"] = launch_url if ( PLAN_CONTEXT_DEVICE in self.executor.plan.context diff --git a/blueprints/schema.json b/blueprints/schema.json index 0c5d13e62eaf..0546a7bae4ea 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -5215,6 +5215,11 @@ "type": "string", "title": "Group" }, + "meta_hide": { + "type": "boolean", + "title": "Meta hide", + "description": "Hide this application from the user's My applications page." + }, "icon": { "type": "string", "minLength": 1, diff --git a/packages/client-ts/src/models/Application.ts b/packages/client-ts/src/models/Application.ts index dca38a5bb8b9..d6b5805ba275 100644 --- a/packages/client-ts/src/models/Application.ts +++ b/packages/client-ts/src/models/Application.ts @@ -127,6 +127,12 @@ export interface Application { * @memberof Application */ group?: string; + /** + * Hide this application from the user's My applications page. + * @type {boolean} + * @memberof Application + */ + metaHide?: boolean; } /** @@ -177,6 +183,7 @@ export function ApplicationFromJSONTyped(json: any, ignoreDiscriminator: boolean ? undefined : PolicyEngineModeFromJSON(json["policy_engine_mode"]), group: json["group"] == null ? undefined : json["group"], + metaHide: json["meta_hide"] == null ? undefined : json["meta_hide"], }; } @@ -212,5 +219,6 @@ export function ApplicationToJSONTyped( meta_publisher: value["metaPublisher"], policy_engine_mode: PolicyEngineModeToJSON(value["policyEngineMode"]), group: value["group"], + meta_hide: value["metaHide"], }; } diff --git a/packages/client-ts/src/models/ApplicationRequest.ts b/packages/client-ts/src/models/ApplicationRequest.ts index 48301eaeedfa..a82b9ec32e47 100644 --- a/packages/client-ts/src/models/ApplicationRequest.ts +++ b/packages/client-ts/src/models/ApplicationRequest.ts @@ -87,6 +87,12 @@ export interface ApplicationRequest { * @memberof ApplicationRequest */ group?: string; + /** + * Hide this application from the user's My applications page. + * @type {boolean} + * @memberof ApplicationRequest + */ + metaHide?: boolean; } /** @@ -125,6 +131,7 @@ export function ApplicationRequestFromJSONTyped( ? undefined : PolicyEngineModeFromJSON(json["policy_engine_mode"]), group: json["group"] == null ? undefined : json["group"], + metaHide: json["meta_hide"] == null ? undefined : json["meta_hide"], }; } @@ -152,5 +159,6 @@ export function ApplicationRequestToJSONTyped( meta_publisher: value["metaPublisher"], policy_engine_mode: PolicyEngineModeToJSON(value["policyEngineMode"]), group: value["group"], + meta_hide: value["metaHide"], }; } diff --git a/packages/client-ts/src/models/PatchedApplicationRequest.ts b/packages/client-ts/src/models/PatchedApplicationRequest.ts index 42817e856789..577ebd795198 100644 --- a/packages/client-ts/src/models/PatchedApplicationRequest.ts +++ b/packages/client-ts/src/models/PatchedApplicationRequest.ts @@ -87,6 +87,12 @@ export interface PatchedApplicationRequest { * @memberof PatchedApplicationRequest */ group?: string; + /** + * Hide this application from the user's My applications page. + * @type {boolean} + * @memberof PatchedApplicationRequest + */ + metaHide?: boolean; } /** @@ -125,6 +131,7 @@ export function PatchedApplicationRequestFromJSONTyped( ? undefined : PolicyEngineModeFromJSON(json["policy_engine_mode"]), group: json["group"] == null ? undefined : json["group"], + metaHide: json["meta_hide"] == null ? undefined : json["meta_hide"], }; } @@ -152,5 +159,6 @@ export function PatchedApplicationRequestToJSONTyped( meta_publisher: value["metaPublisher"], policy_engine_mode: PolicyEngineModeToJSON(value["policyEngineMode"]), group: value["group"], + meta_hide: value["metaHide"], }; } diff --git a/schema.yml b/schema.yml index 1ac159eac0ef..0db783cac157 100644 --- a/schema.yml +++ b/schema.yml @@ -34111,6 +34111,9 @@ components: $ref: '#/components/schemas/PolicyEngineMode' group: type: string + meta_hide: + type: boolean + description: Hide this application from the user's My applications page. required: - backchannel_providers_obj - launch_url @@ -34192,6 +34195,9 @@ components: $ref: '#/components/schemas/PolicyEngineMode' group: type: string + meta_hide: + type: boolean + description: Hide this application from the user's My applications page. required: - name - slug @@ -47428,6 +47434,9 @@ components: $ref: '#/components/schemas/PolicyEngineMode' group: type: string + meta_hide: + type: boolean + description: Hide this application from the user's My applications page. PatchedAuthenticatorDuoStageRequest: type: object description: AuthenticatorDuoStage Serializer diff --git a/web/src/admin/applications/ApplicationForm.ts b/web/src/admin/applications/ApplicationForm.ts index 148fa905c0ce..fbb70574de5c 100644 --- a/web/src/admin/applications/ApplicationForm.ts +++ b/web/src/admin/applications/ApplicationForm.ts @@ -201,6 +201,15 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm + + + +