Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

admin: clean up Recovery process by admin #13124

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

gergosimonyi
Copy link
Collaborator

@gergosimonyi gergosimonyi commented Feb 19, 2025

Details

Closes #9671
Closes #12445 (Therefore, in this regard supersedes #12591)


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 has been updated (make gen-build)

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 website)

Copy link

netlify bot commented Feb 19, 2025

Deploy Preview for authentik-docs ready!

Name Link
🔨 Latest commit bb46027
🔍 Latest deploy log https://app.netlify.com/sites/authentik-docs/deploys/67b60df95509020008fca44d
😎 Deploy Preview https://deploy-preview-13124--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 site configuration.

Copy link

netlify bot commented Feb 19, 2025

Deploy Preview for authentik-storybook ready!

Name Link
🔨 Latest commit bb46027
🔍 Latest deploy log https://app.netlify.com/sites/authentik-storybook/deploys/67b60df9f767040008f76e68
😎 Deploy Preview https://deploy-preview-13124--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 site configuration.

Copy link

codecov bot commented Feb 19, 2025

❌ 6 Tests Failed:

Tests completed Failed Passed Skipped
1711 6 1705 2
View the top 3 failed test(s) by shortest run time
authentik.core.tests.test_users_api.TestUsersAPI::test_recovery_no_flow
Stack Traces | 0.333s run time
self = <unittest.case._Outcome object at 0x7f3127795880>
test_case = <authentik.core.tests.test_users_api.TestUsersAPI testMethod=test_recovery_no_flow>
subTest = False

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

.../hostedtoolcache/Python/3.12.9........./x64/lib/python3.12/unittest/case.py:58: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_users_api.TestUsersAPI testMethod=test_recovery_no_flow>
result = <TestCaseFunction test_recovery_no_flow>

    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.12.9........./x64/lib/python3.12/unittest/case.py:634: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_users_api.TestUsersAPI testMethod=test_recovery_no_flow>
method = <bound method TestUsersAPI.test_recovery_no_flow of <authentik.core.tests.test_users_api.TestUsersAPI testMethod=test_recovery_no_flow>>

    def _callTestMethod(self, method):
>       if method() is not None:

.../hostedtoolcache/Python/3.12.9........./x64/lib/python3.12/unittest/case.py:589: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_users_api.TestUsersAPI testMethod=test_recovery_no_flow>

    def test_recovery_no_flow(self):
        """Test user recovery link (no recovery flow set)"""
        self.client.force_login(self.admin)
        response = self.client.post(
>           reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk})
        )

.../core/tests/test_users_api.py:70: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

viewname = 'authentik_api:user-recovery', urlconf = None, args = []
kwargs = {'pk': 19}, current_app = None

    def reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None):
        if urlconf is None:
            urlconf = get_urlconf()
        resolver = get_resolver(urlconf)
        args = args or []
        kwargs = kwargs or {}
    
        prefix = get_script_prefix()
    
        if not isinstance(viewname, str):
            view = viewname
        else:
            *path, view = viewname.split(":")
    
            if current_app:
                current_path = current_app.split(":")
                current_path.reverse()
            else:
                current_path = None
    
            resolved_path = []
            ns_pattern = ""
            ns_converters = {}
            for ns in path:
                current_ns = current_path.pop() if current_path else None
                # Lookup the name to see if it could be an app identifier.
                try:
                    app_list = resolver.app_dict[ns]
                    # Yes! Path part matches an app in the current Resolver.
                    if current_ns and current_ns in app_list:
                        # If we are reversing for a particular app, use that
                        # namespace.
                        ns = current_ns
                    elif ns not in app_list:
                        # The name isn't shared by one of the instances (i.e.,
                        # the default) so pick the first instance as the default.
                        ns = app_list[0]
                except KeyError:
                    pass
    
                if ns != current_ns:
                    current_path = None
    
                try:
                    extra, resolver = resolver.namespace_dict[ns]
                    resolved_path.append(ns)
                    ns_pattern += extra
                    ns_converters.update(resolver.pattern.converters)
                except KeyError as key:
                    if resolved_path:
                        raise NoReverseMatch(
                            "%s is not a registered namespace inside '%s'"
                            % (key, ":".join(resolved_path))
                        )
                    else:
                        raise NoReverseMatch("%s is not a registered namespace" % key)
            if ns_pattern:
                resolver = get_ns_resolver(
                    ns_pattern, resolver, tuple(ns_converters.items())
                )
    
