mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
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:
committed by
GitHub
parent
baf61056c7
commit
05005f4eb9
@@ -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),
|
||||||
|
]
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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"],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user