Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow auth managers to override the security manager #32525

Merged
merged 13 commits into from
Jul 24, 2023
4 changes: 2 additions & 2 deletions airflow/api/auth/backend/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

from flask import Response

from airflow.configuration import auth_manager
from airflow.www.extensions.init_auth_manager import get_auth_manager

CLIENT_AUTH: tuple[str, str] | Any | None = None

Expand All @@ -39,7 +39,7 @@ def requires_authentication(function: T):

@wraps(function)
def decorated(*args, **kwargs):
if not auth_manager.is_logged_in():
if not get_auth_manager().is_logged_in():
return Response("Unauthorized", 401, {})
return function(*args, **kwargs)

Expand Down
12 changes: 12 additions & 0 deletions airflow/auth/managers/base_auth_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,15 @@ def get_user_name(self) -> str:
def is_logged_in(self) -> bool:
"""Return whether the user is logged in."""
...

def get_security_manager_override_class(self) -> type:
"""
Return the security manager override class.

The security manager override class is responsible for overriding the default security manager
class airflow.www.security.AirflowSecurityManager with a custom implementation. This class is
essentially inherited from airflow.www.security.AirflowSecurityManager.

By default, return an empty class.
"""
return object
5 changes: 5 additions & 0 deletions airflow/auth/managers/fab/fab_auth_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from flask_login import current_user

from airflow.auth.managers.base_auth_manager import BaseAuthManager
from airflow.auth.managers.fab.security_manager_override import FabAirflowSecurityManagerOverride


class FabAuthManager(BaseAuthManager):
Expand All @@ -43,3 +44,7 @@ def get_user_name(self) -> str:
def is_logged_in(self) -> bool:
"""Return whether the user is logged in."""
return current_user and not current_user.is_anonymous

def get_security_manager_override_class(self) -> type:
"""Return the security manager override."""
return FabAirflowSecurityManagerOverride
220 changes: 220 additions & 0 deletions airflow/auth/managers/fab/security_manager_override.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
#
# 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 functools import cached_property

from flask_appbuilder.const import AUTH_DB, AUTH_LDAP, AUTH_OAUTH, AUTH_OID, AUTH_REMOTE_USER
from flask_babel import lazy_gettext


class FabAirflowSecurityManagerOverride:
"""
This security manager overrides the default AirflowSecurityManager security manager.
vincbeck marked this conversation as resolved.
Show resolved Hide resolved

This security manager is used only if the auth manager FabAuthManager is used. It defines everything in
the security manager that is needed for the FabAuthManager to work. Any operation specific to
the AirflowSecurityManager should be defined here instead of AirflowSecurityManager.

:param appbuilder: The appbuilder.
:param actionmodelview: The obj instance for action model view.
:param authdbview: The class for auth db view.
:param authldapview: The class for auth ldap view.
:param authoauthview: The class for auth oauth view.
:param authoidview: The class for auth oid view.
:param authremoteuserview: The class for auth remote user view.
:param permissionmodelview: The class for permission model view.
:param registeruser_view: The class for register user view.
:param registeruserdbview: The class for register user db view.
:param registeruseroauthview: The class for register user oauth view.
:param registerusermodelview: The class for register user model view.
:param registeruseroidview: The class for register user oid view.
:param resetmypasswordview: The class for reset my password view.
:param resetpasswordview: The class for reset password view.
:param rolemodelview: The class for role model view.
:param userinfoeditview: The class for user info edit view.
:param userdbmodelview: The class for user db model view.
:param userldapmodelview: The class for user ldap model view.
:param useroauthmodelview: The class for user oauth model view.
:param useroidmodelview: The class for user oid model view.
:param userremoteusermodelview: The class for user remote user model view.
:param userstatschartview: The class for user stats chart view.
"""

""" The obj instance for authentication view """
auth_view = None
""" The obj instance for user view """
user_view = None

def __init__(self, **kwargs):
super().__init__(**kwargs)

