Skip to content

Commit b8df02f

Browse files
Eason09053360choo121600
authored andcommitted
feat(fab): Add configurable role key for Azure OAuth groups (apache#61585)
* feat(fab): Add configurable role key for Azure OAuth groups Add AUTH_OAUTH_ROLE_KEYS configuration to allow Azure AD OAuth to use 'groups' claim instead of hardcoded 'roles' claim. This enables organizations using Azure AD groups for access control to map group memberships to Airflow roles without requiring custom app role definitions. Changes: - Add AUTH_OAUTH_ROLE_KEYS config support in get_oauth_user_info - Default to 'roles' for backward compatibility - Add test case for Azure OAuth with groups configuration - Add documentation for Azure AD group-based authorization * Fix Azure OAuth to support configurable role key via AUTH_OAUTH_ROLE_KEYS - Add has_app_context() check in get_oauth_user_info() to support both production and test environments - Update test to use real Flask app context instead of mocking current_app * Remove has_app_context() check per review feedback - Removed has_app_context() conditional in Azure OAuth handler - Added Flask app context to tests to simulate production environment - Updated test_get_oauth_user_info to provide Flask context for all providers - Updated test_get_oauth_user_info_azure_with_groups_config with app.config setup Per committer feedback: "this function is always called within the request context"
1 parent 4dc1e4d commit b8df02f

3 files changed

Lines changed: 118 additions & 11 deletions

File tree

providers/fab/docs/auth-manager/sso.rst

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,65 @@ Provider Examples
204204
For Azure app registration and OAuth setup, see :doc:`apache-airflow-providers-microsoft-azure:connections/azure`
205205
and the `Azure OAuth2 documentation <https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow>`_.
206206

207+
**Azure AD with Group-Based Authorization**
208+
209+
.. code-block:: python
210+
211+
from flask_appbuilder.security.manager import AUTH_OAUTH
212+
213+
AUTH_TYPE = AUTH_OAUTH
214+
215+
AUTH_OAUTH_ROLE_KEYS = {
216+
"azure": "groups",
217+
}
218+
219+
OAUTH_PROVIDERS = [
220+
{
221+
"name": "azure",
222+
"token_key": "access_token",
223+
"icon": "fa-windows",
224+
"remote_app": {
225+
"client_id": "your-client-id",
226+
"client_secret": "your-client-secret",
227+
"api_base_url": "https://login.microsoftonline.com/<tenant-id>/v2.0",
228+
"client_kwargs": {
229+
"scope": "openid email profile groups",
230+
"resource": "your-client-id",
231+
"verify_signature": True,
232+
},
233+
"request_token_url": None,
234+
"access_token_url": "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token",
235+
"authorize_url": "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize",
236+
},
237+
}
238+
]
239+
240+
AUTH_ROLES_MAPPING = {
241+
"airflow-admin-group": ["Admin"],
242+
"airflow-op-group": ["Op"],
243+
"airflow-user-group": ["User"],
244+
"airflow-viewer-group": ["Viewer"],
245+
}
246+
247+
AUTH_ROLES_SYNC_AT_LOGIN = True
248+
249+
AUTH_USER_REGISTRATION = True
250+
AUTH_USER_REGISTRATION_ROLE = "Viewer"
251+
252+
.. note::
253+
When using Azure AD groups:
254+
255+
- Ensure the ``groups`` scope is included in ``client_kwargs``
256+
- Configure group claims in your Azure app registration
257+
- The ``AUTH_OAUTH_ROLE_KEYS`` setting allows you to specify which claim field
258+
contains the authorization information (``roles`` or ``groups``)
259+
- Group names from Azure AD will be matched against ``AUTH_ROLES_MAPPING``
260+
261+
.. important::
262+
The ``AUTH_OAUTH_ROLE_KEYS`` configuration is provider-specific. For Azure,
263+
you can set it to ``"roles"`` (default) or ``"groups"`` depending on your
264+
Azure AD setup. Other OAuth providers may use different field names.
265+
207266
**Google OAuth2**
208267

209268
.. code-block:: bash

providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2150,12 +2150,13 @@ def get_oauth_user_info(self, provider: str, resp: dict[str, Any]) -> dict[str,
21502150
me = self._decode_and_validate_azure_jwt(resp["id_token"])
21512151
log.debug("User info from Azure: %s", me)
21522152
# https://learn.microsoft.com/en-us/azure/active-directory/develop/id-token-claims-reference#payload-claims
2153+
role_key = current_app.config.get("AUTH_OAUTH_ROLE_KEYS", {}).get("azure", "roles")
21532154
return {
21542155
"email": me["email"] if "email" in me else me["upn"],
21552156
"first_name": me.get("given_name", ""),
21562157
"last_name": me.get("family_name", ""),
21572158
"username": me["oid"],
2158-
"role_keys": me.get("roles", []),
2159+
"role_keys": me.get(role_key, []),
21592160
}
21602161
# for OpenShift
21612162
if provider == "openshift":

providers/fab/tests/unit/fab/auth_manager/security_manager/test_override.py

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,23 @@ def test_check_password_not_match(self, check_password):
135135
"role_keys": ["admin"],
136136
},
137137
),
138+
(
139+
"azure",
140+
{
141+
"oid": "test",
142+
"given_name": "John",
143+
"family_name": "Doe",
144+
"email": "test@example.com",
145+
"groups": ["group1", "group2"],
146+
},
147+
{
148+
"username": "test",
149+
"first_name": "John",
150+
"last_name": "Doe",
151+
"email": "test@example.com",
152+
"role_keys": [],
153+
},
154+
),
138155
("openshift", {"metadata": {"name": "test"}}, {"username": "openshift_test"}),
139156
(
140157
"okta",
@@ -234,13 +251,43 @@ def test_check_password_not_match(self, check_password):
234251
],
235252
)
236253
def test_get_oauth_user_info(self, provider, resp, user_info):
237-
sm = EmptySecurityManager()
238-
sm.appbuilder = Mock(sm=sm)
239-
sm.oauth_remotes = {}
240-
sm.oauth_remotes[provider] = Mock(
241-
get=Mock(return_value=Mock(json=Mock(return_value=resp))),
242-
userinfo=Mock(return_value=resp),
243-
)
244-
sm._decode_and_validate_azure_jwt = Mock(return_value=resp)
245-
sm._get_authentik_token_info = Mock(return_value=resp)
246-
assert sm.get_oauth_user_info(provider, {"id_token": None}) == user_info
254+
from flask import Flask
255+
256+
app = Flask(__name__)
257+
with app.app_context():
258+
sm = EmptySecurityManager()
259+
sm.appbuilder = Mock(sm=sm)
260+
sm.oauth_remotes = {}
261+
sm.oauth_remotes[provider] = Mock(
262+
get=Mock(return_value=Mock(json=Mock(return_value=resp))),
263+
userinfo=Mock(return_value=resp),
264+
)
265+
sm._decode_and_validate_azure_jwt = Mock(return_value=resp)
266+
sm._get_authentik_token_info = Mock(return_value=resp)
267+
assert sm.get_oauth_user_info(provider, {"id_token": None}) == user_info
268+
269+
def test_get_oauth_user_info_azure_with_groups_config(self):
270+
from flask import Flask
271+
272+
app = Flask(__name__)
273+
app.config["AUTH_OAUTH_ROLE_KEYS"] = {"azure": "groups"}
274+
275+
azure_response = {
276+
"oid": "user-123",
277+
"given_name": "Jane",
278+
"family_name": "Smith",
279+
"email": "jane.smith@example.com",
280+
"groups": ["admin-group", "viewer-group"],
281+
}
282+
283+
with app.app_context():
284+
sm = EmptySecurityManager()
285+
sm.appbuilder = Mock(sm=sm)
286+
sm.oauth_remotes = {}
287+
sm._decode_and_validate_azure_jwt = Mock(return_value=azure_response)
288+
289+
user_info = sm.get_oauth_user_info("azure", {"id_token": "test-token"})
290+
291+
assert user_info["username"] == "user-123"
292+
assert user_info["email"] == "jane.smith@example.com"
293+
assert user_info["role_keys"] == ["admin-group", "viewer-group"]

0 commit comments

Comments
 (0)