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

providers/scim: handle no members in group in consistency check #11801

Merged
merged 1 commit into from
Oct 25, 2024

Conversation

BeryJu
Copy link
Member

@BeryJu BeryJu commented Oct 24, 2024

Details

REPLACE ME


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)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
@BeryJu BeryJu requested a review from a team as a code owner October 24, 2024 23:52
Copy link

sentry-io bot commented Oct 24, 2024

🔍 Existing Issues For Review

Your pull request is modifying functions with the following pre-existing issues:

📄 File: authentik/providers/scim/clients/groups.py

Function Unhandled Issue
patch_compare_users ValueError: n must be at least one authentik.prov...
Event Count: 14
patch_compare_users TypeError: 'NoneType' object is not iterable auth...
Event Count: 1

Did you find this useful? React with a 👍 or 👎

Copy link

netlify bot commented Oct 24, 2024

Deploy Preview for authentik-storybook canceled.

Name Link
🔨 Latest commit 8b75f97
🔍 Latest deploy log https://app.netlify.com/sites/authentik-storybook/deploys/671addbbaff1070008e6374b

Copy link

netlify bot commented Oct 24, 2024

Deploy Preview for authentik-docs ready!

Name Link
🔨 Latest commit 8b75f97
🔍 Latest deploy log https://app.netlify.com/sites/authentik-docs/deploys/671addbb4ffed40008a2240b
😎 Deploy Preview https://deploy-preview-11801--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

codecov bot commented Oct 24, 2024

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
1587 1 1586 1
View the full list of 1 ❄️ flaky tests
tests.e2e.test_provider_proxy_forward.TestProviderProxyForward test_nginx

Flake rate in main: 39.39% (Passed 60 times, Failed 39 times)

Stack Traces | 220s run time
self = &lt;asgiref.sync.AsyncToSync object at 0x7f77fbac04d0&gt;
call_result = &lt;Future at 0x7f77e467b860 state=finished returned NoneType&gt;
exc_info = (&lt;class 'selenium.common.exceptions.NoSuchElementException'&gt;, NoSuchElementException(), &lt;traceback object at 0x7f77e4579a40&gt;)
task_context = None, context = [&lt;_contextvars.Context object at 0x7f77e4519080&gt;]
args = ('group_outpost_c4f73f0f-01c5-40fb-9e98-a5e796262a99', {'type': 'event.update'})
kwargs = {}, __traceback_hide__ = True
current_task = &lt;Task finished name='Task-66' coro=&lt;AsyncToSync.main_wrap() done, defined at ............/home/runner/.cache............/pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12......................../site-packages/asgiref/sync.py:299&gt; result=None&gt;
result = None

    async def main_wrap(
        self,
        call_result: "Future[_R]",
        exc_info: "OptExcInfo",
        task_context: "Optional[List[asyncio.Task[Any]]]",
        context: List[contextvars.Context],
        *args: _P.args,
        **kwargs: _P.kwargs,
    ) -&gt; None:
        """
        Wraps the awaitable with something that puts the result into the
        result/exception future.
        """
    
        __traceback_hide__ = True  # noqa: F841
    
        if context is not None:
            _restore_context(context[0])
    
        current_task = asyncio.current_task()
        if current_task is not None and task_context is not None:
            task_context.append(current_task)
    
        try:
            # If we have an exception, run the function inside the except block
            # after raising it so exc_info is correctly populated.
            if exc_info[1]:
                try:
&gt;                   raise exc_info[1]

../../../.cache............/pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12......................../site-packages/asgiref/sync.py:327: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;asgiref.sync.AsyncToSync object at 0x7f77f83c5e50&gt;
call_result = &lt;Future at 0x7f780298a4e0 state=finished returned NoneType&gt;
exc_info = (&lt;class 'selenium.common.exceptions.NoSuchElementException'&gt;, NoSuchElementException(), &lt;traceback object at 0x7f77f96b79c0&gt;)
task_context = None, context = [&lt;_contextvars.Context object at 0x7f77f8394180&gt;]
args = ('group_outpost_c4f73f0f-01c5-40fb-9e98-a5e796262a99', {'type': 'event.update'})
kwargs = {}, __traceback_hide__ = True
current_task = &lt;Task finished name='Task-61' coro=&lt;AsyncToSync.main_wrap() done, defined at ............/home/runner/.cache............/pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12......................../site-packages/asgiref/sync.py:299&gt; result=None&gt;
result = None

    async def main_wrap(
        self,
        call_result: "Future[_R]",
        exc_info: "OptExcInfo",
        task_context: "Optional[List[asyncio.Task[Any]]]",
        context: List[contextvars.Context],
        *args: _P.args,
        **kwargs: _P.kwargs,
    ) -&gt; None:
        """
        Wraps the awaitable with something that puts the result into the
        result/exception future.
        """
    
        __traceback_hide__ = True  # noqa: F841
    
        if context is not None:
            _restore_context(context[0])
    
        current_task = asyncio.current_task()
        if current_task is not None and task_context is not None:
            task_context.append(current_task)
    
        try:
            # If we have an exception, run the function inside the except block
            # after raising it so exc_info is correctly populated.
            if exc_info[1]:
                try:
