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 228c48c7ac2 Use Airflow config in `SimpleAuthManager` instead of Flask 
app config (#45059)
228c48c7ac2 is described below

commit 228c48c7ac20155e5d049fd40cc58b33a578a867
Author: Vincent <[email protected]>
AuthorDate: Fri Dec 20 17:12:09 2024 -0500

    Use Airflow config in `SimpleAuthManager` instead of Flask app config 
(#45059)
---
 .../auth/managers/simple/simple_auth_manager.py    | 22 +++--
 airflow/auth/managers/simple/views/auth.py         |  3 -
 airflow/config_templates/config.yml                | 18 ++++
 .../config_templates/default_webserver_config.py   | 20 -----
 .../core-concepts/auth-manager/simple.rst          | 27 +++---
 .../managers/simple/test_simple_auth_manager.py    | 97 +++++++++++++---------
 tests/auth/managers/simple/views/test_auth.py      | 13 +--
 7 files changed, 102 insertions(+), 98 deletions(-)

diff --git a/airflow/auth/managers/simple/simple_auth_manager.py 
b/airflow/auth/managers/simple/simple_auth_manager.py
index d63aa480c9d..67eb373ced7 100644
--- a/airflow/auth/managers/simple/simple_auth_manager.py
+++ b/airflow/auth/managers/simple/simple_auth_manager.py
@@ -30,7 +30,7 @@ from termcolor import colored
 from airflow.auth.managers.base_auth_manager import BaseAuthManager, 
ResourceMethod
 from airflow.auth.managers.simple.user import SimpleAuthManagerUser
 from airflow.auth.managers.simple.views.auth import 
SimpleAuthManagerAuthenticationViews
-from airflow.configuration import AIRFLOW_HOME
+from airflow.configuration import AIRFLOW_HOME, conf
 
 if TYPE_CHECKING:
     from airflow.auth.managers.models.resource_details import (
@@ -73,8 +73,6 @@ class 
SimpleAuthManager(BaseAuthManager[SimpleAuthManagerUser]):
 
     Default auth manager used in Airflow. This auth manager should not be used 
in production.
     This auth manager is very basic and only intended for development and 
testing purposes.
-
-    :param appbuilder: the flask app builder
     """
 
     # Cache containing the password associated to a username
@@ -87,9 +85,12 @@ class 
SimpleAuthManager(BaseAuthManager[SimpleAuthManagerUser]):
             "simple_auth_manager_passwords.json.generated",
         )
 
+    @staticmethod
+    def get_users() -> list[dict[str, str]]:
+        users = [u.split(":") for u in conf.getlist("core", 
"simple_auth_manager_users")]
+        return [{"username": username, "role": role} for username, role in 
users]
+
     def init(self) -> None:
-        if not self.appbuilder:
-            return
         user_passwords_from_file = {}
 
         # Read passwords from file
@@ -98,7 +99,7 @@ class 
SimpleAuthManager(BaseAuthManager[SimpleAuthManagerUser]):
                 passwords_str = file.read().strip()
                 user_passwords_from_file = json.loads(passwords_str)
 
-        users = 
self.appbuilder.get_app.config.get("SIMPLE_AUTH_MANAGER_USERS", [])
+        users = self.get_users()
         usernames = {user["username"] for user in users}
         self.passwords = {
             username: password
@@ -116,10 +117,7 @@ class 
SimpleAuthManager(BaseAuthManager[SimpleAuthManagerUser]):
             file.write(json.dumps(self.passwords))
 
     def is_logged_in(self) -> bool:
-        return "user" in session or (
-            self.appbuilder is not None
-            and 
self.appbuilder.get_app.config.get("SIMPLE_AUTH_MANAGER_ALL_ADMINS", False)
-        )
+        return "user" in session or conf.getboolean("core", 
"simple_auth_manager_all_admins")
 
     def get_url_login(self, **kwargs) -> str:
         """Return the login page url."""
@@ -131,7 +129,7 @@ class 
SimpleAuthManager(BaseAuthManager[SimpleAuthManagerUser]):
     def get_user(self) -> SimpleAuthManagerUser | None:
         if not self.is_logged_in():
             return None
-        if self.appbuilder and 
self.appbuilder.get_app.config.get("SIMPLE_AUTH_MANAGER_ALL_ADMINS", False):
+        if conf.getboolean("core", "simple_auth_manager_all_admins"):
             return SimpleAuthManagerUser(username="anonymous", role="admin")
         else:
             return session["user"]
@@ -227,7 +225,7 @@ class 
SimpleAuthManager(BaseAuthManager[SimpleAuthManagerUser]):
             return
         self.appbuilder.add_view_no_menu(
             SimpleAuthManagerAuthenticationViews(
-                
users=self.appbuilder.get_app.config.get("SIMPLE_AUTH_MANAGER_USERS", []),
+                users=self.get_users(),
                 passwords=self.passwords,
             )
         )
diff --git a/airflow/auth/managers/simple/views/auth.py 
b/airflow/auth/managers/simple/views/auth.py
index bd06661d833..6bf92cf0bc7 100644
--- a/airflow/auth/managers/simple/views/auth.py
+++ b/airflow/auth/managers/simple/views/auth.py
@@ -16,7 +16,6 @@
 # under the License.
 from __future__ import annotations
 
-import logging
 from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
 
 from flask import redirect, request, session, url_for
@@ -30,8 +29,6 @@ from airflow.www.app import csrf
 from airflow.www.extensions.init_auth_manager import get_auth_manager
 from airflow.www.views import AirflowBaseView
 
-logger = logging.getLogger(__name__)
-
 
 class SimpleAuthManagerAuthenticationViews(AirflowBaseView):
     """
diff --git a/airflow/config_templates/config.yml 
b/airflow/config_templates/config.yml
index b7526eff837..1a2d664955d 100644
--- a/airflow/config_templates/config.yml
+++ b/airflow/config_templates/config.yml
@@ -92,6 +92,24 @@ core:
       type: string
       example: ~
       default: 
"airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager"
+    simple_auth_manager_users:
+      description: |
+        The list of users and their associated role in simple auth manager. If 
the simple auth manager is
+        used in your environment, this list controls who can access the 
environment.
+
+        List of user-role delimited with a space. Each user-role is a comma 
delimited couple of username and
+        role. Roles are predefined in simple auth managers: viewer, user, op, 
admin.
+      version_added: 3.0.0
+      type: string
+      example: "bob:admin,peter:viewer"
+      default: "admin:admin"
+    simple_auth_manager_all_admins:
+      description: |
+        Whether to disable authentication and allow everyone as admin in the 
environment.
+      version_added: 3.0.0
+      type: string
+      example: ~
+      default: "False"
     parallelism:
       description: |
         This defines the maximum number of task instances that can run 
concurrently per scheduler in
diff --git a/airflow/config_templates/default_webserver_config.py 
b/airflow/config_templates/default_webserver_config.py
index dda8a25ad94..4ad8ee6743f 100644
--- a/airflow/config_templates/default_webserver_config.py
+++ b/airflow/config_templates/default_webserver_config.py
@@ -130,23 +130,3 @@ AUTH_TYPE = AUTH_DB
 # APP_THEME = "superhero.css"
 # APP_THEME = "united.css"
 # APP_THEME = "yeti.css"
-
-# ----------------------------------------------------
-# Simple auth manager config
-# ----------------------------------------------------
-# This list contains the list of users and their associated role in simple 
auth manager.
-# If the simple auth manager is used in your environment, this list controls 
who can access the environment.
-# Example:
-# [{
-#     "username": "admin",
-#     "role": "admin",
-# }]
-SIMPLE_AUTH_MANAGER_USERS = [
-    {
-        "username": "admin",
-        "role": "admin",
-    }
-]
-
-# Turn this flag on to disable authentication and allow everyone as admin
-SIMPLE_AUTH_MANAGER_ALL_ADMINS = False
diff --git a/docs/apache-airflow/core-concepts/auth-manager/simple.rst 
b/docs/apache-airflow/core-concepts/auth-manager/simple.rst
index f418ca15f29..f71084c3d7e 100644
--- a/docs/apache-airflow/core-concepts/auth-manager/simple.rst
+++ b/docs/apache-airflow/core-concepts/auth-manager/simple.rst
@@ -31,23 +31,24 @@ the logic and implementation of the simple auth manager is 
**simple**.
 Manage users
 ------------
 
-Users are managed through the `webserver config file 
<https://airflow.apache.org/docs/apache-airflow/stable/configurations-ref.html#config-file>`__.
-In this file, the list of users are defined in the constant 
``SIMPLE_AUTH_MANAGER_USERS``. Example:
+Users are managed through the Airflow configuration. Example:
 
-.. code-block:: python
+.. code-block:: ini
 
-  SIMPLE_AUTH_MANAGER_USERS = [
-      {
-          "username": "admin",
-          "role": "admin",
-      }
-  ]
+  [core]
+  simple_auth_manager_users = "bob:admin,peter:viewer"
 
+The list of users are separated with a comma and each user is a couple 
username/role separated by a colon.
 Each user needs two pieces of information:
 
 * **username**. The user's username
 * **role**. The role associated to the user. For more information about these 
roles, :ref:`see next section <roles-permissions>`.
 
+In the example above, two users are defined:
+
+* **bob** whose role is **admin**
+* **peter** whose role is **viewer**
+
 The password is auto-generated for each user and printed out in the webserver 
logs.
 When generated, these passwords are also saved in your environment, therefore 
they will not change if you stop or restart your environment.
 
@@ -75,9 +76,9 @@ Disable authentication and allow everyone as admin
 This option allow you to disable authentication and allow everyone as admin.
 As a consequence, whoever access the Airflow UI is automatically logged in as 
an admin with all permissions.
 
-To enable this feature, you need to set the constant 
``SIMPLE_AUTH_MANAGER_ALL_ADMINS`` to ``True`` in the `webserver config file 
<https://airflow.apache.org/docs/apache-airflow/stable/configurations-ref.html#config-file>`__.
-Example:
+you can enable this feature through the config. Example:
 
-.. code-block:: python
+.. code-block:: ini
 
-  SIMPLE_AUTH_MANAGER_ALL_ADMINS = True
+  [core]
+  simple_auth_manager_all_admins = "True"
diff --git a/tests/auth/managers/simple/test_simple_auth_manager.py 
b/tests/auth/managers/simple/test_simple_auth_manager.py
index 0cc553ad420..bf12e6b15d6 100644
--- a/tests/auth/managers/simple/test_simple_auth_manager.py
+++ b/tests/auth/managers/simple/test_simple_auth_manager.py
@@ -28,6 +28,8 @@ from airflow.auth.managers.simple.user import 
SimpleAuthManagerUser
 from airflow.auth.managers.simple.views.auth import 
SimpleAuthManagerAuthenticationViews
 from airflow.www.extensions.init_appbuilder import init_appbuilder
 
+from tests_common.test_utils.config import conf_vars
+
 
 @pytest.fixture
 def auth_manager():
@@ -48,49 +50,62 @@ def test_user():
 
 class TestSimpleAuthManager:
     @pytest.mark.db_test
-    def test_init_with_no_user(self, auth_manager_with_appbuilder):
-        auth_manager_with_appbuilder.init()
-        with open(auth_manager_with_appbuilder.get_generated_password_file()) 
as file:
+    def test_get_users(self, auth_manager):
+        with conf_vars(
+            {
+                ("core", "simple_auth_manager_users"): 
"test1:viewer,test2:viewer",
+            }
+        ):
+            users = auth_manager.get_users()
+            assert users == [{"role": "viewer", "username": "test1"}, {"role": 
"viewer", "username": "test2"}]
+
+    @pytest.mark.db_test
+    def test_init_with_default_user(self, auth_manager):
+        auth_manager.init()
+        with open(auth_manager.get_generated_password_file()) as file:
             passwords_str = file.read().strip()
             user_passwords_from_file = json.loads(passwords_str)
 
-            assert user_passwords_from_file == {}
+            assert len(user_passwords_from_file) == 1
 
     @pytest.mark.db_test
-    def test_init_with_users(self, auth_manager_with_appbuilder):
-        
auth_manager_with_appbuilder.appbuilder.app.config["SIMPLE_AUTH_MANAGER_USERS"] 
= [
+    def test_init_with_users(self, auth_manager):
+        with conf_vars(
             {
-                "username": "test",
-                "role": "admin",
+                ("core", "simple_auth_manager_users"): 
"test1:viewer,test2:viewer",
             }
-        ]
-        auth_manager_with_appbuilder.init()
-        with open(auth_manager_with_appbuilder.get_generated_password_file()) 
as file:
-            passwords_str = file.read().strip()
-            user_passwords_from_file = json.loads(passwords_str)
+        ):
+            auth_manager.init()
+            with open(auth_manager.get_generated_password_file()) as file:
+                passwords_str = file.read().strip()
+                user_passwords_from_file = json.loads(passwords_str)
 
-            assert len(user_passwords_from_file) == 1
+                assert len(user_passwords_from_file) == 2
 
     @pytest.mark.db_test
-    def test_is_logged_in(self, auth_manager_with_appbuilder, app, test_user):
+    def test_is_logged_in(self, auth_manager, app, test_user):
         with app.test_request_context():
             session["user"] = test_user
-            result = auth_manager_with_appbuilder.is_logged_in()
+            result = auth_manager.is_logged_in()
         assert result
 
     @pytest.mark.db_test
-    def test_is_logged_in_return_false_when_no_user_in_session(self, 
auth_manager_with_appbuilder, app):
+    def test_is_logged_in_return_false_when_no_user_in_session(self, 
auth_manager, app):
         with app.test_request_context():
-            result = auth_manager_with_appbuilder.is_logged_in()
+            result = auth_manager.is_logged_in()
 
         assert result is False
 
     @pytest.mark.db_test
-    def test_is_logged_in_with_all_admins(self, auth_manager_with_appbuilder, 
app):
-        
auth_manager_with_appbuilder.appbuilder.app.config["SIMPLE_AUTH_MANAGER_ALL_ADMINS"]
 = True
-        with app.test_request_context():
-            result = auth_manager_with_appbuilder.is_logged_in()
-        assert result
+    def test_is_logged_in_with_all_admins(self, auth_manager, app):
+        with conf_vars(
+            {
+                ("core", "simple_auth_manager_all_admins"): "True",
+            }
+        ):
+            with app.test_request_context():
+                result = auth_manager.is_logged_in()
+            assert result
 
     @patch("airflow.auth.managers.simple.simple_auth_manager.url_for")
     def test_get_url_login(self, mock_url_for, auth_manager):
@@ -104,23 +119,27 @@ class TestSimpleAuthManager:
 
     @pytest.mark.db_test
     @patch.object(SimpleAuthManager, "is_logged_in")
-    def test_get_user(self, mock_is_logged_in, auth_manager_with_appbuilder, 
app, test_user):
+    def test_get_user(self, mock_is_logged_in, auth_manager, app, test_user):
         mock_is_logged_in.return_value = True
 
         with app.test_request_context():
             session["user"] = test_user
-            result = auth_manager_with_appbuilder.get_user()
+            result = auth_manager.get_user()
 
         assert result == test_user
 
     @pytest.mark.db_test
     @patch.object(SimpleAuthManager, "is_logged_in")
-    def test_get_user_with_all_admins(self, mock_is_logged_in, 
auth_manager_with_appbuilder, app):
+    def test_get_user_with_all_admins(self, mock_is_logged_in, auth_manager, 
app):
         mock_is_logged_in.return_value = True
 
-        
auth_manager_with_appbuilder.appbuilder.app.config["SIMPLE_AUTH_MANAGER_ALL_ADMINS"]
 = True
-        with app.test_request_context():
-            result = auth_manager_with_appbuilder.get_user()
+        with conf_vars(
+            {
+                ("core", "simple_auth_manager_all_admins"): "True",
+            }
+        ):
+            with app.test_request_context():
+                result = auth_manager.get_user()
 
         assert result.username == "anonymous"
         assert result.role == "admin"
@@ -167,13 +186,13 @@ class TestSimpleAuthManager:
         ],
     )
     def test_is_authorized_methods(
-        self, mock_is_logged_in, auth_manager_with_appbuilder, app, api, 
is_logged_in, role, method, result
+        self, mock_is_logged_in, auth_manager, app, api, is_logged_in, role, 
method, result
     ):
         mock_is_logged_in.return_value = is_logged_in
 
         with app.test_request_context():
             session["user"] = SimpleAuthManagerUser(username="test", role=role)
-            assert getattr(auth_manager_with_appbuilder, api)(method=method) 
is result
+            assert getattr(auth_manager, api)(method=method) is result
 
     @pytest.mark.db_test
     @patch.object(SimpleAuthManager, "is_logged_in")
@@ -201,13 +220,13 @@ class TestSimpleAuthManager:
         ],
     )
     def test_is_authorized_view_methods(
-        self, mock_is_logged_in, auth_manager_with_appbuilder, app, api, 
kwargs, is_logged_in, role, result
+        self, mock_is_logged_in, auth_manager, app, api, kwargs, is_logged_in, 
role, result
     ):
         mock_is_logged_in.return_value = is_logged_in
 
         with app.test_request_context():
             session["user"] = SimpleAuthManagerUser(username="test", role=role)
