Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions airflow/api_fastapi/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ def init_auth_manager(app: FastAPI | None = None) -> BaseAuthManager:

if app and (auth_manager_fastapi_app := am.get_fastapi_app()):
app.mount("/auth", auth_manager_fastapi_app)
app.state.auth_manager = am

return am

Expand Down
34 changes: 34 additions & 0 deletions airflow/api_fastapi/core_api/openapi/v1-generated.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6676,6 +6676,40 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/VersionInfo'
/public/login:
get:
tags:
- Login
summary: Login
description: Redirect to the login URL depending on the AuthManager configured.
operationId: login
parameters:
- name: next
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: Next
responses:
'200':
description: Successful Response
content:
application/json:
schema: {}
'307':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPExceptionResponse'
description: Temporary Redirect
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
components:
schemas:
AppBuilderMenuItemResponse:
Expand Down
2 changes: 2 additions & 0 deletions airflow/api_fastapi/core_api/routes/public/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from airflow.api_fastapi.core_api.routes.public.import_error import import_error_router
from airflow.api_fastapi.core_api.routes.public.job import job_router
from airflow.api_fastapi.core_api.routes.public.log import task_instances_log_router
from airflow.api_fastapi.core_api.routes.public.login import login_router
from airflow.api_fastapi.core_api.routes.public.monitor import monitor_router
from airflow.api_fastapi.core_api.routes.public.plugins import plugins_router
from airflow.api_fastapi.core_api.routes.public.pools import pools_router
Expand Down Expand Up @@ -87,3 +88,4 @@
# Following routers are not included in common router, for now we don't expect it to have authentication
public_router.include_router(monitor_router)
public_router.include_router(version_router)
public_router.include_router(login_router)
38 changes: 38 additions & 0 deletions airflow/api_fastapi/core_api/routes/public/login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# 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 Request, status
from fastapi.responses import RedirectResponse

from airflow.api_fastapi.common.router import AirflowRouter
from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc

login_router = AirflowRouter(tags=["Login"], prefix="/login")


@login_router.get(
"",
responses=create_openapi_http_exception_doc([status.HTTP_307_TEMPORARY_REDIRECT]),
)
def login(request: Request, next: None | str = None) -> RedirectResponse:
"""Redirect to the login URL depending on the AuthManager configured."""
login_url = request.app.state.auth_manager.get_url_login()

if next:
login_url += f"?next={next}"
return RedirectResponse(login_url)
15 changes: 15 additions & 0 deletions airflow/ui/openapi-gen/queries/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
GridService,
ImportErrorService,
JobService,
LoginService,
MonitorService,
PluginService,
PoolService,
Expand Down Expand Up @@ -1642,6 +1643,20 @@ export const UseVersionServiceGetVersionKeyFn = (queryKey?: Array<unknown>) => [
useVersionServiceGetVersionKey,
...(queryKey ?? []),
];
export type LoginServiceLoginDefaultResponse = Awaited<ReturnType<typeof LoginService.login>>;
export type LoginServiceLoginQueryResult<
TData = LoginServiceLoginDefaultResponse,
TError = unknown,
> = UseQueryResult<TData, TError>;
export const useLoginServiceLoginKey = "LoginServiceLogin";
export const UseLoginServiceLoginKeyFn = (
{
next,
}: {
next?: string;
} = {},
queryKey?: Array<unknown>,
) => [useLoginServiceLoginKey, ...(queryKey ?? [{ next }])];
export type AssetServiceCreateAssetEventMutationResult = Awaited<
ReturnType<typeof AssetService.createAssetEvent>
>;
Expand Down
21 changes: 21 additions & 0 deletions airflow/ui/openapi-gen/queries/prefetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
GridService,
ImportErrorService,
JobService,
LoginService,
MonitorService,
PluginService,
PoolService,
Expand Down Expand Up @@ -2298,3 +2299,23 @@ export const prefetchUseVersionServiceGetVersion = (queryClient: QueryClient) =>
queryKey: Common.UseVersionServiceGetVersionKeyFn(),
queryFn: () => VersionService.getVersion(),
});
/**
* Login
* Redirect to the login URL depending on the AuthManager configured.
* @param data The data for the request.
* @param data.next
* @returns unknown Successful Response
* @throws ApiError
*/
export const prefetchUseLoginServiceLogin = (
queryClient: QueryClient,
{
next,
}: {
next?: string;
} = {},
) =>
queryClient.prefetchQuery({
queryKey: Common.UseLoginServiceLoginKeyFn({ next }),
queryFn: () => LoginService.login({ next }),
});
27 changes: 27 additions & 0 deletions airflow/ui/openapi-gen/queries/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
GridService,
ImportErrorService,
JobService,
LoginService,
MonitorService,
PluginService,
PoolService,
Expand Down Expand Up @@ -2727,6 +2728,32 @@ export const useVersionServiceGetVersion = <
queryFn: () => VersionService.getVersion() as TData,
...options,
});
/**
* Login
* Redirect to the login URL depending on the AuthManager configured.
* @param data The data for the request.
* @param data.next
* @returns unknown Successful Response
* @throws ApiError
*/
export const useLoginServiceLogin = <
TData = Common.LoginServiceLoginDefaultResponse,
TError = unknown,
TQueryKey extends Array<unknown> = unknown[],
>(
{
next,
}: {
next?: string;
} = {},
queryKey?: TQueryKey,
options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
) =>
useQuery<TData, TError>({
queryKey: Common.UseLoginServiceLoginKeyFn({ next }, queryKey),
queryFn: () => LoginService.login({ next }) as TData,
...options,
});
/**
* Create Asset Event
* Create asset events.
Expand Down
27 changes: 27 additions & 0 deletions airflow/ui/openapi-gen/queries/suspense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
GridService,
ImportErrorService,
JobService,
LoginService,
MonitorService,
PluginService,
PoolService,
Expand Down Expand Up @@ -2704,3 +2705,29 @@ export const useVersionServiceGetVersionSuspense = <
queryFn: () => VersionService.getVersion() as TData,
...options,
});
/**
* Login
* Redirect to the login URL depending on the AuthManager configured.
* @param data The data for the request.
* @param data.next
* @returns unknown Successful Response
* @throws ApiError
*/
export const useLoginServiceLoginSuspense = <
TData = Common.LoginServiceLoginDefaultResponse,
TError = unknown,
TQueryKey extends Array<unknown> = unknown[],
>(
{
next,
}: {
next?: string;
} = {},
queryKey?: TQueryKey,
options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
) =>
useSuspenseQuery<TData, TError>({
queryKey: Common.UseLoginServiceLoginKeyFn({ next }, queryKey),
queryFn: () => LoginService.login({ next }) as TData,
...options,
});
26 changes: 26 additions & 0 deletions airflow/ui/openapi-gen/requests/services.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ import type {
ReparseDagFileResponse,
GetHealthResponse,
GetVersionResponse,
LoginData,
LoginResponse,
} from "./types.gen";

