This is an automated email from the ASF dual-hosted git repository.
potiuk 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 64c2a56faf D205 Support - WWW (#33298)
64c2a56faf is described below
commit 64c2a56faf31d5f7e38e7b55e9eb4c38ced54c59
Author: D. Ferruzzi <[email protected]>
AuthorDate: Sun Aug 13 15:48:21 2023 -0700
D205 Support - WWW (#33298)
* D205 Support - WWW
* fix broken link
---
airflow/www/api/experimental/endpoints.py | 13 +++++----
airflow/www/decorators.py | 3 +-
airflow/www/extensions/init_appbuilder.py | 12 ++++----
airflow/www/extensions/init_dagbag.py | 5 ++--
airflow/www/extensions/init_robots.py | 6 ++--
airflow/www/extensions/init_security.py | 5 ++--
airflow/www/fab_security/manager.py | 24 +++++++---------
airflow/www/fab_security/sqla/manager.py | 11 ++++---
airflow/www/forms.py | 5 +---
airflow/www/security.py | 25 +++++++++++-----
airflow/www/utils.py | 25 ++++++++--------
airflow/www/views.py | 48 +++++++++++++++++++------------
12 files changed, 101 insertions(+), 81 deletions(-)
diff --git a/airflow/www/api/experimental/endpoints.py
b/airflow/www/api/experimental/endpoints.py
index 2af00eeee6..ccf024543f 100644
--- a/airflow/www/api/experimental/endpoints.py
+++ b/airflow/www/api/experimental/endpoints.py
@@ -58,8 +58,10 @@ api_experimental = Blueprint("api_experimental", __name__)
def add_deprecation_headers(response: Response):
"""
- Add `Deprecation HTTP Header Field
- <https://tools.ietf.org/id/draft-dalal-deprecation-header-03.html>`__.
+ Add Deprecation HTTP Header Field.
+
+ .. seealso:: IETF proposal for the header field
+ `here
<https://datatracker.ietf.org/doc/draft-dalal-deprecation-header/>`_.
"""
response.headers["Deprecation"] = "true"
doc_url =
get_docs_url("upgrading-to-2.html#migration-guide-from-experimental-api-to-stable-api-v1")
@@ -79,10 +81,7 @@ api_experimental.after_request(add_deprecation_headers) #
type: ignore[arg-type
@api_experimental.route("/dags/<string:dag_id>/dag_runs", methods=["POST"])
@requires_authentication
def trigger_dag(dag_id):
- """
- Trigger a new dag run for a Dag with an execution date of now unless
- specified in the data.
- """
+ """Trigger a new dag run for a Dag with an execution date of now unless
specified in the data."""
data = request.get_json(force=True)
run_id = None
@@ -251,6 +250,7 @@ def dag_is_paused(dag_id):
def task_instance_info(dag_id, execution_date, task_id):
"""
Returns a JSON with a task instance's public instance variables.
+
The format for the exec_date is expected to be
"YYYY-mm-DDTHH:MM:SS", for example: "2016-11-16T11:34:15". This will
of course need to have been encoded for URL in the request.
@@ -287,6 +287,7 @@ def task_instance_info(dag_id, execution_date, task_id):
def dag_run_status(dag_id, execution_date):
"""
Returns a JSON with a dag_run's public instance variables.
+
The format for the exec_date is expected to be
"YYYY-mm-DDTHH:MM:SS", for example: "2016-11-16T11:34:15". This will
of course need to have been encoded for URL in the request.
diff --git a/airflow/www/decorators.py b/airflow/www/decorators.py
index c74be2635e..975910fe50 100644
--- a/airflow/www/decorators.py
+++ b/airflow/www/decorators.py
@@ -41,9 +41,10 @@ logger = logging.getLogger(__name__)
def _mask_variable_fields(extra_fields):
"""
+ Mask the 'val_content' field if 'key_content' is in the mask list.
+
The variable requests values and args comes in this form:
[('key', 'key_content'),('val', 'val_content'), ('description',
'description_content')]
- So we need to mask the 'val_content' field if 'key_content' is in the mask
list.
"""
result = []
keyname = None
diff --git a/airflow/www/extensions/init_appbuilder.py
b/airflow/www/extensions/init_appbuilder.py
index 11c358abb6..9c2948e324 100644
--- a/airflow/www/extensions/init_appbuilder.py
+++ b/airflow/www/extensions/init_appbuilder.py
@@ -71,6 +71,7 @@ def dynamic_class_import(class_path):
class AirflowAppBuilder:
"""
This is the base class for all the framework.
+
This is where you will register all your views
and create the menu structure.
Will hold your flask app object, all your views, and security classes.
@@ -235,10 +236,7 @@ class AirflowAppBuilder:
app.extensions["appbuilder"] = self
def _swap_url_filter(self):
- """
- Use our url filtering util function so there is consistency between
- FAB and Airflow routes.
- """
+ """Use our url filtering util function so there is consistency between
FAB and Airflow routes."""
from flask_appbuilder.security import views as fab_sec_views
from airflow.www.views import get_safe_url
@@ -537,9 +535,9 @@ class AirflowAppBuilder:
def add_view_no_menu(self, baseview, endpoint=None, static_folder=None):
"""
- Add your views without creating a menu.
- :param baseview:
- A BaseView type class instantiated.
+ Add your views without creating a menu.
+
+ :param baseview: A BaseView type class instantiated.
"""
baseview = self._check_and_init(baseview)
log.info(LOGMSG_INF_FAB_ADD_VIEW.format(baseview.__class__.__name__,
""))
diff --git a/airflow/www/extensions/init_dagbag.py
b/airflow/www/extensions/init_dagbag.py
index 0a736b424a..1cab2bff6b 100644
--- a/airflow/www/extensions/init_dagbag.py
+++ b/airflow/www/extensions/init_dagbag.py
@@ -24,8 +24,9 @@ from airflow.settings import DAGS_FOLDER
def init_dagbag(app):
"""
- Create global DagBag for webserver and API. To access it use
- ``flask.current_app.dag_bag``.
+ Create global DagBag for webserver and API.
+
+ To access it use ``flask.current_app.dag_bag``.
"""
if os.environ.get("SKIP_DAGS_PARSING") == "True":
app.dag_bag = DagBag(os.devnull, include_examples=False)
diff --git a/airflow/www/extensions/init_robots.py
b/airflow/www/extensions/init_robots.py
index 3f7fb48556..c5f037db3d 100644
--- a/airflow/www/extensions/init_robots.py
+++ b/airflow/www/extensions/init_robots.py
@@ -23,8 +23,10 @@ log = logging.getLogger(__name__)
def init_robots(app):
"""
- Add X-Robots-Tag header. Use it to avoid search engines indexing airflow.
This mitigates some
- of the risk associated with exposing Airflow to the public internet,
however it does not
+ Add X-Robots-Tag header.
+
+ Use it to avoid search engines indexing airflow. This mitigates some of
the risk
+ associated with exposing Airflow to the public internet, however it does
not
address the real security risks associated with such a deployment.
See also:
https://developers.google.com/search/docs/advanced/robots/robots_meta_tag#xrobotstag
diff --git a/airflow/www/extensions/init_security.py
b/airflow/www/extensions/init_security.py
index ea3c2211c9..41a8dc6afc 100644
--- a/airflow/www/extensions/init_security.py
+++ b/airflow/www/extensions/init_security.py
@@ -30,8 +30,9 @@ log = logging.getLogger(__name__)
def init_xframe_protection(app):
"""
- Add X-Frame-Options header. Use it to avoid click-jacking attacks, by
ensuring that their content is not
- embedded into other sites.
+ Add X-Frame-Options header.
+
+ Use it to avoid click-jacking attacks, by ensuring that their content is
not embedded into other sites.
See also:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
"""
diff --git a/airflow/www/fab_security/manager.py
b/airflow/www/fab_security/manager.py
index 44d6974625..223ffbffc5 100644
--- a/airflow/www/fab_security/manager.py
+++ b/airflow/www/fab_security/manager.py
@@ -80,10 +80,7 @@ log = logging.getLogger(__name__)
def _oauth_tokengetter(token=None):
- """
- Default function to return the current user oauth token
- from session cookie.
- """
+ """Default function to return the current user oauth token from session
cookie."""
token = session.get("oauth")
log.debug("Token Get: %s", token)
return token
@@ -470,11 +467,10 @@ class BaseSecurityManager:
def oauth_user_info_getter(self, f):
"""
- Decorator function to be the OAuth user info getter
- for all the providers, receives provider and response
- return a dict with the information returned from the provider.
- The returned user info dict should have it's keys with the same
- name as the User Model.
+ Decorator function to be the OAuth user info getter for all the
providers.
+
+ Receives provider and response return a dict with the information
returned from the provider.
+ The returned user info dict should have it's keys with the same name
as the User Model.
Use it like this an example for GitHub ::
@@ -500,8 +496,9 @@ class BaseSecurityManager:
def get_oauth_token_key_name(self, provider):
"""
- Returns the token_key name for the oauth provider
- if none is configured defaults to oauth_token
+ Returns the token_key name for the oauth provider.
+
+ If none is configured defaults to oauth_token
this is configured using OAUTH_PROVIDERS and token_key key.
"""
for _provider in self.oauth_providers:
@@ -1504,8 +1501,9 @@ class BaseSecurityManager:
def delete_permission(self, action_name: str, resource_name: str) -> None:
"""
- Deletes the permission linking an action->resource pair. Doesn't
delete the
- underlying action or resource.
+ Deletes the permission linking an action->resource pair.
+
+ Doesn't delete the underlying action or resource.
:param action_name: Name of existing action
:param resource_name: Name of existing resource
diff --git a/airflow/www/fab_security/sqla/manager.py
b/airflow/www/fab_security/sqla/manager.py
index 6ce9580c29..83e2119492 100644
--- a/airflow/www/fab_security/sqla/manager.py
+++ b/airflow/www/fab_security/sqla/manager.py
@@ -42,8 +42,7 @@ log = logging.getLogger(__name__)
class SecurityManager(BaseSecurityManager):
"""
- Responsible for authentication, registering security views,
- role and permission auto management.
+ Responsible for authentication, registering security views, role and
permission auto management.
If you want to change anything just inherit and override, then
pass your own security manager to AppBuilder.
@@ -281,8 +280,7 @@ class SecurityManager(BaseSecurityManager):
self, resource_name: str, action_name: str, role_ids: list[int]
) -> bool:
"""
- Method to efficiently check if a certain permission exists
- on a list of role id's. This is used by `has_access`.
+ Efficiently check if a certain permission exists on a list of role
ids; used by `has_access`.
:param resource_name: The view's name to check if exists on one of the
roles
:param action_name: The permission name to check if exists
@@ -507,8 +505,9 @@ class SecurityManager(BaseSecurityManager):
def delete_permission(self, action_name: str, resource_name: str) -> None:
"""
- Deletes the permission linking an action->resource pair. Doesn't
delete the
- underlying action or resource.
+ Deletes the permission linking an action->resource pair.
+
+ Doesn't delete the underlying action or resource.
:param action_name: Name of existing action
:param resource_name: Name of existing resource
diff --git a/airflow/www/forms.py b/airflow/www/forms.py
index cc16229ac9..cb4743ed71 100644
--- a/airflow/www/forms.py
+++ b/airflow/www/forms.py
@@ -100,10 +100,7 @@ class DateTimeForm(FlaskForm):
class DateTimeWithNumRunsForm(FlaskForm):
- """
- Date time and number of runs form for tree view, task duration
- and landing times.
- """
+ """Date time and number of runs form for tree view, task duration and
landing times."""
base_date = DateTimeWithTimezoneField(
"Anchor date", widget=AirflowDateTimePickerWidget(),
default=timezone.utcnow()
diff --git a/airflow/www/security.py b/airflow/www/security.py
index d9efcec7cb..ef0411eb73 100644
--- a/airflow/www/security.py
+++ b/airflow/www/security.py
@@ -1,4 +1,3 @@
-#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
@@ -458,8 +457,9 @@ class AirflowSecurityManager(SecurityManagerOverride,
SecurityManager, LoggingMi
def has_access(self, action_name: str, resource_name: str, user=None) ->
bool:
"""
- Verify whether a given user could perform a certain action
- (e.g can_read, can_write, can_delete) on the given resource.
+ Verify whether a given user could perform a certain action on the
given resource.
+
+ Example actions might include can_read, can_write, can_delete, etc.
:param action_name: action_name on resource (e.g can_read, can_edit).
:param resource_name: name of view-menu or resource.
@@ -487,7 +487,8 @@ class AirflowSecurityManager(SecurityManagerOverride,
SecurityManager, LoggingMi
def has_all_dags_access(self, user) -> bool:
"""
- Has all the dag access in any of the 3 cases:
+ Has all the dag access in any of the 3 cases.
+
1. Role needs to be in (Admin, Viewer, User, Op).
2. Has can_read action on dags resource.
3. Has can_edit action on dags resource.
@@ -533,6 +534,7 @@ class AirflowSecurityManager(SecurityManagerOverride,
SecurityManager, LoggingMi
def _merge_perm(self, action_name: str, resource_name: str) -> None:
"""
Add the new (action, resource) to assoc_permission_role if it doesn't
exist.
+
It will add the related entry to ab_permission and ab_resource two
meta tables as well.
:param action_name: Name of the action
@@ -576,6 +578,8 @@ class AirflowSecurityManager(SecurityManagerOverride,
SecurityManager, LoggingMi
def _get_all_non_dag_permissions(self) -> dict[tuple[str, str],
Permission]:
"""
+ Get permissions except those that are for specific DAGs.
+
Returns a dict with a key of (action_name, resource_name) and value of
permission
with all permissions except those that are for specific DAGs.
"""
@@ -602,6 +606,8 @@ class AirflowSecurityManager(SecurityManagerOverride,
SecurityManager, LoggingMi
def create_dag_specific_permissions(self) -> None:
"""
+ Add permissions to all DAGs.
+
Creates 'can_read', 'can_edit', and 'can_delete' permissions for all
DAGs, along with any `access_control` permissions provided in them.
@@ -627,7 +633,9 @@ class AirflowSecurityManager(SecurityManagerOverride,
SecurityManager, LoggingMi
def update_admin_permission(self) -> None:
"""
- Admin should have all the permissions, except the dag permissions.
+ Add missing permissions to the table for admin.
+
+ Admin should get all the permissions, except the dag permissions
because Admin already has Dags permission.
Add the missing ones to the table for admin.
@@ -649,6 +657,8 @@ class AirflowSecurityManager(SecurityManagerOverride,
SecurityManager, LoggingMi
def sync_roles(self) -> None:
"""
+ Initialize default and custom roles with related permissions.
+
1. Init the default role(Admin, Viewer, User, Op, public)
with related permissions.
2. Init the custom role(dag-user) with related permissions.
@@ -681,8 +691,9 @@ class AirflowSecurityManager(SecurityManagerOverride,
SecurityManager, LoggingMi
access_control: dict[str, Collection[str]] | None = None,
) -> None:
"""
- Sync permissions for given dag id. The dag id surely exists in our dag
bag
- as only / refresh button or DagBag will call this function.
+ Sync permissions for given dag id.
+
+ The dag id surely exists in our dag bag as only / refresh button or
DagBag will call this function.
:param dag_id: the ID of the DAG whose permissions should be updated
:param access_control: a dict where each key is a rolename and
diff --git a/airflow/www/utils.py b/airflow/www/utils.py
index 6d5f4d7420..7541f6445e 100644
--- a/airflow/www/utils.py
+++ b/airflow/www/utils.py
@@ -251,11 +251,12 @@ def generate_pages(
sorting_direction=None,
):
"""
- Generates the HTML for a paging component using a similar logic to the
paging
- auto-generated by Flask managed views. The paging component defines a
number of
- pages visible in the pager (window) and once the user goes to a page
beyond the
- largest visible, it would scroll to the right the page numbers and keeps
the
- current one in the middle of the pager component. When in the last pages,
+ Generates the HTML for a paging component.
+
+ Uses a similar logic to the paging auto-generated by Flask managed views.
The paging
+ component defines a number of pages visible in the pager (window) and once
the user
+ goes to a page beyond the largest visible, it would scroll to the right
the page numbers
+ and keeps the current one in the middle of the pager component. When in
the last pages,
the pages won't scroll and just keep moving until the last page. Pager
also contains
<first, previous, ..., next, last> pages.
This component takes into account custom parameters such as search,
status, and tags
@@ -646,10 +647,11 @@ def get_attr_renderer():
def get_chart_height(dag):
"""
- We use the number of tasks in the DAG as a heuristic to
- approximate the size of generated chart (otherwise the charts are tiny and
unreadable
- when DAGs have a large number of tasks). Ideally nvd3 should allow for
dynamic-height
- charts, that is charts that take up space based on the size of the
components within.
+ Use the number of tasks in the DAG to approximate the size of generated
chart.
+
+ Without this the charts are tiny and unreadable when DAGs have a large
number of tasks).
+ Ideally nvd3 should allow for dynamic-height charts, that is charts that
take up space
+ based on the size of the components within.
TODO(aoen): See [AIRFLOW-1263].
"""
return 600 + len(dag.tasks) * 10
@@ -787,10 +789,9 @@ class
AirflowFilterConverter(fab_sqlafilters.SQLAFilterConverter):
class CustomSQLAInterface(SQLAInterface):
"""
- FAB does not know how to handle columns with leading underscores because
- they are not supported by WTForm. This hack will remove the leading
- '_' from the key to lookup the column names.
+ FAB does not know how to handle columns with leading underscores because
they are not supported by WTForm.
+ This hack will remove the leading '_' from the key to lookup the column
names.
"""
def __init__(self, obj, session: Session | None = None):
diff --git a/airflow/www/views.py b/airflow/www/views.py
index 697b7674dc..8bfd15ef97 100644
--- a/airflow/www/views.py
+++ b/airflow/www/views.py
@@ -289,8 +289,9 @@ def node_dict(node_id, label, node_class):
def dag_to_grid(dag: DagModel, dag_runs: Sequence[DagRun], session: Session):
"""
- Create a nested dict representation of the DAG's TaskGroup and its children
- used to construct the Graph and Grid views.
+ Create a nested dict representation of the DAG's TaskGroup and its
children.
+
+ Used to construct the Graph and Grid views.
"""
query = session.execute(
select(
@@ -593,8 +594,9 @@ def get_task_stats_from_query(qry):
def redirect_or_json(origin, msg, status="", status_code=200):
"""
- Some endpoints are called by javascript,
- returning json will allow us to more elegantly handle side-effects in-page.
+ Returning json will allow us to more elegantly handle side effects in-page.
+
+ This is useful because some endpoints are called by javascript.
"""
if request.headers.get("Accept") == "application/json":
if status == "error" and status_code == 200:
@@ -705,8 +707,9 @@ class Airflow(AirflowBaseView):
@expose("/health")
def health(self):
"""
- An endpoint helping check the health status of the Airflow instance,
- including metadatabase, scheduler and triggerer.
+ An endpoint helping check the health status of the Airflow instance.
+
+ Includes metadatabase, scheduler and triggerer.
"""
airflow_health_status = get_airflow_health()
@@ -3858,8 +3861,10 @@ class Airflow(AirflowBaseView):
@expose("/object/datasets_summary")
@auth.has_access([(permissions.ACTION_CAN_READ,
permissions.RESOURCE_DATASET)])
def datasets_summary(self):
- """Get a summary of datasets, including the datetime they were last
updated and how many updates
- they've ever had.
+ """
+ Get a summary of datasets.
+
+ Includes the datetime they were last updated and how many updates
they've ever had.
"""
allowed_attrs = ["uri", "last_dataset_update"]
@@ -3953,9 +3958,11 @@ class Airflow(AirflowBaseView):
@action_logging
def robots(self):
"""
- Returns a robots.txt file for blocking certain search engine crawlers.
This mitigates some
- of the risk associated with exposing Airflow to the public internet,
however it does not
- address the real security risks associated with such a deployment.
+ Returns a robots.txt file for blocking certain search engine crawlers.
+
+ This mitigates some of the risk associated with exposing Airflow to
the public
+ internet, however it does not address the real security risks
associated with
+ such a deployment.
"""
return send_from_directory(get_airflow_app().static_folder,
"robots.txt")
@@ -4138,7 +4145,8 @@ class DagFilter(BaseFilter):
class AirflowModelView(ModelView):
- """Airflow Mode View.
+ """
+ Airflow Mode View.
Overridden `__getattribute__` to wraps REST methods with action_logger
"""
@@ -4149,7 +4157,9 @@ class AirflowModelView(ModelView):
CustomSQLAInterface = wwwutils.CustomSQLAInterface
def __getattribute__(self, attr):
- """Wraps action REST methods with `action_logging` wrapper
+ """
+ Wraps action REST methods with `action_logging` wrapper.
+
Overriding enables differentiating resource and generation of event
name at the decorator level.
if attr in ["show", "list", "read", "get", "get_list"]:
@@ -4171,8 +4181,9 @@ class AirflowModelView(ModelView):
class AirflowPrivilegeVerifierModelView(AirflowModelView):
"""
- This ModelView prevents ability to pass primary keys of objects relating
to DAGs you shouldn't be able to
- edit. This only holds for the add, update and delete operations.
+ Prevents ability to pass primary keys of objects relating to DAGs you
shouldn't be able to edit.
+
+ This only holds for the add, update and delete operations.
You will still need to use the `action_has_dag_edit_access()` for actions.
"""
@@ -5907,10 +5918,9 @@ class DagDependenciesView(AirflowBaseView):
def add_user_permissions_to_dag(sender, template, context, **extra):
"""
- Adds `.can_edit`, `.can_trigger`, and `.can_delete` properties
- to DAG based on current user's permissions.
- Located in `views.py` rather than the DAG model to keep
- permissions logic out of the Airflow core.
+ Adds `.can_edit`, `.can_trigger`, and `.can_delete` properties to DAG
based on current user's permissions.
+
+ Located in `views.py` rather than the DAG model to keep permissions logic
out of the Airflow core.
"""
if "dag" not in context:
return