Skip to content

Commit 0675231

Browse files
authored
Add get_additional_menu_items in auth manager interface to extend the menu (apache#47468)
1 parent bdfea80 commit 0675231

File tree

14 files changed

+115
-19
lines changed

14 files changed

+115
-19
lines changed

airflow/api_fastapi/app.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040
if TYPE_CHECKING:
4141
from airflow.api_fastapi.auth.managers.base_auth_manager import BaseAuthManager
4242

43+
# Define the path in which the potential auth manager fastapi is mounted
44+
AUTH_MANAGER_FASTAPI_APP_PREFIX = "/auth"
45+
4346
log = logging.getLogger(__name__)
4447

4548
app: FastAPI | None = None
@@ -141,7 +144,7 @@ def init_auth_manager(app: FastAPI | None = None) -> BaseAuthManager:
141144
am.init()
142145

143146
if app and (auth_manager_fastapi_app := am.get_fastapi_app()):
144-
app.mount("/auth", auth_manager_fastapi_app)
147+
app.mount(AUTH_MANAGER_FASTAPI_APP_PREFIX, auth_manager_fastapi_app)
145148
app.state.auth_manager = am
146149

147150
return am

airflow/api_fastapi/auth/managers/base_auth_manager.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
from airflow.api_fastapi.auth.managers.models.base_user import BaseUser
2828
from airflow.api_fastapi.auth.managers.models.resource_details import DagDetails
29+
from airflow.api_fastapi.common.types import MenuItem
2930
from airflow.configuration import conf
3031
from airflow.models import DagModel
3132
from airflow.typing_compat import Literal
@@ -411,6 +412,14 @@ def get_fastapi_app(self) -> FastAPI | None:
411412
"""
412413
return None
413414

415+
def get_menu_items(self, *, user: T) -> list[MenuItem]:
416+
"""
417+
Provide additional links to be added to the menu.
418+
419+
:param user: the user
420+
"""
421+
return []
422+
414423
@staticmethod
415424
def _get_token_signer(
416425
expiration_time_in_seconds: int = conf.getint("api", "auth_jwt_expiration_time"),

airflow/api_fastapi/auth/managers/simple/simple_auth_manager.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from starlette.templating import Jinja2Templates
3333
from termcolor import colored
3434

35+
from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX
3536
from airflow.api_fastapi.auth.managers.base_auth_manager import BaseAuthManager
3637
from airflow.api_fastapi.auth.managers.simple.user import SimpleAuthManagerUser
3738
from airflow.configuration import AIRFLOW_HOME, conf
@@ -131,9 +132,9 @@ def get_url_login(self, **kwargs) -> str:
131132
"""Return the login page url."""
132133
is_simple_auth_manager_all_admins = conf.getboolean("core", "simple_auth_manager_all_admins")
133134
if is_simple_auth_manager_all_admins:
134-
return "/auth/token"
135+
return AUTH_MANAGER_FASTAPI_APP_PREFIX + "/token"
135136

136-
return "/auth/login"
137+
return AUTH_MANAGER_FASTAPI_APP_PREFIX + "/login"
137138

138139
def deserialize_user(self, token: dict[str, Any]) -> SimpleAuthManagerUser:
139140
return SimpleAuthManagerUser(username=token["username"], role=token["role"])

airflow/api_fastapi/common/types.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# under the License.
1717
from __future__ import annotations
1818

19+
from dataclasses import dataclass
1920
from datetime import timedelta
2021
from enum import Enum
2122
from typing import Annotated
@@ -72,3 +73,11 @@ class Mimetype(str, Enum):
7273
TEXT = "text/plain"
7374
JSON = "application/json"
7475
ANY = "*/*"
76+
77+
78+
@dataclass
79+
class MenuItem:
80+
"""Define a menu item."""
81+
82+
text: str
83+
href: str

providers/amazon/src/airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
from fastapi import FastAPI
2727

28+
from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX
2829
from airflow.api_fastapi.auth.managers.base_auth_manager import BaseAuthManager
2930
from airflow.api_fastapi.auth.managers.models.resource_details import (
3031
AccessView,
@@ -322,7 +323,7 @@ def _has_access_to_dag(request: IsAuthorizedRequest):
322323
return {dag_id for dag_id in dag_ids if _has_access_to_dag(requests[dag_id][method])}
323324

324325
def get_url_login(self, **kwargs) -> str:
325-
return urljoin(self.apiserver_endpoint, "auth/login")
326+
return urljoin(self.apiserver_endpoint, f"{AUTH_MANAGER_FASTAPI_APP_PREFIX}/login")
326327

327328
@staticmethod
328329
def get_cli_commands() -> list[CLICommand]:

providers/amazon/src/airflow/providers/amazon/aws/auth_manager/router/login.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from starlette import status
2727
from starlette.responses import RedirectResponse
2828

29-
from airflow.api_fastapi.app import get_auth_manager
29+
from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX, get_auth_manager
3030
from airflow.api_fastapi.common.router import AirflowRouter
3131
from airflow.configuration import conf
3232
from airflow.providers.amazon.aws.auth_manager.constants import CONF_SAML_METADATA_URL_KEY, CONF_SECTION_NAME
@@ -94,7 +94,7 @@ def _init_saml_auth(request: Request) -> OneLogin_Saml2_Auth:
9494
"sp": {
9595
"entityId": "aws-auth-manager-saml-client",
9696
"assertionConsumerService": {
97-
"url": f"{base_url}/auth/login_callback",
97+
"url": f"{base_url}{AUTH_MANAGER_FASTAPI_APP_PREFIX}/login_callback",
9898
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
9999
},
100100
},

providers/amazon/tests/system/amazon/aws/tests/test_aws_auth_manager.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,16 @@
2121

2222
import boto3
2323
import pytest
24+
25+
from airflow.providers.amazon.version_compat import AIRFLOW_V_3_0_PLUS
26+
27+
if not AIRFLOW_V_3_0_PLUS:
28+
pytest.skip("AWS auth manager is only compatible with Airflow >= 3.0.0", allow_module_level=True)
29+
2430
from fastapi.testclient import TestClient
2531
from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser
2632

27-
from airflow.api_fastapi.app import create_app
33+
from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX, create_app
2834
from system.amazon.aws.utils import set_env_id
2935

3036
from tests_common.test_utils.config import conf_vars
@@ -191,7 +197,9 @@ def delete_avp_policy_store(cls):
191197
client.delete_policy_store(policyStoreId=policy_store_id)
192198

193199
def test_login_admin(self, client_admin_permissions):
194-
response = client_admin_permissions.post("/auth/login_callback", follow_redirects=False)
200+
response = client_admin_permissions.post(
201+
AUTH_MANAGER_FASTAPI_APP_PREFIX + "/login_callback", follow_redirects=False
202+
)
195203
assert response.status_code == 303
196204
assert "location" in response.headers
197205
assert "/?token=" in response.headers["location"]

providers/amazon/tests/unit/amazon/aws/auth_manager/router/test_login.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from fastapi.testclient import TestClient
2929
from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser
3030

31-
from airflow.api_fastapi.app import create_app
31+
from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX, create_app
3232

3333
from tests_common.test_utils.config import conf_vars
3434

@@ -75,7 +75,7 @@ def test_client():
7575

7676
class TestLoginRouter:
7777
def test_login(self, test_client):
78-
response = test_client.get("/auth/login", follow_redirects=False)
78+
response = test_client.get(AUTH_MANAGER_FASTAPI_APP_PREFIX + "/login", follow_redirects=False)
7979
assert response.status_code == 307
8080
assert "location" in response.headers
8181
assert response.headers["location"].startswith(
@@ -114,7 +114,9 @@ def test_login_callback_successful(self):
114114
}
115115
mock_init_saml_auth.return_value = auth
116116
client = TestClient(create_app())
117-
response = client.post("/auth/login_callback", follow_redirects=False)
117+
response = client.post(
118+
AUTH_MANAGER_FASTAPI_APP_PREFIX + "/login_callback", follow_redirects=False
119+
)
118120
assert response.status_code == 303
119121
assert "location" in response.headers
120122
assert response.headers["location"].startswith("http://localhost:8080/?token=")
@@ -145,5 +147,5 @@ def test_login_callback_unsuccessful(self):
145147
auth.is_authenticated.return_value = False
146148
mock_init_saml_auth.return_value = auth
147149
client = TestClient(create_app())
148-
response = client.post("/auth/login_callback")
150+
response = client.post(AUTH_MANAGER_FASTAPI_APP_PREFIX + "/login_callback")
149151
assert response.status_code == 500

providers/amazon/tests/unit/amazon/aws/auth_manager/test_aws_auth_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
if not AIRFLOW_V_3_0_PLUS:
2727
pytest.skip("AWS auth manager is only compatible with Airflow >= 3.0.0", allow_module_level=True)
2828

29+
from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX
2930
from airflow.api_fastapi.auth.managers.models.resource_details import (
3031
AccessView,
3132
ConfigurationDetails,
@@ -569,7 +570,7 @@ def test_filter_permitted_dag_ids(self, method, user, auth_manager, test_user, e
569570

570571
def test_get_url_login(self, auth_manager):
571572
result = auth_manager.get_url_login()
572-
assert result == "http://localhost:8080/auth/login"
573+
assert result == f"http://localhost:8080{AUTH_MANAGER_FASTAPI_APP_PREFIX}/login"
573574

574575
def test_get_cli_commands_return_cli_commands(self, auth_manager):
575576
assert len(auth_manager.get_cli_commands()) > 0

providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from starlette.middleware.wsgi import WSGIMiddleware
3434

3535
from airflow import __version__ as airflow_version
36+
from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX
3637
from airflow.api_fastapi.auth.managers.base_auth_manager import BaseAuthManager
3738
from airflow.api_fastapi.auth.managers.models.resource_details import (
3839
AccessView,
@@ -43,6 +44,7 @@
4344
PoolDetails,
4445
VariableDetails,
4546
)
47+
from airflow.api_fastapi.common.types import MenuItem
4648
from airflow.cli.cli_config import (
4749
DefaultHelpParser,
4850
GroupCommand,
@@ -410,7 +412,7 @@ def security_manager(self) -> FabAirflowSecurityManagerOverride:
410412

411413
def get_url_login(self, **kwargs) -> str:
412414
"""Return the login page url."""
413-
return urljoin(self.apiserver_endpoint, "auth/login/")
415+
return urljoin(self.apiserver_endpoint, f"{AUTH_MANAGER_FASTAPI_APP_PREFIX}/login/")
414416

415417
def get_url_logout(self):
416418
"""Return the logout page url."""
@@ -425,6 +427,49 @@ def logout(self) -> None:
425427
def register_views(self) -> None:
426428
self.security_manager.register_views()
427429

430+
def get_menu_items(self, *, user: User) -> list[MenuItem]:
431+
# Contains the list of menu items. ``resource_type`` is the name of the resource in FAB
432+
# permission model to check whether the user is allowed to see this menu item
433+
items = [
434+
{
435+
"resource_type": "List Users",
436+
"text": "Users",
437+
"href": AUTH_MANAGER_FASTAPI_APP_PREFIX
438+
+ url_for(f"{self.security_manager.user_view.__class__.__name__}.list", _external=False),
439+
},
440+
{
441+
"resource_type": "List Roles",
442+
"text": "Roles",
443+
"href": AUTH_MANAGER_FASTAPI_APP_PREFIX
444+
+ url_for("CustomRoleModelView.list", _external=False),
445+
},
446+
{
447+
"resource_type": "Actions",
448+
"text": "Actions",
449+
"href": AUTH_MANAGER_FASTAPI_APP_PREFIX + url_for("ActionModelView.list", _external=False),
450+
},
451+
{
452+
"resource_type": "Resources",
453+
"text": "Resources",
454+
"href": AUTH_MANAGER_FASTAPI_APP_PREFIX + url_for("ResourceModelView.list", _external=False),
455+
},
456+
{
457+
"resource_type": "Permission Pairs",
458+
"text": "Permissions",
459+
"href": AUTH_MANAGER_FASTAPI_APP_PREFIX
460+
+ url_for(
461+
"PermissionPairModelView.list",
462+
_external=False,
463+
),
464+
},
465+
]
466+
467+
return [
468+
MenuItem(text=item["text"], href=item["href"])
469+
for item in items
470+
if self._is_authorized(method="MENU", resource_type=item["resource_type"], user=user)
471+
]
472+
428473
def _is_authorized(
429474
self,
430475
*,

0 commit comments

Comments
 (0)