This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new b12df0525d7 Add IMPORT_ERRORS_ALL permission for import errors of 
files with no registered Dag (#69790)
b12df0525d7 is described below

commit b12df0525d74b0dc017e828b578543f8f86d32b0
Author: Jarek Potiuk <[email protected]>
AuthorDate: Tue Jul 28 19:55:06 2026 +0200

    Add IMPORT_ERRORS_ALL permission for import errors of files with no 
registered Dag (#69790)
    
    * Add IMPORT_ERRORS_ALL permission for import errors of files with no 
registered Dag
    
    The Import Errors API authorizes each error per Dag defined in its file. 
Files
    with no registered Dag have no per-Dag key to authorize on. Gate the raw 
stack
    trace for those files on a dedicated, admin-by-default IMPORT_ERRORS_ALL 
view,
    scoped per team via the file's bundle where the auth manager supports 
multi-team
    isolation, and redact it for callers without the permission on both the 
list and
    single endpoints.
    
    Closes: #67461
    
    Generated-by: Claude Opus 4.8 (1M context) following the guidelines at
    
https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
    
    * Keep FAB provider importable on core without IMPORT_ERRORS_ALL access view
    
    The provider is released independently of core and may run against an older
    Airflow that predates the AccessView.IMPORT_ERRORS_ALL member (added in 
3.4.0).
    Referencing it directly in the resource map raised AttributeError at import
    time under the provider compatibility matrix. Resolve the member through a
    common.compat shim and map it only when the running core defines it.
    
    * Mark fab and keycloak common-compat dependency as needing the next release
    
    The IMPORT_ERRORS_ALL access-view shim is added to the common.compat 
provider in
    this same PR, so fab and keycloak require the next common.compat release. 
The
    selective-check common-compat guard enforces the '# use next version' 
marker on
    those dependents; add it so the build-info check passes and the 
compatibility
    tests run.
    
    * Scope the has-any-Dag lookup for import errors to the file's bundle
    
    The same relative_fileloc can exist in more than one bundle, so matching 
only on
    the filename treated an import error whose file has no Dag in its own 
bundle as
    registered when another bundle happened to define a Dag for the same path. 
Join
    the has-any-Dag subquery on both filename and bundle_name so 
unregistered-file
    import errors are detected per bundle.
    
    * Add test for the common.compat IMPORT_ERRORS_ALL access-view shim
    
    The shim module was added without a matching test, which the provider
    project-structure check flags. Cover both branches: it resolves to the enum
    member on a core that defines it, and to None on an older core that does 
not.
    
    * Scope the second has-any-Dag lookup for import errors per bundle
    
    The list endpoint joins the has-any-Dag subquery twice; only the first was
    scoped to the bundle. Since the subquery now yields one row per (file, 
bundle),
    the remaining filename-only join could duplicate an error across bundles and
    mis-detect registration. Join on bundle_name there too, add a regression 
test
    for a file registered only in another bundle, and update the existing bundle
    -join test, whose old expectation encoded the pre-fix cross-bundle leak.
    
    * Make the access-view shim test version-agnostic
    
    The provider compatibility matrix runs this test against older cores that
    predate AccessView.IMPORT_ERRORS_ALL (or lack api_fastapi entirely), where
    referencing the member directly raised AttributeError. Compare the shim 
against
    whatever the running core exposes (the member, or None) so it holds 
everywhere.
    
    * Add regression test for per-bundle scoping on single import error endpoint
    
    The has-any-Dag lookup in get_import_error is scoped per bundle. Without
    that scoping a same-named file registered in a different bundle resolved
    as a Dag match, so the endpoint returned the raw stacktrace on the
    strength of an unrelated bundle's read permission instead of gating on
    IMPORT_ERRORS_ALL. The list endpoint already had this coverage.
    
    * Fold team scoping into is_authorized_view instead of a separate method
    
    Per review, add the optional team_name to is_authorized_view directly rather
    than introducing a parallel is_authorized_view_for_team. Duplicating every
    authorization method for its team-aware variant would bloat the auth manager
    interface; the other is_authorized_* methods already carry team scoping 
through
    their details object, and this keeps is_authorized_view consistent with 
them.
    
    This is a breaking change for out-of-tree auth managers: an override that 
keeps
    the old (access_view, user) signature imports cleanly but raises TypeError 
at
    runtime the first time core authorizes a team-scoped view. Documented in a
    significant newsfragment with the one-line fix.
    
    * Degrade gracefully for auth managers without team-aware is_authorized_view
    
    Folding team_name into is_authorized_view made it a hard breaking change: an
    out-of-tree auth manager, or a provider released before the argument 
existed,
    keeps the old (access_view, user) signature and would raise TypeError the 
first
    time core authorized a team-scoped view -- a runtime 500 with no import- or
    startup-time signal.
    
    Route team-scoped authorization through a new 
BaseAuthManager.authorize_view,
    which detects (once, cached) whether the manager's override accepts 
team_name.
    A legacy manager is called without it and keeps working; team scoping is 
ignored
    so the view is authorized globally -- exactly what a manager with no 
multi-team
    support does anyway -- and a RemovedInAirflow4Warning tells the operator 
that
    some views (e.g. import errors for files with no registered Dag) are visible
    across teams until the auth manager is upgraded. The public interface stays 
a
    single is_authorized_view; the fallback is removed in Airflow 4.
    
    * Mark amazon common-compat dependency with '# use next version'
    
    This PR changes the common.compat provider alongside the amazon, fab and 
keycloak
    providers. When common.compat changes with other providers in the same PR, 
each
    provider depending on it must carry a '# use next version' comment so the 
release
    manager bumps the constraint to the not-yet-released common.compat at 
release time.
    fab and keycloak already had it; amazon was pulled into the changed set by 
the auth
    manager update and was missing it, which the selective-checks Build info 
job flagged.
    
    * Document team_name auth-manager support and simplify the changelog note
    
    The team_name argument no longer forces a hard break — auth managers that
    predate it keep working with a deprecation warning — so it is a feature 
rather
    than a significant/breaking change. Replace the significant newsfragment 
with a
    single feature note that states when team_name became available (Airflow 
3.4.0)
    and which bundled auth managers support it, and move the implementation 
detail
    (the authorize_view fallback, the deprecation, how to make a manager 
team-aware)
    into the auth manager documentation where implementers will look for it.
    
    * Trim the explanatory comments and fix the docs heading level
    
    The new "Team-scoped view authorization" section underlined its heading with
    a character the file had not used before. Heading levels in reStructuredText
    are assigned in order of first appearance, so that claimed level 4 and 
pushed
    the existing "Refreshing JWT Token" heading to level 5 under a level-3 
parent,
    which failed the docs build. Reusing the character the file already uses at
    that level keeps the hierarchy as it is on main.
    
    The comments explaining the team-scoped authorization fallback restated in
    prose what the code already says. Keeping the reasoning that is not obvious
    from the code - why unregistered-file errors are denied rather than 
returned,
    and why they are filtered rather than redacted - and dropping the rest.
---
 .../docs/core-concepts/auth-manager/index.rst      |  37 ++-
 airflow-core/newsfragments/69790.feature.rst       |   1 +
 .../api_fastapi/auth/managers/base_auth_manager.py |  33 ++-
 .../auth/managers/models/resource_details.py       |   3 +
 .../auth/managers/simple/simple_auth_manager.py    |  16 +-
 .../core_api/routes/public/import_error.py         |  65 +++-
 .../auth/managers/test_base_auth_manager.py        |  50 +++-
 .../core_api/routes/public/test_import_error.py    | 327 ++++++++++++++++++---
 providers/amazon/pyproject.toml                    |   2 +-
 .../amazon/aws/auth_manager/aws_auth_manager.py    |   3 +
 .../common/compat/security/access_view.py          |  29 ++
 .../common/compat/security/test_access_view.py     |  60 ++++
 providers/fab/pyproject.toml                       |   2 +-
 .../providers/fab/auth_manager/fab_auth_manager.py |  12 +-
 .../fab/auth_manager/security_manager/override.py  |   1 +
 .../providers/fab/www/security/permissions.py      |   1 +
 providers/keycloak/pyproject.toml                  |   2 +-
 .../keycloak/auth_manager/keycloak_auth_manager.py |   5 +-
 18 files changed, 573 insertions(+), 76 deletions(-)

diff --git a/airflow-core/docs/core-concepts/auth-manager/index.rst 
b/airflow-core/docs/core-concepts/auth-manager/index.rst
index b821fbfa135..b5e11baeb8d 100644
--- a/airflow-core/docs/core-concepts/auth-manager/index.rst
+++ b/airflow-core/docs/core-concepts/auth-manager/index.rst
@@ -140,13 +140,48 @@ These authorization methods are:
 * ``is_authorized_asset_alias``: Return whether the user is authorized to 
access Airflow asset aliases. Some details about the asset alias can be 
provided (e.g. the asset alias ID).
 * ``is_authorized_pool``: Return whether the user is authorized to access 
Airflow pools. Some details about the pool can be provided (e.g. the pool name).
 * ``is_authorized_variable``: Return whether the user is authorized to access 
Airflow variables. Some details about the variable can be provided (e.g. the 
variable key).
-* ``is_authorized_view``: Return whether the user is authorized to access a 
specific view in Airflow. The view is specified through ``access_view`` (e.g. 
``AccessView.CLUSTER_ACTIVITY``).
+* ``is_authorized_view``: Return whether the user is authorized to access a 
specific view in Airflow. The view is specified through ``access_view`` (e.g. 
``AccessView.CLUSTER_ACTIVITY``). An optional ``team_name`` scopes the check to 
a team -- see :ref:`team-scoped-view-authorization` below.
 * ``is_authorized_custom_view``: Return whether the user is authorized to 
access a specific view not defined in Airflow. This view can be provided by the 
auth manager itself or a plugin defined by the user.
 * ``filter_authorized_menu_items``: Given the list of menu items in the UI, 
return the list of menu items the user has access to.
 
 It should be noted that the ``method`` parameter listed above may only have 
relevance for a specific subset of the auth manager's authorization methods.
 For example, the ``configuration`` resource is by definition read-only, so 
only the ``GET`` parameter is relevant in the context of 
``is_authorized_configuration``.
 
+.. _team-scoped-view-authorization:
+
+Team-scoped view authorization
+''''''''''''''''''''''''''''''
+
+.. versionadded:: 3.4.0
+   ``is_authorized_view`` accepts an optional ``team_name`` argument.
+
+In a multi-team deployment, access to a read-only view can be restricted to 
the users of a
+specific team. ``is_authorized_view`` accepts an optional ``team_name`` for 
that purpose. An
+auth manager that implements multi-team isolation honors it and only 
authorizes users who
+belong to ``team_name``; an auth manager without multi-team support accepts 
the argument and
+ignores it, which authorizes the view across all teams (the same behaviour it 
had before the
+argument existed).
+
+Core never calls ``is_authorized_view`` with ``team_name`` directly. It goes 
through
+``BaseAuthManager.authorize_view``, which first checks whether the auth 
manager's
+``is_authorized_view`` accepts ``team_name``. This keeps auth managers that 
predate the
+argument working: a custom or out-of-tree auth manager whose 
``is_authorized_view`` still has
+the old ``(access_view, user)`` signature is called *without* ``team_name`` -- 
it does not
+raise -- and a ``RemovedInAirflow4Warning`` is emitted, warning that some 
views that should be
+restricted to a single team are instead authorized across all teams until the 
auth manager is
+upgraded.
+
+To make an auth manager team-aware, add ``team_name`` to the override 
(managers without
+multi-team support may accept and ignore it):
+
+.. code-block:: python
+
+    def is_authorized_view(
+        self, *, access_view: AccessView, user: MyUser, team_name: str | None 
= None
+    ) -> bool: ...
+
+The fallback for the older signature is removed in Airflow 4, which requires 
``team_name``.
+
 JWT token management by auth managers
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 The auth manager is responsible for creating the JWT token needed to interact 
with Airflow public API.
diff --git a/airflow-core/newsfragments/69790.feature.rst 
b/airflow-core/newsfragments/69790.feature.rst
new file mode 100644
index 00000000000..04e9bde8794
--- /dev/null
+++ b/airflow-core/newsfragments/69790.feature.rst
@@ -0,0 +1 @@
+Auth managers can scope read-only view authorization to a team via an optional 
``team_name`` argument on ``is_authorized_view``, added to the auth manager 
interface in Airflow 3.4.0. It is supported by the Simple auth manager (shipped 
in Airflow) from 3.4.0, and by the Amazon, FAB, and Keycloak providers from the 
releases after 9.32.0, 3.7.2, and 0.8.1 respectively; older provider releases 
and auth managers that have not adopted the argument keep working but cannot 
restrict views per tea [...]
diff --git 
a/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py 
b/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py
index d587ec7a680..e0d56258148 100644
--- a/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py
+++ b/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py
@@ -17,12 +17,13 @@
 # under the License.
 from __future__ import annotations
 
+import inspect
 import logging
 import warnings
 from abc import ABCMeta, abstractmethod
 from collections import defaultdict
 from enum import Enum
-from functools import cache
+from functools import cache, cached_property
 from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar
 
 from jwt import InvalidTokenError
@@ -44,6 +45,7 @@ from airflow.api_fastapi.auth.tokens import (
 )
 from airflow.api_fastapi.common.types import ExtraMenuItem, MenuItem
 from airflow.configuration import conf
+from airflow.exceptions import RemovedInAirflow4Warning
 from airflow.models import Connection, DagModel, Pool, Variable
 from airflow.models.dagbundle import DagBundleModel
 from airflow.models.revoked_token import RevokedToken
@@ -357,13 +359,42 @@ class BaseAuthManager(Generic[T], LoggingMixin, 
metaclass=ABCMeta):
         *,
         access_view: AccessView,
         user: T,
+        team_name: str | None = None,
     ) -> bool:
         """
         Return whether the user is authorized to access a read-only state of 
the installation.
 
         :param access_view: the specific read-only view/state the 
authorization request is about.
         :param user: the user to performing the action
+        :param team_name: team the view is scoped to, if any. Managers without 
multi-team
+            support may accept and ignore it, which authorizes the view 
globally.
+        """
+
+    @cached_property
+    def _is_authorized_view_team_aware(self) -> bool:
+        """Whether this manager's ``is_authorized_view`` override accepts 
``team_name``."""
+        params = inspect.signature(self.is_authorized_view).parameters
+        return "team_name" in params or any(p.kind is 
inspect.Parameter.VAR_KEYWORD for p in params.values())
+
+    def authorize_view(self, *, access_view: AccessView, user: T, team_name: 
str | None = None) -> bool:
         """
+        Authorize a read-only view, tolerating auth managers that predate 
``team_name``.
+
+        Core calls this instead of :meth:`is_authorized_view` on team-scoped 
paths: an
+        override still on the old ``(access_view, user)`` signature would 
otherwise raise
+        ``TypeError``. Removed in Airflow 4.
+        """
+        if self._is_authorized_view_team_aware:
+            return self.is_authorized_view(access_view=access_view, user=user, 
team_name=team_name)
+        warnings.warn(
+            f"The '{type(self).__name__}' auth manager is not team-aware, so 
team-scoped views are "
+            "authorized across all teams and may be visible to users of other 
teams. Add the "
+            "'team_name' argument to its is_authorized_view (or upgrade the 
provider). Airflow 4 "
+            "will require team-aware auth managers.",
+            RemovedInAirflow4Warning,
+            stacklevel=2,
+        )
+        return self.is_authorized_view(access_view=access_view, user=user)
 
     @abstractmethod
     def is_authorized_custom_view(self, *, method: ResourceMethod, 
resource_name: str, user: T) -> bool:
diff --git 
a/airflow-core/src/airflow/api_fastapi/auth/managers/models/resource_details.py 
b/airflow-core/src/airflow/api_fastapi/auth/managers/models/resource_details.py
index 41775d8043e..e4222603058 100644
--- 
a/airflow-core/src/airflow/api_fastapi/auth/managers/models/resource_details.py
+++ 
b/airflow-core/src/airflow/api_fastapi/auth/managers/models/resource_details.py
@@ -101,6 +101,9 @@ class AccessView(Enum):
     CLUSTER_ACTIVITY = "CLUSTER_ACTIVITY"
     DOCS = "DOCS"
     IMPORT_ERRORS = "IMPORT_ERRORS"