-            assert getattr(auth_manager_with_appbuilder, api)(**kwargs) is 
result
+            assert getattr(auth_manager, api)(**kwargs) is result
 
     @pytest.mark.db_test
     @patch.object(SimpleAuthManager, "is_logged_in")
@@ -231,13 +250,13 @@ class TestSimpleAuthManager:
         ],
     )
     def test_is_authorized_methods_op_role_required(
-        self, mock_is_logged_in, auth_manager_with_appbuilder, app, api, role, 
method, result
+        self, mock_is_logged_in, auth_manager, app, api, role, method, result
     ):
         mock_is_logged_in.return_value = True
 
         with app.test_request_context():
             session["user"] = SimpleAuthManagerUser(username="test", role=role)
-            assert getattr(auth_manager_with_appbuilder, api)(method=method) 
is result
+            assert getattr(auth_manager, api)(method=method) is result
 
     @pytest.mark.db_test
     @patch.object(SimpleAuthManager, "is_logged_in")
@@ -256,13 +275,13 @@ class TestSimpleAuthManager:
         ],
     )
     def test_is_authorized_methods_user_role_required(
-        self, mock_is_logged_in, auth_manager_with_appbuilder, app, api, role, 
method, result
+        self, mock_is_logged_in, auth_manager, app, api, role, method, result
     ):
         mock_is_logged_in.return_value = True
 
         with app.test_request_context():
             session["user"] = SimpleAuthManagerUser(username="test", role=role)
