diff --git a/airflow/www/api/experimental/endpoints.py b/airflow/www/api/experimental/endpoints.py index 2af00eeee6168..ccf024543f328 100644 --- a/airflow/www/api/experimental/endpoints.py +++ b/airflow/www/api/experimental/endpoints.py @@ -58,8 +58,10 @@ def decorated(*args, **kwargs): def add_deprecation_headers(response: Response): """ - Add `Deprecation HTTP Header Field - `__. + Add Deprecation HTTP Header Field. + + .. seealso:: IETF proposal for the header field + `here `_. """ 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 @@ def add_deprecation_headers(response: Response): @api_experimental.route("/dags//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 c74be2635e1f2..975910fe5077e 100644 --- a/airflow/www/decorators.py +++ b/airflow/www/decorators.py @@ -41,9 +41,10 @@ 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 11c358abb62ec..9c2948e32490f 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 @@ def _init_extension(self, app): 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 @@ def add_separator(self, category, cond=None): 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 0a736b424a069..1cab2bff6bb59 100644 --- a/airflow/www/extensions/init_dagbag.py +++ b/airflow/www/extensions/init_dagbag.py @@ -24,8 +24,9 @@ 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 3f7fb48556281..c5f037db3ddb6 100644 --- a/airflow/www/extensions/init_robots.py +++ b/airflow/www/extensions/init_robots.py @@ -23,8 +23,10 @@ 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 ea3c2211c94f7..41a8dc6afc17f 100644 --- a/airflow/www/extensions/init_security.py +++ b/airflow/www/extensions/init_security.py @@ -30,8 +30,9 @@ 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 44d69746252d2..223ffbffc5153 100644 --- a/airflow/www/fab_security/manager.py +++ b/airflow/www/fab_security/manager.py @@ -80,10 +80,7 @@ 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 @@ def current_user(self): 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 @@ def wraps(provider, response=None): 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 @@ def create_permission(self, action_name: str, resource_name: str) -> Permission 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 6ce9580c29277..83e2119492e65 100644 --- a/airflow/www/fab_security/sqla/manager.py +++ b/airflow/www/fab_security/sqla/manager.py @@ -42,8 +42,7 @@ 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 @@ def permission_exists_in_one_or_more_roles( 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 @@ def create_permission(self, action_name, resource_name) -> Permission | None: 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 cc16229ac966d..cb4743ed71a27 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 fee21dd9d46bd..3fc4a15a1ba74 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 @@ def is_dag_resource(self, resource_name: str) -> bool: 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 @@ def _has_role(self, role_name_or_list: Container, user) -> bool: 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 @@ def clean_perms(self) -> None: 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 @@ def get_all_permissions(self) -> set[tuple[str, str]]: 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 @@ def _get_all_roles_with_permissions(self) -> dict[str, Role]: 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 @@ def create_dag_specific_permissions(self) -> None: 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 @@ def update_admin_permission(self) -> None: 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 @@ def sync_perm_for_dag( 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 6d5f4d74205fe..7541f6445e580 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 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 @@ def __init__(self, datamodel): 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 1e61432c9fcb3..719bc11c1e2f1 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() @@ -3857,8 +3860,10 @@ def dataset_dependencies(self): @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"] @@ -3952,9 +3957,11 @@ def datasets_summary(self): @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") @@ -4137,7 +4144,8 @@ def apply(self, query, func): class AirflowModelView(ModelView): - """Airflow Mode View. + """ + Airflow Mode View. Overridden `__getattribute__` to wraps REST methods with action_logger """ @@ -4148,7 +4156,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"]: @@ -4170,8 +4180,9 @@ def __getattribute__(self, attr): 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 @@ def _calculate_graph(self): 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