self.appbuilder = kwargs["appbuilder"]
self.actionmodelview = kwargs["actionmodelview"]
self.authdbview = kwargs["authdbview"]
self.authldapview = kwargs["authldapview"]
self.authoauthview = kwargs["authoauthview"]
self.authoidview = kwargs["authoidview"]
self.authremoteuserview = kwargs["authremoteuserview"]
self.permissionmodelview = kwargs["permissionmodelview"]
self.registeruser_view = kwargs["registeruser_view"]
self.registeruserdbview = kwargs["registeruserdbview"]
self.registeruseroauthview = kwargs["registeruseroauthview"]
self.registerusermodelview = kwargs["registerusermodelview"]
self.registeruseroidview = kwargs["registeruseroidview"]
self.resetmypasswordview = kwargs["resetmypasswordview"]
self.resetpasswordview = kwargs["resetpasswordview"]
self.rolemodelview = kwargs["rolemodelview"]
self.userinfoeditview = kwargs["userinfoeditview"]
self.userdbmodelview = kwargs["userdbmodelview"]
self.userldapmodelview = kwargs["userldapmodelview"]
self.useroauthmodelview = kwargs["useroauthmodelview"]
self.useroidmodelview = kwargs["useroidmodelview"]
self.userremoteusermodelview = kwargs["userremoteusermodelview"]
self.userstatschartview = kwargs["userstatschartview"]

def register_views(self):
"""Register FAB auth manager related views."""
if not self.appbuilder.app.config.get("FAB_ADD_SECURITY_VIEWS", True):
return

if self.auth_user_registration:
if self.auth_type == AUTH_DB:
self.registeruser_view = self.registeruserdbview()
elif self.auth_type == AUTH_OID:
self.registeruser_view = self.registeruseroidview()
elif self.auth_type == AUTH_OAUTH:
self.registeruser_view = self.registeruseroauthview()
if self.registeruser_view:
self.appbuilder.add_view_no_menu(self.registeruser_view)

self.appbuilder.add_view_no_menu(self.resetpasswordview())
self.appbuilder.add_view_no_menu(self.resetmypasswordview())
self.appbuilder.add_view_no_menu(self.userinfoeditview())

if self.auth_type == AUTH_DB:
self.user_view = self.userdbmodelview
self.auth_view = self.authdbview()
elif self.auth_type == AUTH_LDAP:
self.user_view = self.userldapmodelview
self.auth_view = self.authldapview()
elif self.auth_type == AUTH_OAUTH:
self.user_view = self.useroauthmodelview
self.auth_view = self.authoauthview()
elif self.auth_type == AUTH_REMOTE_USER:
self.user_view = self.userremoteusermodelview
self.auth_view = self.authremoteuserview()
else:
self.user_view = self.useroidmodelview
self.auth_view = self.authoidview()

self.appbuilder.add_view_no_menu(self.auth_view)

# this needs to be done after the view is added, otherwise the blueprint
# is not initialized
if self.is_auth_limited:
self.limiter.limit(self.auth_rate_limit, methods=["POST"])(self.auth_view.blueprint)

self.user_view = self.appbuilder.add_view(
self.user_view,
"List Users",
icon="fa-user",
label=lazy_gettext("List Users"),
category="Security",
category_icon="fa-cogs",
category_label=lazy_gettext("Security"),
)

role_view = self.appbuilder.add_view(
self.rolemodelview,
"List Roles",
icon="fa-group",
label=lazy_gettext("List Roles"),
category="Security",
category_icon="fa-cogs",
)
role_view.related_views = [self.user_view.__class__]

if self.userstatschartview:
self.appbuilder.add_view(
self.userstatschartview,
"User's Statistics",
icon="fa-bar-chart-o",
label=lazy_gettext("User's Statistics"),
category="Security",
)
if self.auth_user_registration:
self.appbuilder.add_view(
self.registerusermodelview,
"User's Statistics",
vincbeck marked this conversation as resolved.
Show resolved Hide resolved
icon="fa-user-plus",
label=lazy_gettext("User Registrations"),
category="Security",
)
self.appbuilder.menu.add_separator("Security")
if self.appbuilder.app.config.get("FAB_ADD_SECURITY_PERMISSION_VIEW", True):
self.appbuilder.add_view(
self.actionmodelview,
"Actions",
icon="fa-lock",
label=lazy_gettext("Actions"),
category="Security",
)
if self.appbuilder.app.config.get("FAB_ADD_SECURITY_VIEW_MENU_VIEW", True):
self.appbuilder.add_view(
self.resourcemodelview,
"Resources",
icon="fa-list-alt",
label=lazy_gettext("Resources"),
category="Security",
)
if self.appbuilder.app.config.get("FAB_ADD_SECURITY_PERMISSION_VIEWS_VIEW", True):
self.appbuilder.add_view(
self.permissionmodelview,
"Permission Pairs",
icon="fa-link",
label=lazy_gettext("Permissions"),
category="Security",
)

@property
def auth_user_registration(self):
"""Will user self registration be allowed."""
return self.appbuilder.get_app.config["AUTH_USER_REGISTRATION"]