+    # Import errors for files with no registered Dag: there is no per-Dag key 
to
+    # authorize on, so they get their own admin-by-default view.
+    IMPORT_ERRORS_ALL = "IMPORT_ERRORS_ALL"
     JOBS = "JOBS"
     PLUGINS = "PLUGINS"
     PROVIDERS = "PROVIDERS"
diff --git 
a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/simple_auth_manager.py
 
b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/simple_auth_manager.py
index 0559a388156..ae8aafdb037 100644
--- 
a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/simple_auth_manager.py
+++ 
b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/simple_auth_manager.py
@@ -37,7 +37,7 @@ from termcolor import colored
 
 from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX
 from airflow.api_fastapi.auth.managers.base_auth_manager import BaseAuthManager
-from airflow.api_fastapi.auth.managers.models.resource_details import 
TeamDetails
+from airflow.api_fastapi.auth.managers.models.resource_details import 
AccessView, TeamDetails
 from airflow.api_fastapi.auth.managers.simple.user import SimpleAuthManagerUser
 from airflow.api_fastapi.common.types import MenuItem
 from airflow.configuration import AIRFLOW_HOME, conf
@@ -47,7 +47,6 @@ if TYPE_CHECKING:
 
     from airflow.api_fastapi.auth.managers.base_auth_manager import 
