core: add support for hiding applications from the user dashboard (#21530)

* Add meta_hide field to hide apps

* exclude hidden applications from user dashboard

* Add the hide option to the UI

* Add schema

* Add hide setting to application wizard

* Add typescript client changes

* fix linting

* Convert blank://blank to meta_hide=True in the migration

* fix tests

* update docs

* fix continuous login

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* Apply suggestions from code review

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>

* fix linting

* fix migrations

* Apply suggestions from code review

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>

* rename all mentions of dashboard to My applications

* generate schema

* generate TS client

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
This commit is contained in:
Marcelo Elizeche Landó
2026-04-28 13:05:56 -03:00
committed by GitHub
parent baf61056c7
commit 05005f4eb9
15 changed files with 109 additions and 15 deletions
+6 -7
View File
@@ -4,7 +4,7 @@ from collections.abc import Iterator
from copy import copy from copy import copy
from django.core.cache import cache 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.db.models.expressions import When
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@@ -120,6 +120,7 @@ class ApplicationSerializer(ModelSerializer):
"meta_publisher", "meta_publisher",
"policy_engine_mode", "policy_engine_mode",
"group", "group",
"meta_hide",
] ]
extra_kwargs = { extra_kwargs = {
"backchannel_providers": {"required": False}, "backchannel_providers": {"required": False},
@@ -283,14 +284,12 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
) == "true" ) == "true"
queryset = self._filter_queryset_for_list(self.get_queryset()) queryset = self._filter_queryset_for_list(self.get_queryset())
queryset = queryset.exclude(meta_hide=True)
if only_with_launch_url: if only_with_launch_url:
# Pre-filter at DB level to skip expensive per-app policy evaluation # Pre-filter at DB level to skip expensive per-app policy evaluation
# for apps that can never appear in the launcher: # for apps that can never appear in the launcher (no meta_launch_url
# - No meta_launch_url AND no provider: no possible launch URL # and no provider, so no possible launch URL).
# - meta_launch_url="blank://blank": documented convention to hide from launcher queryset = queryset.exclude(meta_launch_url="", provider__isnull=True)
queryset = queryset.exclude(
Q(meta_launch_url="", provider__isnull=True) | Q(meta_launch_url="blank://blank")
)
paginator: Pagination = self.paginator paginator: Pagination = self.paginator
paginated_apps = paginator.paginate_queryset(queryset, request) paginated_apps = paginator.paginate_queryset(queryset, request)
@@ -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),
]
+3
View File
@@ -735,6 +735,9 @@ class Application(SerializerModel, PolicyBindingModel):
meta_icon = FileField(default="", blank=True) meta_icon = FileField(default="", blank=True)
meta_description = models.TextField(default="", blank=True) meta_description = models.TextField(default="", blank=True)
meta_publisher = 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() objects = ApplicationQuerySet.as_manager()
@@ -129,6 +129,7 @@ class TestApplicationsAPI(APITestCase):
"meta_icon_url": None, "meta_icon_url": None,
"meta_icon_themed_urls": None, "meta_icon_themed_urls": None,
"meta_description": "", "meta_description": "",
"meta_hide": False,
"meta_publisher": "", "meta_publisher": "",
"policy_engine_mode": "any", "policy_engine_mode": "any",
}, },
@@ -187,12 +188,14 @@ class TestApplicationsAPI(APITestCase):
"meta_icon_url": None, "meta_icon_url": None,
"meta_icon_themed_urls": None, "meta_icon_themed_urls": None,
"meta_description": "", "meta_description": "",
"meta_hide": False,
"meta_publisher": "", "meta_publisher": "",
"policy_engine_mode": "any", "policy_engine_mode": "any",
}, },
{ {
"launch_url": None, "launch_url": None,
"meta_description": "", "meta_description": "",
"meta_hide": False,
"meta_icon": "", "meta_icon": "",
"meta_icon_url": None, "meta_icon_url": None,
"meta_icon_themed_urls": None, "meta_icon_themed_urls": None,
+1 -1
View File
@@ -353,7 +353,7 @@ class IdentificationStageView(ChallengeStageView):
PLAN_CONTEXT_APPLICATION, Application() PLAN_CONTEXT_APPLICATION, Application()
) )
challenge.initial_data["application_pre"] = app.name 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 challenge.initial_data["application_pre_launch"] = launch_url
if ( if (
PLAN_CONTEXT_DEVICE in self.executor.plan.context PLAN_CONTEXT_DEVICE in self.executor.plan.context
+5
View File
@@ -5215,6 +5215,11 @@
"type": "string", "type": "string",
"title": "Group" "title": "Group"
}, },
"meta_hide": {
"type": "boolean",
"title": "Meta hide",
"description": "Hide this application from the user's My applications page."
},
"icon": { "icon": {
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
+8
View File
@@ -127,6 +127,12 @@ export interface Application {
* @memberof Application * @memberof Application
*/ */
group?: string; 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 ? undefined
: PolicyEngineModeFromJSON(json["policy_engine_mode"]), : PolicyEngineModeFromJSON(json["policy_engine_mode"]),
group: json["group"] == null ? undefined : json["group"], 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"], meta_publisher: value["metaPublisher"],
policy_engine_mode: PolicyEngineModeToJSON(value["policyEngineMode"]), policy_engine_mode: PolicyEngineModeToJSON(value["policyEngineMode"]),
group: value["group"], group: value["group"],
meta_hide: value["metaHide"],
}; };
} }
+8
View File
@@ -87,6 +87,12 @@ export interface ApplicationRequest {
* @memberof ApplicationRequest * @memberof ApplicationRequest
*/ */
group?: string; 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 ? undefined
: PolicyEngineModeFromJSON(json["policy_engine_mode"]), : PolicyEngineModeFromJSON(json["policy_engine_mode"]),
group: json["group"] == null ? undefined : json["group"], 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"], meta_publisher: value["metaPublisher"],
policy_engine_mode: PolicyEngineModeToJSON(value["policyEngineMode"]), policy_engine_mode: PolicyEngineModeToJSON(value["policyEngineMode"]),
group: value["group"], group: value["group"],
meta_hide: value["metaHide"],
}; };
} }
@@ -87,6 +87,12 @@ export interface PatchedApplicationRequest {
* @memberof PatchedApplicationRequest * @memberof PatchedApplicationRequest
*/ */
group?: string; 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 ? undefined
: PolicyEngineModeFromJSON(json["policy_engine_mode"]), : PolicyEngineModeFromJSON(json["policy_engine_mode"]),
group: json["group"] == null ? undefined : json["group"], 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"], meta_publisher: value["metaPublisher"],
policy_engine_mode: PolicyEngineModeToJSON(value["policyEngineMode"]), policy_engine_mode: PolicyEngineModeToJSON(value["policyEngineMode"]),
group: value["group"], group: value["group"],
meta_hide: value["metaHide"],
}; };
} }
+9
View File
@@ -34111,6 +34111,9 @@ components:
$ref: '#/components/schemas/PolicyEngineMode' $ref: '#/components/schemas/PolicyEngineMode'
group: group:
type: string type: string
meta_hide:
type: boolean
description: Hide this application from the user's My applications page.
required: required:
- backchannel_providers_obj - backchannel_providers_obj
- launch_url - launch_url
@@ -34192,6 +34195,9 @@ components:
$ref: '#/components/schemas/PolicyEngineMode' $ref: '#/components/schemas/PolicyEngineMode'
group: group:
type: string type: string
meta_hide:
type: boolean
description: Hide this application from the user's My applications page.
required: required:
- name - name
- slug - slug
@@ -47428,6 +47434,9 @@ components:
$ref: '#/components/schemas/PolicyEngineMode' $ref: '#/components/schemas/PolicyEngineMode'
group: group:
type: string type: string
meta_hide:
type: boolean
description: Hide this application from the user's My applications page.
PatchedAuthenticatorDuoStageRequest: PatchedAuthenticatorDuoStageRequest:
type: object type: object
description: AuthenticatorDuoStage Serializer description: AuthenticatorDuoStage Serializer
@@ -201,6 +201,15 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
)} )}
> >
</ak-switch-input> </ak-switch-input>
<ak-switch-input
name="metaHide"
?checked=${this.instance?.metaHide ?? false}
label=${msg("Hide from My applications")}
help=${msg(
"If checked, this application will not be shown on the user's My applications page.",
)}
>
</ak-switch-input>
<ak-file-search-input <ak-file-search-input
name="metaIcon" name="metaIcon"
label=${msg("Icon")} label=${msg("Icon")}
@@ -187,6 +187,15 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
)} )}
> >
</ak-switch-input> </ak-switch-input>
<ak-switch-input
name="metaHide"
?checked=${app.metaHide ?? false}
label=${msg("Hide from My applications")}
help=${msg(
"If checked, this application will not be shown on the user's My applications page.",
)}
>
</ak-switch-input>
<ak-file-search-input <ak-file-search-input
name="metaIcon" name="metaIcon"
label=${msg("Icon")} label=${msg("Icon")}
@@ -40,11 +40,7 @@ export class FlowMultitabController implements ReactiveController {
return; return;
} }
if ( if (isIdentificationChallenge(challenge) && challenge.applicationPreLaunch) {
isIdentificationChallenge(challenge) &&
challenge.applicationPreLaunch &&
challenge.applicationPreLaunch !== "blank://blank"
) {
multiTabOrchestrateLeave(); multiTabOrchestrateLeave();
window.location.assign(challenge.applicationPreLaunch); window.location.assign(challenge.applicationPreLaunch);
return; return;
@@ -30,7 +30,7 @@ The following options can be configured:
For a reference of all fields available, see [the API schema for the User object](https://api.goauthentik.io/reference/core-users-retrieve/). For a reference of all fields available, see [the API schema for the User object](https://api.goauthentik.io/reference/core-users-retrieve/).
Only applications whose launch URL starts with `http://` or `https://` or are relative URLs are shown on the users' **My applications** page. This can also be used to hide applications that shouldn't be visible on the **My applications** page but are still accessible by users, by setting the _Launch URL_ to `blank://blank`. Only apps with launch URLs that begin with `http://` or `https://`, or that use relative paths, appear on the user's **My applications** page. To keep an app accessible but remove it from that page, use the **Hide from My applications** option (see [Hide applications](./manage_apps.mdx#hide-applications)).
- _Icon (URL)_: Optionally configure an icon for the application. You can select from files uploaded to the [Files](../../customize/files.md) library or enter an absolute URL. - _Icon (URL)_: Optionally configure an icon for the application. You can select from files uploaded to the [Files](../../customize/files.md) library or enter an absolute URL.
@@ -104,10 +104,14 @@ return {
## Hide applications ## Hide applications
To hide an application without modifying its policy settings or removing it, you can simply set the _Launch URL_ to `blank://blank`, which will hide the application from users. To hide an application without modifying its policy settings or removing it, you can use the **Hide from My applications** option on the application. The application will no longer appear on the **My applications** page.
Keep in mind that users still have access, so they can still authorize access when the login process is started from the application. Keep in mind that users still have access, so they can still authorize access when the login process is started from the application.
:::info Hiding applications before 2026.5
Before authentik 2026.5, an application was hidden by setting its **Launch URL** to `blank://blank`. Existing applications using that value are automatically migrated to using the **Hide from My applications** option upon upgrading.
:::
## Launch URLs ## Launch URLs
To give users direct links to applications, you can now use a URL like `https://authentik.company/application/launch/<slug>/`. If the user is already logged in, they will be redirected to the application automatically. Otherwise, they'll be sent to the authentication flow and, if successful, forwarded to the application. To give users direct links to applications, you can now use a URL like `https://authentik.company/application/launch/<slug>/`. If the user is already logged in, they will be redirected to the application automatically. Otherwise, they'll be sent to the authentication flow and, if successful, forwarded to the application.