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 d3f0900bbb AIP-56 - Create auth manager interface and very basic
implementation of FAB auth manager (#32217)
d3f0900bbb is described below
commit d3f0900bbb68bda36f81475bf64d83d51ee32b22
Author: Vincent <[email protected]>
AuthorDate: Wed Jul 5 12:52:14 2023 -0400
AIP-56 - Create auth manager interface and very basic implementation of FAB
auth manager (#32217)
---
airflow/auth/__init__.py | 17 ++++++++
airflow/auth/managers/__init__.py | 17 ++++++++
airflow/auth/managers/base_auth_manager.py | 35 ++++++++++++++++
airflow/auth/managers/fab/__init__.py | 17 ++++++++
airflow/auth/managers/fab/fab_auth_manager.py | 41 ++++++++++++++++++
airflow/config_templates/config.yml | 7 ++++
airflow/config_templates/default_airflow.cfg | 3 ++
airflow/configuration.py | 20 +++++++++
airflow/www/extensions/init_jinja_globals.py | 3 +-
airflow/www/templates/appbuilder/navbar_right.html | 9 ++--
tests/auh/__init__.py | 16 ++++++++
tests/auh/managers/__init__.py | 16 ++++++++
tests/auh/managers/fab/__init__.py | 16 ++++++++
tests/auh/managers/fab/test_fab_auth_manager.py | 48 ++++++++++++++++++++++
tests/www/views/conftest.py | 1 +
15 files changed, 259 insertions(+), 7 deletions(-)
diff --git a/airflow/auth/__init__.py b/airflow/auth/__init__.py
new file mode 100644
index 0000000000..217e5db960
--- /dev/null
+++ b/airflow/auth/__init__.py
@@ -0,0 +1,17 @@
+#
+# 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
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/airflow/auth/managers/__init__.py
b/airflow/auth/managers/__init__.py
new file mode 100644
index 0000000000..217e5db960
--- /dev/null
+++ b/airflow/auth/managers/__init__.py
@@ -0,0 +1,17 @@
+#
+# 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
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/airflow/auth/managers/base_auth_manager.py
b/airflow/auth/managers/base_auth_manager.py
new file mode 100644
index 0000000000..6a4bb86f16
--- /dev/null
+++ b/airflow/auth/managers/base_auth_manager.py
@@ -0,0 +1,35 @@
+#
+# 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
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from abc import abstractmethod
+
+from airflow.utils.log.logging_mixin import LoggingMixin
+
+
+class BaseAuthManager(LoggingMixin):
+ """
+ Class to derive in order to implement concrete auth managers.
+
+ Auth managers are responsible for any user management related operation
such as login, logout, authz, ...
+ """
+
+ @abstractmethod
+ def get_user_name(self) -> str:
+ """Return the username associated to the user in session."""
+ ...
diff --git a/airflow/auth/managers/fab/__init__.py
b/airflow/auth/managers/fab/__init__.py
new file mode 100644
index 0000000000..217e5db960
--- /dev/null
+++ b/airflow/auth/managers/fab/__init__.py
@@ -0,0 +1,17 @@
+#
+# 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
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/airflow/auth/managers/fab/fab_auth_manager.py
b/airflow/auth/managers/fab/fab_auth_manager.py
new file mode 100644
index 0000000000..4203857168
--- /dev/null
+++ b/airflow/auth/managers/fab/fab_auth_manager.py
@@ -0,0 +1,41 @@
+#
+# 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
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from flask_login import current_user
+
+from airflow.auth.managers.base_auth_manager import BaseAuthManager
+
+
+class FabAuthManager(BaseAuthManager):
+ """
+ Flask-AppBuilder auth manager.
+
+ This auth manager is responsible for providing a backward compatible user
management experience to users.
+ """
+
+ def get_user_name(self) -> str:
+ """
+ Return the username associated to the user in session.
+
+ For backward compatibility reasons, the username in FAB auth manager
is the concatenation of the
+ first name and the last name.
+ """
+ first_name = current_user.first_name or ""
+ last_name = current_user.last_name or ""
+ return f"{first_name} {last_name}".strip()
diff --git a/airflow/config_templates/config.yml
b/airflow/config_templates/config.yml
index d588a7f26e..273f2a36c7 100644
--- a/airflow/config_templates/config.yml
+++ b/airflow/config_templates/config.yml
@@ -70,6 +70,13 @@ core:
type: string
example: ~
default: "SequentialExecutor"
+ auth_manager:
+ description: |
+ The auth manager class that airflow should use. Full import path to
the auth manager class.
+ version_added: ~
+ type: string
+ example: ~
+ default: "airflow.auth.managers.fab.fab_auth_manager.FabAuthManager"
parallelism:
description: |
This defines the maximum number of task instances that can run
concurrently per scheduler in
diff --git a/airflow/config_templates/default_airflow.cfg
b/airflow/config_templates/default_airflow.cfg
index ae6bdec085..439944022f 100644
--- a/airflow/config_templates/default_airflow.cfg
+++ b/airflow/config_templates/default_airflow.cfg
@@ -58,6 +58,9 @@ default_timezone = utc
# full import path to the class when using a custom executor.
executor = SequentialExecutor
+# The auth manager class that airflow should use. Full import path to the auth
manager class.
+auth_manager = airflow.auth.managers.fab.fab_auth_manager.FabAuthManager
+
# This defines the maximum number of task instances that can run concurrently
per scheduler in
# Airflow, regardless of the worker count. Generally this value, multiplied by
the number of
# schedulers in your cluster, is the maximum number of task instances with the
running
diff --git a/airflow/configuration.py b/airflow/configuration.py
index 288595efbd..6a55a6c99c 100644
--- a/airflow/configuration.py
+++ b/airflow/configuration.py
@@ -42,6 +42,7 @@ from urllib.parse import urlsplit
from typing_extensions import overload
+from airflow.auth.managers.base_auth_manager import BaseAuthManager
from airflow.exceptions import AirflowConfigException
from airflow.secrets import DEFAULT_SECRETS_SEARCH_PATH, BaseSecretsBackend
from airflow.utils import yaml
@@ -1743,6 +1744,24 @@ def initialize_secrets_backends() ->
list[BaseSecretsBackend]:
return backend_list
+def initialize_auth_manager() -> BaseAuthManager:
+ """
+ Initialize auth manager.
+
+ * import user manager class
+ * instantiate it and return it
+ """
+ auth_manager_cls = conf.getimport(section="core", key="auth_manager")
+
+ if not auth_manager_cls:
+ raise AirflowConfigException(
+ "No auth manager defined in the config. "
+ "Please specify one using section/key [core/auth_manager]."
+ )
+
+ return auth_manager_cls()
+
+
@functools.lru_cache(maxsize=None)
def _DEFAULT_CONFIG() -> str:
path = _default_config_file_path("default_airflow.cfg")
@@ -1808,4 +1827,5 @@ WEBSERVER_CONFIG = "" # Set by initialize_config
conf = initialize_config()
secrets_backend_list = initialize_secrets_backends()
+auth_manager = initialize_auth_manager()
conf.validate()
diff --git a/airflow/www/extensions/init_jinja_globals.py
b/airflow/www/extensions/init_jinja_globals.py
index 0674a8e4a3..9ef948084c 100644
--- a/airflow/www/extensions/init_jinja_globals.py
+++ b/airflow/www/extensions/init_jinja_globals.py
@@ -21,7 +21,7 @@ import logging
import pendulum
import airflow
-from airflow.configuration import conf
+from airflow.configuration import auth_manager, conf
from airflow.settings import IS_K8S_OR_K8SCELERY_EXECUTOR, STATE_COLORS
from airflow.utils.net import get_hostname
from airflow.utils.platform import get_airflow_git_version
@@ -68,6 +68,7 @@ def init_jinja_globals(app):
"git_version": git_version,
"k8s_or_k8scelery_executor": IS_K8S_OR_K8SCELERY_EXECUTOR,
"rest_api_enabled": False,
+ "auth_manager": auth_manager,
"config_test_connection": conf.get("core", "test_connection",
fallback="Disabled"),
}
diff --git a/airflow/www/templates/appbuilder/navbar_right.html
b/airflow/www/templates/appbuilder/navbar_right.html
index 8e606c65b3..1ade0847a6 100644
--- a/airflow/www/templates/appbuilder/navbar_right.html
+++ b/airflow/www/templates/appbuilder/navbar_right.html
@@ -66,12 +66,9 @@
{% if not current_user.is_anonymous %}
<li class="dropdown">
<a class="dropdown-toggle" href="#">
- <span class="navbar-user-icon" title="{{g.user.get_full_name()}}">
- {% if current_user.first_name and current_user.last_name %}
- <span>{{ (current_user.first_name[0] +
current_user.last_name[0]).upper() }}</span>
- {% else %}
- <span class="material-icons">person</span>
- {% endif %}
+ <span class="navbar-user-icon" title="{{ auth_manager.get_user_name()
}}">
+ {% set user_names = auth_manager.get_user_name().split(" ", 1) %}
+ <span>{% for name in user_names %}{{ name[0].upper() }}{% endfor
%}</span>
</span>
<b class="caret"></b>
</a>
diff --git a/tests/auh/__init__.py b/tests/auh/__init__.py
new file mode 100644
index 0000000000..13a83393a9
--- /dev/null
+++ b/tests/auh/__init__.py
@@ -0,0 +1,16 @@
+# 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
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/tests/auh/managers/__init__.py b/tests/auh/managers/__init__.py
new file mode 100644
index 0000000000..13a83393a9
--- /dev/null
+++ b/tests/auh/managers/__init__.py
@@ -0,0 +1,16 @@
+# 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
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/tests/auh/managers/fab/__init__.py
b/tests/auh/managers/fab/__init__.py
new file mode 100644
index 0000000000..13a83393a9
--- /dev/null
+++ b/tests/auh/managers/fab/__init__.py
@@ -0,0 +1,16 @@
+# 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
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/tests/auh/managers/fab/test_fab_auth_manager.py
b/tests/auh/managers/fab/test_fab_auth_manager.py
new file mode 100644
index 0000000000..16e5151495
--- /dev/null
+++ b/tests/auh/managers/fab/test_fab_auth_manager.py
@@ -0,0 +1,48 @@
+# 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
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from unittest import mock
+
+import pytest
+
+from airflow.auth.managers.fab.fab_auth_manager import FabAuthManager
+from airflow.www.fab_security.sqla.models import User
+
+
[email protected]
+def auth_manager():
+ return FabAuthManager()
+
+
+class TestFabAuthManager:
+ @pytest.mark.parametrize(
+ "first_name,last_name,expected",
+ [
+ ("First", "Last", "First Last"),
+ ("First", None, "First"),
+ (None, "Last", "Last"),
+ ],
+ )
+ @mock.patch("flask_login.utils._get_user")
+ def test_get_user_name(self, mock_current_user, first_name, last_name,
expected, auth_manager):
+ user = User()
+ user.first_name = first_name
+ user.last_name = last_name
+ mock_current_user.return_value = user
+
+ assert auth_manager.get_user_name() == expected
diff --git a/tests/www/views/conftest.py b/tests/www/views/conftest.py
index 209a106df6..25e3a6d20a 100644
--- a/tests/www/views/conftest.py
+++ b/tests/www/views/conftest.py
@@ -173,6 +173,7 @@ class _TemplateWithContext(NamedTuple):
"scheduler_job",
# airflow.www.views.AirflowBaseView.extra_args
"macros",
+ "auth_manager",
]
for key in keys_to_delete:
del result[key]