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