Skip to content

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

Open
melizeche wants to merge 7 commits intomainfrom
hide_apps
Open

core: add support for hiding applications from the user dashboard#21530
melizeche wants to merge 7 commits intomainfrom
hide_apps

Conversation

@melizeche
Copy link
Copy Markdown
Member

Details

hideapps


Checklist

  • Local tests pass (ak test authentik/)
  • The code has been formatted (make lint-fix)

If an API change has been made

  • The API schema and clients have been updated (make gen)

If changes to the frontend have been made

  • The code has been formatted (make web)

If applicable

  • The documentation has been updated
  • The documentation has been formatted (make docs)

@melizeche melizeche requested review from a team as code owners April 9, 2026 19:22
@netlify
Copy link
Copy Markdown

netlify bot commented Apr 9, 2026

Deploy Preview for authentik-storybook ready!

Name Link
🔨 Latest commit f70ec68
🔍 Latest deploy log https://app.netlify.com/projects/authentik-storybook/deploys/69d7fc6ce468dc0008d481db
😎 Deploy Preview https://deploy-preview-21530--authentik-storybook.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link
Copy Markdown

netlify bot commented Apr 9, 2026

Deploy Preview for authentik-docs ready!

Name Link
🔨 Latest commit f70ec68
🔍 Latest deploy log https://app.netlify.com/projects/authentik-docs/deploys/69d7fc6c201e360008cbac4f
😎 Deploy Preview https://deploy-preview-21530--authentik-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 9, 2026

❌ 3 Tests Failed:

Tests completed Failed Passed Skipped
3167 3 3164 1
View the top 3 failed test(s) by shortest run time
authentik.flows.tests.test_executor.TestFlowExecutor::test_reevaluate_remove_consecutive
Stack Traces | 0.119s run time
self = <unittest.case._Outcome object at 0x7f875342bcb0>
test_case = <authentik.flows.tests.test_executor.TestFlowExecutor testMethod=test_reevaluate_remove_consecutive>
subTest = False

    @contextlib.contextmanager
    def testPartExecutor(self, test_case, subTest=False):
        old_success = self.success
        self.success = True
        try:
>           yield

.../hostedtoolcache/Python/3.14.3.............../x64/lib/python3.14/unittest/case.py:58: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.flows.tests.test_executor.TestFlowExecutor testMethod=test_reevaluate_remove_consecutive>
result = <TestCaseFunction test_reevaluate_remove_consecutive>

    def run(self, result=None):
        if result is None:
            result = self.defaultTestResult()
            startTestRun = getattr(result, 'startTestRun', None)
            stopTestRun = getattr(result, 'stopTestRun', None)
            if startTestRun is not None:
                startTestRun()
        else:
            stopTestRun = None
    
        result.startTest(self)
        try:
            testMethod = getattr(self, self._testMethodName)
            if (getattr(self.__class__, "__unittest_skip__", False) or
                getattr(testMethod, "__unittest_skip__", False)):
                # If the class or method was skipped.
                skip_why = (getattr(self.__class__, '__unittest_skip_why__', '')
                            or getattr(testMethod, '__unittest_skip_why__', ''))
                _addSkip(result, self, skip_why)
                return result
    
            expecting_failure = (
                getattr(self, "__unittest_expecting_failure__", False) or
                getattr(testMethod, "__unittest_expecting_failure__", False)
            )
            outcome = _Outcome(result)
            start_time = time.perf_counter()
            try:
                self._outcome = outcome
    
                with outcome.testPartExecutor(self):
                    self._callSetUp()
                if outcome.success:
                    outcome.expecting_failure = expecting_failure
                    with outcome.testPartExecutor(self):
>                       self._callTestMethod(testMethod)

.../hostedtoolcache/Python/3.14.3.............../x64/lib/python3.14/unittest/case.py:669: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.flows.tests.test_executor.TestFlowExecutor testMethod=test_reevaluate_remove_consecutive>
method = <bound method TestFlowExecutor.test_reevaluate_remove_consecutive of <authentik.flows.tests.test_executor.TestFlowExecutor testMethod=test_reevaluate_remove_consecutive>>

    def _callTestMethod(self, method):