>       return resolver._reverse_with_prefix(view, prefix, *args, **kwargs)

../../../..../pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12.../django/urls/base.py:88: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <URLResolver <URLResolver list> (None:None) '^/'>
lookup_view = 'user-recovery', _prefix = '/', args = (), kwargs = {'pk': 19}
possibilities = []

    def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs):
        if args and kwargs:
            raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
    
        if not self._populated:
            self._populate()
    
        possibilities = self.reverse_dict.getlist(lookup_view)
    
        for possibility, pattern, defaults, converters in possibilities:
            for result, params in possibility:
                if args:
                    if len(args) != len(params):
                        continue
                    candidate_subs = dict(zip(params, args))
                else:
                    if set(kwargs).symmetric_difference(params).difference(defaults):
                        continue
                    matches = True
                    for k, v in defaults.items():
                        if k in params:
                            continue
                        if kwargs.get(k, v) != v:
                            matches = False
                            break
                    if not matches:
                        continue
                    candidate_subs = kwargs
                # Convert the candidate subs to text using Converter.to_url().
                text_candidate_subs = {}
                match = True
                for k, v in candidate_subs.items():
                    if k in converters:
                        try:
                            text_candidate_subs[k] = converters[k].to_url(v)
                        except ValueError:
                            match = False
                            break
                    else:
                        text_candidate_subs[k] = str(v)
                if not match:
                    continue
                # WSGI provides decoded URLs, without %xx escapes, and the URL
                # resolver operates on such URLs. First substitute arguments
                # without quoting to build a decoded URL and look for a match.
                # Then, if we have a match, redo the substitution with quoted
                # arguments in order to return a properly encoded URL.
                candidate_pat = _prefix.replace("%", "%%") + result
                if re.search(
                    "^%s%s" % (re.escape(_prefix), pattern),
                    candidate_pat % text_candidate_subs,
                ):
                    # safe characters from `pchar` definition of RFC 3986
                    url = quote(
                        candidate_pat % text_candidate_subs,
                        safe=RFC3986_SUBDELIMS + "/~:@",
                    )
                    # Don't allow construction of scheme relative urls.
                    return escape_leading_slashes(url)
        # lookup_view can be URL name or callable, but callables are not
        # friendly in error messages.
        m = getattr(lookup_view, "__module__", None)
        n = getattr(lookup_view, "__name__", None)
        if m is not None and n is not None:
            lookup_view_s = "%s.%s" % (m, n)
        else:
            lookup_view_s = lookup_view
    
        patterns = [pattern for (_, pattern, _, _) in possibilities]
        if patterns:
            if args:
                arg_msg = "arguments '%s'" % (args,)
            elif kwargs:
                arg_msg = "keyword arguments '%s'" % kwargs
            else:
                arg_msg = "no arguments"
            msg = "Reverse for '%s' with %s not found. %d pattern(s) tried: %s" % (
                lookup_view_s,
                arg_msg,
                len(patterns),
                patterns,
            )
        else:
            msg = (
                "Reverse for '%(view)s' not found. '%(view)s' is not "
                "a valid view function or pattern name." % {"view": lookup_view_s}
            )
>       raise NoReverseMatch(msg)
E       django.urls.exceptions.NoReverseMatch: Reverse for 'user-recovery' not found. 'user-recovery' is not a valid view function or pattern name.

../../../..../pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12.../django/urls/resolvers.py:851: NoReverseMatch
authentik.core.tests.test_users_api.TestUsersAPI::test_recovery_email_no_flow
Stack Traces | 0.334s run time
self = <unittest.case._Outcome object at 0x7f3127ad59a0>
test_case = <authentik.core.tests.test_users_api.TestUsersAPI testMethod=test_recovery_email_no_flow>
subTest = False

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

.../hostedtoolcache/Python/3.12.9........./x64/lib/python3.12/unittest/case.py:58: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_users_api.TestUsersAPI testMethod=test_recovery_email_no_flow>
result = <TestCaseFunction test_recovery_email_no_flow>

    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.12.9........./x64/lib/python3.12/unittest/case.py:634: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_users_api.TestUsersAPI testMethod=test_recovery_email_no_flow>