&gt;                   raise exc_info[1]

../../../.cache............/pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12......................../site-packages/asgiref/sync.py:327: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;tests.e2e.test_provider_proxy_forward.TestProviderProxyForward testMethod=test_nginx&gt;
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:
&gt;           return func(self, *args, **kwargs)

tests/e2e/utils.py:287: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;tests.e2e.test_provider_proxy_forward.TestProviderProxyForward testMethod=test_nginx&gt;

    @retry()
    def test_nginx(self):
        """Test nginx"""
        self.prepare()
    
        # Start nginx last so all hosts are resolvable, otherwise nginx exits
        self.run_container(
            image="docker.io/library/nginx:1.27",
            ports={
                "80": "80",
            },
            volumes={
                f"{Path(__file__).parent / "proxy_forward_auth" / "nginx_single" / "nginx.conf"}": {
                    "bind": "........./etc/nginx/conf.d/default.conf",
                }
            },
        )
    
        self.driver.get("http:.../localhost/api")
&gt;       self.login()

tests/e2e/test_provider_proxy_forward.py:145: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;tests.e2e.test_provider_proxy_forward.TestProviderProxyForward testMethod=test_nginx&gt;

    def login(self):
        """Do entire login flow and check user afterwards"""
&gt;       flow_executor = self.get_shadow_root("ak-flow-executor")

tests/e2e/utils.py:232: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;tests.e2e.test_provider_proxy_forward.TestProviderProxyForward testMethod=test_nginx&gt;
selector = 'ak-flow-executor'
container = &lt;selenium.webdriver.remote.webdriver.WebDriver (session="8a08e289b7a5665f0cb0316038d9c6a6")&gt;

    def get_shadow_root(
        self, selector: str, container: WebElement | WebDriver | None = None
    ) -&gt; WebElement:
        """Get shadow root element's inner shadowRoot"""
        if not container:
            container = self.driver
&gt;       shadow_root = container.find_element(By.CSS_SELECTOR, selector)

tests/e2e/utils.py:226: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;selenium.webdriver.remote.webdriver.WebDriver (session="8a08e289b7a5665f0cb0316038d9c6a6")&gt;
by = 'css selector', value = 'ak-flow-executor'

    def find_element(self, by=By.ID, value: Optional[str] = None) -&gt; WebElement:
        """Find an element given a By strategy and locator.
    
        :Usage:
            ::
    
                element = driver.find_element(By.ID, 'foo')
    
        :rtype: WebElement
        """
        if isinstance(by, RelativeBy):
            elements = self.find_elements(by=by, value=value)
            if not elements:
                raise NoSuchElementException(f"Cannot locate relative element with: {by.root}")
            return elements[0]
    
        if by == By.ID:
            by = By.CSS_SELECTOR
            value = f'[id="{value}"]'
        elif by == By.CLASS_NAME:
            by = By.CSS_SELECTOR
            value = f".{value}"
        elif by == By.NAME:
            by = By.CSS_SELECTOR
            value = f'[name="{value}"]'
    
&gt;       return self.execute(Command.FIND_ELEMENT, {"using": by, "value": value})["value"]

../../../.cache............/pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12.../webdriver/remote/webdriver.py:748: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;selenium.webdriver.remote.webdriver.WebDriver (session="8a08e289b7a5665f0cb0316038d9c6a6")&gt;
driver_command = 'findElement'
params = {'using': 'css selector', 'value': 'ak-flow-executor'}

    def execute(self, driver_command: str, params: dict = None) -&gt; dict:
        """Sends a command to be executed by a command.CommandExecutor.
    
        :Args:
         - driver_command: The name of the command to execute as a string.
         - params: A dictionary of named parameters to send with the command.
    
        :Returns:
          The command's JSON response loaded into a dictionary object.
        """
        params = self._wrap_value(params)
    
        if self.session_id:
            if not params:
                params = {"sessionId": self.session_id}
            elif "sessionId" not in params:
                params["sessionId"] = self.session_id
    
        response = self.command_executor.execute(driver_command, params)
        if response:
&gt;           self.error_handler.check_response(response)

