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

pierrejeambrun 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 68616610887 AIP-84 | Add UI Auth Links Endpoint (#47662)
68616610887 is described below

commit 68616610887406163c53e664e688e0b772aceae9
Author: LIU ZHE YOU <[email protected]>
AuthorDate: Thu Mar 13 22:01:54 2025 +0800

    AIP-84 | Add UI Auth Links Endpoint (#47662)
    
    * AIP-84 | Add UI Auth Links Endpoint
    
    * Fix test_auth
    
    * style: fix coding style issues
    
    * test(api_fastapi): add db_test mark
    
    ---------
    
    Co-authored-by: Wei Lee <[email protected]>
---
 airflow/api_fastapi/core_api/datamodels/ui/auth.py | 34 ++++++++++++++
 .../api_fastapi/core_api/openapi/v1-generated.yaml | 45 ++++++++++++++++++
 airflow/api_fastapi/core_api/routes/ui/__init__.py |  2 +
 airflow/api_fastapi/core_api/routes/ui/auth.py     | 39 ++++++++++++++++
 airflow/ui/openapi-gen/queries/common.ts           | 13 ++++++
 airflow/ui/openapi-gen/queries/prefetch.ts         | 11 +++++
 airflow/ui/openapi-gen/queries/queries.ts          | 19 ++++++++
 airflow/ui/openapi-gen/queries/suspense.ts         | 19 ++++++++
 airflow/ui/openapi-gen/requests/schemas.gen.ts     | 37 +++++++++++++++
 airflow/ui/openapi-gen/requests/services.gen.ts    | 15 ++++++
 airflow/ui/openapi-gen/requests/types.gen.ts       | 28 +++++++++++
 tests/api_fastapi/core_api/routes/ui/test_auth.py  | 54 ++++++++++++++++++++++
 12 files changed, 316 insertions(+)

diff --git a/airflow/api_fastapi/core_api/datamodels/ui/auth.py 
b/airflow/api_fastapi/core_api/datamodels/ui/auth.py
new file mode 100644
index 00000000000..ab445be12f8
--- /dev/null
+++ b/airflow/api_fastapi/core_api/datamodels/ui/auth.py
@@ -0,0 +1,34 @@
+# 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 airflow.api_fastapi.core_api.base import BaseModel
+
+
+class MenuItem(BaseModel):
+    """Menu Item for responses."""
+
+    text: str
+    href: str
+
+
+class MenuItemCollectionResponse(BaseModel):
+    """Menu Item Collection serializer for responses."""
+
+    menu_items: list[MenuItem]
+    total_entries: int
diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml 
b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
index fb2e3c79d45..cc1668a14ef 100644
--- a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
+++ b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
@@ -7,6 +7,21 @@ info:
     Users should not rely on those but use the public ones instead.
   version: 0.1.0
 paths:
+  /ui/auth/links:
+    get:
+      tags:
+      - Auth Links
+      summary: Get Auth Links
+      operationId: get_auth_links
+      responses:
+        '200':
+          description: Successful Response
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/MenuItemCollectionResponse'
+      security:
+      - OAuth2PasswordBearer: []
   /ui/next_run_assets/{dag_id}:
     get:
       tags:
@@ -10008,6 +10023,36 @@ components:
       - unixname
       title: JobResponse
       description: Job serializer for responses.
+    MenuItem:
+      properties:
+        text:
+          type: string
+          title: Text
+        href:
+          type: string
+          title: Href
+      type: object
+      required:
+      - text
+      - href
+      title: MenuItem
+      description: Menu Item for responses.
+    MenuItemCollectionResponse:
+      properties:
+        menu_items:
+          items:
+            $ref: '#/components/schemas/MenuItem'
+          type: array
+          title: Menu Items
+        total_entries:
+          type: integer
+          title: Total Entries
+      type: object
+      required:
+      - menu_items
+      - total_entries
+      title: MenuItemCollectionResponse
+      description: Menu Item Collection serializer for responses.
     NodeResponse:
       properties:
         id:
diff --git a/airflow/api_fastapi/core_api/routes/ui/__init__.py 
b/airflow/api_fastapi/core_api/routes/ui/__init__.py
index b43b4141b9d..f7b19c53ce2 100644
--- a/airflow/api_fastapi/core_api/routes/ui/__init__.py
+++ b/airflow/api_fastapi/core_api/routes/ui/__init__.py
@@ -18,6 +18,7 @@ from __future__ import annotations
 
 from airflow.api_fastapi.common.router import AirflowRouter
 from airflow.api_fastapi.core_api.routes.ui.assets import assets_router
+from airflow.api_fastapi.core_api.routes.ui.auth import auth_router
 from airflow.api_fastapi.core_api.routes.ui.backfills import backfills_router
 from airflow.api_fastapi.core_api.routes.ui.config import config_router
 from airflow.api_fastapi.core_api.routes.ui.connections import 
connections_router
@@ -29,6 +30,7 @@ from airflow.api_fastapi.core_api.routes.ui.structure import 
structure_router
 
 ui_router = AirflowRouter(prefix="/ui", include_in_schema=False)
 
+ui_router.include_router(auth_router)
 ui_router.include_router(assets_router)
 ui_router.include_router(config_router)
 ui_router.include_router(connections_router)
diff --git a/airflow/api_fastapi/core_api/routes/ui/auth.py 
b/airflow/api_fastapi/core_api/routes/ui/auth.py
new file mode 100644
index 00000000000..aafb90acb95
--- /dev/null
+++ b/airflow/api_fastapi/core_api/routes/ui/auth.py
@@ -0,0 +1,39 @@
+# 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 typing import cast
+
+from airflow.api_fastapi.app import get_auth_manager
+from airflow.api_fastapi.common.router import AirflowRouter
+from airflow.api_fastapi.core_api.datamodels.ui.auth import MenuItem, 
MenuItemCollectionResponse
+from airflow.api_fastapi.core_api.security import GetUserDep
+
+auth_router = AirflowRouter(tags=["Auth Links"])
+
+
+@auth_router.get("/auth/links")
+def get_auth_links(
+    user: GetUserDep,
+) -> MenuItemCollectionResponse:
+    menu_items = get_auth_manager().get_menu_items(user=user)
+
+    return MenuItemCollectionResponse(
+        menu_items=cast(list[MenuItem], menu_items),
+        total_entries=len(menu_items),
+    )
diff --git a/airflow/ui/openapi-gen/queries/common.ts 
b/airflow/ui/openapi-gen/queries/common.ts
index 728d3e8fcda..f3f17b7da77 100644
--- a/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow/ui/openapi-gen/queries/common.ts
@@ -3,6 +3,7 @@ import { UseQueryResult } from "@tanstack/react-query";
 
 import {
   AssetService,
+  AuthLinksService,
   BackfillService,
   ConfigService,
   ConnectionService,
@@ -36,6 +37,18 @@ import {
 } from "../requests/services.gen";
 import { DagRunState, DagWarningType } from "../requests/types.gen";
 
+export type AuthLinksServiceGetAuthLinksDefaultResponse = Awaited<
+  ReturnType<typeof AuthLinksService.getAuthLinks>
+>;
+export type AuthLinksServiceGetAuthLinksQueryResult<
+  TData = AuthLinksServiceGetAuthLinksDefaultResponse,
+  TError = unknown,
+> = UseQueryResult<TData, TError>;
+export const useAuthLinksServiceGetAuthLinksKey = 
"AuthLinksServiceGetAuthLinks";
+export const UseAuthLinksServiceGetAuthLinksKeyFn = (queryKey?: 
Array<unknown>) => [
+  useAuthLinksServiceGetAuthLinksKey,
+  ...(queryKey ?? []),
+];
 export type AssetServiceNextRunAssetsDefaultResponse = 
Awaited<ReturnType<typeof AssetService.nextRunAssets>>;
 export type AssetServiceNextRunAssetsQueryResult<
   TData = AssetServiceNextRunAssetsDefaultResponse,
diff --git a/airflow/ui/openapi-gen/queries/prefetch.ts 
b/airflow/ui/openapi-gen/queries/prefetch.ts
index ef029329f6b..447a3dbbff7 100644
--- a/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -3,6 +3,7 @@ import { type QueryClient } from "@tanstack/react-query";
 
 import {
   AssetService,
+  AuthLinksService,
   BackfillService,
   ConfigService,
   ConnectionService,
@@ -36,6 +37,16 @@ import {
 import { DagRunState, DagWarningType } from "../requests/types.gen";
 import * as Common from "./common";
 
+/**
+ * Get Auth Links
+ * @returns MenuItemCollectionResponse Successful Response
+ * @throws ApiError
+ */
+export const prefetchUseAuthLinksServiceGetAuthLinks = (queryClient: 
QueryClient) =>
+  queryClient.prefetchQuery({
+    queryKey: Common.UseAuthLinksServiceGetAuthLinksKeyFn(),
+    queryFn: () => AuthLinksService.getAuthLinks(),
+  });
 /**
  * Next Run Assets
  * @param data The data for the request.
diff --git a/airflow/ui/openapi-gen/queries/queries.ts 
b/airflow/ui/openapi-gen/queries/queries.ts
index 8aac17090b3..32cb6d467e3 100644
--- a/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow/ui/openapi-gen/queries/queries.ts
@@ -3,6 +3,7 @@ import { UseMutationOptions, UseQueryOptions, useMutation, 
useQuery } from "@tan
 
 import {
   AssetService,
+  AuthLinksService,
   BackfillService,
   ConfigService,
   ConnectionService,
@@ -59,6 +60,24 @@ import {
 } from "../requests/types.gen";
 import * as Common from "./common";
 
+/**
+ * Get Auth Links
+ * @returns MenuItemCollectionResponse Successful Response
+ * @throws ApiError
+ */
+export const useAuthLinksServiceGetAuthLinks = <
+  TData = Common.AuthLinksServiceGetAuthLinksDefaultResponse,
+  TError = unknown,
+  TQueryKey extends Array<unknown> = unknown[],
+>(
+  queryKey?: TQueryKey,
+  options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
+) =>
+  useQuery<TData, TError>({
+    queryKey: Common.UseAuthLinksServiceGetAuthLinksKeyFn(queryKey),
+    queryFn: () => AuthLinksService.getAuthLinks() as TData,
+    ...options,
+  });
 /**
  * Next Run Assets
  * @param data The data for the request.
diff --git a/airflow/ui/openapi-gen/queries/suspense.ts 
b/airflow/ui/openapi-gen/queries/suspense.ts
index 3e5ff4194bc..b9806dc9070 100644
--- a/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow/ui/openapi-gen/queries/suspense.ts
@@ -3,6 +3,7 @@ import { UseQueryOptions, useSuspenseQuery } from 
"@tanstack/react-query";
 
 import {
   AssetService,
+  AuthLinksService,
   BackfillService,
   ConfigService,
   ConnectionService,
@@ -36,6 +37,24 @@ import {
 import { DagRunState, DagWarningType } from "../requests/types.gen";
 import * as Common from "./common";
 
+/**
+ * Get Auth Links
+ * @returns MenuItemCollectionResponse Successful Response
+ * @throws ApiError
+ */
+export const useAuthLinksServiceGetAuthLinksSuspense = <
+  TData = Common.AuthLinksServiceGetAuthLinksDefaultResponse,
+  TError = unknown,
+  TQueryKey extends Array<unknown> = unknown[],
+>(
+  queryKey?: TQueryKey,
+  options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
+) =>
+  useSuspenseQuery<TData, TError>({
+    queryKey: Common.UseAuthLinksServiceGetAuthLinksKeyFn(queryKey),
+    queryFn: () => AuthLinksService.getAuthLinks() as TData,
+    ...options,
+  });
 /**
  * Next Run Assets
  * @param data The data for the request.
diff --git a/airflow/ui/openapi-gen/requests/schemas.gen.ts 
b/airflow/ui/openapi-gen/requests/schemas.gen.ts
index acaf4580c32..2ef11a91fab 100644
--- a/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -4150,6 +4150,43 @@ export const $JobResponse = {
   description: "Job serializer for responses.",
 } as const;
 
+export const $MenuItem = {
+  properties: {
+    text: {
+      type: "string",
+      title: "Text",
+    },
+    href: {
+      type: "string",
+      title: "Href",
+    },
+  },
+  type: "object",
+  required: ["text", "href"],
+  title: "MenuItem",
+  description: "Menu Item for responses.",
+} as const;
+
+export const $MenuItemCollectionResponse = {
+  properties: {
+    menu_items: {
+      items: {
+        $ref: "#/components/schemas/MenuItem",
+      },
+      type: "array",
+      title: "Menu Items",
+    },
+    total_entries: {
+      type: "integer",
+      title: "Total Entries",
+    },
+  },
+  type: "object",
+  required: ["menu_items", "total_entries"],
+  title: "MenuItemCollectionResponse",
+  description: "Menu Item Collection serializer for responses.",
+} as const;
+
 export const $NodeResponse = {
   properties: {
     id: {
diff --git a/airflow/ui/openapi-gen/requests/services.gen.ts 
b/airflow/ui/openapi-gen/requests/services.gen.ts
index ef8a2d833af..700ab409409 100644
--- a/airflow/ui/openapi-gen/requests/services.gen.ts
+++ b/airflow/ui/openapi-gen/requests/services.gen.ts
@@ -3,6 +3,7 @@ import type { CancelablePromise } from 
"./core/CancelablePromise";
 import { OpenAPI } from "./core/OpenAPI";
 import { request as __request } from "./core/request";
 import type {
+  GetAuthLinksResponse,
   NextRunAssetsData,
   NextRunAssetsResponse,
   GetAssetsData,
@@ -214,6 +215,20 @@ import type {
   LoginResponse,
 } from "./types.gen";
 
+export class AuthLinksService {
+  /**
+   * Get Auth Links
+   * @returns MenuItemCollectionResponse Successful Response
+   * @throws ApiError
+   */
+  public static getAuthLinks(): CancelablePromise<GetAuthLinksResponse> {
+    return __request(OpenAPI, {
+      method: "GET",
+      url: "/ui/auth/links",
+    });
+  }
+}
+
 export class AssetService {
   /**
    * Next Run Assets
diff --git a/airflow/ui/openapi-gen/requests/types.gen.ts 
b/airflow/ui/openapi-gen/requests/types.gen.ts
index ce1b31a13ea..3c0016a6349 100644
--- a/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -1094,6 +1094,22 @@ export type JobResponse = {
   unixname: string | null;
 };
 
+/**
+ * Menu Item for responses.
+ */
+export type MenuItem = {
+  text: string;
+  href: string;
+};
+
+/**
+ * Menu Item Collection serializer for responses.
+ */
+export type MenuItemCollectionResponse = {
+  menu_items: Array<MenuItem>;
+  total_entries: number;
+};
+
 /**
  * Node serializer for responses.
  */
@@ -1653,6 +1669,8 @@ export type XComUpdateBody = {
   map_index?: number;
 };
 
+export type GetAuthLinksResponse = MenuItemCollectionResponse;
+
 export type NextRunAssetsData = {
   dagId: string;
 };
@@ -2576,6 +2594,16 @@ export type LoginData = {
 export type LoginResponse = unknown;
 
 export type $OpenApiTs = {
+  "/ui/auth/links": {
+    get: {
+      res: {
+        /**
+         * Successful Response
+         */
+        200: MenuItemCollectionResponse;
+      };
+    };
+  };
   "/ui/next_run_assets/{dag_id}": {
     get: {
       req: NextRunAssetsData;
diff --git a/tests/api_fastapi/core_api/routes/ui/test_auth.py 
b/tests/api_fastapi/core_api/routes/ui/test_auth.py
new file mode 100644
index 00000000000..fd082821821
--- /dev/null
+++ b/tests/api_fastapi/core_api/routes/ui/test_auth.py
@@ -0,0 +1,54 @@
+# 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 unittest import mock
+
+import pytest
+
+from airflow.api_fastapi.common.types import MenuItem
+
+pytestmark = pytest.mark.db_test
+
+
+class TestGetAuthLinks:
+    @mock.patch("airflow.api_fastapi.core_api.routes.ui.auth.get_auth_manager")
+    def test_should_response_200(self, mock_get_auth_manager, test_client):
+        mock_get_auth_manager.return_value.get_menu_items.return_value = [
+            MenuItem(text="name1", href="path1"),
+            MenuItem(text="name2", href="path2"),
+        ]
+        response = test_client.get("/ui/auth/links")
+
+        assert response.status_code == 200
+        assert response.json() == {
+            "menu_items": [
+                {"text": "name1", "href": "path1"},
+                {"text": "name2", "href": "path2"},
+            ],
+            "total_entries": 2,
+        }
+
+    def test_with_unauthenticated_user(self, unauthenticated_test_client):
+        response = unauthenticated_test_client.get("/ui/auth/links")
+        assert response.status_code == 401
+        assert response.json() == {"detail": "Not authenticated"}
+
+    def test_with_unauthorized_user(self, unauthorized_test_client):
+        response = unauthorized_test_client.get("/ui/auth/links")
+        assert response.status_code == 200
+        assert response.json() == {"menu_items": [], "total_entries": 0}

Reply via email to