This is an automated email from the ASF dual-hosted git repository.
vincbeck 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 e3dd28607b9 Document team-scoped plugins in multi-team mode (#73995)
e3dd28607b9 is described below
commit e3dd28607b93a5614c30781cd6d51ee594d1be47
Author: Niko Oliveira <[email protected]>
AuthorDate: Fri Oct 2 08:48:58 2026 -0700
Document team-scoped plugins in multi-team mode (#73995)
The plugins page now says what team_name does for each plugin attribute,
what
stays global and why, and where the scoping is logical rather than an
isolation boundary. There is a new section in the multi-team docs page
with a few notes that then links to a new section in the plugins.rst
which contains most of the information.
---
.../docs/administration-and-deployment/plugins.rst | 172 ++++++++++++++++++++-
airflow-core/docs/core-concepts/multi-team.rst | 16 +-
2 files changed, 185 insertions(+), 3 deletions(-)
diff --git a/airflow-core/docs/administration-and-deployment/plugins.rst
b/airflow-core/docs/administration-and-deployment/plugins.rst
index c209a403bbe..c0cd215b1cd 100644
--- a/airflow-core/docs/administration-and-deployment/plugins.rst
+++ b/airflow-core/docs/administration-and-deployment/plugins.rst
@@ -119,6 +119,9 @@ looks like:
class AirflowPlugin:
# The name of your plugin (str)
name = None
+ # The team owning this plugin (str), when multi-team mode is enabled.
None means the
+ # plugin is global. Ignored when multi-team mode is off. See
"Multi-team deployments" below.
+ team_name = None
# A list of references to inject into the macros namespace
macros = []
# A list of dictionaries containing FastAPI app objects and some
metadata. See the example below.
@@ -276,8 +279,12 @@ definitions in Airflow.
Authentication is not authorization. ``GetUserDep`` establishes *who* is
calling;
whether that user may perform a given action remains the plugin's own
decision. This
- applies to team scoping too — in a multi-team deployment, a plugin that
does not check
- the caller's team serves every team's users the same data.
+ applies to team scoping too and a global plugin that does not check the
caller's team
+ serves every team's users the same data. The one exception is a plugin
that declares a
+ ``team_name`` in a deployment with ``[core] multi_team`` enabled: Airflow
then
+ authenticates its app and restricts it to that team's users, as described
in
+ :ref:`plugins-multi-team`. With multi-team mode off, that plugin's app is
mounted like
+ any other (unauthenticated).
The core API's access helpers can enforce that decision for you. For
example,
``requires_access_dag`` restricts a route to callers allowed the requested
action on a
@@ -514,6 +521,167 @@ Right-to-left languages are handled automatically: the UI
derives text direction
code (via the browser's locale data, e.g. Persian ``fa`` or Urdu ``ur``), so a
custom RTL language
flips the whole UI to right-to-left without any extra configuration.
+.. _plugins-multi-team:
+
+Multi-team deployments
+----------------------
+
+.. versionadded:: 3.4.0
+
+A plugin can name the team that owns it by setting ``team_name``. Airflow then
offers what the
+plugin contributes to that team only, instead of to the whole deployment:
+
+.. code-block:: python
+
+ from airflow.plugins_manager import AirflowPlugin
+
+ from my_package.payments import PaymentWindowTimetable, settlement_date
+
+
+ class PaymentsPlugin(AirflowPlugin):
+ name = "payments"
+ # Only this team's Dags, tasks and users get the pieces below.
+ team_name = "payments"
+ macros = [settlement_date]
+ timetables = [PaymentWindowTimetable]
+
+``team_name`` is part of the plugin's code, so the plugin author decides it;
there is no
+deployment-time override. Leaving it unset (the default) makes the plugin
**global**: everything
+it contributes is available to every team, which is how plugins written before
multi-team support
+behaved.
+
+``team_name`` only takes effect when :doc:`multi-team mode
</core-concepts/multi-team>` is
+enabled. With ``[core] multi_team = False`` it is ignored and every plugin is
global.
+
+The team must already exist in the metadata database (``airflow teams create
<team_name>``). The
+API server checks this when it loads plugins for the API: a plugin naming an
unknown team is
+recorded as a plugin import error (surfaced under *Admin → Plugins* and at
+``GET /api/v2/plugins/importErrors``) and logged as a warning. It is
deliberately not raised, so
+one misconfigured plugin does not stop the API server, or the other plugins,
from starting. This is
+cached for the life of the process, so creating the team afterwards does not
clear
+the error until the API server restarts. The plugin still loads, and
everything it scopes to the
+nonexistent team is unusable in the meantime. Its scheduling classes are
refused for every Dag,
+its macros resolve for no task, and its extra links are shown nowhere.
+
+Team validation of plugins runs in the API server. Plugin loading in the other
components
+does no team lookup at all, and the subprocess that imports Dag files has no
database session.
+
+Plugin *discovery* is unchanged: every Airflow component still loads every
installed plugin, and
+``team_name`` decides who is offered what. ``airflow plugins`` lists each
plugin's ``team_name``.
+
+.. list-table::
+ :header-rows: 1
+ :widths: 34 66
+
+ * - Plugin attribute
+ - Effect of ``team_name``
+ * - ``fastapi_apps``
+ - App is mounted behind a team check; only that team's users may call it.
+ * - ``fastapi_root_middlewares``
+ - Skipped, with a warning. A root middleware cannot be scoped to one team.
+ * - ``external_views``, ``react_apps``
+ - Only offered in the UI to that team's users.
+ * - ``macros``
+ - Only resolvable when rendering templates for that team's tasks.
+ * - ``global_operator_extra_links``, ``operator_extra_links``
+ - Only shown on task instances of that team's Dags.
+ * - ``timetables``, ``partition_mappers``, ``windows``,
``deadline_references``,
+ ``priority_weight_strategies``
+ - Only usable by that team's Dags.
+ * - ``listeners``
+ - None. Every listener receives events for all teams.
+ * - ``ui_translations``, ``hook_lineage_readers``, ``flask_blueprints``,
+ ``appbuilder_views``, ``appbuilder_menu_items``, ``admin_views``,
``menu_links``
+ - None. These stay global.
+
+Listeners are deployment-wide by design: every listener, including one a team
plugin registers,
+receives events for every team's Dags. Their hooks fire in shared components
such as the scheduler
+and the API server, so choosing which listeners run is the Deployment
Manager's responsibility.
+
+API endpoints
+^^^^^^^^^^^^^
+
+A team plugin's ``fastapi_apps`` are mounted with a middleware that resolves
the caller (bearer
+token or UI session cookie, exactly as the core API does) and asks the auth
manager whether that
+user is authorized for the team. A caller the middleware cannot authenticate
gets the same
+``401``/``403`` the core API returns for that token; an authenticated caller
who is not authorized
+for the team gets ``403``. The request's HTTP method is mapped onto one of
Airflow's resource
+methods (``GET``/``HEAD``/``OPTIONS`` → ``GET``, ``POST`` → ``POST``,
``PUT``/``PATCH`` → ``PUT``,
+``DELETE`` → ``DELETE``) before it is passed on, so an auth manager that
distinguishes methods can
+grant a team read-only access to its own plugin. Auth managers that only check
membership can
+ignore it.
+
+This is the only authorization Airflow adds to a plugin app. Whether the
caller may perform a
+given action *within* the team is still the plugin's decision, and a
**global** plugin's app gets
+no authentication and no team check at all — see the warning in :ref:`the
example above
+<plugin-example>`.
+
+``fastapi_root_middlewares`` are not scoped. A root middleware wraps every
request to the API
+server, including core routes and other teams' plugins, so one declared by a
team plugin is
+skipped and a warning is logged. A team plugin that needs middleware should
apply it inside its
+own FastAPI app, where it only sees that app's requests.
+
+UI elements
+^^^^^^^^^^^
+
+The UI builds its navigation items, external views and React apps from ``GET
/api/v2/plugins``,
+which returns global plugins plus the plugins of teams the caller is
authorized for. A team
+plugin's UI pieces are therefore only offered to that team's users. The
endpoint still requires
+the existing *Plugins* view permission; team scoping narrows what that
permission returns rather
+than introducing a separate one.
+
+``GET /api/v2/plugins/importErrors`` is **not** filtered by team: anyone with
the *Plugins* view
+permission sees the import errors of all plugins, including their source paths
and error text. A
+plugin load failure is deployment-level information that the person debugging
it needs, so it is
+reported the same way to everyone who may see the plugins page at all.
+
+Macros
+^^^^^^
+
+``{{ macros.<plugin_name>.<macro> }}`` resolves only for that team's tasks. A
task of another
+team, or of a teamless Dag, gets an ``AttributeError`` naming the owning team.
Built-in macros and
+global plugins' macros are unaffected.
+
+This is logical scoping not an isolation boundary. The plugin's macro module
is imported into
+the worker process like any other, so task code that goes looking for it —
through
+``sys.modules``, say — will still find it. The scoping keeps one team's macros
out of another
+team's templates; it does not stop a task author who sets out to reach them.
+
+Operator extra links
+^^^^^^^^^^^^^^^^^^^^
+
+Extra links a team plugin registers, through either
``global_operator_extra_links`` or
+``operator_extra_links``, are shown only on task instances of that team's Dags
(not on another
+team's Dags and not on teamless ones). Filtering is by the team of the Dag the
link would be
+rendered for.
+
+Links that a global plugin registers as well as links that an operator defines
itself are
+untouched. A link class registered by both a team plugin and a global plugin
stays visible
+everywhere: the global registration wins, so a team plugin cannot withdraw a
link from the
+rest of the deployment.
+
+Scheduling classes
+^^^^^^^^^^^^^^^^^^
+
+Timetables, partition mappers, windows, deadline references and priority
weight strategies are
+named by a Dag directly, so scoping them means deciding which Dags may name
them. A Dag's team is
+the team that owns the bundle it was parsed from (see
:ref:`multi-team-dag-bundles`). If only
+team-scoped plugins register a class, a Dag that names it and does not belong
to one of those
+teams is **not stored**, and gets an import error naming the class, its owning
team and the two
+ways out: move the Dag into a bundle owned by that team, or have the plugin
provide the class
+globally. The rest of the bundle is stored normally, so one such Dag does not
take its neighbours
+down with it.
+
+A class that any global plugin also registers stays available to every Dag.
Airflow's own
+timetables, partition mappers and windows (anything under
``airflow.timetables.`` or
+``airflow.partition_mappers.``) are never team-owned, even if a team plugin
lists them:
+deserialization imports those paths directly and never consults plugins.
+
+One gap remains: a partition mapper that a timetable picks inside
``get_partition_mapper()`` is
+not covered, because nothing names it until the timetable runs. As with
macros, this is logical
+scoping, it decides which Dags Airflow will schedule with a class, not what
Dag code is able to
+import.
+
Exclude views from CSRF protection
----------------------------------
diff --git a/airflow-core/docs/core-concepts/multi-team.rst
b/airflow-core/docs/core-concepts/multi-team.rst
index da1ca415a0e..30124ee395b 100644
--- a/airflow-core/docs/core-concepts/multi-team.rst
+++ b/airflow-core/docs/core-concepts/multi-team.rst
@@ -83,6 +83,7 @@ When Multi-Team mode is enabled, the following resources can
be scoped to specif
- **Connections**: Team members can only access connections owned by their
team or global connections
- **Pools**: Pools can be assigned to teams
- **XComs**: Tasks can only access XComs of Dags in their own team (plus, for
reads, global Dags)
+- **Plugins**: A plugin can declare the team that owns it, scoping what it
contributes to that team
Resources without a team assignment are considered **global** and accessible
to all teams.
@@ -109,6 +110,20 @@ Secrets Backends are supported on a case by case basis.
When a task requests a Variable or Connection, the secrets backend will return
a team-specific value, if any. The
backend will automatically resolve the correct value based on the requesting
task's team.
+Plugins
+"""""""
+
+A plugin declares the team that owns it by setting ``team_name`` in its own
code. Airflow then offers its
+API endpoints, UI views, macros, operator extra links and scheduling classes
(timetables, partition mappers,
+deadline references, etc) to that team only. A plugin that leaves
``team_name`` unset is global, as every
+plugin written before this feature is.
+
+Listeners are deployment-wide: every listener receives events for all teams,
and choosing which listeners run
+is the Deployment Manager's responsibility.
+
+See :ref:`plugins-multi-team` for what each plugin attribute does, and for the
cases where the scoping is
+logical rather than enforceable.
+
Auth Manager
""""""""""""
@@ -1147,7 +1162,6 @@ Multi-Team mode is currently an experimental feature in
preview. It is not yet f
- Some UI elements may not be fully team-aware
- Command and Secrets based lookup for team based configuration
-- Plugin support
Global Uniqueness of Identifiers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^