../../../.cache............/pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12.../webdriver/remote/webdriver.py:354: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;selenium.webdriver.remote.errorhandler.ErrorHandler object at 0x7f77f839d520&gt;
response = {'status': 404, 'value': '{"value":{"error":"no such element","message":"no such element: Unable to locate element: {\...\\n#16 0x555d94ac4fc3 \\u003Cunknown&gt;\\n#17 0x555d94ad5944 \\u003Cunknown&gt;\\n#18 0x7ff2e4afaac3 \\u003Cunknown&gt;\\n"}}'}

    def check_response(self, response: Dict[str, Any]) -&gt; None:
        """Checks that a JSON response from the WebDriver does not have an
        error.
    
        :Args:
         - response - The JSON response from the WebDriver server as a dictionary
           object.
    
        :Raises: If the response contains an error message.
        """
        status = response.get("status", None)
        if not status or status == ErrorCode.SUCCESS:
            return
        value = None
        message = response.get("message", "")
        screen: str = response.get("screen", "")
        stacktrace = None
        if isinstance(status, int):
            value_json = response.get("value", None)
            if value_json and isinstance(value_json, str):
                import json
    
                try:
                    value = json.loads(value_json)
                    if len(value) == 1:
                        value = value["value"]
                    status = value.get("error", None)
                    if not status:
                        status = value.get("status", ErrorCode.UNKNOWN_ERROR)
                        message = value.get("value") or value.get("message")
                        if not isinstance(message, str):
                            value = message
                            message = message.get("message")
                    else:
                        message = value.get("message", None)
                except ValueError:
                    pass
    
        exception_class: Type[WebDriverException]
        e = ErrorCode()
        error_codes = [item for item in dir(e) if not item.startswith("__")]
        for error_code in error_codes:
            error_info = getattr(ErrorCode, error_code)
            if isinstance(error_info, list) and status in error_info:
                exception_class = getattr(ExceptionMapping, error_code, WebDriverException)
                break
        else:
            exception_class = WebDriverException
    
        if not value:
            value = response["value"]
        if isinstance(value, str):
            raise exception_class(value)
        if message == "" and "message" in value:
            message = value["message"]
    
        screen = None  # type: ignore[assignment]
        if "screen" in value:
            screen = value["screen"]
    
        stacktrace = None
        st_value = value.get("stackTrace") or value.get("stacktrace")
        if st_value:
            if isinstance(st_value, str):
                stacktrace = st_value.split("\n")
            else:
                stacktrace = []
                try:
                    for frame in st_value:
                        line = frame.get("lineNumber", "")
                        file = frame.get("fileName", "&lt;anonymous&gt;")
                        if line:
                            file = f"{file}:{line}"
                        meth = frame.get("methodName", "&lt;anonymous&gt;")
                        if "className" in frame:
                            meth = f"{frame['className']}.{meth}"
                        msg = "    at %s (%s)"
                        msg = msg % (meth, file)
                        stacktrace.append(msg)
                except TypeError:
                    pass
        if exception_class == UnexpectedAlertPresentException:
            alert_text = None
            if "data" in value:
                alert_text = value["data"].get("text")
            elif "alert" in value:
                alert_text = value["alert"].get("text")
            raise exception_class(message, screen, stacktrace, alert_text)  # type: ignore[call-arg]  # mypy is not smart enough here
&gt;       raise exception_class(message, screen, stacktrace)
E       selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"ak-flow-executor"}
E         (Session info: chrome=122.0.6261.69); For documentation on this error, please visit: https://www.selenium..../webdriver/troubleshooting/errors#no-such-element-exception
E       Stacktrace:
E       #0 0x555d94ad6793 &lt;unknown&gt;
E       #1 0x555d947ca1c6 &lt;unknown&gt;
E       #2 0x555d94815358 &lt;unknown&gt;
E       #3 0x555d94815411 &lt;unknown&gt;
E       #4 0x555d94858934 &lt;unknown&gt;
E       #5 0x555d948373fd &lt;unknown&gt;
E       #6 0x555d94855dd9 &lt;unknown&gt;
E       #7 0x555d94837173 &lt;unknown&gt;
E       #8 0x555d948082d3 &lt;unknown&gt;
E       #9 0x555d94808c9e &lt;unknown&gt;
E       #10 0x555d94a9a8cb &lt;unknown&gt;
E       #11 0x555d94a9e745 &lt;unknown&gt;
E       #12 0x555d94a872e1 &lt;unknown&gt;
E       #13 0x555d94a9f2d2 &lt;unknown&gt;
E       #14 0x555d94a6b17f &lt;unknown&gt;
E       #15 0x555d94ac4dc8 &lt;unknown&gt;
E       #16 0x555d94ac4fc3 &lt;unknown&gt;
E       #17 0x555d94ad5944 &lt;unknown&gt;
E       #18 0x7ff2e4afaac3 &lt;unknown&gt;

../../../.cache............/pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12.../webdriver/remote/errorhandler.py:229: NoSuchElementException

During handling of the above exception, another exception occurred:

self = &lt;asgiref.sync.AsyncToSync object at 0x7f77fbc22180&gt;
call_result = &lt;Future at 0x7f77fbc22a80 state=finished returned NoneType&gt;
exc_info = (&lt;class 'selenium.common.exceptions.NoSuchElementException'&gt;, NoSuchElementException(), &lt;traceback object at 0x7f77fbe08d00&gt;)
task_context = None, context = [&lt;_contextvars.Context object at 0x7f77fbe23300&gt;]
args = ('group_outpost_b4891c3e-0ced-4c70-b2ec-a26e6c7dae60', {'type': 'event.update'})
kwargs = {}, __traceback_hide__ = True
current_task = &lt;Task finished name='Task-76' coro=&lt;AsyncToSync.main_wrap() done, defined at ............/home/runner/.cache............/pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12......................../site-packages/asgiref/sync.py:299&gt; result=None&gt;
result = None

    async def main_wrap(
        self,
        call_result: "Future[_R]",
        exc_info: "OptExcInfo",
        task_context: "Optional[List[asyncio.Task[Any]]]",
        context: List[contextvars.Context],
        *args: _P.args,
        **kwargs: _P.kwargs,
    ) -&gt; None:
        """
        Wraps the awaitable with something that puts the result into the
        result/exception future.
        """
    
        __traceback_hide__ = True  # noqa: F841
    
        if context is not None:
            _restore_context(context[0])
    
        current_task = asyncio.current_task()
        if current_task is not None and task_context is not None:
            task_context.append(current_task)
    
        try:
            # If we have an exception, run the function inside the except block
            # after raising it so exc_info is correctly populated.
            if exc_info[1]:
                try:
&gt;                   raise exc_info[1]

../../../.cache............/pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12......................../site-packages/asgiref/sync.py:327: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;asgiref.sync.AsyncToSync object at 0x7f77e47d5c40&gt;
call_result = &lt;Future at 0x7f77e4701a00 state=finished returned NoneType&gt;
exc_info = (&lt;class 'selenium.common.exceptions.NoSuchElementException'&gt;, NoSuchElementException(), &lt;traceback object at 0x7f77fbdbcbc0&gt;)
task_context = None, context = [&lt;_contextvars.Context object at 0x7f77e4518e40&gt;]
args = ('group_outpost_b4891c3e-0ced-4c70-b2ec-a26e6c7dae60', {'type': 'event.update'})
kwargs = {}, __traceback_hide__ = True
current_task = &lt;Task finished name='Task-71' coro=&lt;AsyncToSync.main_wrap() done, defined at ............/home/runner/.cache............/pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12......................../site-packages/asgiref/sync.py:299&gt; result=None&gt;
result = None

    async def main_wrap(
        self,
        call_result: "Future[_R]",
        exc_info: "OptExcInfo",
        task_context: "Optional[List[asyncio.Task[Any]]]",
        context: List[contextvars.Context],
        *args: _P.args,
        **kwargs: _P.kwargs,
    ) -&gt; None:
        """
        Wraps the awaitable with something that puts the result into the
        result/exception future.
        """
    
        __traceback_hide__ = True  # noqa: F841
    
        if context is not None:
            _restore_context(context[0])
    
        current_task = asyncio.current_task()
        if current_task is not None and task_context is not None:
            task_context.append(current_task)
    
        try:
            # If we have an exception, run the function inside the except block
            # after raising it so exc_info is correctly populated.
            if exc_info[1]:
                try:
&gt;                   raise exc_info[1]

../../../.cache............/pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12......................../site-packages/asgiref/sync.py:327: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;tests.e2e.test_provider_proxy_forward.TestProviderProxyForward testMethod=test_nginx&gt;
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:
&gt;           return func(self, *args, **kwargs)

tests/e2e/utils.py:287: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;tests.e2e.test_provider_proxy_forward.TestProviderProxyForward testMethod=test_nginx&gt;

    @retry()
    def test_nginx(self):
        """Test nginx"""
        self.prepare()
    
        # Start nginx last so all hosts are resolvable, otherwise nginx exits
        self.run_container(
            image="docker.io/library/nginx:1.27",
            ports={
                "80": "80",
            },
            volumes={
                f"{Path(__file__).parent / "proxy_forward_auth" / "nginx_single" / "nginx.conf"}": {
                    "bind": "........./etc/nginx/conf.d/default.conf",
                }
            },
        )
    
        self.driver.get("http:.../localhost/api")
&gt;       self.login()

tests/e2e/test_provider_proxy_forward.py:145: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;tests.e2e.test_provider_proxy_forward.TestProviderProxyForward testMethod=test_nginx&gt;

    def login(self):
        """Do entire login flow and check user afterwards"""
&gt;       flow_executor = self.get_shadow_root("ak-flow-executor")

tests/e2e/utils.py:232: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;tests.e2e.test_provider_proxy_forward.TestProviderProxyForward testMethod=test_nginx&gt;
selector = 'ak-flow-executor'
container = &lt;selenium.webdriver.remote.webdriver.WebDriver (session="fbfe82c9a68519047a214d1d0e5cdb1e")&gt;

    def get_shadow_root(
        self, selector: str, container: WebElement | WebDriver | None = None
    ) -&gt; WebElement:
        """Get shadow root element's inner shadowRoot"""
        if not container:
            container = self.driver
&gt;       shadow_root = container.find_element(By.CSS_SELECTOR, selector)