ResourceMethod
     from airflow.api_fastapi.auth.managers.models.resource_details import (
-        AccessView,
         AssetAliasDetails,
         AssetDetails,
         ConfigurationDetails,
@@ -352,8 +351,17 @@ class 
SimpleAuthManager(BaseAuthManager[SimpleAuthManagerUser]):
             team_name=details.team_name if details else None,
         )
 
-    def is_authorized_view(self, *, access_view: AccessView, user: 
SimpleAuthManagerUser) -> bool:
-        return self._is_authorized(method="GET", 
allow_role=SimpleAuthManagerRole.VIEWER, user=user)
+    def is_authorized_view(
+        self, *, access_view: AccessView, user: SimpleAuthManagerUser, 
team_name: str | None = None
+    ) -> bool:
+        # Import errors for files with no registered Dag are admin-only; every 
other
+        # view stays readable by viewers.
+        allow_role = (
+            SimpleAuthManagerRole.ADMIN
+            if access_view == AccessView.IMPORT_ERRORS_ALL
+            else SimpleAuthManagerRole.VIEWER
+        )
+        return self._is_authorized(method="GET", allow_role=allow_role, 
user=user, team_name=team_name)
 
     def is_authorized_custom_view(
         self, *, method: ResourceMethod, resource_name: str, user: 
SimpleAuthManagerUser
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/import_error.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/import_error.py
index 63d97a9b7b9..d5801e6b7e4 100644
--- 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/import_error.py
+++ 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/import_error.py
@@ -55,6 +55,7 @@ from airflow.api_fastapi.core_api.security import (
     requires_access_view,
 )
 from airflow.models import DagModel
+from airflow.models.dagbundle import DagBundleModel
 from airflow.models.errors import ParseImportError
 
 REDACTED_STACKTRACE = "REDACTED - you do not have read permission on all Dags 
in the file"
@@ -99,12 +100,21 @@ def get_import_error(
         ).all()
     )
 
-    # No Dags matched for this file -- either the file genuinely contains
-    # no Dags (parse failed before any Dag was defined), or the name keys
-    # did not resolve. Return the raw error in this case; a proper
-    # admin-only path for unregistered files is tracked in a follow-up
-    # issue (see https://github.com/apache/airflow/issues/67461).
+    # No Dags matched for this file, so there is no per-Dag key to authorize 
on:
+    # gate on ``IMPORT_ERRORS_ALL``, scoped to the file's team via its bundle.
+    # Denying rather than failing open -- returning the row would leak the 
file's
+    # existence, and for team-scoped bundles the cross-team repository 
structure.
     if not file_dag_ids:
+        team_name = (
+            DagBundleModel.get_team_name(error.bundle_name, session=session) 
if error.bundle_name else None
+        )
+        if not auth_manager.authorize_view(
+            access_view=AccessView.IMPORT_ERRORS_ALL, user=user, 
team_name=team_name
+        ):
+            raise HTTPException(
+                status.HTTP_403_FORBIDDEN,
+                "You do not have permission to view import errors for files 
with no registered Dag",
+            )
         return error
 
     # Can the user read any Dags in the file?