>       result = method()
                 ^^^^^^^^

.../hostedtoolcache/Python/3.14.3.............../x64/lib/python3.14/unittest/case.py:615: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.flows.tests.test_executor.TestFlowExecutor testMethod=test_reevaluate_remove_consecutive>

    def test_reevaluate_remove_consecutive(self):
        """Test planner with re-evaluate (consecutive stages are removed)"""
        flow = create_test_flow(
            FlowDesignation.AUTHENTICATION,
        )
        false_policy = DummyPolicy.objects.create(
            name=generate_id(), result=False, wait_min=1, wait_max=2
        )
    
        binding = FlowStageBinding.objects.create(
            target=flow,
            stage=DummyStage.objects.create(name=generate_id()),
            order=0,
            evaluate_on_plan=True,
            re_evaluate_policies=False,
        )
        binding2 = FlowStageBinding.objects.create(
            target=flow,
            stage=DummyStage.objects.create(name=generate_id()),
            order=1,
            re_evaluate_policies=True,
        )
        binding3 = FlowStageBinding.objects.create(
            target=flow,
            stage=DummyStage.objects.create(name=generate_id()),
            order=2,
            re_evaluate_policies=True,
        )
        binding4 = FlowStageBinding.objects.create(
            target=flow,
            stage=DummyStage.objects.create(name=generate_id()),
            order=2,
            evaluate_on_plan=True,
            re_evaluate_policies=False,
        )
    
        PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
        PolicyBinding.objects.create(policy=false_policy, target=binding3, order=0)
    
        # Here we patch the dummy policy to evaluate to true so the stage is included
        with patch("authentik.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE):
            exec_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug})
            # First request, run the planner
            response = self.client.get(exec_url)
            self.assertEqual(response.status_code, 200)
            self.assertStageResponse(response, flow, component="ak-stage-dummy")
    
            plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
    
            self.assertEqual(plan.bindings[0], binding)
            self.assertEqual(plan.bindings[1], binding2)
>           self.assertEqual(plan.bindings[2], binding3)

.../flows/tests/test_executor.py:516: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.flows.tests.test_executor.TestFlowExecutor testMethod=test_reevaluate_remove_consecutive>
first = <FlowStageBinding: Flow-stage binding #2 to 159cb0bf-2802-4722-bbcf-5a0f4ab41b51>
second = <FlowStageBinding: Flow-stage binding #2 to 159cb0bf-2802-4722-bbcf-5a0f4ab41b51>
msg = None

    def assertEqual(self, first, second, msg=None):
        """Fail if the two objects are unequal as determined by the '=='
           operator.
        """
        assertion_func = self._getAssertEqualityFunc(first, second)
>       assertion_func(first, second, msg=msg)

.../hostedtoolcache/Python/3.14.3.............../x64/lib/python3.14/unittest/case.py:925: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.flows.tests.test_executor.TestFlowExecutor testMethod=test_reevaluate_remove_consecutive>
first = <FlowStageBinding: Flow-stage binding #2 to 159cb0bf-2802-4722-bbcf-5a0f4ab41b51>
second = <FlowStageBinding: Flow-stage binding #2 to 159cb0bf-2802-4722-bbcf-5a0f4ab41b51>
msg = '<Flow[13 chars] Flow-stage binding #2 to 159cb0bf-2802-4722-bbcf-5a0f4ab41b51> != <Flow[13 chars] Flow-stage binding #2 to 159cb0bf-2802-4722-bbcf-5a0f4ab41b51>'

    def _baseAssertEqual(self, first, second, msg=None):
        """The default assertEqual implementation, not type specific."""
        if not first == second:
            standardMsg = '%s != %s' % _common_shorten_repr(first, second)
            msg = self._formatMessage(msg, standardMsg)
>           raise self.failureException(msg)
E           AssertionError: <Flow[13 chars] Flow-stage binding #2 to 159cb0bf-2802-4722-bbcf-5a0f4ab41b51> != <Flow[13 chars] Flow-stage binding #2 to 159cb0bf-2802-4722-bbcf-5a0f4ab41b51>