tests/e2e/utils.py:226: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;selenium.webdriver.remote.webdriver.WebDriver (session="fbfe82c9a68519047a214d1d0e5cdb1e")&gt;
by = 'css selector', value = 'ak-flow-executor'

    def find_element(self, by=By.ID, value: Optional[str] = None) -&gt; WebElement:
        """Find an element given a By strategy and locator.
    
        :Usage:
            ::
    
                element = driver.find_element(By.ID, 'foo')
    
        :rtype: WebElement
        """
        if isinstance(by, RelativeBy):
            elements = self.find_elements(by=by, value=value)
            if not elements:
                raise NoSuchElementException(f"Cannot locate relative element with: {by.root}")
            return elements[0]
    
        if by == By.ID:
            by = By.CSS_SELECTOR
            value = f'[id="{value}"]'
        elif by == By.CLASS_NAME:
            by = By.CSS_SELECTOR
            value = f".{value}"
        elif by == By.NAME:
            by = By.CSS_SELECTOR
            value = f'[name="{value}"]'
    
&gt;       return self.execute(Command.FIND_ELEMENT, {"using": by, "value": value})["value"]

../../../.cache............/pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12.../webdriver/remote/webdriver.py:748: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;selenium.webdriver.remote.webdriver.WebDriver (session="fbfe82c9a68519047a214d1d0e5cdb1e")&gt;
driver_command = 'findElement'
params = {'using': 'css selector', 'value': 'ak-flow-executor'}

    def execute(self, driver_command: str, params: dict = None) -&gt; dict:
        """Sends a command to be executed by a command.CommandExecutor.
    
        :Args:
         - driver_command: The name of the command to execute as a string.
         - params: A dictionary of named parameters to send with the command.
    
        :Returns:
          The command's JSON response loaded into a dictionary object.
        """
        params = self._wrap_value(params)
    
        if self.session_id:
            if not params:
                params = {"sessionId": self.session_id}
            elif "sessionId" not in params:
                params["sessionId"] = self.session_id
    
        response = self.command_executor.execute(driver_command, params)
        if response:
&gt;           self.error_handler.check_response(response)

../../../.cache............/pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12.../webdriver/remote/webdriver.py:354: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;selenium.webdriver.remote.errorhandler.ErrorHandler object at 0x7f77f839cbf0&gt;
response = {'status': 404, 'value': '{"value":{"error":"no such element","message":"no such element: Unable to locate element: {\...\\n#16 0x55a891f2efc3 \\u003Cunknown&gt;\\n#17 0x55a891f3f944 \\u003Cunknown&gt;\\n#18 0x7f9e995a4ac3 \\u003Cunknown&gt;\\n"}}'}

    def check_response(self, response: Dict[str, Any]) -&gt; None:
        """Checks that a JSON response from the WebDriver does not have an
        error.
    
        :Args:
         - response - The JSON response from the WebDriver server as a dictionary
           object.
    
        :Raises: If the response contains an error message.
        """
        status = response.get("status", None)
        if not status or status == ErrorCode.SUCCESS:
            return
        value = None
        message = response.get("message", "")
        screen: str = response.get("screen", "")
        stacktrace = None
        if isinstance(status, int):
            value_json = response.get("value", None)
            if value_json and isinstance(value_json, str):
                import json
    
                try:
                    value = json.loads(value_json)
                    if len(value) == 1:
                        value = value["value"]
                    status = value.get("error", None)
                    if not status:
                        status = value.get("status", ErrorCode.UNKNOWN_ERROR)
                        message = value.get("value") or value.get("message")
                        if not isinstance(message, str):
                            value = message
                            message = message.get("message")
                    else:
                        message = value.get("message", None)
                except ValueError:
                    pass
    
        exception_class: Type[WebDriverException]
        e = ErrorCode()
        error_codes = [item for item in dir(e) if not item.startswith("__")]
        for error_code in error_codes:
            error_info = getattr(ErrorCode, error_code)
            if isinstance(error_info, list) and status in error_info:
                exception_class = getattr(ExceptionMapping, error_code, WebDriverException)
                break
        else:
            exception_class = WebDriverException
    
        if not value:
            value = response["value"]
        if isinstance(value, str):
            raise exception_class(value)
        if message == "" and "message" in value:
            message = value["message"]
    
        screen = None  # type: ignore[assignment]
        if "screen" in value:
            screen = value["screen"]
    
        stacktrace = None
        st_value = value.get("stackTrace") or value.get("stacktrace")
        if st_value:
            if isinstance(st_value, str):
                stacktrace = st_value.split("\n")
            else:
                stacktrace = []
                try:
                    for frame in st_value:
                        line = frame.get("lineNumber", "")
                        file = frame.get("fileName", "&lt;anonymous&gt;")
                        if line:
                            file = f"{file}:{line}"
                        meth = frame.get("methodName", "&lt;anonymous&gt;")
                        if "className" in frame:
                            meth = f"{frame['className']}.{meth}"
                        msg = "    at %s (%s)"
                        msg = msg % (meth, file)
                        stacktrace.append(msg)
                except TypeError:
                    pass
        if exception_class == UnexpectedAlertPresentException:
            alert_text = None
            if "data" in value:
                alert_text = value["data"].get("text")
            elif "alert" in value:
                alert_text = value["alert"].get("text")
            raise exception_class(message, screen, stacktrace, alert_text)  # type: ignore[call-arg]  # mypy is not smart enough here