@@ -157,7 +167,33 @@ def get_import_errors(
     readable_dag_ids = auth_manager.get_authorized_dag_ids(method="GET", 
user=user)
 
     # Subquery for files that have any Dags
-    files_with_any_dags = 
select(DagModel.relative_fileloc).distinct().subquery()
+    files_with_any_dags = select(DagModel.relative_fileloc, 
DagModel.bundle_name).distinct().subquery()
+
+    # Errors for files with no registered Dag are authorized on 
``IMPORT_ERRORS_ALL``,
+    # scoped to the file's team via its bundle. Filtering here rather than 
redacting in
+    # the loop keeps unauthorized rows out of the count and pagination too, so 
their
+    # existence -- and for team-scoped bundles the cross-team file names -- 
does not leak.
+    unregistered_errors = session.execute(
+        select(ParseImportError.id, ParseImportError.bundle_name)
+        .outerjoin(
+            files_with_any_dags,
+            (ParseImportError.filename == 
files_with_any_dags.c.relative_fileloc)
+            & (ParseImportError.bundle_name == 
files_with_any_dags.c.bundle_name),
+        )
+        .where(files_with_any_dags.c.relative_fileloc.is_(None))
+    ).all()
+    team_name_by_bundle = DagBundleModel.get_team_names(
+        {bundle_name for _, bundle_name in unregistered_errors if 
bundle_name}, session=session
+    )
+    authorized_unregistered_ids = {
+        error_id
+        for error_id, bundle_name in unregistered_errors
+        if auth_manager.authorize_view(
+            access_view=AccessView.IMPORT_ERRORS_ALL,
+            user=user,
+            team_name=team_name_by_bundle.get(bundle_name) if bundle_name else 
None,
+        )
+    }
 
     # Files (identified by ``(relative_fileloc, bundle_name)``) where the
     # user can read at least one Dag. Used to decide which import errors
@@ -195,7 +231,10 @@ def get_import_errors(
         select(ParseImportError, file_dags_cte.c.dag_id)
         .outerjoin(
             files_with_any_dags,
-            ParseImportError.filename == 
files_with_any_dags.c.relative_fileloc,
+            and_(
+                ParseImportError.filename == 
files_with_any_dags.c.relative_fileloc,
+                ParseImportError.bundle_name == 
files_with_any_dags.c.bundle_name,
+            ),
         )
         .outerjoin(
             file_dags_cte,
@@ -206,7 +245,9 @@ def get_import_errors(
         )
         .where(
             or_(
-                files_with_any_dags.c.relative_fileloc.is_(None),
+                # Unregistered-file errors the caller is authorized to see.
+                ParseImportError.id.in_(authorized_unregistered_ids),
+                # Files where the caller can read at least one Dag.
                 file_dags_cte.c.dag_id.isnot(None),
             )
         )
@@ -259,12 +300,8 @@ def get_import_errors(
     for import_error, file_dag_ids_iter in import_errors_result:
         dag_ids = [dag_id for _, dag_id in file_dag_ids_iter if dag_id is not 
None]
 
-        # No Dags matched for this file -- either the file genuinely has
-        # no Dags yet (parse failed before any Dag was defined), or the
-        # name keys did not resolve. Append the raw error in this case;
-        # a proper admin-only path for unregistered files is tracked in
-        # a follow-up issue
-        # (see https://github.com/apache/airflow/issues/67461).
+        # Unauthorized unregistered-file errors were already filtered out 
above,
+        # so anything reaching here may show its raw stacktrace.
         if not dag_ids:
             import_errors.append(import_error)
             continue
diff --git 
a/airflow-core/tests/unit/api_fastapi/auth/managers/test_base_auth_manager.py 
b/airflow-core/tests/unit/api_fastapi/auth/managers/test_base_auth_manager.py
index 348bce778ec..5ca46908e3a 100644
--- 
a/airflow-core/tests/unit/api_fastapi/auth/managers/test_base_auth_manager.py
+++ 
b/airflow-core/tests/unit/api_fastapi/auth/managers/test_base_auth_manager.py
@@ -26,6 +26,7 @@ from jwt import InvalidTokenError
 from airflow.api_fastapi.auth.managers.base_auth_manager import 
BaseAuthManager, T
 from airflow.api_fastapi.auth.managers.models.base_user import BaseUser
 from airflow.api_fastapi.auth.managers.models.resource_details import (
+    AccessView,
     ConnectionDetails,
     DagDetails,
     PoolDetails,
@@ -34,6 +35,7 @@ from 
airflow.api_fastapi.auth.managers.models.resource_details import (
 )
 from airflow.api_fastapi.auth.tokens import JWTGenerator, JWTValidator
 from airflow.api_fastapi.common.types import MenuItem
+from airflow.exceptions import RemovedInAirflow4Warning
 from airflow.models.team import Team
 
 from tests_common.test_utils.config import conf_vars
@@ -41,7 +43,6 @@ from tests_common.test_utils.config import conf_vars
 if TYPE_CHECKING:
     from airflow.api_fastapi.auth.managers.base_auth_manager import 
ResourceMethod
     from airflow.api_fastapi.auth.managers.models.resource_details import (
-        AccessView,
         AssetAliasDetails,
         AssetDetails,
         ConfigurationDetails,
@@ -132,7 +133,11 @@ class 
EmptyAuthManager(BaseAuthManager[BaseAuthManagerUserTest]):
         raise NotImplementedError()
 
     def is_authorized_view(
-        self, *, access_view: AccessView, user: BaseAuthManagerUserTest | None 
= None
+        self,
+        *,
+        access_view: AccessView,
+        user: BaseAuthManagerUserTest | None = None,
+        team_name: str | None = None,
     ) -> bool:
         raise NotImplementedError()
 
@@ -157,6 +162,47 @@ class TestBaseAuthManager:
     def test_init_non_multi_team_mode(self, auth_manager):
         assert auth_manager.init() is None
 
+    @patch.object(EmptyAuthManager, "is_authorized_view")
+    def test_authorize_view_passes_team_name_to_team_aware_manager(
+        self, mock_is_authorized_view, auth_manager
+    ):
+        mock_is_authorized_view.return_value = True
+
+        result = auth_manager.authorize_view(access_view=AccessView.DOCS, 
user=None, team_name="team_a")
+
+        assert result is True
+        mock_is_authorized_view.assert_called_once_with(
+            access_view=AccessView.DOCS, user=None, team_name="team_a"
+        )
+
+    def test_authorize_view_drops_team_name_and_warns_for_legacy_manager(self):
+        # A manager whose is_authorized_view predates team_name (out-of-tree, 
or a provider
+        # released before the argument existed) must keep working: 
authorize_view falls back
+        # to a global check and warns that some views are not team-restricted.
+        class LegacyAuthManager(EmptyAuthManager):
+            def is_authorized_view(self, *, access_view, user=None):  # old 
signature, no team_name
+                return True
+
+        manager = LegacyAuthManager()
+
+        with pytest.warns(RemovedInAirflow4Warning, match="not team-aware"):
+            result = manager.authorize_view(access_view=AccessView.DOCS, 
user=None, team_name="team_a")
+
+        assert result is True
+
+    def test_authorize_view_treats_kwargs_override_as_team_aware(self):
+        class KwargsAuthManager(EmptyAuthManager):
+            def is_authorized_view(self, *, access_view, user=None, **kwargs):
+                return kwargs.get("team_name") == "team_a"
+
+        manager = KwargsAuthManager()
+
+        with warnings.catch_warnings():
+            warnings.simplefilter("error")  # a warning here would fail the 
test
+            result = manager.authorize_view(access_view=AccessView.DOCS, 
user=None, team_name="team_a")
+
+        assert result is True
+
     @conf_vars({("core", "multi_team"): "True"})
     @pytest.mark.parametrize(
         ("auth_manager_teams", "db_teams", "expected"),
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_import_error.py
 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_import_error.py
index e7be4fa3219..e703d98b2ad 100644
--- 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_import_error.py
+++ 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_import_error.py
@@ -21,19 +21,22 @@ from typing import TYPE_CHECKING
 from unittest import mock
 
 import pytest
+from sqlalchemy import delete
 
-from airflow.api_fastapi.auth.managers.models.resource_details import 
DagDetails
+from airflow.api_fastapi.auth.managers.models.resource_details import 
AccessView, DagDetails
 from airflow.api_fastapi.core_api.routes.public.import_error import 
REDACTED_STACKTRACE
 from airflow.models import DagModel
 from airflow.models.dagbundle import DagBundleModel
 from airflow.models.errors import ParseImportError
-from airflow.utils.session import NEW_SESSION, provide_session
+from airflow.utils.session import NEW_SESSION, create_session, provide_session
 
 from tests_common.test_utils.asserts import assert_queries_count
 from tests_common.test_utils.db import clear_db_dag_bundles, clear_db_dags, 
clear_db_import_errors
 from tests_common.test_utils.format_datetime import 
from_datetime_to_zulu_without_ms
 
 if TYPE_CHECKING:
+    from collections.abc import Generator
+
     from sqlalchemy.orm import Session
 
 pytestmark = pytest.mark.db_test
@@ -150,6 +153,14 @@ def set_mock_auth_manager__batch_is_authorized_dag(
     return mock_batch_is_authorized_dag
 
 
+def set_mock_auth_manager__authorize_view(
+    mock_auth_manager: mock.Mock, authorize_view_return_value: bool = False
+) -> mock.Mock:
+    mock_method = mock_auth_manager.return_value.authorize_view
+    mock_method.return_value = authorize_view_return_value
+    return mock_method
+
+
 class TestGetImportError:
     @pytest.mark.parametrize(
         ("prepared_import_error_idx", "expected_status_code", "expected_body"),
@@ -259,28 +270,93 @@ class TestGetImportError:
             "bundle_name": BUNDLE_NAME,
         }
 
+    @pytest.mark.parametrize(
+        ("can_view_all_import_errors", "expected_status_code"),
+        [
+            pytest.param(True, 200, id="with-IMPORT_ERRORS_ALL-sees-raw"),
+            pytest.param(False, 403, id="without-IMPORT_ERRORS_ALL-forbidden"),
+        ],
+    )
     
@mock.patch("airflow.api_fastapi.core_api.routes.public.import_error.get_auth_manager")
-    def test_get_import_error__no_dag_in_dagmodel(self, mock_get_auth_manager, 
test_client, import_errors):
-        """Import error is returned with the raw stacktrace when no Dag exists
-        in ``DagModel`` for the file.
-
-        Proper handling of unregistered files (separate admin-only permission,
-        multi-team isolation) is tracked as follow-up work; for now the 
endpoint
-        returns the raw error rather than redacting it.
+    def test_get_import_error__no_dag_in_dagmodel(
+        self,
+        mock_get_auth_manager,
+        test_client,
+        import_errors,
+        can_view_all_import_errors,
+        expected_status_code,
+    ):
+        """A file with no registered Dag has no per-Dag key to authorize on, so
+        visibility is gated on the dedicated ``IMPORT_ERRORS_ALL`` view: 
callers
+        holding it (admins by default) see the raw stacktrace, everyone else is
+        denied (403) rather than the endpoint failing open or leaking the row.
         """
         import_error_id = import_errors[0].id
         set_mock_auth_manager__get_authorized_dag_ids(mock_get_auth_manager, 
set())
+        mock_method = 
set_mock_auth_manager__authorize_view(mock_get_auth_manager, 
can_view_all_import_errors)
 
         response = test_client.get(f"/importErrors/{import_error_id}")
 
-        assert response.status_code == 200
+        assert response.status_code == expected_status_code
+        if expected_status_code == 200:
+            assert response.json() == {
+                "import_error_id": import_error_id,
+                "timestamp": from_datetime_to_zulu_without_ms(TIMESTAMP1),
+                "filename": FILENAME1,
+                "stack_trace": STACKTRACE1,
+                "bundle_name": BUNDLE_NAME,
+            }
+        # The unregistered-file view is what gates access, scoped to the file's
+        # team (None for the un-teamed "testing" bundle).
+        mock_method.assert_called_once_with(
+            access_view=AccessView.IMPORT_ERRORS_ALL, user=mock.ANY, 
team_name=None
+        )
+
+    
@mock.patch("airflow.api_fastapi.core_api.routes.public.import_error.get_auth_manager")
+    def 
test_get_import_error__file_registered_only_in_other_bundle_is_unregistered(
+        self,
+        mock_get_auth_manager,
+        test_client,
+        import_errors,
+        session,
+    ):
+        """Regression: the has-any-Dag lookup is scoped per bundle. Matching on
+        ``relative_fileloc`` alone would resolve the same-named file's Dag in a
+        *different* bundle, treat this error as registered, and hand back the 
raw
+        stacktrace on the strength of an unrelated bundle's read permission --
+        never consulting ``IMPORT_ERRORS_ALL``.
+        """
+        import_error_id = import_errors[0].id
+        other_bundle = "other_bundle"
+        session.add(DagBundleModel(name=other_bundle))
+        session.flush()
+        # FILENAME1 has a registered Dag only in ``other_bundle``, while the
+        # import error for the same path lives in BUNDLE_NAME, which has none.
+        session.add(
+            DagModel(
+                fileloc=f"/other/{FILENAME1}",
+                relative_fileloc=FILENAME1,
+                dag_id="dag_in_other_bundle",
+                is_paused=False,
+                bundle_name=other_bundle,
+            )
+        )
+        session.commit()
+
+        # Readable other-bundle Dag must not grant visibility into the
+        # same-named file's error in BUNDLE_NAME.
+        set_mock_auth_manager__get_authorized_dag_ids(mock_get_auth_manager, 
{"dag_in_other_bundle"})
+        mock_method = 
set_mock_auth_manager__authorize_view(mock_get_auth_manager, False)
+
+        response = test_client.get(f"/importErrors/{import_error_id}")
+
+        assert response.status_code == 403
         assert response.json() == {
-            "import_error_id": import_error_id,
-            "timestamp": from_datetime_to_zulu_without_ms(TIMESTAMP1),
-            "filename": FILENAME1,
-            "stack_trace": STACKTRACE1,
-            "bundle_name": BUNDLE_NAME,
+            "detail": "You do not have permission to view import errors for 
files with no registered Dag"
         }
+        mock_method.assert_called_once_with(
+            access_view=AccessView.IMPORT_ERRORS_ALL, user=mock.ANY, 
team_name=None
+        )
 
 
 class TestGetImportErrors:
@@ -373,7 +449,10 @@ class TestGetImportErrors:
         set_mock_auth_manager__get_authorized_dag_ids(mock_get_auth_manager, 
permitted_dag_model_all)
         set_mock_auth_manager__batch_is_authorized_dag(mock_get_auth_manager, 
True)
 
-        with assert_queries_count(5):
+        # One extra query vs. the has-Dag path: the list endpoint probes for
+        # import errors of files with no registered Dag so it can authorize 
them
+        # on the IMPORT_ERRORS_ALL view before building the main query.
+        with assert_queries_count(6):
             response = test_client.get("/importErrors", params=query_params)
 
         assert response.status_code == expected_status_code
@@ -491,6 +570,63 @@ class TestGetImportErrors:
         assert response_json["import_errors"][0]["bundle_name"] == BUNDLE_NAME
         assert response_json["import_errors"][0]["stack_trace"] == STACKTRACE1
 
+    
@mock.patch("airflow.api_fastapi.core_api.routes.public.import_error.get_auth_manager")
+    def 
test_get_import_errors__file_registered_only_in_other_bundle_is_unregistered(
+        self,
+        mock_get_auth_manager,
+        test_client,
+        session,
+    ):
+        """Regression: the has-any-Dag lookup is scoped per bundle. The same
+        ``relative_fileloc`` can exist in several bundles, so an import error
+        whose file has a registered Dag only in a *different* bundle must still
+        be treated as unregistered in its own bundle and gated on
+        ``IMPORT_ERRORS_ALL`` -- matching on the filename alone leaked it as
+        registered and could duplicate the row across bundles.
+        """
+        clear_db_import_errors()
+        other_bundle = "other_bundle"
+        session.add(DagBundleModel(name=other_bundle))
+        session.flush()
+        # FILENAME1 has a registered Dag only in ``other_bundle`` ...
+        session.add(
+            DagModel(
+                fileloc=f"/other/{FILENAME1}",
+                relative_fileloc=FILENAME1,
+                dag_id="dag_in_other_bundle",
+                is_paused=False,
+                bundle_name=other_bundle,
+            )
+        )
+        # ... while the import error for the same path is in BUNDLE_NAME, which
+        # has no Dag for it.
+        error = ParseImportError(
+            bundle_name=BUNDLE_NAME,
+            filename=FILENAME1,
+            stacktrace=STACKTRACE1,
+            timestamp=TIMESTAMP1,
+        )
+        session.add(error)
+        session.commit()
+
+        # Being able to read the other-bundle Dag must not grant visibility 
into
+        # the same-named file's error in BUNDLE_NAME.
+        set_mock_auth_manager__get_authorized_dag_ids(mock_get_auth_manager, 
{"dag_in_other_bundle"})
+        set_mock_auth_manager__batch_is_authorized_dag(mock_get_auth_manager, 
True)
+        mock_view = 
set_mock_auth_manager__authorize_view(mock_get_auth_manager, True)
+
+        response = test_client.get("/importErrors")
+
+        assert response.status_code == 200
+        body = response.json()
+        # Exactly one row (no filename-only-join duplication), admitted only 
via
+        # the IMPORT_ERRORS_ALL view.
+        assert body["total_entries"] == 1
+        assert [e["bundle_name"] for e in body["import_errors"]] == 
[BUNDLE_NAME]
+        mock_view.assert_called_once_with(
+            access_view=AccessView.IMPORT_ERRORS_ALL, user=mock.ANY, 
team_name=None
+        )
+
     def test_should_raises_401_unauthenticated(self, 
unauthenticated_test_client):
         response = unauthenticated_test_client.get("/importErrors")
         assert response.status_code == 401
@@ -678,6 +814,13 @@ class TestGetImportErrors:
         session.merge(dag_model1)
         session.commit()
 
+        # FILENAME1 now has no Dag in BUNDLE_NAME (its only Dag moved to 
another
+        # bundle), so its error there is unregistered and gated on the 
dedicated
+        # IMPORT_ERRORS_ALL view -- which this caller does not hold, so the row
+        # is hidden. (Matching on filename alone would have wrongly treated it 
as
+        # registered via the same-named Dag in the other bundle.)
+        set_mock_auth_manager__authorize_view(mock_get_auth_manager, False)
+
         response2 = test_client.get("/importErrors")
 
         # Assert - should return 0 entries because bundle_name no longer 
matches
@@ -686,29 +829,45 @@ class TestGetImportErrors:
         assert response_json2["total_entries"] == 0
         assert response_json2["import_errors"] == []
 
+    @pytest.mark.parametrize(
+        ("can_view_all_import_errors", "expected_total_entries", 
"expected_stacktraces"),
+        [
+            pytest.param(
+                True,
+                3,
+                {FILENAME1: STACKTRACE1, FILENAME2: STACKTRACE2, FILENAME3: 
STACKTRACE3},
+                id="with-IMPORT_ERRORS_ALL-sees-raw",
+            ),
+            pytest.param(False, 0, {}, id="without-IMPORT_ERRORS_ALL-hidden"),
+        ],
+    )
     
@mock.patch("airflow.api_fastapi.core_api.routes.public.import_error.get_auth_manager")
-    def test_get_import_errors__no_dag_in_dagmodel(self, 
mock_get_auth_manager, test_client, import_errors):
-        """Import errors are returned with their raw stacktraces when no Dag
-        exists in ``DagModel`` for the file.
-
-        Proper handling of unregistered files is deferred to a follow-up issue
-        that introduces a dedicated permission and respects multi-team 
isolation.
+    def test_get_import_errors__no_dag_in_dagmodel(
+        self,
+        mock_get_auth_manager,
+        test_client,
+        import_errors,
+        can_view_all_import_errors,
+        expected_total_entries,
+        expected_stacktraces,
+    ):
+        """List endpoint gates unregistered-file import errors on the dedicated
+        ``IMPORT_ERRORS_ALL`` view: callers holding it see the raw stacktrace,
+        callers without it do not see the rows at all (excluded from both the
+        results and ``total_entries``, so the file's existence does not leak).
         """
         set_mock_auth_manager__get_authorized_dag_ids(mock_get_auth_manager, 
set())
+        set_mock_auth_manager__authorize_view(mock_get_auth_manager, 
can_view_all_import_errors)
 
         response = test_client.get("/importErrors")
 
         assert response.status_code == 200
         response_json = response.json()
-        assert response_json["total_entries"] == 3
+        assert response_json["total_entries"] == expected_total_entries
         stacktrace_by_filename = {
             error["filename"]: error["stack_trace"] for error in 
response_json["import_errors"]
         }
-        assert stacktrace_by_filename == {
-            FILENAME1: STACKTRACE1,
-            FILENAME2: STACKTRACE2,
-            FILENAME3: STACKTRACE3,
-        }
+        assert stacktrace_by_filename == expected_stacktraces
 
 
 class TestImportErrorFileAuthorization:
@@ -838,25 +997,35 @@ class TestImportErrorFileAuthorization:
         response = 
test_client.get(f"/importErrors/{lonely_file_import_error.id}")
         assert response.status_code == 403
 
+    @pytest.mark.parametrize(
+        ("can_view_all_import_errors", "expected_status_code"),
+        [
+            pytest.param(True, 200, id="with-IMPORT_ERRORS_ALL-sees-raw"),
+            pytest.param(False, 403, id="without-IMPORT_ERRORS_ALL-forbidden"),
+        ],
+    )
     
@mock.patch("airflow.api_fastapi.core_api.routes.public.import_error.get_auth_manager")
-    def 
test_single_endpoint_returns_raw_stacktrace_when_file_has_no_known_dags(
+    def 
test_single_endpoint_gates_unregistered_file_stacktrace_on_import_errors_all(
         self,
         mock_get_auth_manager,
         test_client,
         import_errors,
+        can_view_all_import_errors,
+        expected_status_code,
     ):
-        """Single endpoint returns the raw stacktrace when the
-        ``ParseImportError`` refers to a file with no matching ``DagModel``
-        rows at all -- for example a file that failed to parse before any
-        Dag was defined. Proper handling of this case (dedicated permission,
-        multi-team isolation) is tracked as follow-up work.
+        """Single endpoint gates a file with no matching ``DagModel`` rows (for
+        example a file that failed to parse before any Dag was defined) on the
+        dedicated ``IMPORT_ERRORS_ALL`` view instead of failing open. Callers
+        holding it see the raw stacktrace; others are denied (403).
         """
         set_mock_auth_manager__get_authorized_dag_ids(mock_get_auth_manager, 
set())
+        set_mock_auth_manager__authorize_view(mock_get_auth_manager, 
can_view_all_import_errors)
         response = test_client.get(f"/importErrors/{import_errors[0].id}")
-        assert response.status_code == 200
-        body = response.json()
-        assert body["filename"] == FILENAME1
-        assert body["stack_trace"] == STACKTRACE1
+        assert response.status_code == expected_status_code
+        if expected_status_code == 200:
+            body = response.json()
+            assert body["filename"] == FILENAME1
+            assert body["stack_trace"] == STACKTRACE1
 
     
@mock.patch("airflow.api_fastapi.core_api.routes.public.import_error.get_auth_manager")
     def 
test_list_endpoint_redacts_mixed_file_with_colocated_dag_outside_callers_scope(
@@ -894,26 +1063,86 @@ class TestImportErrorFileAuthorization:
         assert len(mixed_entries) == 1
         assert mixed_entries[0]["stack_trace"] == REDACTED_STACKTRACE
 
+    @pytest.mark.parametrize(
+        ("can_view_all_import_errors", "expected_total_entries", 
"expected_stacktraces"),
+        [
+            pytest.param(
+                True,
+                3,
+                {FILENAME1: STACKTRACE1, FILENAME2: STACKTRACE2, FILENAME3: 
STACKTRACE3},
+                id="with-IMPORT_ERRORS_ALL-sees-raw",
+            ),
+            pytest.param(False, 0, {}, id="without-IMPORT_ERRORS_ALL-hidden"),
+        ],
+    )
     
@mock.patch("airflow.api_fastapi.core_api.routes.public.import_error.get_auth_manager")
-    def test_list_endpoint_returns_raw_stacktrace_when_file_has_no_known_dags(
+    def 
test_list_endpoint_gates_unregistered_file_stacktrace_on_import_errors_all(
         self,
         mock_get_auth_manager,
         test_client,
         import_errors,
+        can_view_all_import_errors,
+        expected_total_entries,
+        expected_stacktraces,
     ):
-        """List endpoint returns the raw stacktrace for import errors whose
-        file has no matching ``DagModel`` rows. Proper handling of this case
-        (dedicated permission, multi-team isolation) is tracked as follow-up
-        work.
+        """List endpoint gates import errors whose file has no matching
+        ``DagModel`` rows on the dedicated ``IMPORT_ERRORS_ALL`` view instead 
of
+        failing open. Callers holding it see the raw stacktrace; callers 
without
+        it do not see the rows at all (excluded from results and
+        ``total_entries``).
         """
         set_mock_auth_manager__get_authorized_dag_ids(mock_get_auth_manager, 
set())
+        set_mock_auth_manager__authorize_view(mock_get_auth_manager, 
can_view_all_import_errors)
         response = test_client.get("/importErrors")
         assert response.status_code == 200
         body = response.json()
-        assert body["total_entries"] == 3
+        assert body["total_entries"] == expected_total_entries
         stacktrace_by_filename = {entry["filename"]: entry["stack_trace"] for 
entry in body["import_errors"]}
-        assert stacktrace_by_filename == {
-            FILENAME1: STACKTRACE1,
-            FILENAME2: STACKTRACE2,
-            FILENAME3: STACKTRACE3,
-        }
+        assert stacktrace_by_filename == expected_stacktraces
+
+    @pytest.fixture
+    def team_scoped_unregistered_import_error(self) -> 
Generator[ParseImportError, None, None]:
+        """An import error for a file with no registered Dag, whose bundle is
+        mapped to a team, so the endpoint must scope the ``IMPORT_ERRORS_ALL``
+        check to that team."""
+        from airflow.models.team import Team
+
+        with create_session() as session:
+            team = Team(name="team_b")
+            bundle = DagBundleModel(name="team_b_bundle")
+            bundle.teams = [team]
+            session.add_all([team, bundle])
+            error = ParseImportError(
+                bundle_name="team_b_bundle",
+                filename="team_b_unregistered.py",
+                stacktrace="team b stack trace",
+                timestamp=TIMESTAMP1,
+            )
+            session.add(error)
+            session.commit()
+            session.refresh(error)
+            session.expunge(error)
+        yield error
+        with create_session() as cleanup_session:
+            cleanup_session.execute(delete(Team).where(Team.name == "team_b"))
+            cleanup_session.commit()
+
+    
@mock.patch("airflow.api_fastapi.core_api.routes.public.import_error.get_auth_manager")
+    def test_single_endpoint_scopes_unregistered_file_view_to_bundle_team(
+        self,
+        mock_get_auth_manager,
+        test_client,
+        team_scoped_unregistered_import_error,
+    ):
+        """The ``IMPORT_ERRORS_ALL`` check for an unregistered file is scoped 
to
+        the file's team, resolved from its bundle. A caller not in that team is
+        denied (403)."""
+        set_mock_auth_manager__get_authorized_dag_ids(mock_get_auth_manager, 
set())
+        mock_method = 
set_mock_auth_manager__authorize_view(mock_get_auth_manager, False)
+
+        response = 
test_client.get(f"/importErrors/{team_scoped_unregistered_import_error.id}")
+
+        assert response.status_code == 403
+        mock_method.assert_called_once_with(
+            access_view=AccessView.IMPORT_ERRORS_ALL, user=mock.ANY, 
team_name="team_b"
+        )
diff --git a/providers/amazon/pyproject.toml b/providers/amazon/pyproject.toml
index e1c816c4395..a8a4224b7d0 100644
--- a/providers/amazon/pyproject.toml
+++ b/providers/amazon/pyproject.toml
@@ -60,7 +60,7 @@ requires-python = ">=3.10"
 # After you modify the dependencies, and rebuild your Breeze CI image with 
``breeze ci-image build``
 dependencies = [
     "apache-airflow>=2.11.0",
-    "apache-airflow-providers-common-compat>=1.14.3",
+    "apache-airflow-providers-common-compat>=1.14.3",  # use next version
     "apache-airflow-providers-common-sql>=1.32.0",
     "apache-airflow-providers-http",
     # We should update minimum version of boto3 and here regularly to avoid 
`pip` backtracking with the number
diff --git 
a/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py
 
b/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py
index 03bd4c79b0b..c2733d90246 100644
--- 
a/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py
+++ 
b/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py
@@ -236,7 +236,10 @@ class AwsAuthManager(BaseAuthManager[AwsAuthManagerUser]):
         *,
         access_view: AccessView,
         user: AwsAuthManagerUser,
+        team_name: str | None = None,
     ) -> bool:
+        # ``team_name`` is ignored: the VIEW entity is not team-scoped in the
+        # Verified Permissions schema.
         return self.avp_facade.is_authorized(
             method="GET",
             entity_type=AvpEntities.VIEW,
diff --git 
a/providers/common/compat/src/airflow/providers/common/compat/security/access_view.py
 
b/providers/common/compat/src/airflow/providers/common/compat/security/access_view.py
new file mode 100644
index 00000000000..4c79c5a139c
--- /dev/null
+++ 
b/providers/common/compat/src/airflow/providers/common/compat/security/access_view.py
@@ -0,0 +1,29 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+# ``AccessView.IMPORT_ERRORS_ALL`` was added in Airflow 3.4.0, and providers 
are
+# released independently of core. ``None`` signals the view is unavailable on 
the
+# running core and callers should skip mapping it.
+try:
+    from airflow.api_fastapi.auth.managers.models.resource_details import 
AccessView
+
+    IMPORT_ERRORS_ALL_ACCESS_VIEW: AccessView | None = getattr(AccessView, 
"IMPORT_ERRORS_ALL", None)
+except ImportError:
+    IMPORT_ERRORS_ALL_ACCESS_VIEW = None
+
+__all__ = ["IMPORT_ERRORS_ALL_ACCESS_VIEW"]
diff --git 
a/providers/common/compat/tests/unit/common/compat/security/test_access_view.py 
b/providers/common/compat/tests/unit/common/compat/security/test_access_view.py
new file mode 100644
index 00000000000..1804ff96e24
--- /dev/null
+++ 
b/providers/common/compat/tests/unit/common/compat/security/test_access_view.py
@@ -0,0 +1,60 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import importlib
+import sys
+import types
+from unittest import mock
+
+RESOURCE_DETAILS_MODULE = 
"airflow.api_fastapi.auth.managers.models.resource_details"
+ACCESS_VIEW_SHIM_MODULE = 
"airflow.providers.common.compat.security.access_view"
+
+
+def test_resolves_to_the_core_access_view_member_or_none():
+    """The shim mirrors the running core: the ``AccessView.IMPORT_ERRORS_ALL``
+    member on a core that defines it (>= 3.4.0), otherwise ``None``. Kept
+    version-agnostic so it holds across the whole provider compatibility 
matrix,
+    including cores that predate the member or lack ``api_fastapi`` entirely.
+    """
+    from airflow.providers.common.compat.security.access_view import 
IMPORT_ERRORS_ALL_ACCESS_VIEW
+
+    try:
+        from airflow.api_fastapi.auth.managers.models.resource_details import 
AccessView
+
+        expected = getattr(AccessView, "IMPORT_ERRORS_ALL", None)
+    except ImportError:
+        expected = None
+
+    assert IMPORT_ERRORS_ALL_ACCESS_VIEW is expected
+
+
+def test_is_none_on_older_core_without_the_member():
+    """On a core that predates ``AccessView.IMPORT_ERRORS_ALL`` the shim 
resolves to ``None``."""
+
+    class _AccessViewWithoutImportErrorsAll:
+        """Stand-in for an older core AccessView that lacks the new member."""
+
+    fake_resource_details = types.ModuleType(RESOURCE_DETAILS_MODULE)
+    fake_resource_details.AccessView = _AccessViewWithoutImportErrorsAll
+
+    with mock.patch.dict(sys.modules, {RESOURCE_DETAILS_MODULE: 
fake_resource_details}):
+        reloaded = 
importlib.reload(importlib.import_module(ACCESS_VIEW_SHIM_MODULE))
+        assert reloaded.IMPORT_ERRORS_ALL_ACCESS_VIEW is None
+
+    # Restore the module against the real core so later imports see the real 
value.
+    importlib.reload(importlib.import_module(ACCESS_VIEW_SHIM_MODULE))
diff --git a/providers/fab/pyproject.toml b/providers/fab/pyproject.toml
index caf0f6e7f42..cef0a4b4728 100644
--- a/providers/fab/pyproject.toml
+++ b/providers/fab/pyproject.toml
@@ -67,7 +67,7 @@ requires-python = ">=3.10"
 # After you modify the dependencies, and rebuild your Breeze CI image with 
``breeze ci-image build``
 dependencies = [
     "apache-airflow>=3.0.2",
-    "apache-airflow-providers-common-compat>=1.12.0",
+    "apache-airflow-providers-common-compat>=1.12.0",  # use next version
     # Blinker use for signals in Flask, this is an optional dependency in 
Flask 2.2 and lower.
     # In Flask 2.3 it becomes a mandatory dependency, and flask signals are 
always available.
     "blinker>=1.6.2",
diff --git 
a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py 
b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py
index b6a79d032ae..046278a0408 100644
--- a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py
+++ b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py
@@ -56,6 +56,7 @@ from airflow.api_fastapi.common.types import ExtraMenuItem, 
MenuItem
 from airflow.exceptions import AirflowConfigException, 
AirflowProviderDeprecationWarning
 from airflow.models import Connection, DagModel, Pool, Variable
 from airflow.providers.common.compat.sdk import AirflowException, conf
+from airflow.providers.common.compat.security.access_view import 
IMPORT_ERRORS_ALL_ACCESS_VIEW
 from airflow.providers.fab.auth_manager.models import Permission, Role, User
 from airflow.providers.fab.auth_manager.models.anonymous_user import 
AnonymousUser
 from airflow.providers.fab.version_compat import AIRFLOW_V_3_1_PLUS
@@ -77,6 +78,7 @@ from airflow.providers.fab.www.security.permissions import (
     RESOURCE_DAG_WARNING,
     RESOURCE_DOCS,
     RESOURCE_IMPORT_ERROR,
+    RESOURCE_IMPORT_ERROR_ALL,
     RESOURCE_JOB,
     RESOURCE_PLUGIN,
     RESOURCE_POOL,
@@ -146,6 +148,11 @@ _MAP_ACCESS_VIEW_TO_FAB_RESOURCE_TYPE = {
     AccessView.WEBSITE: RESOURCE_WEBSITE,
 }
 
+# ``AccessView.IMPORT_ERRORS_ALL`` only exists on core >= 3.4.0; the compat 
shim
+# yields ``None`` on older core so this provider still imports there.
+if IMPORT_ERRORS_ALL_ACCESS_VIEW is not None:
+    _MAP_ACCESS_VIEW_TO_FAB_RESOURCE_TYPE[IMPORT_ERRORS_ALL_ACCESS_VIEW] = 
RESOURCE_IMPORT_ERROR_ALL
+
 _MAP_MENU_ITEM_TO_FAB_RESOURCE_TYPE = {
     MenuItem.ASSETS: RESOURCE_ASSET,
     MenuItem.AUDIT_LOG: RESOURCE_AUDIT_LOG,
@@ -512,7 +519,10 @@ class FabAuthManager(BaseAuthManager[User]):
     ) -> bool:
         return self._is_authorized(method=method, 
resource_type=RESOURCE_VARIABLE, user=user)
 
-    def is_authorized_view(self, *, access_view: AccessView, user: User) -> 
bool:
+    def is_authorized_view(
+        self, *, access_view: AccessView, user: User, team_name: str | None = 
None
+    ) -> bool:
+        # ``team_name`` is ignored: FAB has no multi-team support.
         # "Docs" are only links in the menu, there is no page associated
         method: ExtendedResourceMethod = "MENU" if access_view == 
AccessView.DOCS else "GET"
         return self._is_authorized(
diff --git 
a/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py
 
b/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py
index 07c05dcb909..05a72235f7d 100644
--- 
a/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py
+++ 
b/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py
@@ -332,6 +332,7 @@ class 
FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
     ADMIN_PERMISSIONS = [
         (permissions.ACTION_CAN_READ, permissions.RESOURCE_AUDIT_LOG),
         (permissions.ACTION_CAN_ACCESS_MENU, permissions.RESOURCE_AUDIT_LOG),
+        (permissions.ACTION_CAN_READ, permissions.RESOURCE_IMPORT_ERROR_ALL),
         (permissions.ACTION_CAN_READ, permissions.RESOURCE_TASK_RESCHEDULE),
         (permissions.ACTION_CAN_ACCESS_MENU, 
permissions.RESOURCE_TASK_RESCHEDULE),
         (permissions.ACTION_CAN_READ, permissions.RESOURCE_TRIGGER),
diff --git 
a/providers/fab/src/airflow/providers/fab/www/security/permissions.py 
b/providers/fab/src/airflow/providers/fab/www/security/permissions.py
index 630a95ea541..10dfd6f45f2 100644
--- a/providers/fab/src/airflow/providers/fab/www/security/permissions.py
+++ b/providers/fab/src/airflow/providers/fab/www/security/permissions.py
@@ -40,6 +40,7 @@ RESOURCE_DOCS = "Documentation"
 RESOURCE_DOCS_MENU = "Docs"
 RESOURCE_HITL_DETAIL = "HITL Detail"
 RESOURCE_IMPORT_ERROR = "ImportError"
+RESOURCE_IMPORT_ERROR_ALL = "All Import Errors"
 RESOURCE_JOB = "Jobs"
 RESOURCE_MY_PASSWORD = "My Password"
 RESOURCE_MY_PROFILE = "My Profile"
diff --git a/providers/keycloak/pyproject.toml 
b/providers/keycloak/pyproject.toml
index 2eed4950abe..b76d1ec72e7 100644
--- a/providers/keycloak/pyproject.toml
+++ b/providers/keycloak/pyproject.toml
@@ -60,7 +60,7 @@ requires-python = ">=3.10"
 # After you modify the dependencies, and rebuild your Breeze CI image with 
``breeze ci-image build``
 dependencies = [
     "apache-airflow>=3.0.0",
-    "apache-airflow-providers-common-compat>=1.12.0",
+    "apache-airflow-providers-common-compat>=1.12.0",  # use next version
     "python-keycloak>=5.0.0",
 ]
 
diff --git 
a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py
 
b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py
index bef6632fe3d..fd7bfaf8990 100644
--- 
a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py
+++ 
b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py
@@ -313,12 +313,15 @@ class 
KeycloakAuthManager(BaseAuthManager[KeycloakAuthManagerUser]):
             team_name=team_name,
         )
 
-    def is_authorized_view(self, *, access_view: AccessView, user: 
KeycloakAuthManagerUser) -> bool:
+    def is_authorized_view(
+        self, *, access_view: AccessView, user: KeycloakAuthManagerUser, 
team_name: str | None = None
+    ) -> bool:
         return self._is_authorized(
             method="GET",
             resource_type=KeycloakResource.VIEW,
             user=user,
             resource_id=access_view.value,
+            team_name=team_name,
         )
 
     def is_authorized_custom_view(

Reply via email to