.../hostedtoolcache/Python/3.14.3.............../x64/lib/python3.14/unittest/case.py:918: AssertionError
authentik.core.tests.test_applications_api.TestApplicationsAPI::test_list_superuser_full_list
Stack Traces | 1.38s run time
self = <unittest.case._Outcome object at 0x7f8762a8a900>
test_case = <authentik.core.tests.test_applications_api.TestApplicationsAPI testMethod=test_list_superuser_full_list>
subTest = False

    @contextlib.contextmanager
    def testPartExecutor(self, test_case, subTest=False):
        old_success = self.success
        self.success = True
        try:
>           yield

.../hostedtoolcache/Python/3.14.3................../x64/lib/python3.14/unittest/case.py:58: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_applications_api.TestApplicationsAPI testMethod=test_list_superuser_full_list>
result = <TestCaseFunction test_list_superuser_full_list>

    def run(self, result=None):
        if result is None:
            result = self.defaultTestResult()
            startTestRun = getattr(result, 'startTestRun', None)
            stopTestRun = getattr(result, 'stopTestRun', None)
            if startTestRun is not None:
                startTestRun()
        else:
            stopTestRun = None
    
        result.startTest(self)
        try:
            testMethod = getattr(self, self._testMethodName)
            if (getattr(self.__class__, "__unittest_skip__", False) or
                getattr(testMethod, "__unittest_skip__", False)):
                # If the class or method was skipped.
                skip_why = (getattr(self.__class__, '__unittest_skip_why__', '')
                            or getattr(testMethod, '__unittest_skip_why__', ''))
                _addSkip(result, self, skip_why)
                return result
    
            expecting_failure = (
                getattr(self, "__unittest_expecting_failure__", False) or
                getattr(testMethod, "__unittest_expecting_failure__", False)
            )
            outcome = _Outcome(result)
            start_time = time.perf_counter()
            try:
                self._outcome = outcome
    
                with outcome.testPartExecutor(self):
                    self._callSetUp()
                if outcome.success:
                    outcome.expecting_failure = expecting_failure
                    with outcome.testPartExecutor(self):
>                       self._callTestMethod(testMethod)

.../hostedtoolcache/Python/3.14.3................../x64/lib/python3.14/unittest/case.py:669: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_applications_api.TestApplicationsAPI testMethod=test_list_superuser_full_list>
method = <bound method TestApplicationsAPI.test_list_superuser_full_list of <authentik.core.tests.test_applications_api.TestApplicationsAPI testMethod=test_list_superuser_full_list>>

    def _callTestMethod(self, method):
>       result = method()
                 ^^^^^^^^

.../hostedtoolcache/Python/3.14.3................../x64/lib/python3.14/unittest/case.py:615: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_applications_api.TestApplicationsAPI testMethod=test_list_superuser_full_list>

    def test_list_superuser_full_list(self):
        """Test list operation with superuser_full_list"""
        self.client.force_login(self.user)
        response = self.client.get(
            reverse("authentik_api:application-list") + "?superuser_full_list=true"
        )
