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}