method = <bound method TestUsersAPI.test_recovery_email_no_flow of <authentik.core.tests.test_users_api.TestUsersAPI testMethod=test_recovery_email_no_flow>>

    def _callTestMethod(self, method):
>       if method() is not None:

.../hostedtoolcache/Python/3.12.9........./x64/lib/python3.12/unittest/case.py:589: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_users_api.TestUsersAPI testMethod=test_recovery_email_no_flow>

    def test_recovery_email_no_flow(self):
        """Test user recovery link (no recovery flow set)"""
        self.client.force_login(self.admin)
        response = self.client.post(
>           reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk})
        )

.../core/tests/test_users_api.py:103: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

viewname = 'authentik_api:user-recovery-email', urlconf = None, args = []
kwargs = {'pk': 30}, current_app = None

    def reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None):
        if urlconf is None:
            urlconf = get_urlconf()
        resolver = get_resolver(urlconf)
        args = args or []
        kwargs = kwargs or {}
    
        prefix = get_script_prefix()
    
        if not isinstance(viewname, str):
            view = viewname
        else:
            *path, view = viewname.split(":")
    
            if current_app:
                current_path = current_app.split(":")
                current_path.reverse()
            else:
                current_path = None
    
            resolved_path = []
            ns_pattern = ""
            ns_converters = {}
            for ns in path:
                current_ns = current_path.pop() if current_path else None
                # Lookup the name to see if it could be an app identifier.
                try:
                    app_list = resolver.app_dict[ns]
                    # Yes! Path part matches an app in the current Resolver.
                    if current_ns and current_ns in app_list:
                        # If we are reversing for a particular app, use that
                        # namespace.
                        ns = current_ns
                    elif ns not in app_list:
                        # The name isn't shared by one of the instances (i.e.,
                        # the default) so pick the first instance as the default.
                        ns = app_list[0]
                except KeyError:
                    pass
    
                if ns != current_ns:
                    current_path = None
    
                try:
                    extra, resolver = resolver.namespace_dict[ns]
                    resolved_path.append(ns)
                    ns_pattern += extra
                    ns_converters.update(resolver.pattern.converters)
                except KeyError as key:
                    if resolved_path:
                        raise NoReverseMatch(
                            "%s is not a registered namespace inside '%s'"
                            % (key, ":".join(resolved_path))
                        )
                    else:
                        raise NoReverseMatch("%s is not a registered namespace" % key)
            if ns_pattern:
                resolver = get_ns_resolver(
                    ns_pattern, resolver, tuple(ns_converters.items())
                )
    
>       return resolver._reverse_with_prefix(view, prefix, *args, **kwargs)

../../../..../pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12.../django/urls/base.py:88: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <URLResolver <URLResolver list> (None:None) '^/'>
lookup_view = 'user-recovery-email', _prefix = '/', args = ()
kwargs = {'pk': 30}, possibilities = []

    def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs):
        if args and kwargs:
            raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
    
        if not self._populated:
            self._populate()
    
        possibilities = self.reverse_dict.getlist(lookup_view)
    
        for possibility, pattern, defaults, converters in possibilities:
            for result, params in possibility:
                if args:
                    if len(args) != len(params):
                        continue
                    candidate_subs = dict(zip(params, args))
                else:
                    if set(kwargs).symmetric_difference(params).difference(defaults):
                        continue
                    matches = True
                    for k, v in defaults.items():
                        if k in params:
                            continue
                        if kwargs.get(k, v) != v:
                            matches = False
                            break
                    if not matches:
                        continue
                    candidate_subs = kwargs
                # Convert the candidate subs to text using Converter.to_url().
                text_candidate_subs = {}
                match = True
                for k, v in candidate_subs.items():
                    if k in converters:
                        try:
                            text_candidate_subs[k] = converters[k].to_url(v)
                        except ValueError:
                            match = False
                            break
                    else:
                        text_candidate_subs[k] = str(v)
                if not match:
                    continue
                # WSGI provides decoded URLs, without %xx escapes, and the URL
                # resolver operates on such URLs. First substitute arguments
                # without quoting to build a decoded URL and look for a match.
                # Then, if we have a match, redo the substitution with quoted
                # arguments in order to return a properly encoded URL.
                candidate_pat = _prefix.replace("%", "%%") + result
                if re.search(
                    "^%s%s" % (re.escape(_prefix), pattern),
                    candidate_pat % text_candidate_subs,
                ):
                    # safe characters from `pchar` definition of RFC 3986
                    url = quote(
                        candidate_pat % text_candidate_subs,
                        safe=RFC3986_SUBDELIMS + "/~:@",
                    )
                    # Don't allow construction of scheme relative urls.
                    return escape_leading_slashes(url)
        # lookup_view can be URL name or callable, but callables are not
        # friendly in error messages.
        m = getattr(lookup_view, "__module__", None)
        n = getattr(lookup_view, "__name__", None)
        if m is not None and n is not None:
            lookup_view_s = "%s.%s" % (m, n)
        else:
            lookup_view_s = lookup_view
    
        patterns = [pattern for (_, pattern, _, _) in possibilities]
        if patterns:
            if args:
                arg_msg = "arguments '%s'" % (args,)
            elif kwargs:
                arg_msg = "keyword arguments '%s'" % kwargs
            else:
                arg_msg = "no arguments"
            msg = "Reverse for '%s' with %s not found. %d pattern(s) tried: %s" % (
                lookup_view_s,
                arg_msg,
                len(patterns),
                patterns,
            )
        else:
            msg = (
                "Reverse for '%(view)s' not found. '%(view)s' is not "
                "a valid view function or pattern name." % {"view": lookup_view_s}
            )