>       self.assertJSONEqual(
            response.content.decode(),
            {
                "autocomplete": {},
                "pagination": {
                    "next": 0,
                    "previous": 0,
                    "count": 2,
                    "current": 1,
                    "total_pages": 1,
                    "start_index": 1,
                    "end_index": 2,
                },
                "results": [
                    {
                        "pk": str(self.allowed.pk),
                        "name": "allowed",
                        "slug": "allowed",
                        "group": "",
                        "provider": self.provider.pk,
                        "provider_obj": {
                            "assigned_application_name": "allowed",
                            "assigned_application_slug": "allowed",
                            "assigned_backchannel_application_name": None,
                            "assigned_backchannel_application_slug": None,
                            "authentication_flow": None,
                            "invalidation_flow": None,
                            "authorization_flow": str(self.provider.authorization_flow.pk),
                            "component": "ak-provider-oauth2-form",
                            "meta_model_name": "authentik_providers_oauth2.oauth2provider",
                            "name": self.provider.name,
                            "pk": self.provider.pk,
                            "property_mappings": [],
                            "verbose_name": "OAuth2/OpenID Provider",
                            "verbose_name_plural": "OAuth2/OpenID Providers",
                        },
                        "backchannel_providers": [],
                        "backchannel_providers_obj": [],
                        "launch_url": f"https://goauthentik.io/{self.user.username}",
                        "meta_launch_url": "https://goauthentik.io/%(username)s",
                        "open_in_new_tab": True,
                        "meta_icon": "",
                        "meta_icon_url": None,
                        "meta_icon_themed_urls": None,
                        "meta_description": "",
                        "meta_publisher": "",
                        "policy_engine_mode": "any",
                    },
                    {
                        "launch_url": None,
                        "meta_description": "",
                        "meta_icon": "",
                        "meta_icon_url": None,
                        "meta_icon_themed_urls": None,
                        "meta_launch_url": "",
                        "open_in_new_tab": False,
                        "meta_publisher": "",
                        "group": "",
                        "name": "denied",
                        "pk": str(self.denied.pk),
                        "policy_engine_mode": "any",
                        "provider": None,
                        "provider_obj": None,
                        "backchannel_providers": [],
                        "backchannel_providers_obj": [],
                        "slug": "denied",
                    },
                ],
            },
        )

.../core/tests/test_applications_api.py:145: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_applications_api.TestApplicationsAPI testMethod=test_list_superuser_full_list>
raw = '{"pagination":{"next":0,"previous":0,"count":2,"current":1,"total_pages":1,"start_index":1,"end_index":2},"results":[..."meta_description":"","meta_publisher":"","policy_engine_mode":"any","group":"","meta_hide":false}],"autocomplete":{}}'
expected_data = {'autocomplete': {}, 'pagination': {'count': 2, 'current': 1, 'end_index': 2, 'next': 0, ...}, 'results': [{'backchann...JSidlXYZ', ...}, {'backchannel_providers': [], 'backchannel_providers_obj': [], 'group': '', 'launch_url': None, ...}]}
msg = None

    def assertJSONEqual(self, raw, expected_data, msg=None):
        """
        Assert that the JSON fragments raw and expected_data are equal.
        Usual JSON non-significant whitespace rules apply as the heavyweight
        is delegated to the json library.
        """
        try:
            data = json.loads(raw)
        except json.JSONDecodeError:
            self.fail("First argument is not valid JSON: %r" % raw)
        if isinstance(expected_data, str):
            try:
                expected_data = json.loads(expected_data)
            except ValueError:
                self.fail("Second argument is not valid JSON: %r" % expected_data)
>       self.assertEqual(data, expected_data, msg=msg)

.venv/lib/python3.14.../django/test/testcases.py:1032: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_applications_api.TestApplicationsAPI testMethod=test_list_superuser_full_list>
first = {'autocomplete': {}, 'pagination': {'count': 2, 'current': 1, 'end_index': 2, 'next': 0, ...}, 'results': [{'backchann...JSidlXYZ', ...}, {'backchannel_providers': [], 'backchannel_providers_obj': [], 'group': '', 'launch_url': None, ...}]}
second = {'autocomplete': {}, 'pagination': {'count': 2, 'current': 1, 'end_index': 2, 'next': 0, ...}, 'results': [{'backchann...JSidlXYZ', ...}, {'backchannel_providers': [], 'backchannel_providers_obj': [], 'group': '', 'launch_url': None, ...}]}
msg = None

    def assertEqual(self, first, second, msg=None):
        """Fail if the two objects are unequal as determined by the '=='
           operator.
        """
        assertion_func = self._getAssertEqualityFunc(first, second)
>       assertion_func(first, second, msg=msg)

