Coverage for /home/airflow/.local/lib/python3.12/site-packages/airflow/api_fastapi/core_api/routes/public/auth.py: 57%

36 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 14:22 +0000

1# Licensed to the Apache Software Foundation (ASF) under one 

2# or more contributor license agreements. See the NOTICE file 

3# distributed with this work for additional information 

4# regarding copyright ownership. The ASF licenses this file 

5# to you under the Apache License, Version 2.0 (the 

6# "License"); you may not use this file except in compliance 

7# with the License. You may obtain a copy of the License at 

8# 

9# http://www.apache.org/licenses/LICENSE-2.0 

10# 

11# Unless required by applicable law or agreed to in writing, 

12# software distributed under the License is distributed on an 

13# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 

14# KIND, either express or implied. See the License for the 

15# specific language governing permissions and limitations 

16# under the License. 

17from __future__ import annotations 

18 

19from urllib.parse import urlencode 

20 

21import structlog 

22from fastapi import Depends, HTTPException, Request, status 

23from fastapi.responses import RedirectResponse 

24from fastapi.security import HTTPAuthorizationCredentials 

25 

26from airflow.api_fastapi.app import get_cookie_path 

27from airflow.api_fastapi.auth.managers.base_auth_manager import COOKIE_NAME_JWT_TOKEN 

28from airflow.api_fastapi.common.router import AirflowRouter 

29from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc 

30from airflow.api_fastapi.core_api.security import ( 

31 AuthManagerDep, 

32 bearer_scheme, 

33 collect_request_tokens, 

34 is_safe_url, 

35 oauth2_scheme, 

36) 

37from airflow.configuration import conf 

38 

39log = structlog.get_logger(logger_name=__name__) 

40 

41auth_router = AirflowRouter(tags=["Login"], prefix="/auth") 

42 

43 

44@auth_router.get( 

45 "/login", 

46 responses=create_openapi_http_exception_doc( 

47 [status.HTTP_307_TEMPORARY_REDIRECT, status.HTTP_400_BAD_REQUEST] 

48 ), 

49) 

50def login(request: Request, auth_manager: AuthManagerDep, next: None | str = None) -> RedirectResponse: 

51 """Redirect to the login URL depending on the AuthManager configured.""" 

52 login_url = auth_manager.get_url_login() 

53 

54 if next and not is_safe_url(next, request=request): 54 ↛ 55line 54 didn't jump to line 55 because the condition on line 54 was never true

55 raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Invalid or unsafe next URL") 

56 

57 if next: 

58 login_url += f"?{urlencode({'next': next})}" 

59 

60 return RedirectResponse(login_url) 

61 

62 

63@auth_router.get( 

64 "/logout", 

65 responses=create_openapi_http_exception_doc([status.HTTP_307_TEMPORARY_REDIRECT]), 

66) 

67def logout( 

68 request: Request, 

69 auth_manager: AuthManagerDep, 

70 # Kept for the OpenAPI security spec so ``/docs`` still renders the OAuth2 password 

71 # login form. It resolves to the same ``Authorization: Bearer`` header 

72 # ``bearer_scheme`` reads, so the value is unused at runtime. 

73 _oauth_token: str | None = Depends(oauth2_scheme), 

74 bearer_credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme), 

75) -> RedirectResponse: 

76 """Logout the user.""" 

77 # Invalidate both tokens from the Authorization header and the _token cookie, if present. 

78 for token_str in collect_request_tokens(request, bearer_credentials): 

79 auth_manager.revoke_token(token_str) 

80 

81 logout_url = auth_manager.get_url_logout() 

82 if logout_url: 

83 return RedirectResponse(logout_url) 

84 

85 secure = request.base_url.scheme == "https" or bool(conf.get("api", "ssl_cert", fallback="")) 

86 cookie_path = get_cookie_path() 

87 response = RedirectResponse(auth_manager.get_url_login()) 

88 response.delete_cookie( 

89 key=COOKIE_NAME_JWT_TOKEN, 

90 path=cookie_path, 

91 secure=secure, 

92 httponly=True, 

93 ) 

94 # Clear any stale _token cookie at root path "/" left by 

95 # older Airflow instances to prevent redirect loops. 

96 if cookie_path != "/": 

97 response.delete_cookie( 

98 key=COOKIE_NAME_JWT_TOKEN, 

99 path="/", 

100 secure=secure, 

101 httponly=True, 

102 ) 

103 

104 return response