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
15 changes: 15 additions & 0 deletions airflow/config_templates/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1828,6 +1828,21 @@ webserver:
type: boolean
example: ~
default: "False"
allow_raw_html_descriptions:
description: |
A DAG author is able to provide any raw HTML into ``doc_md`` or params description in
``description_md`` for text formatting. This is including potentially unsafe javascript.
Displaying the DAG or trigger form in web UI provides the DAG author the potential to
inject malicious code into clients browsers. To ensure the web UI is safe by default,
raw HTML is disabled by default. If you trust your DAG authors, you can enable HTML
support in markdown by setting this option to True.

This parameter also enables the deprecated fields ``description_html`` and
``custom_html_form`` in DAG params until the feature is removed in a future version.
version_added: 2.8.0
type: boolean
example: "False"
default: "False"
email:
description: |
Configuration email backend and whether to
Expand Down
101 changes: 19 additions & 82 deletions airflow/example_dags/example_params_ui_tutorial.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,17 @@
"flag": False,
"a_simple_list": ["one", "two", "three", "actually one value is made per line"],
# But of course you might want to have it nicer! Let's add some description to parameters.
# Note if you can add any HTML formatting to the description, you need to use the description_html
# Note if you can add any Markdown formatting to the description, you need to use the description_md
# attribute.
"most_loved_number": Param(
42,
type="integer",
title="Your favorite number",
description_html="""Everybody should have a favorite number. Not only math teachers.
If you can not think of any at the moment please think of the 42 which is very famous because
of the book
<a href='https://en.wikipedia.org/wiki/Phrases_from_The_Hitchhiker%27s_Guide_to_the_Galaxy#
The_Answer_to_the_Ultimate_Question_of_Life,_the_Universe,_and_Everything_is_42'>
The Hitchhiker's Guide to the Galaxy</a>""",
description_md="Everybody should have a **favorite** number. Not only _math teachers_. "
"If you can not think of any at the moment please think of the 42 which is very famous because"
"of the book [The Hitchhiker's Guide to the Galaxy]"
"(https://en.wikipedia.org/wiki/Phrases_from_The_Hitchhiker%27s_Guide_to_the_Galaxy#"
"The_Answer_to_the_Ultimate_Question_of_Life,_the_Universe,_and_Everything_is_42).",
),
# If you want to have a selection list box then you can use the enum feature of JSON schema
"pick_one": Param(
Expand Down Expand Up @@ -177,8 +176,8 @@
"optional text, you can trigger also w/o text",
type=["null", "string"],
title="Optional text field",
description_html="This field is optional. As field content is JSON schema validated you must "
"allow the <code>null</code> type.",
description_md="This field is optional. As field content is JSON schema validated you must "
"allow the `null` type.",
),
# You can arrange the entry fields in sections so that you can have a better overview for the user
# Therefore you can add the "section" attribute.
Expand All @@ -188,10 +187,10 @@
"length-checked-field",
type="string",
title="Text field with length check",
description_html="""This field is required. And you need to provide something between 10 and 30
characters. See the
<a href='https://json-schema.org/understanding-json-schema/reference/string.html'>
JSON schema description (string)</a> in for more details""",
description_md="""This field is required. And you need to provide something between 10 and 30
characters. See the JSON
[schema description (string)](https://json-schema.org/understanding-json-schema/reference/string.html)
for more details""",
minLength=10,
maxLength=20,
section="JSON Schema validation options",
Expand All @@ -200,9 +199,10 @@
100,
type="number",
title="Number field with value check",
description_html="""This field is required. You need to provide any number between 64 and 128.
See the <a href='https://json-schema.org/understanding-json-schema/reference/numeric.html'>
JSON schema description (numbers)</a> in for more details""",
description_md="""This field is required. You need to provide any number between 64 and 128.
See the JSON
[schema description (numbers)](https://json-schema.org/understanding-json-schema/reference/numeric.html)
for more details""",
minimum=64,
maximum=128,
section="JSON Schema validation options",
Expand All @@ -217,9 +217,9 @@
),
"array_of_objects": Param(
[{"name": "account_name", "country": "country_name"}],
"Array with complex objects and validation rules. "
"See <a href='https://json-schema.org/understanding-json-schema"
"/reference/array.html#items'>JSON Schema validation options in specs.</a>",
description_md="Array with complex objects and validation rules. "
"See [JSON Schema validation options in specs]"
"(https://json-schema.org/understanding-json-schema/reference/array.html#items).",
type="array",
title="JSON array field",
items={
Expand All @@ -233,69 +233,6 @@
# then you can use the JSON schema option of passing constant values. These parameters
# will not be displayed but passed to the DAG
"hidden_secret_field": Param("constant value", const="constant value"),
# Finally besides the standard provided field generator you can have you own HTML form code
# injected - but be careful, you can also mess-up the layout!
"color_picker": Param(
"#FF8800",
type="string",
title="Pick a color",
description_html="""This is a special HTML widget as custom implementation in the DAG code.
It is templated with the following parameter to render proper HTML form fields:
<ul>
<li><code>{name}</code>: Name of the HTML input field that is expected.</li>
<li><code>{value}</code>:
(Default) value that should be displayed when showing/loading the form.</li>
<li>Note: If you have elements changing a value, call <code>updateJSONconf()</code> to update
the form data to be posted as <code>dag_run.conf</code>.</li>
</ul>
Example: <code>&lt;input name='{name}' value='{value}' onchange='updateJSONconf()' /&gt;</code>
""",
custom_html_form="""
<table width="100%" cellspacing="5"><tbody><tr><td>
<label for="r_{name}">Red:</label>
</td><td width="80%">
<input id="r_{name}" type="range" min="0" max="255" value="0" onchange="u_{name}()"/>
</td><td rowspan="3" style="padding-left: 10px;">
<div id="preview_{name}"
style="line-height: 40px; margin-bottom: 7px; width: 100%; background-color: {value};"
>&nbsp;</div>
<input class="form-control" type="text" maxlength="7" id="{name}" name="{name}"
value="{value}" onchange="v_{name}()" />
</td></tr><tr><td>
<label for="g_{name}">Green:</label>
</td><td>
<input id="g_{name}" type="range" min="0" max="255" value="0" onchange="u_{name}()"/>
</td></tr><tr><td>
<label for="b_{name}">Blue:</label>
</td><td>
<input id="b_{name}" type="range" min="0" max="255" value="0" onchange="u_{name}()"/>
</td></tr></tbody></table>
<script lang="javascript">
const hex_chars = "0123456789ABCDEF";
function i2hex(name) {
var i = document.getElementById(name).value;
return hex_chars.substr(parseInt(i / 16), 1) + hex_chars.substr(parseInt(i % 16), 1)
}
function u_{name}() {
var hex_val = "#"+i2hex("r_{name}")+i2hex("g_{name}")+i2hex("b_{name}");
document.getElementById("{name}").value = hex_val;
document.getElementById("preview_{name}").style.background = hex_val;
updateJSONconf();
}
function hex2i(text) {
return hex_chars.indexOf(text.substr(0,1)) * 16 + hex_chars.indexOf(text.substr(1,1));
}
function v_{name}() {
var value = document.getElementById("{name}").value.toUpperCase();
document.getElementById("r_{name}").value = hex2i(value.substr(1,2));
document.getElementById("g_{name}").value = hex2i(value.substr(3,2));
document.getElementById("b_{name}").value = hex2i(value.substr(5,2));
document.getElementById("preview_{name}").style.background = value;
}
v_{name}();
</script>""",
section="Special advanced stuff with form fields",
),
},
) as dag:

