Skip to content

web/elements: render selected value in ak-search-select regardless of page - #25610

Open
GirlBossRush wants to merge 1 commit into
mainfrom
web/search-select-self-render-selected
Open

web/elements: render selected value in ak-search-select regardless of page#25610
GirlBossRush wants to merge 1 commit into
mainfrom
web/search-select-self-render-selected

Conversation

@GirlBossRush

@GirlBossRush GirlBossRush commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

This PR generalizes the fix from #25609 (now merged) by moving it into the component, so new call sites don't have to remember to work around it.

ak-search-select only ever rendered options from the fetched API page, so a selected object that sorted onto a later page of a paginated list had no matching option and the field fell back to its placeholder. #25609 worked around this by merging the current value into each fetchObjects result — one copy per call site, easy to forget.

Instead, getGroupedItems now includes the component's own selectedObject as an option when the fetched page doesn't already contain it (skipped while the user is actively searching, so a stale selection doesn't pin itself atop filtered results). Call sites just pass the object they already have via .selectedObject and drop the bespoke merges:

ak-provider-search-input is intentionally left as-is: it has only a pk (no embedded object) and already fetches the single provider by id, which is the correct approach for that case.

Relates to #25406.

@GirlBossRush
GirlBossRush requested a review from a team as a code owner August 31, 2026 19:53
@netlify

netlify Bot commented Aug 31, 2026

Copy link
Copy Markdown

Deploy Preview for authentik-storybook ready!

Name Link
🔨 Latest commit 171be88
🔍 Latest deploy log https://app.netlify.com/projects/authentik-storybook/deploys/6a95db9fd1b6830008f6ef19
😎 Deploy Preview https://deploy-preview-25610--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

netlify Bot commented Aug 31, 2026

Copy link
Copy Markdown

Deploy Preview for authentik-docs ready!

Name Link
🔨 Latest commit 171be88
🔍 Latest deploy log https://app.netlify.com/projects/authentik-docs/deploys/6a95db9ffbf1cd0008bac006
😎 Deploy Preview https://deploy-preview-25610--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.

@GirlBossRush GirlBossRush self-assigned this Aug 31, 2026
@GirlBossRush GirlBossRush added area:frontend Features or issues related to the browser, TypeScript, Node.js, etc backport/version-2026.8 Add this label to PRs to backport changes to version-2026.8 labels Aug 31, 2026
@GirlBossRush GirlBossRush moved this from Todo to Needs review in authentik Core Aug 31, 2026
@codecov

codecov Bot commented Aug 31, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
4250 1 4249 1
View the top 1 failed test(s) by shortest run time
tests.e2e.test_provider_radius.TestProviderRadius::test_radius_bind_success
Stack Traces | 16.8s run time
self = <unittest.case._Outcome object at 0x7f44262a4f50>
test_case = <tests.e2e.test_provider_radius.TestProviderRadius testMethod=test_radius_bind_success>
subTest = False

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

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

self = <tests.e2e.test_provider_radius.TestProviderRadius testMethod=test_radius_bind_success>
result = <TestCaseFunction test_radius_bind_success>

    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.7........./x64/lib/python3.14/unittest/case.py:669: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_radius.TestProviderRadius testMethod=test_radius_bind_success>
method = <bound method TestProviderRadius.test_radius_bind_success of <tests.e2e.test_provider_radius.TestProviderRadius testMethod=test_radius_bind_success>>

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

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

self = <tests.e2e.test_provider_radius.TestProviderRadius testMethod=test_radius_bind_success>
args = (), kwargs = {}

    @wraps(func)
    def wrapper(self: TransactionTestCase, *args, **kwargs):
        """Run test again if we're below max_retries, including tearDown and
        setUp. Otherwise raise the error"""
        nonlocal count
        try:
>           return func(self, *args, **kwargs)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^

tests/decorators.py:60: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_provider_radius.TestProviderRadius testMethod=test_radius_bind_success>,)
kwargs = {}

    @wraps(func)
    def wrapper(*args, **kwargs):
        for file in files:
            content = BlueprintInstance(path=file).retrieve()
            Importer.from_string(content).apply()