export class AssetService {
Expand Down Expand Up @@ -3359,3 +3361,27 @@ export class VersionService {
});
}
}

export class LoginService {
/**
* Login
* Redirect to the login URL depending on the AuthManager configured.
* @param data The data for the request.
* @param data.next
* @returns unknown Successful Response
* @throws ApiError
*/
public static login(data: LoginData = {}): CancelablePromise<LoginResponse> {
return __request(OpenAPI, {
method: "GET",
url: "/public/login",
query: {
next: data.next,
},
errors: {
307: "Temporary Redirect",
422: "Validation Error",
},
});
}
}
25 changes: 25 additions & 0 deletions airflow/ui/openapi-gen/requests/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2424,6 +2424,12 @@ export type GetHealthResponse = HealthInfoResponse;

export type GetVersionResponse = VersionInfo;

export type LoginData = {
next?: string | null;
};

export type LoginResponse = unknown;

export type $OpenApiTs = {
"/ui/next_run_assets/{dag_id}": {
get: {
Expand Down Expand Up @@ -5088,4 +5094,23 @@ export type $OpenApiTs = {
};
};
};
"/public/login": {
get: {
req: LoginData;
res: {
/**
* Successful Response
*/
200: unknown;
/**
* Temporary Redirect
*/
307: HTTPExceptionResponse;
/**
* Validation Error
*/
422: HTTPValidationError;
};
};
};
};
2 changes: 1 addition & 1 deletion airflow/ui/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ axios.interceptors.response.use(
const params = new URLSearchParams();

params.set("next", globalThis.location.href);
globalThis.location.replace(`${import.meta.env.VITE_LEGACY_API_URL}/login?${params.toString()}`);
globalThis.location.replace(`/public/login?${params.toString()}`);
}

return Promise.reject(error);
Expand Down
54 changes: 54 additions & 0 deletions tests/api_fastapi/core_api/routes/public/test_login.py
Original file line number Diff line number Diff line change
@@ -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.mock import MagicMock

import pytest

AUTH_MANAGER_LOGIN_URL = "http://some_login_url"

pytestmark = pytest.mark.db_test


class TestLoginEndpoint:
@pytest.fixture(autouse=True)
def setup(self, test_client) -> None:
auth_manager_mock = MagicMock()
auth_manager_mock.get_url_login.return_value = AUTH_MANAGER_LOGIN_URL
test_client.app.state.auth_manager = auth_manager_mock


class TestGetLogin(TestLoginEndpoint):
@pytest.mark.parametrize(
"params",
[
{},
{"next": None},
{"next": "http://localhost:29091"},
{"next": "http://localhost:29091", "other_param": "something_else"},
],
)
def test_should_respond_308(self, test_client, params):
response = test_client.get("/public/login", follow_redirects=False, params=params)

assert response.status_code == 307
assert (
response.headers["location"] == f"{AUTH_MANAGER_LOGIN_URL}?next={params.get('next')}"
if params.get("next")
else AUTH_MANAGER_LOGIN_URL
)
1 change: 1 addition & 0 deletions tests/api_fastapi/core_api/routes/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

# Set of paths that are allowed to be accessible without authentication
NO_AUTH_PATHS = {
"/public/login",
"/public/version",
"/public/monitor/health",
}
Expand Down
Loading