&gt;       raise exception_class(message, screen, stacktrace)
E       selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"ak-flow-executor"}
E         (Session info: chrome=122.0.6261.69); For documentation on this error, please visit: https://www.selenium..../webdriver/troubleshooting/errors#no-such-element-exception
E       Stacktrace:
E       #0 0x55a891f40793 &lt;unknown&gt;
E       #1 0x55a891c341c6 &lt;unknown&gt;
E       #2 0x55a891c7f358 &lt;unknown&gt;
E       #3 0x55a891c7f411 &lt;unknown&gt;
E       #4 0x55a891cc2934 &lt;unknown&gt;
E       #5 0x55a891ca13fd &lt;unknown&gt;
E       #6 0x55a891cbfdd9 &lt;unknown&gt;
E       #7 0x55a891ca1173 &lt;unknown&gt;
E       #8 0x55a891c722d3 &lt;unknown&gt;
E       #9 0x55a891c72c9e &lt;unknown&gt;
E       #10 0x55a891f048cb &lt;unknown&gt;
E       #11 0x55a891f08745 &lt;unknown&gt;
E       #12 0x55a891ef12e1 &lt;unknown&gt;
E       #13 0x55a891f092d2 &lt;unknown&gt;
E       #14 0x55a891ed517f &lt;unknown&gt;
E       #15 0x55a891f2edc8 &lt;unknown&gt;
E       #16 0x55a891f2efc3 &lt;unknown&gt;
E       #17 0x55a891f3f944 &lt;unknown&gt;
E       #18 0x7f9e995a4ac3 &lt;unknown&gt;

../../../.cache............/pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12.../webdriver/remote/errorhandler.py:229: NoSuchElementException

During handling of the above exception, another exception occurred:

self = &lt;unittest.case._Outcome object at 0x7f77f839f770&gt;
test_case = &lt;tests.e2e.test_provider_proxy_forward.TestProviderProxyForward testMethod=test_nginx&gt;
subTest = False

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

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

self = &lt;tests.e2e.test_provider_proxy_forward.TestProviderProxyForward testMethod=test_nginx&gt;
result = &lt;TestCaseFunction test_nginx&gt;

    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):
&gt;                       self._callTestMethod(testMethod)

.../hostedtoolcache/Python/3.12.7........./x64/lib/python3.12/unittest/case.py:634: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;tests.e2e.test_provider_proxy_forward.TestProviderProxyForward testMethod=test_nginx&gt;
method = &lt;bound method TestProviderProxyForward.test_nginx of &lt;tests.e2e.test_provider_proxy_forward.TestProviderProxyForward testMethod=test_nginx&gt;&gt;

    def _callTestMethod(self, method):
&gt;       if method() is not None:

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

self = &lt;tests.e2e.test_provider_proxy_forward.TestProviderProxyForward testMethod=test_nginx&gt;
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)
    
        except tuple(exceptions) as exc:
            count += 1
            if count &gt; max_retires:
                logger.debug("Exceeded retry count", exc=exc, test=self)
    
                raise exc
            logger.debug("Retrying on error", exc=exc, test=self)
            self.tearDown()
            self._post_teardown()
            self._pre_setup()
            self.setUp()
&gt;           return wrapper(self, *args, **kwargs)

tests/e2e/utils.py:300: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;tests.e2e.test_provider_proxy_forward.TestProviderProxyForward testMethod=test_nginx&gt;
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)
    
        except tuple(exceptions) as exc:
            count += 1
            if count &gt; max_retires:
                logger.debug("Exceeded retry count", exc=exc, test=self)
    
                raise exc
            logger.debug("Retrying on error", exc=exc, test=self)
            self.tearDown()
            self._post_teardown()
            self._pre_setup()
            self.setUp()
&gt;           return wrapper(self, *args, **kwargs)

tests/e2e/utils.py:300: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;tests.e2e.test_provider_proxy_forward.TestProviderProxyForward testMethod=test_nginx&gt;
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)
    
        except tuple(exceptions) as exc:
            count += 1
            if count &gt; max_retires:
                logger.debug("Exceeded retry count", exc=exc, test=self)
    
&gt;               raise exc

tests/e2e/utils.py:294: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;tests.e2e.test_provider_proxy_forward.TestProviderProxyForward testMethod=test_nginx&gt;
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:
&gt;           return func(self, *args, **kwargs)

tests/e2e/utils.py:287: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;tests.e2e.test_provider_proxy_forward.TestProviderProxyForward testMethod=test_nginx&gt;

    @retry()
    def test_nginx(self):
        """Test nginx"""
        self.prepare()
    
        # Start nginx last so all hosts are resolvable, otherwise nginx exits
        self.run_container(
            image="docker.io/library/nginx:1.27",
            ports={
                "80": "80",
            },
            volumes={
                f"{Path(__file__).parent / "proxy_forward_auth" / "nginx_single" / "nginx.conf"}": {
                    "bind": "........./etc/nginx/conf.d/default.conf",
                }
            },
        )
    
        self.driver.get("http:.../localhost/api")