-            assert getattr(auth_manager_with_appbuilder, api)(method=method) 
is result
+            assert getattr(auth_manager, api)(method=method) is result
 
     @pytest.mark.db_test
     @patch.object(SimpleAuthManager, "is_logged_in")
@@ -281,13 +300,13 @@ class TestSimpleAuthManager:
         ],
     )
     def test_is_authorized_methods_viewer_role_required_for_get(
-        self, mock_is_logged_in, auth_manager_with_appbuilder, app, api, role, 
method, result
+        self, mock_is_logged_in, auth_manager, app, api, role, method, result
     ):
         mock_is_logged_in.return_value = True
 
         with app.test_request_context():
             session["user"] = SimpleAuthManagerUser(username="test", role=role)
-            assert getattr(auth_manager_with_appbuilder, api)(method=method) 
is result
+            assert getattr(auth_manager, api)(method=method) is result
 
     @pytest.mark.db_test
     def test_register_views(self, auth_manager_with_appbuilder):
diff --git a/tests/auth/managers/simple/views/test_auth.py 
b/tests/auth/managers/simple/views/test_auth.py
index 86a8be4f444..0eccf0dc9ec 100644
--- a/tests/auth/managers/simple/views/test_auth.py
+++ b/tests/auth/managers/simple/views/test_auth.py
@@ -36,23 +36,14 @@ def simple_app():
                 "core",
                 "auth_manager",
             ): 
"airflow.auth.managers.simple.simple_auth_manager.SimpleAuthManager",
+            ("core", "simple_auth_manager_users"): "test:admin",
         }
     ):
         with open(SimpleAuthManager.get_generated_password_file(), "w") as 
file:
             user = {"test": "test"}
             file.write(json.dumps(user))
 
-        return application.create_app(
-            testing=True,
-            config={
-                "SIMPLE_AUTH_MANAGER_USERS": [
-                    {
-                        "username": "test",
-                        "role": "admin",
-                    }
-                ]
-            },
-        )
+        return application.create_app(testing=True)
 
 
 @pytest.mark.db_test

Reply via email to