You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by ep...@apache.org on 2022/05/17 07:46:11 UTC

[airflow] 03/20: Handle invalid date parsing in webserver views. (#23161)

This is an automated email from the ASF dual-hosted git repository.

ephraimanierobi pushed a commit to branch v2-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 540beb6f2b5f82d9f0b9f71ae0f06ce0f5bc9bdf
Author: Karthikeyan Singaravelan <ti...@gmail.com>
AuthorDate: Tue May 17 00:45:56 2022 +0530

    Handle invalid date parsing in webserver views. (#23161)
    
    * Handle invalid date from query parameters in views.
    
    * Add tests.
    
    * Use common parsing helper.
    
    * Add type hint.
    
    * Remove unwanted error check.
    
    * Fix extra_links endpoint.
    
    (cherry picked from commit 9e25bc211f6f7bba1aff133d21fe3865dabda53d)
---
 airflow/www/views.py          | 46 ++++++++++++++++++++++------------
 tests/www/views/test_views.py | 57 +++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 88 insertions(+), 15 deletions(-)

diff --git a/airflow/www/views.py b/airflow/www/views.py
index 6a4a191c67..fe4ae3ceb1 100644
--- a/airflow/www/views.py
+++ b/airflow/www/views.py
@@ -186,13 +186,13 @@ def get_date_time_num_runs_dag_runs_form_data(www_request, session, dag):
     """Get Execution Data, Base Date & Number of runs from a Request"""
     date_time = www_request.args.get('execution_date')
     if date_time:
-        date_time = timezone.parse(date_time)
+        date_time = _safe_parse_datetime(date_time)
     else:
         date_time = dag.get_latest_execution_date(session=session) or timezone.utcnow()
 
     base_date = www_request.args.get('base_date')
     if base_date:
-        base_date = timezone.parse(base_date)
+        base_date = _safe_parse_datetime(base_date)
     else:
         # The DateTimeField widget truncates milliseconds and would loose
         # the first dag run. Round to next second.
@@ -231,6 +231,14 @@ def get_date_time_num_runs_dag_runs_form_data(www_request, session, dag):
     }
 
 
+def _safe_parse_datetime(v):
+    """Parse datetime and return error message for invalid dates"""
+    try:
+        return timezone.parse(v)
+    except (TypeError, ParserError):
+        abort(400, f"Invalid datetime: {v!r}")
+
+
 def task_group_to_grid(task_item_or_group, dag, dag_runs, tis, session):
     """
     Create a nested dict representation of this TaskGroup and its children used to construct
@@ -1244,7 +1252,7 @@ class Airflow(AirflowBaseView):
         task_id = request.args.get('task_id')
         map_index = request.args.get('map_index', -1, type=int)
         execution_date = request.args.get('execution_date')
-        dttm = timezone.parse(execution_date)
+        dttm = _safe_parse_datetime(execution_date)
         form = DateTimeForm(data={'execution_date': dttm})
         root = request.args.get('root', '')
 
@@ -1345,7 +1353,8 @@ class Airflow(AirflowBaseView):
         dag_id = request.args.get('dag_id')
         task_id = request.args.get('task_id')
         execution_date = request.args.get('execution_date')
-        dttm = timezone.parse(execution_date)
+        dttm = _safe_parse_datetime(execution_date)
+
         form = DateTimeForm(data={'execution_date': dttm})
         root = request.args.get('root', '')
         map_index = request.args.get('map_index', -1, type=int)
@@ -1494,7 +1503,12 @@ class Airflow(AirflowBaseView):
         task_id = request.args.get('task_id')
         map_index = request.args.get('map_index', -1, type=int)
         execution_date = request.args.get('execution_date')
-        dttm = timezone.parse(execution_date) if execution_date else None
+
+        if execution_date:
+            dttm = _safe_parse_datetime(execution_date)
+        else:
+            dttm = None
+
         form = DateTimeForm(data={'execution_date': dttm})
         dag_model = DagModel.get_dagmodel(dag_id)
 
@@ -1541,7 +1555,7 @@ class Airflow(AirflowBaseView):
         dag_id = request.args.get('dag_id')
         task_id = request.args.get('task_id')
         execution_date = request.args.get('execution_date')
-        dttm = timezone.parse(execution_date)
+        dttm = _safe_parse_datetime(execution_date)
         map_index = request.args.get('map_index', -1, type=int)
         try_number = request.args.get('try_number', 1)
 
@@ -1578,7 +1592,7 @@ class Airflow(AirflowBaseView):
         dag_id = request.args.get('dag_id')
         task_id = request.args.get('task_id')
         execution_date = request.args.get('execution_date')
-        dttm = timezone.parse(execution_date)
+        dttm = _safe_parse_datetime(execution_date)
         map_index = request.args.get('map_index', -1, type=int)
         form = DateTimeForm(data={'execution_date': dttm})
         root = request.args.get('root', '')
@@ -1710,7 +1724,8 @@ class Airflow(AirflowBaseView):
         # Carrying execution_date through, even though it's irrelevant for
         # this context
         execution_date = request.args.get('execution_date')
-        dttm = timezone.parse(execution_date)
+        dttm = _safe_parse_datetime(execution_date)
+
         form = DateTimeForm(data={'execution_date': dttm})
         root = request.args.get('root', '')
         dag = DagModel.get_dagmodel(dag_id)
@@ -2052,7 +2067,7 @@ class Airflow(AirflowBaseView):
             map_indexes = request.form.getlist('map_index', type=int)
 
         execution_date = request.form.get('execution_date')
-        execution_date = timezone.parse(execution_date)
+        execution_date = _safe_parse_datetime(execution_date)
         confirmed = request.form.get('confirmed') == "true"
         upstream = request.form.get('upstream') == "true"
         downstream = request.form.get('downstream') == "true"
@@ -2583,7 +2598,7 @@ class Airflow(AirflowBaseView):
             num_runs = conf.getint('webserver', 'default_dag_run_display_number')
 
         try:
-            base_date = timezone.parse(request.args["base_date"])
+            base_date = _safe_parse_datetime(request.args["base_date"])
         except (KeyError, ValueError):
             base_date = dag.get_latest_execution_date() or timezone.utcnow()
 
@@ -2805,6 +2820,7 @@ class Airflow(AirflowBaseView):
         edges = dag_edges(dag)
 
         dt_nr_dr_data = get_date_time_num_runs_dag_runs_form_data(request, session, dag)
+
         dt_nr_dr_data['arrange'] = arrange
         dttm = dt_nr_dr_data['dttm']
         dag_run = dag.get_dagrun(execution_date=dttm)
@@ -2911,7 +2927,7 @@ class Airflow(AirflowBaseView):
         num_runs = request.args.get('num_runs', default=default_dag_run, type=int)
 
         if base_date:
-            base_date = timezone.parse(base_date)
+            base_date = _safe_parse_datetime(base_date)
         else:
             base_date = dag.get_latest_execution_date() or timezone.utcnow()
 
@@ -3058,7 +3074,7 @@ class Airflow(AirflowBaseView):
         num_runs = request.args.get('num_runs', default=default_dag_run, type=int)
 
         if base_date:
-            base_date = timezone.parse(base_date)
+            base_date = _safe_parse_datetime(base_date)
         else:
             base_date = dag.get_latest_execution_date() or timezone.utcnow()
 
@@ -3148,7 +3164,7 @@ class Airflow(AirflowBaseView):
         num_runs = request.args.get('num_runs', default=default_dag_run, type=int)
 
         if base_date:
-            base_date = timezone.parse(base_date)
+            base_date = _safe_parse_datetime(base_date)
         else:
             base_date = dag.get_latest_execution_date() or timezone.utcnow()
 
@@ -3382,7 +3398,7 @@ class Airflow(AirflowBaseView):
         map_index = request.args.get('map_index', -1, type=int)
         execution_date = request.args.get('execution_date')
         link_name = request.args.get('link_name')
-        dttm = timezone.parse(execution_date)
+        dttm = _safe_parse_datetime(execution_date)
         dag = current_app.dag_bag.get_dag(dag_id)
 
         if not dag or task_id not in dag.task_ids:
@@ -3437,7 +3453,7 @@ class Airflow(AirflowBaseView):
 
         dttm = request.args.get('execution_date')
         if dttm:
-            dttm = timezone.parse(dttm)
+            dttm = _safe_parse_datetime(dttm)
         else:
             response = jsonify({'error': f"Invalid execution_date {dttm}"})
             response.status_code = 400
diff --git a/tests/www/views/test_views.py b/tests/www/views/test_views.py
index c7900d64fd..887bd4898a 100644
--- a/tests/www/views/test_views.py
+++ b/tests/www/views/test_views.py
@@ -373,3 +373,60 @@ def test_get_task_stats_from_query():
 
     data = get_task_stats_from_query(query_data)
     assert data == expected_data
+
+
+@pytest.mark.parametrize(
+    "url, content",
+    [
+        (
+            '/rendered-templates?execution_date=invalid',
+            "Invalid datetime: 'invalid'",
+        ),
+        (
+            '/log?execution_date=invalid',
+            "Invalid datetime: 'invalid'",
+        ),
+        (
+            '/redirect_to_external_log?execution_date=invalid',
+            "Invalid datetime: 'invalid'",
+        ),
+        (
+            '/task?execution_date=invalid',
+            "Invalid datetime: 'invalid'",
+        ),
+        (
+            'dags/example_bash_operator/graph?execution_date=invalid',
+            "Invalid datetime: 'invalid'",
+        ),
+        (
+            'dags/example_bash_operator/graph?execution_date=invalid',
+            "Invalid datetime: 'invalid'",
+        ),
+        (
+            'dags/example_bash_operator/duration?base_date=invalid',
+            "Invalid datetime: 'invalid'",
+        ),
+        (
+            'dags/example_bash_operator/tries?base_date=invalid',
+            "Invalid datetime: 'invalid'",
+        ),
+        (
+            'dags/example_bash_operator/landing-times?base_date=invalid',
+            "Invalid datetime: 'invalid'",
+        ),
+        (
+            'dags/example_bash_operator/gantt?execution_date=invalid',
+            "Invalid datetime: 'invalid'",
+        ),
+        (
+            'extra_links?execution_date=invalid',
+            "Invalid datetime: 'invalid'",
+        ),
+    ],
+)
+def test_invalid_dates(app, admin_client, url, content):
+    """Test invalid date format doesn't crash page."""
+    resp = admin_client.get(url, follow_redirects=True)
+
+    assert resp.status_code == 400
+    assert content in resp.get_data().decode()