>       return func(*args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^

.../blueprints/tests/__init__.py:25: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_radius.TestProviderRadius testMethod=test_radius_bind_success>

    @retry(exceptions=[Timeout])
    @apply_blueprint(
        "default/flow-default-authentication-flow.yaml",
        "default/flow-default-invalidation-flow.yaml",
    )
    def test_radius_bind_success(self):
        """Test simple bind"""
        self._prepare()
        srv = Client(
            server="localhost",
            secret=self.shared_secret.encode(),
            dict=Dictionary(".../radius/dictionaries/dictionary"),
        )
    
        req = srv.CreateAuthPacket(
            code=AccessRequest, User_Name=self.user.username, NAS_Identifier="localhost"
        )
        req["User-Password"] = req.PwCrypt(self.user.username)
        req.add_message_authenticator()
    
>       reply = srv.SendPacket(req)
                ^^^^^^^^^^^^^^^^^^^

tests/e2e/test_provider_radius.py:78: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pyrad.client.Client object at 0x7f4426b9bed0>
pkt = AuthPacket({'User-Name': ['8e391yl56pHPg8KL9oOe'], 'NAS-Identifier': ['localhost'], 'User-Password': ['\x7f������s�h�E����Ll��оR�;c\x08����'], 'Message-Authenticator': [b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00']})

    def SendPacket(self, pkt):
        """Send a packet to a RADIUS server.
    
        :param pkt: the packet to send
        :type pkt:  pyrad.packet.Packet
        :return:    the reply packet received
        :rtype:     pyrad.packet.Packet
        :raise Timeout: RADIUS server does not reply
        """
        if isinstance(pkt, packet.AuthPacket):
            if pkt.auth_type == 'eap-md5':
                # Creating EAP-Identity
                password = pkt[2][0] if 2 in pkt else pkt[1][0]
                pkt[79] = [struct.pack('!BBHB%ds' % len(password),
                                       EAP_CODE_RESPONSE,
                                       packet.CurrentID,
                                       len(password) + 5,
                                       EAP_TYPE_IDENTITY,
                                       password)]
>           reply = self._SendPacket(pkt, self.authport)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.venv/lib/python3.14....../site-packages/pyrad/client.py:201: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pyrad.client.Client object at 0x7f4426b9bed0>
pkt = AuthPacket({'User-Name': ['8e391yl56pHPg8KL9oOe'], 'NAS-Identifier': ['localhost'], 'User-Password': ['\x7f������s�h�E����Ll��оR�;c\x08����'], 'Message-Authenticator': [b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00']})
port = 1812

    def _SendPacket(self, pkt, port):
        """Send a packet to a RADIUS server.
    
        :param pkt:  the packet to send
        :type pkt:   pyrad.packet.Packet
        :param port: UDP port to send packet to
        :type port:  integer
        :return:     the reply packet received
        :rtype:      pyrad.packet.Packet
        :raise Timeout: RADIUS server does not reply
        """
        self._SocketOpen()
    
        for attempt in range(self.retries):
            if attempt and pkt.code == packet.AccountingRequest:
                if "Acct-Delay-Time" in pkt:
                    pkt["Acct-Delay-Time"] = \
                            pkt["Acct-Delay-Time"][0] + self.timeout
                else:
                    pkt["Acct-Delay-Time"] = self.timeout
    
            now = time.time()
            waitto = now + self.timeout
    
>           self._socket.sendto(pkt.RequestPacket(), (self.server, port))
                                ^^^^^^^^^^^^^^^^^^^

.venv/lib/python3.14....../site-packages/pyrad/client.py:158: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = AuthPacket({'User-Name': ['8e391yl56pHPg8KL9oOe'], 'NAS-Identifier': ['localhost'], 'User-Password': ['\x7f������s�h�E����Ll��оR�;c\x08����'], 'Message-Authenticator': [b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00']})

    def RequestPacket(self):
        """Create a ready-to-transmit authentication request packet.
        Return a RADIUS packet which can be directly transmitted
        to a RADIUS server.
    
        :return: raw packet
        :rtype:  string
        """
        if self.authenticator is None:
            self.authenticator = self.CreateAuthenticator()
    
        if self.id is None:
            self.id = self.CreateID()
    
        if self.message_authenticator:
>           self._refresh_message_authenticator()

.venv/lib/python3.14.............../site-packages/pyrad/packet.py:685: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = AuthPacket({'User-Name': ['8e391yl56pHPg8KL9oOe'], 'NAS-Identifier': ['localhost'], 'User-Password': ['\x7f������s�h�E����Ll��оR�;c\x08����'], 'Message-Authenticator': [b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00']})

    def _refresh_message_authenticator(self):
        hmac_constructor = hmac.new(self.secret, digestmod='MD5')
    
        # Maintain a zero octets content for md5 and hmac calculation.
        self['Message-Authenticator'] = 16 * b'\00'
        attr = self._PktEncodeAttributes()
    
        header = struct.pack('!BBH', self.code, self.id,
                             (20 + len(attr)))
    
        hmac_constructor.update(header[0:4])
        if self.code in (AccountingRequest, DisconnectRequest,
                         CoARequest, AccountingResponse):
            hmac_constructor.update(16 * b'\00')
        else:
            # NOTE: self.authenticator on reply packet is initialized
            #       with request authenticator by design.
            #       For AccessAccept, AccessReject and AccessChallenge
            #       it is needed use original Authenticator.
            #       For AccessAccept, AccessReject and AccessChallenge
            #       it is needed use original Authenticator.
            if self.authenticator is None:
                raise Exception('No authenticator found')
            hmac_constructor.update(self.authenticator)
    
        hmac_constructor.update(attr)
>       self['Message-Authenticator'] = hmac_constructor.digest()
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.venv/lib/python3.14.............../site-packages/pyrad/packet.py:155: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = AuthPacket({'User-Name': ['8e391yl56pHPg8KL9oOe'], 'NAS-Identifier': ['localhost'], 'User-Password': ['\x7f������s�h�E����Ll��оR�;c\x08����'], 'Message-Authenticator': [b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00']})
key = 'Message-Authenticator'
item = b'0x\xd6b\x0f\x02\xe61\x9fSu\xcam\xb6\x8a\x07'

    def __setitem__(self, key, item):
        if isinstance(key, str):
>           (key, item) = self._EncodeKeyValues(key, item)
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.venv/lib/python3.14.............../site-packages/pyrad/packet.py:352: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = AuthPacket({'User-Name': ['8e391yl56pHPg8KL9oOe'], 'NAS-Identifier': ['localhost'], 'User-Password': ['\x7f������s�h�E����Ll��оR�;c\x08����'], 'Message-Authenticator': [b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00']})
key = 80, values = [b'0x\xd6b\x0f\x02\xe61\x9fSu\xcam\xb6\x8a\x07']

    def _EncodeKeyValues(self, key, values):
        if not isinstance(key, str):
            return (key, values)
    
        if not isinstance(values, (list, tuple)):
            values = [values]
    
        key, _, tag = key.partition(":")
        attr = self.dict.attributes[key]
        key = self._EncodeKey(key)
        if tag:
            tag = struct.pack('B', int(tag))
            if attr.type == "integer":
                return (key, [tag + self._EncodeValue(attr, v)[1:] for v in values])
            else:
                return (key, [tag + self._EncodeValue(attr, v) for v in values])
        else:
>           return (key, [self._EncodeValue(attr, v) for v in values])
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^

.venv/lib/python3.14.............../site-packages/pyrad/packet.py:272: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = AuthPacket({'User-Name': ['8e391yl56pHPg8KL9oOe'], 'NAS-Identifier': ['localhost'], 'User-Password': ['\x7f������s�h�E����Ll��оR�;c\x08����'], 'Message-Authenticator': [b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00']})
attr = <pyrad.dictionary.Attribute object at 0x7f4425b99770>
value = b'0x\xd6b\x0f\x02\xe61\x9fSu\xcam\xb6\x8a\x07'

    def _EncodeValue(self, attr, value):
        result = ''
        if attr.values.HasForward(value):
            result = attr.values.GetForward(value)
        else:
>           result = tools.EncodeAttr(attr.type, value)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.venv/lib/python3.14.............../site-packages/pyrad/packet.py:247: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

datatype = 'octets', value = b'0x\xd6b\x0f\x02\xe61\x9fSu\xcam\xb6\x8a\x07'

    def EncodeAttr(datatype, value):
        if datatype == "string":
            return EncodeString(value)
        elif datatype == "octets":
>           return EncodeOctets(value)
                   ^^^^^^^^^^^^^^^^^^^

.venv/lib/python3.14....../site-packages/pyrad/tools.py:284: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

value = b'0x\xd6b\x0f\x02\xe61\x9fSu\xcam\xb6\x8a\x07'

    def EncodeOctets(value):
        """
        Encodes RADIUS attributes of type "octets" into a byte sequence.
    
        Supported inputs:
        - bytes / bytearray:
            * If the value starts with b"0x", it is treated as a hex string and decoded.
            * Otherwise the byte value is passed through unchanged.
        - str:
            * "0x..."  → hexadecimal representation, decoded into bytes
            * Decimal string (e.g. "65"):
                - 0..255 → encoded as a single byte
                - >255   → encoded as a minimal big-endian byte sequence
            * Any other string is UTF-8 encoded
    
        Constraints:
        - The resulting byte sequence must not exceed 253 bytes
          (RADIUS attribute size limit).
    
        This behavior preserves compatibility with legacy pyrad dictionary
        definitions and existing test cases.
        """
        if value is None:
            return b""
    
        if isinstance(value, (bytes, bytearray)):
            b = bytes(value)
            if b.startswith(b"0x"):
>               out = binascii.unhexlify(b[2:])
                      ^^^^^^^^^^^^^^^^^^^^^^^^^
E               binascii.Error: Non-hexadecimal digit found

.venv/lib/python3.14....../site-packages/pyrad/tools.py:59: Error

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

@github-actions

Copy link
Copy Markdown
Contributor

authentik PR Installation instructions

Instructions for docker-compose

Add the following block to your .env file:

AUTHENTIK_IMAGE=ghcr.io/goauthentik/dev-server
AUTHENTIK_TAG=gh-171be88e1672d70004ebad76d18c8b5d09854f3a
AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE=ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s

Afterwards, run the upgrade commands from the latest release notes.

Instructions for Kubernetes

Add the following block to your values.yml file:

authentik:
    outposts:
        container_image_base: ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s
global:
    image:
        repository: ghcr.io/goauthentik/dev-server
        tag: gh-171be88e1672d70004ebad76d18c8b5d09854f3a

Afterwards, run the upgrade commands from the latest release notes.

… page

Move the 'keep the current selection visible' responsibility into the
component. getGroupedItems now includes selectedObject as an option when the
fetched page doesn't contain it (skipped while searching), so a paginated
selection renders instead of falling back to the placeholder.

Call sites pass the object they already have via .selectedObject and drop the
bespoke fetchObjects merges: PolicyBindingForm (policy/group/user),
StageBindingForm (stage), and TokenForm (user).
@GirlBossRush
GirlBossRush force-pushed the web/search-select-self-render-selected branch from 171be88 to 8ed1749 Compare August 31, 2026 21:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:frontend Features or issues related to the browser, TypeScript, Node.js, etc backport/version-2026.8 Add this label to PRs to backport changes to version-2026.8

1 participant