Expand Down
15 changes: 8 additions & 7 deletions airflow/www/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from sqlalchemy import delete, func, select, types
from sqlalchemy.ext.associationproxy import AssociationProxy

from airflow.configuration import conf
from airflow.exceptions import RemovedInAirflow3Warning
from airflow.models import errors
from airflow.models.dagrun import DagRun
Expand Down Expand Up @@ -154,16 +155,16 @@ def get_mapped_summary(parent_instance, task_instances):
def get_dag_run_conf(
dag_run_conf: Any, *, json_encoder: type[json.JSONEncoder] = json.JSONEncoder
) -> tuple[str | None, bool]:
conf: str | None = None
result: str | None = None

conf_is_json: bool = False
if isinstance(dag_run_conf, str):
conf = dag_run_conf
result = dag_run_conf
elif isinstance(dag_run_conf, (dict, list)) and any(dag_run_conf):
conf = json.dumps(dag_run_conf, sort_keys=True, cls=json_encoder, ensure_ascii=False)
result = json.dumps(dag_run_conf, sort_keys=True, cls=json_encoder, ensure_ascii=False)
conf_is_json = True

return conf, conf_is_json
return result, conf_is_json


def encode_dag_run(
Expand All @@ -172,7 +173,7 @@ def encode_dag_run(
if not dag_run:
return None

conf, conf_is_json = get_dag_run_conf(dag_run.conf, json_encoder=json_encoder)
dag_run_conf, conf_is_json = get_dag_run_conf(dag_run.conf, json_encoder=json_encoder)

return {
"run_id": dag_run.run_id,
Expand All @@ -186,7 +187,7 @@ def encode_dag_run(
"run_type": dag_run.run_type,
"last_scheduling_decision": datetime_to_string(dag_run.last_scheduling_decision),
"external_trigger": dag_run.external_trigger,
"conf": conf,
"conf": dag_run_conf,
"conf_is_json": conf_is_json,
"note": dag_run.note,
}
Expand Down Expand Up @@ -613,7 +614,7 @@ def json_render(obj, lexer):

def wrapped_markdown(s, css_class="rich_doc"):
"""Convert a Markdown string to HTML."""
md = MarkdownIt("gfm-like")
md = MarkdownIt("gfm-like", {"html": conf.getboolean("webserver", "allow_raw_html_descriptions")})
if s is None:
return None
s = textwrap.dedent(s)
Expand Down
81 changes: 60 additions & 21 deletions airflow/www/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1956,30 +1956,69 @@ def trigger(self, dag_id: str, session: Session = NEW_SESSION):

# Prepare form fields with param struct details to render a proper form with schema information
form_fields = {}
allow_raw_html_descriptions = conf.getboolean("webserver", "allow_raw_html_descriptions")
form_trust_problems = []
for k, v in dag.params.items():
form_fields[k] = v.dump()
form_field: dict = form_fields[k]
# If no schema is provided, auto-detect on default values
if "schema" not in form_fields[k]:
form_fields[k]["schema"] = {}
if "type" not in form_fields[k]["schema"]:
if isinstance(form_fields[k]["value"], bool):
form_fields[k]["schema"]["type"] = "boolean"
elif isinstance(form_fields[k]["value"], int):
form_fields[k]["schema"]["type"] = ["integer", "null"]
elif isinstance(form_fields[k]["value"], list):
form_fields[k]["schema"]["type"] = ["array", "null"]
elif isinstance(form_fields[k]["value"], dict):
form_fields[k]["schema"]["type"] = ["object", "null"]
# Mark markup fields as safe
if (
"description_html" in form_fields[k]["schema"]
and form_fields[k]["schema"]["description_html"]
):
form_fields[k]["description"] = Markup(form_fields[k]["schema"]["description_html"])
if "custom_html_form" in form_fields[k]["schema"]:
form_fields[k]["schema"]["custom_html_form"] = Markup(
form_fields[k]["schema"]["custom_html_form"]
)
if "schema" not in form_field:
form_field["schema"] = {}
form_field_schema: dict = form_field["schema"]
if "type" not in form_field_schema:
form_field_value = form_field["value"]
if isinstance(form_field_value, bool):
form_field_schema["type"] = "boolean"
elif isinstance(form_field_value, int):
form_field_schema["type"] = ["integer", "null"]
elif isinstance(form_field_value, list):
form_field_schema["type"] = ["array", "null"]
elif isinstance(form_field_value, dict):
form_field_schema["type"] = ["object", "null"]
# Mark HTML fields as safe if allowed
if allow_raw_html_descriptions:
if "description_html" in form_field_schema:
form_field["description"] = Markup(form_field_schema["description_html"])
if "custom_html_form" in form_field_schema:
form_field_schema["custom_html_form"] = Markup(form_field_schema["custom_html_form"])
else:
if "description_html" in form_field_schema and "description_md" not in form_field_schema:
form_trust_problems.append(f"Field {k} uses HTML description")
form_field["description"] = form_field_schema.pop("description_html")
if "custom_html_form" in form_field_schema:
form_trust_problems.append(f"Field {k} uses custom HTML form definition")
form_field_schema.pop("custom_html_form")
if "description_md" in form_field_schema:
form_field["description"] = wwwutils.wrapped_markdown(form_field_schema["description_md"])
if form_trust_problems:
flash(
Markup(
"At least one field in the trigger form uses a raw HTML form definition. This is not allowed for "
"security. Please switch to markdown description via <code>description_md</code>. "
"Raw HTML is deprecated and must be enabled via "
"<code>webserver.allow_raw_html_descriptions</code> configuration parameter. Using plain text "
"as fallback for these fields. "
f"<ul><li>{'</li><li>'.join(form_trust_problems)}</li></ul>"
),
"warning",
)
if allow_raw_html_descriptions and any("description_html" in p.schema for p in dag.params.values()):
flash(
Markup(
"The form params use raw HTML in <code>description_html</code> which is deprecated. "
"Please migrate to <code>description_md</code>."
),
"warning",
)
if allow_raw_html_descriptions and any("custom_html_form" in p.schema for p in dag.params.values()):
flash(
Markup(
"The form params use <code>custom_html_form</code> definition. "
"This is deprecated with Airflow 2.8.0 and will be removed in a future release."
),
"warning",
)

ui_fields_defined = any("const" not in f["schema"] for f in form_fields.values())
show_trigger_form_if_no_params = conf.getboolean("webserver", "show_trigger_form_if_no_params")

Expand Down
Loading