>       raise NoReverseMatch(msg)
E       django.urls.exceptions.NoReverseMatch: Reverse for 'user-recovery-email' not found. 'user-recovery-email' is not a valid view function or pattern name.

../../../..../pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12.../django/urls/resolvers.py:851: NoReverseMatch
authentik.core.tests.test_users_api.TestUsersAPI::test_recovery
Stack Traces | 0.348s run time
self = <unittest.case._Outcome object at 0x7f3127a27dd0>
test_case = <authentik.core.tests.test_users_api.TestUsersAPI testMethod=test_recovery>
subTest = False

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

.../hostedtoolcache/Python/3.12.9........./x64/lib/python3.12/unittest/case.py:58: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_users_api.TestUsersAPI testMethod=test_recovery>
result = <TestCaseFunction test_recovery>

    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.12.9........./x64/lib/python3.12/unittest/case.py:634: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_users_api.TestUsersAPI testMethod=test_recovery>
method = <bound method TestUsersAPI.test_recovery of <authentik.core.tests.test_users_api.TestUsersAPI testMethod=test_recovery>>

    def _callTestMethod(self, method):
>       if method() is not None:

.../hostedtoolcache/Python/3.12.9........./x64/lib/python3.12/unittest/case.py:589: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.core.tests.test_users_api.TestUsersAPI testMethod=test_recovery>

    def test_recovery(self):
        """Test user recovery link (no recovery flow set)"""
        flow = create_test_flow(FlowDesignation.RECOVERY)
        brand: Brand = create_test_brand()
        brand.flow_recovery = flow
        brand.save()
        self.client.force_login(self.admin)
        response = self.client.post(
>           reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk})
        )

.../core/tests/test_users_api.py:95: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

viewname = 'authentik_api:user-recovery', urlconf = None, args = []
kwargs = {'pk': 8}, current_app = None

    def reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None):
        if urlconf is None:
            urlconf = get_urlconf()
        resolver = get_resolver(urlconf)
        args = args or []
        kwargs = kwargs or {}
    
        prefix = get_script_prefix()
    
        if not isinstance(viewname, str):
            view = viewname
        else:
            *path, view = viewname.split(":")
    
            if current_app:
                current_path = current_app.split(":")
                current_path.reverse()
            else:
                current_path = None
    
            resolved_path = []
            ns_pattern = ""
            ns_converters = {}
            for ns in path:
                current_ns = current_path.pop() if current_path else None
                # Lookup the name to see if it could be an app identifier.
                try:
                    app_list = resolver.app_dict[ns]
                    # Yes! Path part matches an app in the current Resolver.
                    if current_ns and current_ns in app_list:
                        # If we are reversing for a particular app, use that
                        # namespace.
                        ns = current_ns
                    elif ns not in app_list:
                        # The name isn't shared by one of the instances (i.e.,
                        # the default) so pick the first instance as the default.
                        ns = app_list[0]
                except KeyError:
                    pass
    
                if ns != current_ns:
                    current_path = None
    
                try:
                    extra, resolver = resolver.namespace_dict[ns]
                    resolved_path.append(ns)
                    ns_pattern += extra
                    ns_converters.update(resolver.pattern.converters)
                except KeyError as key:
                    if resolved_path:
                        raise NoReverseMatch(
                            "%s is not a registered namespace inside '%s'"
                            % (key, ":".join(resolved_path))
                        )
                    else:
                        raise NoReverseMatch("%s is not a registered namespace" % key)
            if ns_pattern:
                resolver = get_ns_resolver(
                    ns_pattern, resolver, tuple(ns_converters.items())
                )
    
>       return resolver._reverse_with_prefix(view, prefix, *args, **kwargs)

../../../..../pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12.../django/urls/base.py:88: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <URLResolver <URLResolver list> (None:None) '^/'>
lookup_view = 'user-recovery', _prefix = '/', args = (), kwargs = {'pk': 8}
possibilities = []

    def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs):
        if args and kwargs:
            raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
    
        if not self._populated:
            self._populate()
    
        possibilities = self.reverse_dict.getlist(lookup_view)
    
        for possibility, pattern, defaults, converters in possibilities:
            for result, params in possibility:
                if args:
                    if len(args) != len(params):
                        continue
                    candidate_subs = dict(zip(params, args))
                else:
                    if set(kwargs).symmetric_difference(params).difference(defaults):
                        continue
                    matches = True
                    for k, v in defaults.items():
                        if k in params:
                            continue
                        if kwargs.get(k, v) != v:
                            matches = False
                            break
                    if not matches:
                        continue
                    candidate_subs = kwargs
                # Convert the candidate subs to text using Converter.to_url().
                text_candidate_subs = {}
                match = True
                for k, v in candidate_subs.items():
                    if k in converters:
                        try:
                            text_candidate_subs[k] = converters[k].to_url(v)
                        except ValueError:
                            match = False
                            break
                    else:
                        text_candidate_subs[k] = str(v)
                if not match:
                    continue
                # WSGI provides decoded URLs, without %xx escapes, and the URL
                # resolver operates on such URLs. First substitute arguments
                # without quoting to build a decoded URL and look for a match.
                # Then, if we have a match, redo the substitution with quoted
                # arguments in order to return a properly encoded URL.
                candidate_pat = _prefix.replace("%", "%%") + result
                if re.search(
                    "^%s%s" % (re.escape(_prefix), pattern),
                    candidate_pat % text_candidate_subs,
                ):
                    # safe characters from `pchar` definition of RFC 3986
                    url = quote(
                        candidate_pat % text_candidate_subs,
                        safe=RFC3986_SUBDELIMS + "/~:@",
                    )
                    # Don't allow construction of scheme relative urls.
                    return escape_leading_slashes(url)
        # lookup_view can be URL name or callable, but callables are not
        # friendly in error messages.
        m = getattr(lookup_view, "__module__", None)
        n = getattr(lookup_view, "__name__", None)
        if m is not None and n is not None:
            lookup_view_s = "%s.%s" % (m, n)
        else:
            lookup_view_s = lookup_view
    
        patterns = [pattern for (_, pattern, _, _) in possibilities]
        if patterns:
            if args:
                arg_msg = "arguments '%s'" % (args,)
            elif kwargs:
                arg_msg = "keyword arguments '%s'" % kwargs
            else:
                arg_msg = "no arguments"
            msg = "Reverse for '%s' with %s not found. %d pattern(s) tried: %s" % (
                lookup_view_s,
                arg_msg,
                len(patterns),
                patterns,
            )
        else:
            msg = (
                "Reverse for '%(view)s' not found. '%(view)s' is not "
                "a valid view function or pattern name." % {"view": lookup_view_s}
            )
>       raise NoReverseMatch(msg)
E       django.urls.exceptions.NoReverseMatch: Reverse for 'user-recovery' not found. 'user-recovery' is not a valid view function or pattern name.

../../../..../pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12.../django/urls/resolvers.py:851: NoReverseMatch

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

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.

Regression 2024.12 breaks recovery flow Recovery emails / links do not respect token_expiry
1 participant