This is an automated email from the ASF dual-hosted git repository.
potiuk 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 9334537621f Make KeycloakAuthManager _CACHE_TTL_SECONDS configurable
(#70839)
9334537621f is described below
commit 9334537621feaae56c59e872d3898d6356dcbf4c
Author: stephen-bracken <[email protected]>
AuthorDate: Thu Aug 13 13:33:36 2026 +0100
Make KeycloakAuthManager _CACHE_TTL_SECONDS configurable (#70839)
Co-authored-by: Stephen Bracken <email-protected>
---
providers/keycloak/provider.yaml | 14 +++++++++
.../providers/keycloak/auth_manager/cache.py | 33 ++++++++++++++++++----
.../providers/keycloak/auth_manager/constants.py | 2 ++
.../providers/keycloak/get_provider_info.py | 14 +++++++++
.../tests/unit/keycloak/auth_manager/test_cache.py | 30 ++++++++++++++++++++
.../unit/keycloak/auth_manager/test_constants.py | 8 ++++++
6 files changed, 96 insertions(+), 5 deletions(-)
diff --git a/providers/keycloak/provider.yaml b/providers/keycloak/provider.yaml
index 6daadb4ab60..59b8a11d9ea 100644
--- a/providers/keycloak/provider.yaml
+++ b/providers/keycloak/provider.yaml
@@ -62,6 +62,20 @@ config:
keycloak_auth_manager:
description: This section contains settings for Keycloak auth manager
integration.
options:
+ cache_ttl_seconds:
+ description: |
+ Time (in seconds) to cache auth decisions in bulk
``is_authorized_{resource}`` methods.
+ type: integer
+ version_added: 0.9.0
+ example: ~
+ default: "30"
+ cache_timeout_seconds:
+ description: |
+ Timeout (in seconds) to cache auth decisions in bulk
``is_authorized_{resource}`` methods.
+ type: integer
+ version_added: 0.9.0
+ example: ~
+ default: "60"
client_id:
description: |
Client ID configured in Keycloak to integrate with Airflow.
diff --git
a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/cache.py
b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/cache.py
index 75ccbf99c51..a3f950695e3 100644
--- a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/cache.py
+++ b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/cache.py
@@ -20,8 +20,15 @@ import threading
import time
from collections.abc import Callable
-_CACHE_TTL_SECONDS = 30
-_SINGLE_FLIGHT_TIMEOUT_SECONDS = 60
+from airflow.providers.common.compat.sdk import conf
+from airflow.providers.keycloak.auth_manager.constants import (
+ CONF_CACHE_TIMEOUT_SECONDS_KEY,
+ CONF_CACHE_TTL_SECONDS_KEY,
+ CONF_SECTION_NAME,
+)
+
+_CACHE_TTL_SECONDS: int | None = None
+_SINGLE_FLIGHT_TIMEOUT_SECONDS: int | None = None
# Maps cache keys to (timestamp, result) pairs for TTL-based expiration.
_cache: dict[tuple, tuple[float, frozenset[str]]] = {}
@@ -30,9 +37,25 @@ _pending_requests: dict[tuple, threading.Event] = {}
_cache_lock = threading.Lock()
+def _cache_ttl_seconds() -> int:
+ global _CACHE_TTL_SECONDS
+ if not _CACHE_TTL_SECONDS:
+ _CACHE_TTL_SECONDS = conf.getint(CONF_SECTION_NAME,
CONF_CACHE_TTL_SECONDS_KEY, fallback=30)
+ return _CACHE_TTL_SECONDS
+
+
+def _cache_timeout_seconds() -> int:
+ global _SINGLE_FLIGHT_TIMEOUT_SECONDS
+ if not _SINGLE_FLIGHT_TIMEOUT_SECONDS:
+ _SINGLE_FLIGHT_TIMEOUT_SECONDS = conf.getint(
+ CONF_SECTION_NAME, CONF_CACHE_TIMEOUT_SECONDS_KEY, fallback=60
+ )
+ return _SINGLE_FLIGHT_TIMEOUT_SECONDS
+
+
def _cache_get(key: tuple) -> frozenset[str] | None:
entry = _cache.get(key)
- if entry and (time.monotonic() - entry[0]) < _CACHE_TTL_SECONDS:
+ if entry and (time.monotonic() - entry[0]) < _cache_ttl_seconds():
return entry[1]
return None
@@ -41,7 +64,7 @@ def _cache_set(key: tuple, value: frozenset[str]) -> None:
with _cache_lock:
_cache[key] = (time.monotonic(), value)
now = time.monotonic()
- for k in [k for k, (ts, _) in _cache.items() if now - ts >
_CACHE_TTL_SECONDS * 2]:
+ for k in [k for k, (ts, _) in _cache.items() if now - ts >
_cache_ttl_seconds() * 2]:
_cache.pop(k, None)
@@ -67,7 +90,7 @@ def single_flight(cache_key: tuple, query_keycloak:
Callable[[], set[str]]) -> s
if not is_worker:
# Wait for the other thread to finish
- event.wait(timeout=_SINGLE_FLIGHT_TIMEOUT_SECONDS)
+ event.wait(timeout=_cache_timeout_seconds())
cached = _cache_get(cache_key)
if cached is not None:
return set(cached)
diff --git
a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/constants.py
b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/constants.py
index 7a6cd2e0ece..169042d1f45 100644
---
a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/constants.py
+++
b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/constants.py
@@ -19,6 +19,8 @@
from __future__ import annotations
CONF_SECTION_NAME = "keycloak_auth_manager"
+CONF_CACHE_TTL_SECONDS_KEY = "cache_ttl_seconds"
+CONF_CACHE_TIMEOUT_SECONDS_KEY = "cache_timeout_seconds"
CONF_CLIENT_ID_KEY = "client_id"
CONF_CLIENT_SECRET_KEY = "client_secret"
CONF_REALM_KEY = "realm"
diff --git
a/providers/keycloak/src/airflow/providers/keycloak/get_provider_info.py
b/providers/keycloak/src/airflow/providers/keycloak/get_provider_info.py
index 66647731640..d186cf2b868 100644
--- a/providers/keycloak/src/airflow/providers/keycloak/get_provider_info.py
+++ b/providers/keycloak/src/airflow/providers/keycloak/get_provider_info.py
@@ -42,6 +42,20 @@ def get_provider_info():
"keycloak_auth_manager": {
"description": "This section contains settings for Keycloak
auth manager integration.",
"options": {
+ "cache_ttl_seconds": {
+ "description": "Time (in seconds) to cache auth
decisions in bulk ``is_authorized_{resource}`` methods.\n",
+ "type": "integer",
+ "version_added": "0.9.0",
+ "example": None,
+ "default": "30",
+ },
+ "cache_timeout_seconds": {
+ "description": "Timeout (in seconds) to cache auth
decisions in bulk ``is_authorized_{resource}`` methods.\n",
+ "type": "integer",
+ "version_added": "0.9.0",
+ "example": None,
+ "default": "60",
+ },
"client_id": {
"description": "Client ID configured in Keycloak to
integrate with Airflow.\nThis client must follow the standard OpenID Connect
authentication flow.\n",
"type": "string",
diff --git a/providers/keycloak/tests/unit/keycloak/auth_manager/test_cache.py
b/providers/keycloak/tests/unit/keycloak/auth_manager/test_cache.py
index 9125d8d4b72..22c86057b94 100644
--- a/providers/keycloak/tests/unit/keycloak/auth_manager/test_cache.py
+++ b/providers/keycloak/tests/unit/keycloak/auth_manager/test_cache.py
@@ -23,6 +23,13 @@ import pytest
from airflow.providers.keycloak.auth_manager import cache as cache_module
from airflow.providers.keycloak.auth_manager.cache import single_flight
+from airflow.providers.keycloak.auth_manager.constants import (
+ CONF_CACHE_TIMEOUT_SECONDS_KEY,
+ CONF_CACHE_TTL_SECONDS_KEY,
+ CONF_SECTION_NAME,
+)
+
+from tests_common.test_utils.config import conf_vars
@pytest.fixture(autouse=True)
@@ -34,7 +41,30 @@ def _clear_cache():
cache_module._pending_requests.clear()
[email protected](autouse=True)
+def _set_cache_ttl():
+ with conf_vars(
+ {
+ (CONF_SECTION_NAME, CONF_CACHE_TTL_SECONDS_KEY): "60",
+ (CONF_SECTION_NAME, CONF_CACHE_TIMEOUT_SECONDS_KEY): "120",
+ }
+ ):
+ yield
+
+
class TestSingleFlight:
+ def test_configure_ttl(self):
+ assert cache_module._CACHE_TTL_SECONDS is None
+ with conf_vars({(CONF_SECTION_NAME, CONF_CACHE_TTL_SECONDS_KEY):
"120"}):
+ cache_module._cache_ttl_seconds()
+ assert cache_module._CACHE_TTL_SECONDS == 120
+
+ def test_configure_timeout(self):
+ assert cache_module._SINGLE_FLIGHT_TIMEOUT_SECONDS is None
+ with conf_vars({(CONF_SECTION_NAME, CONF_CACHE_TIMEOUT_SECONDS_KEY):
"120"}):
+ cache_module._cache_timeout_seconds()
+ assert cache_module._SINGLE_FLIGHT_TIMEOUT_SECONDS == 120
+
def test_returns_query_result(self):
result = single_flight(("key",), lambda: {"a", "b"})
assert result == {"a", "b"}
diff --git
a/providers/keycloak/tests/unit/keycloak/auth_manager/test_constants.py
b/providers/keycloak/tests/unit/keycloak/auth_manager/test_constants.py
index 923e6cf256b..19ba0aa11d0 100644
--- a/providers/keycloak/tests/unit/keycloak/auth_manager/test_constants.py
+++ b/providers/keycloak/tests/unit/keycloak/auth_manager/test_constants.py
@@ -17,6 +17,8 @@
from __future__ import annotations
from airflow.providers.keycloak.auth_manager.constants import (
+ CONF_CACHE_TIMEOUT_SECONDS_KEY,
+ CONF_CACHE_TTL_SECONDS_KEY,
CONF_CLIENT_ID_KEY,
CONF_CLIENT_SECRET_KEY,
CONF_REALM_KEY,
@@ -29,6 +31,12 @@ class TestKeycloakAuthManagerConstants:
def test_conf_section_name(self):
assert CONF_SECTION_NAME == "keycloak_auth_manager"
+ def test_conf_cache_ttl_seconds_key(self):
+ assert CONF_CACHE_TTL_SECONDS_KEY == "cache_ttl_seconds"
+
+ def test_conf_cache_timeout_seconds_key(self):
+ assert CONF_CACHE_TIMEOUT_SECONDS_KEY == "cache_timeout_seconds"
+
def test_conf_client_id_key(self):
assert CONF_CLIENT_ID_KEY == "client_id"