jedcunningham commented on code in PR #76329:
URL: https://github.com/apache/airflow/pull/76329#discussion_r4450514622


##########
airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dag_bundles.py:
##########
@@ -0,0 +1,44 @@
+# 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
+
+from fastapi import Depends
+from sqlalchemy import func, select
+
+from airflow.api_fastapi.common.db.common import SessionDep
+from airflow.api_fastapi.common.router import AirflowRouter
+from airflow.api_fastapi.core_api.datamodels.ui.dag_bundles import 
DagBundleCountResponse
+from airflow.api_fastapi.core_api.security import ReadableDagsFilterDep, 
requires_access_dag
+from airflow.models import DagModel
+
+dag_bundles_router = AirflowRouter(tags=["Dag Bundle"], prefix="/dagBundles")
+
+
+@dag_bundles_router.get("/count", 
dependencies=[Depends(requires_access_dag(method="GET"))])
+def get_dag_bundle_count(
+    readable_dags_filter: ReadableDagsFilterDep,
+    session: SessionDep,
+) -> DagBundleCountResponse:
+    """
+    Count the Dag bundles holding a Dag the caller may read.
+
+    Readable by anyone who can read Dags, unlike ``GET /dagBundles``, so the 
UI can tell whether a
+    bundle name is worth showing next to an import error. A bundle holding 
only stale Dags is
+    counted too: that is a bundle whose every file fails to import, where the 
name matters most.
+    """
+    count = 
session.scalar(readable_dags_filter.to_orm(select(func.count(DagModel.bundle_name.distinct()))))

Review Comment:
   Fixed. The count now includes bundles with readable import errors, and 
there's a test.



##########
providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py:
##########
@@ -91,9 +91,10 @@
 
 RESOURCE_ID_ATTRIBUTE_NAME = "resource_id"
 
-# ``AccessView`` values covering records that carry no per-Dag or per-team key 
to authorize on. They
-# are admin-by-default in core. Compared by value because older Airflow 
versions lack some members.
-ADMIN_ACCESS_VIEW_NAMES = frozenset({"AUDIT_LOGS_ALL", "IMPORT_ERRORS_ALL", 
"REPARSE_ALL"})
+# ``AccessView`` values showing records beyond the Dags a caller can read. In 
multi-team mode they
+# are checked against ``AdminView``, which only ``SuperAdmin`` holds. Compared 
by value because
+# older Airflow versions lack some members.
+ADMIN_ACCESS_VIEW_NAMES = frozenset({"AUDIT_LOGS_ALL", "DAG_BUNDLES", 
"IMPORT_ERRORS_ALL", "REPARSE_ALL"})

Review Comment:
   Added a changelog note and updated `permissions.rst`.



##########
airflow-core/src/airflow/api_fastapi/core_api/security.py:
##########
@@ -1284,6 +1270,26 @@ def inner(
     return inner
 
 
+def requires_access_dag_bundles() -> Callable[[BaseUser, BaseAuthManager], 
None]:
+    """
+    Require ``AccessView.DAG_BUNDLES``, without a team.
+
+    In a multi-team deployment this only admits the caller to the route, and
+    ``ReadableDagBundlesFilterDep`` then narrows the bundles to the caller's 
teams. Goes through
+    ``authorize_view`` rather than ``requires_access_view`` so that an auth 
manager that cannot map
+    the view (raises ``KeyError``) denies it instead of failing the request.
+    """
+
+    def inner(user: GetUserDep, auth_manager: AuthManagerDep) -> None:
+        _requires_access(
+            is_authorized_callback=lambda: auth_manager.authorize_view(
+                access_view=AccessView.DAG_BUNDLES, user=user

Review Comment:
   Done. The gate now admits anyone holding the view for any of their teams.



##########
airflow-core/src/airflow/ui/src/pages/DagBundle/FileImportError.tsx:
##########
@@ -40,25 +47,55 @@ type Props = {
  * error. A dialog rather than a page of its own, because a single stack trace 
is not worth an
  * addressable route.
  */
-export const FileImportError = ({ bundleName, onClose, relativeFileloc }: 
Props) => {
+export const FileImportError = ({
+  bundleName,
+  hasImportError,
+  onClose,
+  recheckImportError,
+  relativeFileloc,
+}: Props) => {
   const { t: translate } = useTranslation("browse");
+  const [recheckedFileloc, setRecheckedFileloc] = useState<string | 
undefined>(undefined);
 
-  const { data, isLoading } = useImportErrorServiceGetImportErrors(
+  const { data, error, isLoading } = useImportErrorServiceGetImportErrors(
     { bundleName, filename: relativeFileloc ?? "" },
     undefined,
     { enabled: relativeFileloc !== undefined },
   );
 
   const [importError] = data?.import_errors ?? [];
 
+  // ``GET /importErrors`` leaves out errors the caller may not read, so an 
empty result is told
+  // apart by the file list, whose count is not filtered by caller. The badge 
was clicked on a
+  // count that may predate a fix, so the list is refetched once before the 
count is trusted.
+  const needsRecheck =
+    relativeFileloc !== undefined &&
+    data !== undefined &&
+    importError === undefined &&
+    hasImportError &&
+    recheckedFileloc !== relativeFileloc;
+
+  useEffect(() => {
+    if (needsRecheck) {
+      void recheckImportError().finally(() => {
+        setRecheckedFileloc(relativeFileloc);
+      });
+    }
+  }, [needsRecheck, recheckImportError, relativeFileloc]);
+
+  const close = () => {
+    setRecheckedFileloc(undefined);
+    onClose();
+  };
+
   if (importError !== undefined) {
     return (
-      <DagImportErrorModal importError={importError} onClose={onClose} 
open={relativeFileloc !== undefined} />
+      <DagImportErrorModal importError={importError} onClose={close} 
open={relativeFileloc !== undefined} />
     );
   }
 
-  // The table polls, so the error can be gone by the time its badge is 
clicked. Say so rather
-  // than leaving the click with no visible effect.
+  const isForbidden = getErrorStatus(error) === 403 || (data !== undefined && 
hasImportError);

Review Comment:
   Fixed. The recheck refetches the import error too.



##########
airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_bundles.py:
##########
@@ -44,17 +44,13 @@ class DagBundleResponse(BaseModel):
         )
     )
     bundle_url: str | None = Field(
-        description=(
-            "A link to view the bundle at ``version``, when one is configured 
and the caller may "
-            "read Dag versions."
-        )
+        description=("A link to view the bundle at ``version``, when the 
bundle is configured with one.")

Review Comment:
   Updated.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to