This is an automated email from the ASF dual-hosted git repository.
vincbeck pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new d63b5e9c24e fix(keycloak): attach default role policies (#67031)
d63b5e9c24e is described below
commit d63b5e9c24e56cd629171ae6198cfed15cf6eeac
Author: Anmol Mishra <[email protected]>
AuthorDate: Tue May 19 20:26:08 2026 +0530
fix(keycloak): attach default role policies (#67031)
---
.../keycloak/auth_manager/cli/commands.py | 61 ++++++++++++
.../keycloak/auth_manager/cli/test_commands.py | 108 ++++++++++++++++++++-
2 files changed, 168 insertions(+), 1 deletion(-)
diff --git
a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/cli/commands.py
b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/cli/commands.py
index 22bb187996d..705fc4eb0bf 100644
---
a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/cli/commands.py
+++
b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/cli/commands.py
@@ -137,7 +137,11 @@ def create_permissions_command(args):
for role_name in TEAM_ROLE_NAMES:
_ensure_role_policy(client, client_uuid, role_name,
_dry_run=args.dry_run)
_ensure_role_policy(client, client_uuid, SUPER_ADMIN_ROLE_NAME,
_dry_run=args.dry_run)
+ else:
+ _ensure_default_role_policies(client, client_uuid,
_dry_run=args.dry_run)
_create_permissions(client, client_uuid, teams=teams,
_dry_run=args.dry_run)
+ if not teams:
+ _attach_default_role_permissions(client, client_uuid,
_dry_run=args.dry_run)
@cli_utils.action_cli
@@ -158,7 +162,11 @@ def create_all_command(args):
for role_name in TEAM_ROLE_NAMES:
_ensure_role_policy(client, client_uuid, role_name,
_dry_run=args.dry_run)
_ensure_role_policy(client, client_uuid, SUPER_ADMIN_ROLE_NAME,
_dry_run=args.dry_run)
+ else:
+ _ensure_default_role_policies(client, client_uuid,
_dry_run=args.dry_run)
_create_permissions(client, client_uuid, teams=teams,
_dry_run=args.dry_run)
+ if not teams:
+ _attach_default_role_permissions(client, client_uuid,
_dry_run=args.dry_run)
def _get_client(args):
@@ -244,6 +252,59 @@ def _ensure_multi_team_enabled(*, teams: list[str],
command_name: str) -> None:
raise SystemExit(f"{command_name} requires core.multi_team=True when
--teams is used.")
+def _ensure_default_role_policies(client: KeycloakAdmin, client_uuid: str, *,
_dry_run: bool = False) -> None:
+ for role_name in (*TEAM_ROLE_NAMES, SUPER_ADMIN_ROLE_NAME):
+ _ensure_role_policy(client, client_uuid, role_name, _dry_run=_dry_run)
+
+
+def _attach_default_role_permissions(
+ client: KeycloakAdmin, client_uuid: str, *, _dry_run: bool = False
+) -> None:
+ for role_name in TEAM_ROLE_NAMES:
+ _attach_policy_to_scope_permission(
+ client,
+ client_uuid,
+ permission_name="ReadOnly",
+ policy_name=_role_policy_name(role_name),
+ scope_names=["GET", "MENU", "LIST"],
+ resource_names=[],
+ decision_strategy="AFFIRMATIVE",
+ _dry_run=_dry_run,
+ )
+
+ _attach_policy_to_resource_permission(
+ client,
+ client_uuid,
+ permission_name="User",
+ policy_name=_role_policy_name("User"),
+ resource_names=[KeycloakResource.DAG.value,
KeycloakResource.ASSET.value],
+ _dry_run=_dry_run,
+ )
+ _attach_policy_to_resource_permission(
+ client,
+ client_uuid,
+ permission_name="Op",
+ policy_name=_role_policy_name("Op"),
+ resource_names=[
+ KeycloakResource.CONNECTION.value,
+ KeycloakResource.POOL.value,
+ KeycloakResource.VARIABLE.value,
+ KeycloakResource.BACKFILL.value,
+ ],
+ _dry_run=_dry_run,
+ )
+ for role_name in ("Admin", SUPER_ADMIN_ROLE_NAME):
+ _attach_policy_to_scope_permission(
+ client,
+ client_uuid,
+ permission_name="Admin",
+ policy_name=_role_policy_name(role_name),
+ scope_names=_get_extended_resource_methods() + ["LIST"],
+ resource_names=[],
+ _dry_run=_dry_run,
+ )
+
+
def _preview_scopes(*args, **kwargs):
"""Preview scopes that would be created."""
scopes = _get_scopes_to_create()
diff --git
a/providers/keycloak/tests/unit/keycloak/auth_manager/cli/test_commands.py
b/providers/keycloak/tests/unit/keycloak/auth_manager/cli/test_commands.py
index 07bb80c9448..62c61f95f72 100644
--- a/providers/keycloak/tests/unit/keycloak/auth_manager/cli/test_commands.py
+++ b/providers/keycloak/tests/unit/keycloak/auth_manager/cli/test_commands.py
@@ -24,6 +24,8 @@ import pytest
from airflow.api_fastapi.common.types import MenuItem
from airflow.cli import cli_parser
from airflow.providers.keycloak.auth_manager.cli.commands import (
+ SUPER_ADMIN_ROLE_NAME,
+ TEAM_ROLE_NAMES,
TEAM_SCOPED_RESOURCE_NAMES,
_get_extended_resource_methods,
_get_resource_methods,
@@ -213,10 +215,14 @@ class TestCommands:
}
assert expected_team_resources.issubset(created_resource_names)
+
@patch("airflow.providers.keycloak.auth_manager.cli.commands._attach_default_role_permissions")
+
@patch("airflow.providers.keycloak.auth_manager.cli.commands._ensure_default_role_policies")
@patch("airflow.providers.keycloak.auth_manager.cli.commands._get_client")
def test_create_permissions(
self,
mock_get_client,
+ mock_ensure_default_role_policies,
+ mock_attach_default_role_permissions,
):
client = Mock()
mock_get_client.return_value = client
@@ -309,6 +315,8 @@ class TestCommands:
),
]
client.create_client_authz_resource_based_permission.assert_has_calls(resource_calls,
any_order=True)
+ mock_ensure_default_role_policies.assert_called_once_with(client,
"test-id", _dry_run=False)
+ mock_attach_default_role_permissions.assert_called_once_with(client,
"test-id", _dry_run=False)
@patch("airflow.providers.keycloak.auth_manager.cli.commands._get_client")
def test_create_permissions_with_teams(self, mock_get_client):
@@ -419,6 +427,86 @@ class TestCommands:
skip_exists=True,
)
+
@patch("airflow.providers.keycloak.auth_manager.cli.commands._attach_policy_to_resource_permission")
+
@patch("airflow.providers.keycloak.auth_manager.cli.commands._attach_policy_to_scope_permission")
+
@patch("airflow.providers.keycloak.auth_manager.cli.commands._ensure_role_policy")
+
@patch("airflow.providers.keycloak.auth_manager.cli.commands._create_permissions")
+ @patch("airflow.providers.keycloak.auth_manager.cli.commands._get_client")
+ def test_create_permissions_attaches_default_role_policies(
+ self,
+ mock_get_client,
+ mock_create_permissions,
+ mock_ensure_role_policy,
+ mock_attach_scope_policy,
+ mock_attach_resource_policy,
+ ):
+ client = Mock()
+ mock_get_client.return_value = client
+ client.get_clients.return_value = [
+ {"id": "test-id", "clientId": "test_client_id"},
+ ]
+
+ params = [
+ "keycloak-auth-manager",
+ "create-permissions",
+ "--username",
+ "test",
+ "--password",
+ "test",
+ ]
+ with conf_vars({("keycloak_auth_manager", "client_id"):
"test_client_id"}):
+ create_permissions_command(self.arg_parser.parse_args(params))
+
+ for role_name in (*TEAM_ROLE_NAMES, SUPER_ADMIN_ROLE_NAME):
+ mock_ensure_role_policy.assert_any_call(client, "test-id",
role_name, _dry_run=False)
+
+ mock_create_permissions.assert_called_once_with(client, "test-id",
teams=[], _dry_run=False)
+ for role_name in TEAM_ROLE_NAMES:
+ mock_attach_scope_policy.assert_any_call(
+ client,
+ "test-id",
+ permission_name="ReadOnly",
+ policy_name=f"Allow-{role_name}",
+ scope_names=["GET", "MENU", "LIST"],
+ resource_names=[],
+ decision_strategy="AFFIRMATIVE",
+ _dry_run=False,
+ )
+ mock_attach_scope_policy.assert_any_call(
+ client,
+ "test-id",
+ permission_name="Admin",
+ policy_name="Allow-Admin",
+ scope_names=_get_extended_resource_methods() + ["LIST"],
+ resource_names=[],
+ _dry_run=False,
+ )
+ mock_attach_scope_policy.assert_any_call(
+ client,
+ "test-id",
+ permission_name="Admin",
+ policy_name="Allow-SuperAdmin",
+ scope_names=_get_extended_resource_methods() + ["LIST"],
+ resource_names=[],
+ _dry_run=False,
+ )
+ mock_attach_resource_policy.assert_any_call(
+ client,
+ "test-id",
+ permission_name="User",
+ policy_name="Allow-User",
+ resource_names=["Dag", "Asset"],
+ _dry_run=False,
+ )
+ mock_attach_resource_policy.assert_any_call(
+ client,
+ "test-id",
+ permission_name="Op",
+ policy_name="Allow-Op",
+ resource_names=["Connection", "Pool", "Variable", "Backfill"],
+ _dry_run=False,
+ )
+
@patch("airflow.providers.keycloak.auth_manager.cli.commands._update_admin_permission_resources")
@patch("airflow.providers.keycloak.auth_manager.cli.commands._ensure_scope_permission")
@patch("airflow.providers.keycloak.auth_manager.cli.commands._attach_policy_to_resource_permission")
@@ -628,6 +716,8 @@ class TestCommands:
mock_ensure_group.assert_called_once_with(client, "team-a",
_dry_run=False)
mock_add_user.assert_called_once_with(client, username="user-a",
team="team-a", _dry_run=False)
+
@patch("airflow.providers.keycloak.auth_manager.cli.commands._attach_default_role_permissions")
+
@patch("airflow.providers.keycloak.auth_manager.cli.commands._ensure_default_role_policies")
@patch("airflow.providers.keycloak.auth_manager.cli.commands._create_permissions")
@patch("airflow.providers.keycloak.auth_manager.cli.commands._create_resources")
@patch("airflow.providers.keycloak.auth_manager.cli.commands._create_scopes")
@@ -638,6 +728,8 @@ class TestCommands:
mock_create_scopes,
mock_create_resources,
mock_create_permissions,
+ mock_ensure_default_role_policies,
+ mock_attach_default_role_permissions,
):
client = Mock()
mock_get_client.return_value = client
@@ -674,6 +766,8 @@ class TestCommands:
mock_create_scopes.assert_called_once_with(client, "test-id",
_dry_run=False)
mock_create_resources.assert_called_once_with(client, "test-id",
teams=[], _dry_run=False)
mock_create_permissions.assert_called_once_with(client, "test-id",
teams=[], _dry_run=False)
+ mock_ensure_default_role_policies.assert_called_once_with(client,
"test-id", _dry_run=False)
+ mock_attach_default_role_permissions.assert_called_once_with(client,
"test-id", _dry_run=False)
@patch("airflow.providers.keycloak.auth_manager.cli.commands._get_client")
def test_create_scopes_dry_run(self, mock_get_client):
@@ -738,8 +832,12 @@ class TestCommands:
# In dry-run mode, no resources should be created
client.create_client_authz_resource.assert_not_called()
+
@patch("airflow.providers.keycloak.auth_manager.cli.commands._attach_default_role_permissions")
+
@patch("airflow.providers.keycloak.auth_manager.cli.commands._ensure_default_role_policies")
@patch("airflow.providers.keycloak.auth_manager.cli.commands._get_client")
- def test_create_permissions_dry_run(self, mock_get_client):
+ def test_create_permissions_dry_run(
+ self, mock_get_client, mock_ensure_default_role_policies,
mock_attach_default_role_permissions
+ ):
client = Mock()
mock_get_client.return_value = client
scopes = [{"id": "1", "name": "GET"}, {"id": "2", "name": "MENU"},
{"id": "3", "name": "LIST"}]
@@ -777,7 +875,11 @@ class TestCommands:
# In dry-run mode, no permissions should be created
client.create_client_authz_scope_permission.assert_not_called()
client.create_client_authz_resource_based_permission.assert_not_called()
+ mock_ensure_default_role_policies.assert_called_once_with(client,
"test-id", _dry_run=True)
+ mock_attach_default_role_permissions.assert_called_once_with(client,
"test-id", _dry_run=True)
+
@patch("airflow.providers.keycloak.auth_manager.cli.commands._attach_default_role_permissions")
+
@patch("airflow.providers.keycloak.auth_manager.cli.commands._ensure_default_role_policies")
@patch("airflow.providers.keycloak.auth_manager.cli.commands._create_permissions")
@patch("airflow.providers.keycloak.auth_manager.cli.commands._create_resources")
@patch("airflow.providers.keycloak.auth_manager.cli.commands._create_scopes")
@@ -788,6 +890,8 @@ class TestCommands:
mock_create_scopes,
mock_create_resources,
mock_create_permissions,
+ mock_ensure_default_role_policies,
+ mock_attach_default_role_permissions,
):
client = Mock()
mock_get_client.return_value = client
@@ -824,3 +928,5 @@ class TestCommands:
mock_create_scopes.assert_called_once_with(client, "test-id",
_dry_run=True)
mock_create_resources.assert_called_once_with(client, "test-id",
teams=[], _dry_run=True)
mock_create_permissions.assert_called_once_with(client, "test-id",
teams=[], _dry_run=True)
+ mock_ensure_default_role_policies.assert_called_once_with(client,
"test-id", _dry_run=True)
+ mock_attach_default_role_permissions.assert_called_once_with(client,
"test-id", _dry_run=True)