.../hostedtoolcache/Python/3.14.3................../x64/lib/python3.14/unittest/case.py:925: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_applications_api.TestApplicationsAPI testMethod=test_list_superuser_full_list>
d1 = {'autocomplete': {}, 'pagination': {'count': 2, 'current': 1, 'end_index': 2, 'next': 0, ...}, 'results': [{'backchann...JSidlXYZ', ...}, {'backchannel_providers': [], 'backchannel_providers_obj': [], 'group': '', 'launch_url': None, ...}]}
d2 = {'autocomplete': {}, 'pagination': {'count': 2, 'current': 1, 'end_index': 2, 'next': 0, ...}, 'results': [{'backchann...JSidlXYZ', ...}, {'backchannel_providers': [], 'backchannel_providers_obj': [], 'group': '', 'launch_url': None, ...}]}
msg = None

    def assertDictEqual(self, d1, d2, msg=None):
        self.assertIsInstance(d1, dict, 'First argument is not a dictionary')
        self.assertIsInstance(d2, dict, 'Second argument is not a dictionary')
    
        if d1 != d2:
            standardMsg = '%s != %s' % _common_shorten_repr(d1, d2)
            diff = ('\n' + '\n'.join(difflib.ndiff(
                           pprint.pformat(d1).splitlines(),
                           pprint.pformat(d2).splitlines())))
            standardMsg = self._truncateMessage(standardMsg, diff)
>           self.fail(self._formatMessage(msg, standardMsg))

.../hostedtoolcache/Python/3.14.3................../x64/lib/python3.14/unittest/case.py:1224: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_applications_api.TestApplicationsAPI testMethod=test_list_superuser_full_list>
msg = "{'pagination': {'next': 0, 'previous': 0, '[1581 chars]: {}} != {'autocomplete': {}, 'pagination': {'next':[1541 char... 'any',\n                'provider': None,\n                'provider_obj': None,\n                'slug': 'denied'}]}"

    def fail(self, msg=None):
        """Fail immediately, with the given message."""
>       raise self.failureException(msg)
E       AssertionError: {'pagination': {'next': 0, 'previous': 0, '[1581 chars]: {}} != {'autocomplete': {}, 'pagination': {'next':[1541 chars]d'}]}
E         {'autocomplete': {},
E          'pagination': {'count': 2,
E                         'current': 1,
E                         'end_index': 2,
E                         'next': 0,
E                         'previous': 0,
E                         'start_index': 1,
E                         'total_pages': 1},
E          'results': [{'backchannel_providers': [],
E                       'backchannel_providers_obj': [],
E                       'group': '',
E                       'launch_url': 'https://goauthentik.io/U5kJkDEQn5rmJSidlXYZ',
E                       'meta_description': '',
E       -               'meta_hide': False,
E                       'meta_icon': '',
E                       'meta_icon_themed_urls': None,
E                       'meta_icon_url': None,
E                       'meta_launch_url': 'https://goauthentik.io/%(username)s',
E                       'meta_publisher': '',
E                       'name': 'allowed',
E                       'open_in_new_tab': True,
E                       'pk': 'b691a74b-f90e-4831-ae3b-65cb6967663f',
E                       'policy_engine_mode': 'any',
E                       'provider': 9,
E                       'provider_obj': {'assigned_application_name': 'allowed',
E                                        'assigned_application_slug': 'allowed',
E                                        'assigned_backchannel_application_name': None,
E                                        'assigned_backchannel_application_slug': None,
E                                        'authentication_flow': None,
E                                        'authorization_flow': '98348186-9058-4ee7-9a32-f638c59ee886',
E                                        'component': 'ak-provider-oauth2-form',
E                                        'invalidation_flow': None,
E                                        'meta_model_name': 'authentik_providers_oauth2.oauth2provider',
E                                        'name': 'test',
E                                        'pk': 9,
E                                        'property_mappings': [],
E                                        'verbose_name': 'OAuth2/OpenID Provider',
E                                        'verbose_name_plural': 'OAuth2/OpenID '
E                                                               'Providers'},
E                       'slug': 'allowed'},
E                      {'backchannel_providers': [],
E                       'backchannel_providers_obj': [],
E                       'group': '',
E                       'launch_url': None,
E                       'meta_description': '',
E       -               'meta_hide': False,
E                       'meta_icon': '',
E                       'meta_icon_themed_urls': None,
E                       'meta_icon_url': None,
E                       'meta_launch_url': '',
E                       'meta_publisher': '',
E                       'name': 'denied',
E                       'open_in_new_tab': False,
E                       'pk': 'b7c63337-941e-487e-a461-89ca7717bce4',
E                       'policy_engine_mode': 'any',
E                       'provider': None,
E                       'provider_obj': None,
E                       'slug': 'denied'}]}

.../hostedtoolcache/Python/3.14.3................../x64/lib/python3.14/unittest/case.py:750: AssertionError
authentik.core.tests.test_applications_api.TestApplicationsAPI::test_list
Stack Traces | 2.54s run time
self = <unittest.case._Outcome object at 0x7f8762f34d70>
test_case = <authentik.core.tests.test_applications_api.TestApplicationsAPI testMethod=test_list>
subTest = False

    @contextlib.contextmanager
    def testPartExecutor(self, test_case, subTest=False):
        old_success = self.success
        self.success = True
        try:
>           yield

.../hostedtoolcache/Python/3.14.3................../x64/lib/python3.14/unittest/case.py:58: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_applications_api.TestApplicationsAPI testMethod=test_list>
result = <TestCaseFunction test_list>

    def run(self, result=None):
        if result is None:
            result = self.defaultTestResult()
            startTestRun = getattr(result, 'startTestRun', None)
            stopTestRun = getattr(result, 'stopTestRun', None)
            if startTestRun is not None:
                startTestRun()
        else:
            stopTestRun = None
    
        result.startTest(self)
        try:
            testMethod = getattr(self, self._testMethodName)
            if (getattr(self.__class__, "__unittest_skip__", False) or
                getattr(testMethod, "__unittest_skip__", False)):
                # If the class or method was skipped.
                skip_why = (getattr(self.__class__, '__unittest_skip_why__', '')
                            or getattr(testMethod, '__unittest_skip_why__', ''))
                _addSkip(result, self, skip_why)
                return result
    
            expecting_failure = (
                getattr(self, "__unittest_expecting_failure__", False) or
                getattr(testMethod, "__unittest_expecting_failure__", False)
            )
            outcome = _Outcome(result)
            start_time = time.perf_counter()
            try:
                self._outcome = outcome
    
                with outcome.testPartExecutor(self):
                    self._callSetUp()
                if outcome.success:
                    outcome.expecting_failure = expecting_failure
                    with outcome.testPartExecutor(self):
>                       self._callTestMethod(testMethod)

.../hostedtoolcache/Python/3.14.3................../x64/lib/python3.14/unittest/case.py:669: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_applications_api.TestApplicationsAPI testMethod=test_list>
method = <bound method TestApplicationsAPI.test_list of <authentik.core.tests.test_applications_api.TestApplicationsAPI testMethod=test_list>>

    def _callTestMethod(self, method):
>       result = method()
                 ^^^^^^^^

.../hostedtoolcache/Python/3.14.3................../x64/lib/python3.14/unittest/case.py:615: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_applications_api.TestApplicationsAPI testMethod=test_list>

    def test_list(self):
        """Test list operation without superuser_full_list"""
        self.client.force_login(self.user)
        response = self.client.get(reverse("authentik_api:application-list"))
>       self.assertJSONEqual(
            response.content.decode(),
            {
                "autocomplete": {},
                "pagination": {
                    "next": 0,
                    "previous": 0,
                    "count": 2,
                    "current": 1,
                    "total_pages": 1,
                    "start_index": 1,
                    "end_index": 2,
                },
                "results": [
                    {
                        "pk": str(self.allowed.pk),
                        "name": "allowed",
                        "slug": "allowed",
                        "group": "",
                        "provider": self.provider.pk,
                        "provider_obj": {
                            "assigned_application_name": "allowed",
                            "assigned_application_slug": "allowed",
                            "assigned_backchannel_application_name": None,
                            "assigned_backchannel_application_slug": None,
                            "authentication_flow": None,
                            "invalidation_flow": None,
                            "authorization_flow": str(self.provider.authorization_flow.pk),
                            "component": "ak-provider-oauth2-form",
                            "meta_model_name": "authentik_providers_oauth2.oauth2provider",
                            "name": self.provider.name,
                            "pk": self.provider.pk,
                            "property_mappings": [],
                            "verbose_name": "OAuth2/OpenID Provider",
                            "verbose_name_plural": "OAuth2/OpenID Providers",
                        },
                        "backchannel_providers": [],
                        "backchannel_providers_obj": [],
                        "launch_url": f"https://goauthentik.io/{self.user.username}",
                        "meta_launch_url": "https://goauthentik.io/%(username)s",
                        "open_in_new_tab": True,
                        "meta_icon": "",
                        "meta_icon_url": None,
                        "meta_icon_themed_urls": None,
                        "meta_description": "",
                        "meta_publisher": "",
                        "policy_engine_mode": "any",
                    },
                ],
            },
        )

