diff --git a/airflow/config_templates/config.yml b/airflow/config_templates/config.yml index d0b97308a22e5..f929e738dbc5d 100644 --- a/airflow/config_templates/config.yml +++ b/airflow/config_templates/config.yml @@ -1303,6 +1303,13 @@ type: integer example: ~ default: "3" + - name: warn_deployment_exposure + description: | + Boolean for displaying warning for publicly viewable deployment + version_added: 2.3.0 + type: boolean + example: ~ + default: "True" - name: email description: | diff --git a/airflow/config_templates/default_airflow.cfg b/airflow/config_templates/default_airflow.cfg index 496616bbcfb66..f81312b8931e2 100644 --- a/airflow/config_templates/default_airflow.cfg +++ b/airflow/config_templates/default_airflow.cfg @@ -655,6 +655,9 @@ session_lifetime_minutes = 43200 # when auto-refresh is turned on auto_refresh_interval = 3 +# Boolean for displaying warning for publicly viewable deployment +warn_deployment_exposure = True + [email] # Configuration email backend and whether to diff --git a/airflow/www/views.py b/airflow/www/views.py index 8144c9d0481e0..388e69cd0f70f 100644 --- a/airflow/www/views.py +++ b/airflow/www/views.py @@ -120,7 +120,7 @@ from airflow.utils.session import create_session, provide_session from airflow.utils.state import State from airflow.utils.strings import to_boolean -from airflow.utils.timezone import td_format +from airflow.utils.timezone import td_format, utcnow from airflow.version import version from airflow.www import auth, utils as wwwutils from airflow.www.decorators import action_logging, gzipped @@ -788,6 +788,29 @@ def _iter_parsed_moved_data_table_names(): # Second segment is a version marker that we don't need to show. yield segments[2], table_name + if ( + permissions.ACTION_CAN_ACCESS_MENU, + permissions.RESOURCE_ADMIN_MENU, + ) in user_permissions and conf.getboolean("webserver", "warn_deployment_exposure"): + robots_file_access_count = ( + session.query(Log) + .filter(Log.event == "robots") + .filter(Log.dttm > (utcnow() - timedelta(days=7))) + .count() + ) + if robots_file_access_count > 0: + flash( + Markup( + 'Recent requests have been made to /robots.txt. ' + 'This indicates that this deployment may be accessible to the public internet. ' + 'This warning can be disabled by setting webserver.warn_deployment_exposure=False in ' + 'airflow.cfg. Read more about web deployment security ' + 'here' + ), + "warning", + ) + return self.render_template( 'airflow/dags.html', dags=dags, diff --git a/docs/apache-airflow/security/webserver.rst b/docs/apache-airflow/security/webserver.rst index f168faaedc47e..91a06ad1235ea 100644 --- a/docs/apache-airflow/security/webserver.rst +++ b/docs/apache-airflow/security/webserver.rst @@ -31,6 +31,17 @@ set the below: [webserver] x_frame_enabled = False +Disable Deployment Exposure Warning +--------------------------------------- + +Airflow warns when recent requests are made to ``/robot.txt``. To disable this warning set ``warn_deployment_exposure`` to +``False`` as below: + +.. code-block:: ini + + [webserver] + warn_deployment_exposure = False + Sensitive Variable fields ------------------------- diff --git a/tests/www/views/test_views_base.py b/tests/www/views/test_views_base.py index cc9968276e006..5759369265fbb 100644 --- a/tests/www/views/test_views_base.py +++ b/tests/www/views/test_views_base.py @@ -30,7 +30,7 @@ def test_index(admin_client): - with assert_queries_count(49): + with assert_queries_count(50): resp = admin_client.get('/', follow_redirects=True) check_content_in_response('DAGs', resp) diff --git a/tests/www/views/test_views_robots.py b/tests/www/views/test_views_robots.py index e9fa98666d0e5..232bfe03dbfeb 100644 --- a/tests/www/views/test_views_robots.py +++ b/tests/www/views/test_views_robots.py @@ -14,8 +14,21 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +from tests.test_utils.config import conf_vars def test_robots(viewer_client): resp = viewer_client.get('/robots.txt', follow_redirects=True) assert resp.data.decode('utf-8') == "User-agent: *\nDisallow: /\n" + + +def test_deployment_warning_config(admin_client): + warn_text = "webserver.warn_deployment_exposure" + admin_client.get('/robots.txt', follow_redirects=True) + resp = admin_client.get('', follow_redirects=True) + assert warn_text in resp.data.decode('utf-8') + + with conf_vars({('webserver', 'warn_deployment_exposure'): 'False'}): + admin_client.get('/robots.txt', follow_redirects=True) + resp = admin_client.get('/robots.txt', follow_redirects=True) + assert warn_text not in resp.data.decode('utf-8')