Content-Length: 743538 | pFad | http://github.com/astronomer/airflow/commit/fffbb5d07b8b23eb5aebf2fd8747cbcbfd5e7fdc

AC Implement authorization methods of `KeycloakAuthManager` (#51403) · astronomer/airflow@fffbb5d · GitHub
Skip to content

Commit fffbb5d

Browse files
authored
Implement authorization methods of KeycloakAuthManager (apache#51403)
1 parent ab44850 commit fffbb5d

File tree

4 files changed

+416
-23
lines changed

4 files changed

+416
-23
lines changed

providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py

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

19-
from functools import cached_property
2019
from typing import TYPE_CHECKING, Any
2120
from urllib.parse import urljoin
2221

22+
import requests
2323
from fastapi import FastAPI
24-
from starlette.middleware.sessions import SessionMiddleware
2524

2625
from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX
2726
from airflow.api_fastapi.auth.managers.base_auth_manager import BaseAuthManager, T
2827
from airflow.configuration import conf
28+
from airflow.exceptions import AirflowException
29+
from airflow.providers.keycloak.auth_manager.resources import KeycloakResource
2930
from airflow.providers.keycloak.auth_manager.user import KeycloakAuthManagerUser
3031

3132
if TYPE_CHECKING:
@@ -52,10 +53,6 @@ class KeycloakAuthManager(BaseAuthManager[KeycloakAuthManagerUser]):
5253
Leverages Keycloak to perform authentication and authorization in Airflow.
5354
"""
5455

55-
@cached_property
56-
def api_server_endpoint(self) -> str:
57-
return conf.get("api", "base_url", fallback="/")
58-
5956
def deserialize_user(self, token: dict[str, Any]) -> KeycloakAuthManagerUser:
6057
return KeycloakAuthManagerUser(
6158
user_id=token.pop("user_id"),
@@ -73,7 +70,8 @@ def serialize_user(self, user: KeycloakAuthManagerUser) -> dict[str, Any]:
7370
}
7471

7572
def get_url_login(self, **kwargs) -> str:
76-
return urljoin(self.api_server_endpoint, f"{AUTH_MANAGER_FASTAPI_APP_PREFIX}/login")
73+
base_url = conf.get("api", "base_url", fallback="/")
74+
return urljoin(base_url, f"{AUTH_MANAGER_FASTAPI_APP_PREFIX}/login")
7775

7876
def is_authorized_configuration(
7977
self,
@@ -82,7 +80,18 @@ def is_authorized_configuration(
8280
user: KeycloakAuthManagerUser,
8381
details: ConfigurationDetails | None = None,
8482
) -> bool:
85-
return True
83+
config_section = details.section if details else None
84+
return self._is_authorized(
85+
method=method, resource_type=KeycloakResource.CONFIGURATION, user=user
86+
) or (
87+
config_section is not None
88+
and self._is_authorized(
89+
method=method,
90+
resource_type=KeycloakResource.CONFIGURATION,
91+
user=user,
92+
resource_id=config_section,
93+
)
94+
)
8695

8796
def is_authorized_connection(
8897
self,
@@ -91,7 +100,13 @@ def is_authorized_connection(
91100
user: KeycloakAuthManagerUser,
92101
details: ConnectionDetails | None = None,
93102
) -> bool:
94-
return True
103+
connection_id = details.conn_id if details else None
104+
return self._is_authorized(method=method, resource_type=KeycloakResource.CONNECTION, user=user) or (
105+
connection_id is not None
106+
and self._is_authorized(
107+
method=method, resource_type=KeycloakResource.CONNECTION, user=user, resource_id=connection_id
108+
)
109+
)
95110

96111
def is_authorized_dag(
97112
self,
@@ -106,12 +121,24 @@ def is_authorized_dag(
106121
def is_authorized_backfill(
107122
self, *, method: ResourceMethod, user: KeycloakAuthManagerUser, details: BackfillDetails | None = None
108123
) -> bool:
109-
return True
124+
backfill_id = str(details.id) if details else None
125+
return self._is_authorized(method=method, resource_type=KeycloakResource.BACKFILL, user=user) or (
126+
backfill_id is not None
127+
and self._is_authorized(
128+
method=method, resource_type=KeycloakResource.BACKFILL, user=user, resource_id=backfill_id
129+
)
130+
)
110131

111132
def is_authorized_asset(
112133
self, *, method: ResourceMethod, user: KeycloakAuthManagerUser, details: AssetDetails | None = None
113134
) -> bool:
114-
return True
135+
asset_id = details.id if details else None
136+
return self._is_authorized(method=method, resource_type=KeycloakResource.ASSET, user=user) or (
137+
asset_id is not None
138+
and self._is_authorized(
139+
method=method, resource_type=KeycloakResource.ASSET, user=user, resource_id=asset_id
140+
)
141+
)
115142

116143
def is_authorized_asset_alias(
117144
self,
@@ -120,25 +147,57 @@ def is_authorized_asset_alias(
120147
user: KeycloakAuthManagerUser,
121148
details: AssetAliasDetails | None = None,
122149
) -> bool:
123-
return True
150+
asset_alias_id = details.id if details else None
151+
return self._is_authorized(method=method, resource_type=KeycloakResource.ASSET_ALIAS, user=user) or (
152+
asset_alias_id is not None
153+
and self._is_authorized(
154+
method=method,
155+
resource_type=KeycloakResource.ASSET_ALIAS,
156+
user=user,
157+
resource_id=asset_alias_id,
158+
)
159+
)
124160

125161
def is_authorized_variable(
126162
self, *, method: ResourceMethod, user: KeycloakAuthManagerUser, details: VariableDetails | None = None
127163
) -> bool:
128-
return True
164+
variable_key = details.key if details else None
165+
return self._is_authorized(method=method, resource_type=KeycloakResource.VARIABLE, user=user) or (
166+
variable_key is not None
167+
and self._is_authorized(
168+
method=method, resource_type=KeycloakResource.VARIABLE, user=user, resource_id=variable_key
169+
)
170+
)
129171

130172
def is_authorized_pool(
131173
self, *, method: ResourceMethod, user: KeycloakAuthManagerUser, details: PoolDetails | None = None
132174
) -> bool:
133-
return True
175+
pool_name = details.name if details else None
176+
return self._is_authorized(method=method, resource_type=KeycloakResource.POOL, user=user) or (
177+
pool_name is not None
178+
and self._is_authorized(
179+
method=method, resource_type=KeycloakResource.POOL, user=user, resource_id=pool_name
180+
)
181+
)
134182

135183
def is_authorized_view(self, *, access_view: AccessView, user: KeycloakAuthManagerUser) -> bool:
136-
return True
184+
return self._is_authorized(
185+
method="GET", resource_type=KeycloakResource.VIEW, user=user
186+
) or self._is_authorized(
187+
method="GET",
188+
resource_type=KeycloakResource.VIEW,
189+
user=user,
190+
resource_id=access_view.value,
191+
)
137192

138193
def is_authorized_custom_view(
139194
self, *, method: ResourceMethod | str, resource_name: str, user: KeycloakAuthManagerUser
140195
) -> bool:
141-
return True
196+
return self._is_authorized(
197+
method=method, resource_type=KeycloakResource.CUSTOM, user=user
198+
) or self._is_authorized(
199+
method=method, resource_type=KeycloakResource.CUSTOM, user=user, resource_id=resource_name
200+
)
142201

143202
def filter_authorized_menu_items(
144203
self, menu_items: list[MenuItem], *, user: KeycloakAuthManagerUser
@@ -156,12 +215,54 @@ def get_fastapi_app(self) -> FastAPI | None:
156215
"This sub application provides login routes."
157216
),
158217
)
159-
# authlib requires ``SessionMiddleware``
160-
app.add_middleware(
161-
SessionMiddleware,
162-
secret_key=conf.get("api_auth", "jwt_secret"),
163-
https_only=False,
164-
)
165218
app.include_router(login_router)
166219

167220
return app
221+
222+
def _is_authorized(
223+
self,
224+
*,
225+
method: ResourceMethod | str,
226+
resource_type: KeycloakResource,
227+
user: KeycloakAuthManagerUser,
228+
resource_id: str | None = None,
229+
) -> bool:
230+
client_id = conf.get("keycloak_auth_manager", "client_id")
231+
realm = conf.get("keycloak_auth_manager", "realm")
232+
server_url = conf.get("keycloak_auth_manager", "server_url")
233+
234+
permission = (
235+
f"{resource_type.value}:{resource_id}#{method}"
236+
if resource_id
237+
else f"{resource_type.value}#{method}"
238+
)
239+
resp = requests.post(
240+
self._get_token_url(server_url, realm),
241+
data=self._get_payload(client_id, permission),
242+
headers=self._get_headers(user.access_token),
243+
)
244+
245+
if resp.status_code == 200:
246+
return True
247+
if resp.status_code == 403:
248+
return False
249+
raise AirflowException(f"Unexpected error: {resp.status_code} - {resp.text}")
250+
251+
@staticmethod
252+
def _get_token_url(server_url, realm):
253+
return f"{server_url}/realms/{realm}/protocol/openid-connect/token"
254+
255+
@staticmethod
256+
def _get_payload(client_id, permission):
257+
return {
258+
"grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket",
259+
"audience": client_id,
260+
"permission": permission,
261+
}
262+
263+
@staticmethod
264+
def _get_headers(access_token):
265+
return {
266+
"Authorization": f"Bearer {access_token}",
267+
"Content-Type": "application/x-www-form-urlencoded",
268+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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.
17+
from __future__ import annotations
18+
19+
from enum import Enum
20+
21+
22+
class KeycloakResource(Enum):
23+
"""Enum of Keycloak resources."""
24+
25+
ASSET = "Asset"
26+
ASSET_ALIAS = "AssetAlias"
27+
BACKFILL = "Backfill"
28+
CONFIGURATION = "Configuration"
29+
CONNECTION = "Connection"
30+
CUSTOM = "Custom"
31+
DAG = "Dag"
32+
MENU = "Menu"
33+
POOL = "Pool"
34+
VARIABLE = "Variable"
35+
VIEW = "View"

0 commit comments

Comments
 (0)








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/astronomer/airflow/commit/fffbb5d07b8b23eb5aebf2fd8747cbcbfd5e7fdc

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy