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
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,7 @@
)
from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc
from airflow.api_fastapi.core_api.security import (
DagAccessEntity,
ReadableEventLogsFilterDep,
requires_access_dag,
requires_access_event_log,
)
from airflow.models import Log
Expand All @@ -75,7 +73,7 @@ def get_event_log(

@event_logs_router.get(
"",
dependencies=[Depends(requires_access_dag("GET", DagAccessEntity.AUDIT_LOG))],
dependencies=[Depends(requires_access_event_log("GET"))],
)
def get_event_logs(
limit: QueryLimit,
Expand Down
14 changes: 8 additions & 6 deletions airflow-core/src/airflow/api_fastapi/core_api/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,12 +352,14 @@ async def inner(
dag_id = None

event_log_id_raw = request.path_params.get("event_log_id")
try:
event_log_id = int(event_log_id_raw) if event_log_id_raw is not None else None
except ValueError:
event_log_id = None

if event_log_id is not None:
if event_log_id_raw is not None:
try:
event_log_id = int(event_log_id_raw)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="'event_log_id' must be an integer",
)
dag_id = session.scalar(select(Log.dag_id).where(Log.id == event_log_id))

requires_access_dag(method, DagAccessEntity.AUDIT_LOG, dag_id)(
Expand Down
48 changes: 48 additions & 0 deletions airflow-core/tests/unit/api_fastapi/core_api/test_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,54 @@ async def test_requires_access_event_log_row_not_found(self, mock_get_auth_manag
)
mock_get_team_name.assert_not_called()

@pytest.mark.db_test
@pytest.mark.parametrize("bad_event_log_id", ["abc", "1.5", "1,2", ""])
@patch("airflow.api_fastapi.core_api.security.requires_access_dag")
async def test_requires_access_event_log_non_integer_id_returns_400(
self, mock_requires_access_dag, bad_event_log_id
):
"""Non-integer event_log_id in the path must be rejected with 400 before authz."""
request = Mock()
request.path_params = {"event_log_id": bad_event_log_id}
user = Mock()
session = Mock()

with pytest.raises(HTTPException) as exc_info:
await requires_access_event_log("GET")(request, user, session)

assert exc_info.value.status_code == 400
assert "event_log_id" in exc_info.value.detail
mock_requires_access_dag.assert_not_called()
session.scalar.assert_not_called()

@pytest.mark.db_test
@patch.object(DagModel, "get_team_name")
@patch("airflow.api_fastapi.core_api.security.get_auth_manager")
async def test_requires_access_event_log_no_path_param_uses_generic_check(
self, mock_get_auth_manager, mock_get_team_name
):
"""When called on the list endpoint (no event_log_id), the generic AUDIT_LOG check applies."""
auth_manager = Mock()
auth_manager.is_authorized_dag.return_value = True
mock_get_auth_manager.return_value = auth_manager

session = Mock()
request = Mock()
request.path_params = {}
request.query_params = {}
user = Mock()

await requires_access_event_log("GET")(request, user, session)

auth_manager.is_authorized_dag.assert_called_once_with(
method="GET",
access_entity=DagAccessEntity.AUDIT_LOG,
details=DagDetails(id=None, team_name=None),
user=user,
)
session.scalar.assert_not_called()
mock_get_team_name.assert_not_called()

@pytest.mark.parametrize(
("url", "expected_is_safe"),
[
Expand Down
Loading