&gt;       self.login()

tests/e2e/test_provider_proxy_forward.py:145: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;tests.e2e.test_provider_proxy_forward.TestProviderProxyForward testMethod=test_nginx&gt;

    def login(self):
        """Do entire login flow and check user afterwards"""
&gt;       flow_executor = self.get_shadow_root("ak-flow-executor")

tests/e2e/utils.py:232: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;tests.e2e.test_provider_proxy_forward.TestProviderProxyForward testMethod=test_nginx&gt;
selector = 'ak-flow-executor'
container = &lt;selenium.webdriver.remote.webdriver.WebDriver (session="2338bdb15c4643633201fbde7808e6f2")&gt;

    def get_shadow_root(
        self, selector: str, container: WebElement | WebDriver | None = None
    ) -&gt; WebElement:
        """Get shadow root element's inner shadowRoot"""
        if not container:
            container = self.driver
&gt;       shadow_root = container.find_element(By.CSS_SELECTOR, selector)

tests/e2e/utils.py:226: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;selenium.webdriver.remote.webdriver.WebDriver (session="2338bdb15c4643633201fbde7808e6f2")&gt;
by = 'css selector', value = 'ak-flow-executor'

    def find_element(self, by=By.ID, value: Optional[str] = None) -&gt; WebElement:
        """Find an element given a By strategy and locator.
    
        :Usage:
            ::
    
                element = driver.find_element(By.ID, 'foo')
    
        :rtype: WebElement
        """
        if isinstance(by, RelativeBy):
            elements = self.find_elements(by=by, value=value)
            if not elements:
                raise NoSuchElementException(f"Cannot locate relative element with: {by.root}")
            return elements[0]
    
        if by == By.ID:
            by = By.CSS_SELECTOR
            value = f'[id="{value}"]'
        elif by == By.CLASS_NAME:
            by = By.CSS_SELECTOR
            value = f".{value}"
        elif by == By.NAME:
            by = By.CSS_SELECTOR
            value = f'[name="{value}"]'
    
&gt;       return self.execute(Command.FIND_ELEMENT, {"using": by, "value": value})["value"]

../../../.cache............/pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12.../webdriver/remote/webdriver.py:748: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;selenium.webdriver.remote.webdriver.WebDriver (session="2338bdb15c4643633201fbde7808e6f2")&gt;
driver_command = 'findElement'
params = {'using': 'css selector', 'value': 'ak-flow-executor'}

    def execute(self, driver_command: str, params: dict = None) -&gt; dict:
        """Sends a command to be executed by a command.CommandExecutor.
    
        :Args:
         - driver_command: The name of the command to execute as a string.
         - params: A dictionary of named parameters to send with the command.
    
        :Returns:
          The command's JSON response loaded into a dictionary object.
        """
        params = self._wrap_value(params)
    
        if self.session_id:
            if not params:
                params = {"sessionId": self.session_id}
            elif "sessionId" not in params:
                params["sessionId"] = self.session_id
    
        response = self.command_executor.execute(driver_command, params)
        if response:
&gt;           self.error_handler.check_response(response)

../../../.cache............/pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12.../webdriver/remote/webdriver.py:354: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = &lt;selenium.webdriver.remote.errorhandler.ErrorHandler object at 0x7f77e444b4a0&gt;
response = {'status': 404, 'value': '{"value":{"error":"no such element","message":"no such element: Unable to locate element: {\...\\n#16 0x559176714fc3 \\u003Cunknown&gt;\\n#17 0x559176725944 \\u003Cunknown&gt;\\n#18 0x7f6ded7e3ac3 \\u003Cunknown&gt;\\n"}}'}

    def check_response(self, response: Dict[str, Any]) -&gt; None:
        """Checks that a JSON response from the WebDriver does not have an
        error.
    
        :Args:
         - response - The JSON response from the WebDriver server as a dictionary
           object.
    
        :Raises: If the response contains an error message.
        """
        status = response.get("status", None)
        if not status or status == ErrorCode.SUCCESS:
            return
        value = None
        message = response.get("message", "")
        screen: str = response.get("screen", "")
        stacktrace = None
        if isinstance(status, int):
            value_json = response.get("value", None)
            if value_json and isinstance(value_json, str):
                import json
    
                try:
                    value = json.loads(value_json)
                    if len(value) == 1:
                        value = value["value"]
                    status = value.get("error", None)
                    if not status:
                        status = value.get("status", ErrorCode.UNKNOWN_ERROR)
                        message = value.get("value") or value.get("message")
                        if not isinstance(message, str):
                            value = message
                            message = message.get("message")
                    else:
                        message = value.get("message", None)
                except ValueError:
                    pass
    
        exception_class: Type[WebDriverException]
        e = ErrorCode()
        error_codes = [item for item in dir(e) if not item.startswith("__")]
        for error_code in error_codes:
            error_info = getattr(ErrorCode, error_code)
            if isinstance(error_info, list) and status in error_info:
                exception_class = getattr(ExceptionMapping, error_code, WebDriverException)
                break
        else:
            exception_class = WebDriverException
    
        if not value:
            value = response["value"]
        if isinstance(value, str):
            raise exception_class(value)
        if message == "" and "message" in value:
            message = value["message"]
    
        screen = None  # type: ignore[assignment]
        if "screen" in value:
            screen = value["screen"]
    
        stacktrace = None
        st_value = value.get("stackTrace") or value.get("stacktrace")
        if st_value:
            if isinstance(st_value, str):
                stacktrace = st_value.split("\n")
            else:
                stacktrace = []
                try:
                    for frame in st_value:
                        line = frame.get("lineNumber", "")
                        file = frame.get("fileName", "&lt;anonymous&gt;")
                        if line:
                            file = f"{file}:{line}"
                        meth = frame.get("methodName", "&lt;anonymous&gt;")
                        if "className" in frame:
                            meth = f"{frame['className']}.{meth}"
                        msg = "    at %s (%s)"
                        msg = msg % (meth, file)
                        stacktrace.append(msg)
                except TypeError:
                    pass
        if exception_class == UnexpectedAlertPresentException:
            alert_text = None
            if "data" in value:
                alert_text = value["data"].get("text")
            elif "alert" in value:
                alert_text = value["alert"].get("text")
            raise exception_class(message, screen, stacktrace, alert_text)  # type: ignore[call-arg]  # mypy is not smart enough here