@property
def auth_type(self):
"""Get the auth type."""
return self.appbuilder.get_app.config["AUTH_TYPE"]

@property
def is_auth_limited(self) -> bool:
"""Is the auth rate limited."""
return self.appbuilder.get_app.config["AUTH_RATE_LIMITED"]

@property
def auth_rate_limit(self) -> str:
"""Get the auth rate limit."""
return self.appbuilder.get_app.config["AUTH_RATE_LIMIT"]

@cached_property
def resourcemodelview(self):
"""Return the resource model view."""
from airflow.www.views import ResourceModelView

return ResourceModelView
34 changes: 33 additions & 1 deletion airflow/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2209,6 +2209,39 @@ def initialize_secrets_backends() -> list[BaseSecretsBackend]:
return backend_list


@functools.lru_cache(maxsize=None)
def _DEFAULT_CONFIG() -> str:
path = _default_config_file_path("default_airflow.cfg")
with open(path) as fh:
return fh.read()


@functools.lru_cache(maxsize=None)
def _TEST_CONFIG() -> str:
path = _default_config_file_path("default_test.cfg")
with open(path) as fh:
return fh.read()


_deprecated = {
"DEFAULT_CONFIG": _DEFAULT_CONFIG,
"TEST_CONFIG": _TEST_CONFIG,
"TEST_CONFIG_FILE_PATH": functools.partial(_default_config_file_path, "default_test.cfg"),
"DEFAULT_CONFIG_FILE_PATH": functools.partial(_default_config_file_path, "default_airflow.cfg"),
}


def __getattr__(name):
if name in _deprecated:
warnings.warn(
f"{__name__}.{name} is deprecated and will be removed in future",
DeprecationWarning,
stacklevel=2,
)
return _deprecated[name]()
raise AttributeError(f"module {__name__} has no attribute {name}")


def initialize_auth_manager() -> BaseAuthManager:
vincbeck marked this conversation as resolved.
Show resolved Hide resolved
"""
Initialize auth manager.
Expand Down Expand Up @@ -2257,5 +2290,4 @@ def initialize_auth_manager() -> BaseAuthManager:

conf = initialize_config()
secrets_backend_list = initialize_secrets_backends()
auth_manager = initialize_auth_manager()
conf.validate()
5 changes: 3 additions & 2 deletions airflow/www/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@

from flask import current_app, flash, g, redirect, render_template, request, url_for

from airflow.configuration import auth_manager, conf
from airflow.configuration import conf
from airflow.utils.net import get_hostname
from airflow.www.extensions.init_auth_manager import get_auth_manager

T = TypeVar("T", bound=Callable)

Expand All @@ -46,7 +47,7 @@ def decorated(*args, **kwargs):
)
if appbuilder.sm.check_authorization(permissions, dag_id):
return func(*args, **kwargs)
elif auth_manager.is_logged_in() and not g.user.perms:
elif get_auth_manager().is_logged_in() and not g.user.perms:
return (
render_template(
"airflow/no_roles_permissions.html",
Expand Down
4 changes: 2 additions & 2 deletions airflow/www/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@
from flask import after_this_request, g, request
from pendulum.parsing.exceptions import ParserError

from airflow.configuration import auth_manager
from airflow.models import Log
from airflow.utils.log import secrets_masker
from airflow.utils.session import create_session
from airflow.www.extensions.init_auth_manager import get_auth_manager

T = TypeVar("T", bound=Callable)

Expand Down Expand Up @@ -85,7 +85,7 @@ def wrapper(*args, **kwargs):
__tracebackhide__ = True # Hide from pytest traceback.

with create_session() as session:
if not auth_manager.is_logged_in():
if not get_auth_manager().is_logged_in():
user = "anonymous"
else:
user = f"{g.user.username} ({g.user.get_full_name()})"
Expand Down
7 changes: 0 additions & 7 deletions airflow/www/extensions/init_appbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,6 @@ def init_app(self, app, session):

if self.update_perms: # default is True, if False takes precedence from config
self.update_perms = app.config.get("FAB_UPDATE_PERMS", True)
_security_manager_class_name = app.config.get("FAB_SECURITY_MANAGER_CLASS", None)
if _security_manager_class_name is not None:
self.security_manager_class = dynamic_class_import(_security_manager_class_name)
if self.security_manager_class is None:
from flask_appbuilder.security.sqla.manager import SecurityManager

self.security_manager_class = SecurityManager

self._addon_managers = app.config["ADDON_MANAGERS"]
self.session = session
Expand Down
Loading