Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions airflow/api_fastapi/core_api/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from airflow.auth.managers.models.base_user import BaseUser
from airflow.auth.managers.models.resource_details import DagAccessEntity, DagDetails
from airflow.configuration import conf
from airflow.utils.jwt_signer import JWTSigner
from airflow.utils.jwt_signer import JWTSigner, get_signing_key

if TYPE_CHECKING:
from airflow.auth.managers.base_auth_manager import ResourceMethod
Expand All @@ -38,7 +38,7 @@
@cache
def get_signer() -> JWTSigner:
return JWTSigner(
secret_key=conf.get("api", "auth_jwt_secret"),
secret_key=get_signing_key("api", "auth_jwt_secret"),
expiration_time_in_seconds=conf.getint("api", "auth_jwt_expiration_time"),
audience="front-apis",
)
Expand Down
4 changes: 2 additions & 2 deletions airflow/auth/managers/base_auth_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from airflow.exceptions import AirflowException
from airflow.models import DagModel
from airflow.typing_compat import Literal
from airflow.utils.jwt_signer import JWTSigner
from airflow.utils.jwt_signer import JWTSigner, get_signing_key
from airflow.utils.log.logging_mixin import LoggingMixin
from airflow.utils.session import NEW_SESSION, provide_session

Expand Down Expand Up @@ -464,7 +464,7 @@ def _get_token_signer():
:meta private:
"""
return JWTSigner(
secret_key=conf.get("api", "auth_jwt_secret"),
secret_key=get_signing_key("api", "auth_jwt_secret"),
expiration_time_in_seconds=conf.getint("api", "auth_jwt_expiration_time"),
audience="front-apis",
)
5 changes: 2 additions & 3 deletions airflow/auth/managers/simple/services/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@
from airflow.auth.managers.simple.datamodels.login import LoginBody, LoginResponse
from airflow.auth.managers.simple.simple_auth_manager import SimpleAuthManager
from airflow.auth.managers.simple.user import SimpleAuthManagerUser
from airflow.configuration import conf
from airflow.utils.jwt_signer import JWTSigner
from airflow.utils.jwt_signer import JWTSigner, get_signing_key


class SimpleAuthManagerLogin:
Expand Down Expand Up @@ -66,7 +65,7 @@ def create_token(cls, body: LoginBody, expiration_time_in_sec: int) -> LoginResp
)

signer = JWTSigner(
secret_key=conf.get("api", "auth_jwt_secret"),
secret_key=get_signing_key("api", "auth_jwt_secret"),
expiration_time_in_seconds=expiration_time_in_sec,
audience="front-apis",
)
Expand Down
23 changes: 10 additions & 13 deletions airflow/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1751,7 +1751,6 @@ def load_test_config(self):
with StringIO(unit_test_config) as test_config_file:
self.read_file(test_config_file)
# set fernet key to a random value
global FERNET_KEY
FERNET_KEY = Fernet.generate_key().decode()
self.expand_all_configuration_values()
log.info("Unit test configuration loaded from 'config_unit_tests.cfg'")
Expand Down Expand Up @@ -1881,7 +1880,7 @@ def get_airflow_config(airflow_home: str) -> str:


def get_all_expansion_variables() -> dict[str, Any]:
return {k: v for d in [globals(), locals()] for k, v in d.items()}
return {k: v for d in [globals(), locals()] for k, v in d.items() if not k.startswith("_")}


def _generate_fernet_key() -> str:
Expand Down Expand Up @@ -1942,6 +1941,7 @@ def create_provider_config_fallback_defaults() -> ConfigParser:


def write_default_airflow_configuration_if_needed() -> AirflowConfigParser:
global FERNET_KEY, JWT_SECRET_KEY
airflow_config = pathlib.Path(AIRFLOW_CONFIG)
if airflow_config.is_dir():
msg = (
Expand All @@ -1953,10 +1953,7 @@ def write_default_airflow_configuration_if_needed() -> AirflowConfigParser:
log.debug("Creating new Airflow config file in: %s", airflow_config.__fspath__())
config_directory = airflow_config.parent
if not config_directory.exists():
# Compatibility with Python 3.8, ``PurePath.is_relative_to`` was added in Python 3.9
try:
config_directory.relative_to(AIRFLOW_HOME)
except ValueError:
if not config_directory.is_relative_to(AIRFLOW_HOME):
msg = (
f"Config directory {config_directory.__fspath__()!r} not exists "
f"and it is not relative to AIRFLOW_HOME {AIRFLOW_HOME!r}. "
Expand All @@ -1965,13 +1962,14 @@ def write_default_airflow_configuration_if_needed() -> AirflowConfigParser:
raise FileNotFoundError(msg) from None
log.debug("Create directory %r for Airflow config", config_directory.__fspath__())
config_directory.mkdir(parents=True, exist_ok=True)
if conf.get("core", "fernet_key", fallback=None) is None:
if conf.get("core", "fernet_key", fallback=None) in (None, ""):
# We know that FERNET_KEY is not set, so we can generate it, set as global key
# and also write it to the config file so that same key will be used next time
global FERNET_KEY
FERNET_KEY = _generate_fernet_key()
conf.remove_option("core", "fernet_key")
conf.set("core", "fernet_key", FERNET_KEY)
conf.configuration_description["core"]["options"]["fernet_key"]["default"] = FERNET_KEY

JWT_SECRET_KEY = b64encode(os.urandom(16)).decode("utf-8")
conf.configuration_description["api"]["options"]["auth_jwt_secret"]["default"] = JWT_SECRET_KEY
pathlib.Path(airflow_config.__fspath__()).touch()
make_group_other_inaccessible(airflow_config.__fspath__())
with open(airflow_config, "w") as file:
Expand Down Expand Up @@ -2134,8 +2132,7 @@ def initialize_auth_manager() -> BaseAuthManager:

if not auth_manager_cls:
raise AirflowConfigException(
"No auth manager defined in the config. "
"Please specify one using section/key [core/auth_manager]."
"No auth manager defined in the config. Please specify one using section/key [core/auth_manager]."
)

return auth_manager_cls()
Expand Down Expand Up @@ -2166,8 +2163,8 @@ def initialize_auth_manager() -> BaseAuthManager:
TEST_PLUGINS_FOLDER = os.path.join(AIRFLOW_HOME, "plugins")

SECRET_KEY = b64encode(os.urandom(16)).decode("utf-8")
JWT_SECRET_KEY = b64encode(os.urandom(16)).decode("utf-8")
FERNET_KEY = "" # Set only if needed when generating a new file
JWT_SECRET_KEY = ""
WEBSERVER_CONFIG = "" # Set by initialize_config

conf: AirflowConfigParser = initialize_config()
Expand Down
18 changes: 18 additions & 0 deletions airflow/utils/jwt_signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
# under the License.
from __future__ import annotations

import logging
import os
from base64 import b64encode
from datetime import timedelta
from typing import Any

Expand All @@ -24,6 +27,21 @@
from airflow.utils import timezone


def get_signing_key(section: str, key: str) -> str:
from airflow.configuration import conf

secret_key = conf.get(section, key, fallback="")

if secret_key == "":
logging.getLogger(__name__).warning(
"`%s/%s` was empty, using a generated one for now. Please set this in your config", section, key
)
secret_key = b64encode(os.urandom(16)).decode("utf-8")
# Set it back so any other callers get the same value for the duration of this process
conf.set(section, key, secret_key)
return secret_key


class JWTSigner:
"""
Signs and verifies JWT Token. Used to authorise and verify requests.
Expand Down
30 changes: 30 additions & 0 deletions tests/core/test_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1748,3 +1748,33 @@ def test_mask_conf_values(self, mock_sensitive_config_values):
mock_mask_secret.assert_any_call("supersecret2")

assert mock_mask_secret.call_count == 2


@conf_vars({("core", "unit_test_mode"): "False"})
def test_write_default_config_contains_generated_secrets(tmp_path, monkeypatch):
import airflow.configuration

cfgpath = tmp_path / "airflow-gneerated.cfg"
# Patch these globals so it gets reverted by monkeypath after this test is over.
monkeypatch.setattr(airflow.configuration, "FERNET_KEY", "")
monkeypatch.setattr(airflow.configuration, "JWT_SECRET_KEY", "")
monkeypatch.setattr(airflow.configuration, "AIRFLOW_CONFIG", str(cfgpath))

# Create a new global conf object so our changes don't persist
localconf: AirflowConfigParser = airflow.configuration.initialize_config()
monkeypatch.setattr(airflow.configuration, "conf", localconf)

airflow.configuration.write_default_airflow_configuration_if_needed()

assert cfgpath.is_file()

lines = cfgpath.read_text().splitlines()

assert airflow.configuration.FERNET_KEY
assert airflow.configuration.JWT_SECRET_KEY

fernet_line = next(line for line in lines if line.startswith("fernet_key = "))
jwt_secret_line = next(line for line in lines if line.startswith("auth_jwt_secret = "))

assert fernet_line == f"fernet_key = {airflow.configuration.FERNET_KEY}"
assert jwt_secret_line == f"auth_jwt_secret = {airflow.configuration.JWT_SECRET_KEY}"