&gt;       raise exception_class(message, screen, stacktrace)
E       selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"ak-flow-executor"}
E         (Session info: chrome=122.0.6261.69); For documentation on this error, please visit: https://www.selenium..../webdriver/troubleshooting/errors#no-such-element-exception
E       Stacktrace:
E       #0 0x559176726793 &lt;unknown&gt;
E       #1 0x55917641a1c6 &lt;unknown&gt;
E       #2 0x559176465358 &lt;unknown&gt;
E       #3 0x559176465411 &lt;unknown&gt;
E       #4 0x5591764a8934 &lt;unknown&gt;
E       #5 0x5591764873fd &lt;unknown&gt;
E       #6 0x5591764a5dd9 &lt;unknown&gt;
E       #7 0x559176487173 &lt;unknown&gt;
E       #8 0x5591764582d3 &lt;unknown&gt;
E       #9 0x559176458c9e &lt;unknown&gt;
E       #10 0x5591766ea8cb &lt;unknown&gt;
E       #11 0x5591766ee745 &lt;unknown&gt;
E       #12 0x5591766d72e1 &lt;unknown&gt;
E       #13 0x5591766ef2d2 &lt;unknown&gt;
E       #14 0x5591766bb17f &lt;unknown&gt;
E       #15 0x559176714dc8 &lt;unknown&gt;
E       #16 0x559176714fc3 &lt;unknown&gt;
E       #17 0x559176725944 &lt;unknown&gt;
E       #18 0x7f6ded7e3ac3 &lt;unknown&gt;

../../../.cache............/pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12.../webdriver/remote/errorhandler.py:229: NoSuchElementException

To view individual test run time comparison to the main branch, go to the Test Analytics Dashboard

@BeryJu BeryJu merged commit 97e7736 into main Oct 25, 2024
62 of 65 checks passed
@BeryJu BeryJu deleted the providers/scim/handle-no-members branch October 25, 2024 10:48
@BeryJu
Copy link
Member Author

BeryJu commented Oct 25, 2024

/cherry-pick version-2024.8

gcp-cherry-pick-bot bot pushed a commit that referenced this pull request Oct 25, 2024
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
BeryJu added a commit that referenced this pull request Oct 25, 2024
…ry-pick #11801) (#11812)

providers/scim: handle no members in group in consistency check (#11801)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
kensternberg-authentik added a commit that referenced this pull request Oct 29, 2024
* main: (22 commits)
  lifecycle: fix missing krb5 deps for full testing in image (#11815)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#11810)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#11809)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#11808)
  web: bump API Client version (#11807)
  core: bump goauthentik.io/api/v3 from 3.2024083.12 to 3.2024083.13 (#11806)
  core: bump ruff from 0.7.0 to 0.7.1 (#11805)
  core: bump twilio from 9.3.4 to 9.3.5 (#11804)
  core, web: update translations (#11803)
  providers/scim: handle no members in group in consistency check (#11801)
  stages/identification: add captcha to identification stage (#11711)
  website/docs: improve root page and redirect (#11798)
  providers/scim: clamp batch size for patch requests (#11797)
  web/admin: fix missing div in wizard forms (#11794)
  providers/proxy: fix handling of AUTHENTIK_HOST_BROWSER (#11722)
  core, web: update translations (#11789)
  core: bump goauthentik.io/api/v3 from 3.2024083.11 to 3.2024083.12 (#11790)
  core: bump gssapi from 1.8.3 to 1.9.0 (#11791)
  web: bump API Client version (#11792)
  stages/authenticator_validate: autoselect last used 2fa device (#11087)
  ...
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.

1 participant