.../core/tests/test_applications_api.py:87: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_applications_api.TestApplicationsAPI testMethod=test_list>
raw = '{"pagination":{"next":0,"previous":0,"count":2,"current":1,"total_pages":1,"start_index":1,"end_index":2},"results":[..."meta_description":"","meta_publisher":"","policy_engine_mode":"any","group":"","meta_hide":false}],"autocomplete":{}}'
expected_data = {'autocomplete': {}, 'pagination': {'count': 2, 'current': 1, 'end_index': 2, 'next': 0, ...}, 'results': [{'backchann...: [], 'backchannel_providers_obj': [], 'group': '', 'launch_url': 'https://goauthentik.io/3l9NKPgUYN43EtducW8l', ...}]}
msg = None

    def assertJSONEqual(self, raw, expected_data, msg=None):
        """
        Assert that the JSON fragments raw and expected_data are equal.
        Usual JSON non-significant whitespace rules apply as the heavyweight
        is delegated to the json library.
        """
        try:
            data = json.loads(raw)
        except json.JSONDecodeError:
            self.fail("First argument is not valid JSON: %r" % raw)
        if isinstance(expected_data, str):
            try:
                expected_data = json.loads(expected_data)
            except ValueError:
                self.fail("Second argument is not valid JSON: %r" % expected_data)
>       self.assertEqual(data, expected_data, msg=msg)

.venv/lib/python3.14.../django/test/testcases.py:1032: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_applications_api.TestApplicationsAPI testMethod=test_list>
first = {'autocomplete': {}, 'pagination': {'count': 2, 'current': 1, 'end_index': 2, 'next': 0, ...}, 'results': [{'backchann...: [], 'backchannel_providers_obj': [], 'group': '', 'launch_url': 'https://goauthentik.io/3l9NKPgUYN43EtducW8l', ...}]}
second = {'autocomplete': {}, 'pagination': {'count': 2, 'current': 1, 'end_index': 2, 'next': 0, ...}, 'results': [{'backchann...: [], 'backchannel_providers_obj': [], 'group': '', 'launch_url': 'https://goauthentik.io/3l9NKPgUYN43EtducW8l', ...}]}
msg = None

    def assertEqual(self, first, second, msg=None):
        """Fail if the two objects are unequal as determined by the '=='
           operator.
        """
        assertion_func = self._getAssertEqualityFunc(first, second)
>       assertion_func(first, second, msg=msg)

.../hostedtoolcache/Python/3.14.3................../x64/lib/python3.14/unittest/case.py:925: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_applications_api.TestApplicationsAPI testMethod=test_list>
d1 = {'autocomplete': {}, 'pagination': {'count': 2, 'current': 1, 'end_index': 2, 'next': 0, ...}, 'results': [{'backchann...: [], 'backchannel_providers_obj': [], 'group': '', 'launch_url': 'https://goauthentik.io/3l9NKPgUYN43EtducW8l', ...}]}
d2 = {'autocomplete': {}, 'pagination': {'count': 2, 'current': 1, 'end_index': 2, 'next': 0, ...}, 'results': [{'backchann...: [], 'backchannel_providers_obj': [], 'group': '', 'launch_url': 'https://goauthentik.io/3l9NKPgUYN43EtducW8l', ...}]}
msg = None

    def assertDictEqual(self, d1, d2, msg=None):
        self.assertIsInstance(d1, dict, 'First argument is not a dictionary')
        self.assertIsInstance(d2, dict, 'Second argument is not a dictionary')
    
        if d1 != d2:
            standardMsg = '%s != %s' % _common_shorten_repr(d1, d2)
            diff = ('\n' + '\n'.join(difflib.ndiff(
                           pprint.pformat(d1).splitlines(),
                           pprint.pformat(d2).splitlines())))
            standardMsg = self._truncateMessage(standardMsg, diff)
>           self.fail(self._formatMessage(msg, standardMsg))

.../hostedtoolcache/Python/3.14.3................../x64/lib/python3.14/unittest/case.py:1224: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_applications_api.TestApplicationsAPI testMethod=test_list>
msg = "{'pagination': {'next': 0, 'previous': 0, '[1147 chars]: {}} != {'autocomplete': {}, 'pagination': {'next':[1127 char.../OpenID '\n                                                        'Providers'},\n                'slug': 'allowed'}]}"

    def fail(self, msg=None):
        """Fail immediately, with the given message."""
>       raise self.failureException(msg)
E       AssertionError: {'pagination': {'next': 0, 'previous': 0, '[1147 chars]: {}} != {'autocomplete': {}, 'pagination': {'next':[1127 chars]y'}]}
E         {'autocomplete': {},
E          'pagination': {'count': 2,
E                         'current': 1,
E                         'end_index': 2,
E                         'next': 0,
E                         'previous': 0,
E                         'start_index': 1,
E                         'total_pages': 1},
E          'results': [{'backchannel_providers': [],
E                       'backchannel_providers_obj': [],
E                       'group': '',
E                       'launch_url': 'https://goauthentik.io/3l9NKPgUYN43EtducW8l',
E                       'meta_description': '',
E       -               'meta_hide': False,
E                       'meta_icon': '',
E                       'meta_icon_themed_urls': None,
E                       'meta_icon_url': None,
E                       'meta_launch_url': 'https://goauthentik.io/%(username)s',
E                       'meta_publisher': '',
E                       'name': 'allowed',
E                       'open_in_new_tab': True,
E                       'pk': '4cf8d0ce-d4b3-43e2-8100-0d4b528761ce',
E                       'policy_engine_mode': 'any',
E                       'provider': 8,
E                       'provider_obj': {'assigned_application_name': 'allowed',
E                                        'assigned_application_slug': 'allowed',
E                                        'assigned_backchannel_application_name': None,
E                                        'assigned_backchannel_application_slug': None,
E                                        'authentication_flow': None,
E                                        'authorization_flow': '654502fc-5062-4545-811d-fb640d4cde79',
E                                        'component': 'ak-provider-oauth2-form',
E                                        'invalidation_flow': None,
E                                        'meta_model_name': 'authentik_providers_oauth2.oauth2provider',
E                                        'name': 'test',
E                                        'pk': 8,
E                                        'property_mappings': [],
E                                        'verbose_name': 'OAuth2/OpenID Provider',
E                                        'verbose_name_plural': 'OAuth2/OpenID '
E                                                               'Providers'},
E                       'slug': 'allowed'}]}

.../hostedtoolcache/Python/3.14.3................../x64/lib/python3.14/unittest/case.py:750: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@kensternberg-authentik kensternberg-authentik changed the title core: add support for hidding applications from the user dashboard core: add support for hiding applications from the user dashboard Apr 9, 2026
Copy link
Copy Markdown
Contributor

@kensternberg-authentik kensternberg-authentik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The front-end changes look straightforward.

Questions:

  1. this hides the application from all users; does that mean that even someone with superuser permission wouldn't see them on their User dashboard? I didn't look too closely at the Python to be sure.

  2. Are there docs to go with this change? I'm sure @tanberry would love them!

Comment on lines +13 to +19
migrations.AddField(
model_name="application",
name="meta_hide",
field=models.BooleanField(
default=False, help_text="Hide this application from the user dashboard."
),
),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@melizeche could you add a RunPython step here that checks if the meta_launch_url is set to blank://blank, and if so enables this flag and empties the meta launch URL?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants