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

husseinawala 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 d8f647cdef [#38284] Update Confirmation Logic for Config Changes on 
Sensitive Environments Like Production (#38299)
d8f647cdef is described below

commit d8f647cdef5aa526cbf3732aa47e03817b476f83
Author: Nguyễn Anh Bình <[email protected]>
AuthorDate: Wed Mar 20 21:42:26 2024 +0700

    [#38284] Update Confirmation Logic for Config Changes on Sensitive 
Environments Like Production (#38299)
    
    * - [#38284] Update Confirmation Logic for Dag status Changes on Sensitive 
Environments Like Production
    
    * - Update pause/unpause confirm messages
    
    * - Not version added yet
    
    * - Fix static check
    
    * - Ruff check
    
    * Revert "- Fix static check"
    
    This reverts commit cccfeb757ab1d6f8ac35a6730ae6cc1c7a9a2edc.
    
    * - Fix ruff check
---
 airflow/config_templates/config.yml       |  9 +++++++++
 airflow/www/app.py                        |  4 ++++
 airflow/www/extensions/init_appbuilder.py | 13 +++++++++++++
 airflow/www/static/js/dag.js              | 10 ++++++++++
 airflow/www/templates/airflow/dag.html    | 12 +++++++++++-
 5 files changed, 47 insertions(+), 1 deletion(-)

diff --git a/airflow/config_templates/config.yml 
b/airflow/config_templates/config.yml
index 2fc72cbb2d..0a1b292fdc 100644
--- a/airflow/config_templates/config.yml
+++ b/airflow/config_templates/config.yml
@@ -1943,6 +1943,15 @@ webserver:
       type: float
       example: ~
       default: "1.0"
+    require_confirmation_dag_change:
+      description: |
+        Require confirmation when changing a DAG in the web UI. This is to 
prevent accidental changes
+        to a DAG that may be running on sensitive environments like production.
+        When set to True, confirmation dialog will be shown when a user tries 
to Pause/Unpause, Trigger a DAG
+      version_added: ~
+      type: boolean
+      example: ~
+      default: "False"
 email:
   description: |
     Configuration email backend and whether to
diff --git a/airflow/www/app.py b/airflow/www/app.py
index 749efe8912..17a4c681dd 100644
--- a/airflow/www/app.py
+++ b/airflow/www/app.py
@@ -86,6 +86,9 @@ def create_app(config=None, testing=False):
     flask_app.config["SQLALCHEMY_DATABASE_URI"] = conf.get("database", 
"SQL_ALCHEMY_CONN")
 
     instance_name = conf.get(section="webserver", key="instance_name", 
fallback="Airflow")
+    require_confirmation_dag_change = conf.getboolean(
+        section="webserver", key="require_confirmation_dag_change", 
fallback=False
+    )
     instance_name_has_markup = conf.getboolean(
         section="webserver", key="instance_name_has_markup", fallback=False
     )
@@ -93,6 +96,7 @@ def create_app(config=None, testing=False):
         instance_name = Markup(instance_name).striptags()
 
     flask_app.config["APP_NAME"] = instance_name
+    flask_app.config["REQUIRE_CONFIRMATION_DAG_CHANGE"] = 
require_confirmation_dag_change
 
     url = make_url(flask_app.config["SQLALCHEMY_DATABASE_URI"])
     if url.drivername == "sqlite" and url.database and not 
url.database.startswith("/"):
diff --git a/airflow/www/extensions/init_appbuilder.py 
b/airflow/www/extensions/init_appbuilder.py
index ed972af271..7bb71ba980 100644
--- a/airflow/www/extensions/init_appbuilder.py
+++ b/airflow/www/extensions/init_appbuilder.py
@@ -293,6 +293,19 @@ class AirflowAppBuilder:
         """
         return self.get_app.config["APP_NAME"]
 
+    @property
+    def require_confirmation_dag_change(self):
+        """Get the value of the require_confirmation_dag_change configuration.
+
+        The logic is:
+         - return True, in page dag.html, when user trigger/pause the dag from 
UI.
+           Once confirmation box will be shown before triggering the dag.
+         - Default value is False.
+
+        :return: Boolean
+        """
+        return self.get_app.config["REQUIRE_CONFIRMATION_DAG_CHANGE"]
+
     @property
     def app_theme(self):
         """
diff --git a/airflow/www/static/js/dag.js b/airflow/www/static/js/dag.js
index 39a26f28e6..b70587a70c 100644
--- a/airflow/www/static/js/dag.js
+++ b/airflow/www/static/js/dag.js
@@ -116,6 +116,16 @@ $("#pause_resume").on("change", function onChange() {
   const $input = $(this);
   const id = $input.data("dag-id");
   const isPaused = $input.is(":checked");
+  const requireConfirmation = $input.data("require-confirmation");
+  if (requireConfirmation) {
+    const confirmation = window.confirm(
+      `Are you sure you want to ${isPaused ? "resume" : "pause"} this DAG?`
+    );
+    if (!confirmation) {
+      $input.prop("checked", !isPaused);
+      return;
+    }
+  }
   const url = `${pausedUrl}?is_paused=${isPaused}&dag_id=${encodeURIComponent(
     id
   )}`;
diff --git a/airflow/www/templates/airflow/dag.html 
b/airflow/www/templates/airflow/dag.html
index 6f5a187f61..a09fa7ffa6 100644
--- a/airflow/www/templates/airflow/dag.html
+++ b/airflow/www/templates/airflow/dag.html
@@ -123,6 +123,7 @@
           {% endif %}
           <label class="switch-label{{' disabled' if not can_edit_dag else ''  
}} js-tooltip" title="{{ switch_tooltip }}">
             <input class="switch-input" id="pause_resume" data-dag-id="{{ 
dag.dag_id }}"
+                   data-require-confirmation="{{ 
appbuilder.require_confirmation_dag_change }}"
                    type="checkbox"{{ " checked" if not dag_is_paused else "" }}
                    {{ " disabled" if not can_edit_dag else "" }}>
             <span class="switch" aria-hidden="true"></span>
@@ -261,7 +262,8 @@
           {% else %}
             <a href="{{ url_for('Airflow.trigger', dag_id=dag.dag_id, 
origin=url_for(request.endpoint, dag_id=dag.dag_id, **request.args)) }}"
           {% endif %}
-              onclick="return triggerDag(this, '{{ dag.dag_id }}')" 
title="Trigger&nbsp;DAG"
+          onclick="return {{ 'triggerDag' if not 
appbuilder.require_confirmation_dag_change else 'confirmTriggerDag' }}(this, 
'{{ dag.dag_id }}')"
+          title="Trigger&nbsp;DAG"
               aria-label="Trigger DAG"
               class="btn btn-default btn-icon-only{{ ' disabled' if not 
dag.can_trigger }} trigger-dropdown-btn">
               <span class="material-icons" aria-hidden="true">play_arrow</span>
@@ -301,5 +303,13 @@
       postAsForm(link.href, {});
       return false;
     }
+
+    function confirmTriggerDag(link, dagId) {
+      if (confirm(`Are you sure you want to trigger '${dagId}' now?`)) {
+        postAsForm(link.href, {});
+      }
+      return false;
+    }
+
   </script>
 {% endblock %}

Reply via email to