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

Handle AUTH_ROLE_PUBLIC in FAB auth manager #42280

Merged
merged 10 commits into from
Sep 20, 2024
5 changes: 0 additions & 5 deletions airflow/api_connexion/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,6 @@ def check_authentication() -> None:
if response.status_code == 200:
return

# Even if the current_user is anonymous, the AUTH_ROLE_PUBLIC might still have permission.
appbuilder = get_airflow_app().appbuilder
if appbuilder.get_app.config.get("AUTH_ROLE_PUBLIC", None):
return

# since this handler only checks authentication, not authorization,
# we should always return 401
raise Unauthenticated(headers=response.headers)
Expand Down
2 changes: 1 addition & 1 deletion airflow/config_templates/default_webserver_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
WTF_CSRF_TIME_LIMIT = None

# ----------------------------------------------------
# AUTHENTICATION CONFIG
# AUTHENTICATION CONFIG (specific to FAB auth manager)
# ----------------------------------------------------
# For details on how to set up each of the following authentication, see
# http://flask-appbuilder.readthedocs.io/en/latest/security.html# authentication-methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from functools import wraps
from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast

from flask import Response, request
from flask import Response, current_app, request
from flask_appbuilder.const import AUTH_LDAP
from flask_login import login_user

Expand Down Expand Up @@ -62,7 +62,9 @@ def requires_authentication(function: T):

@wraps(function)
def decorated(*args, **kwargs):
if auth_current_user() is not None:
if auth_current_user() is not None or current_app.appbuilder.get_app.config.get(
"AUTH_ROLE_PUBLIC", None
):
return function(*args, **kwargs)
else:
return Response("Unauthorized", 401, {"WWW-Authenticate": "Basic"})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from typing import TYPE_CHECKING, Any, Callable, NamedTuple, TypeVar, cast

import kerberos
from flask import Response, g, make_response, request
from flask import Response, current_app, g, make_response, request
from requests_kerberos import HTTPKerberosAuth

from airflow.configuration import conf
Expand Down Expand Up @@ -124,6 +124,10 @@ def requires_authentication(function: T, find_user: Callable[[str], BaseUser] |

@wraps(function)
def decorated(*args, **kwargs):
if current_app.appbuilder.get_app.config.get("AUTH_ROLE_PUBLIC", None):
response = function(*args, **kwargs)
return make_response(response)

header = request.headers.get("Authorization")
if header:
token = "".join(header.split()[1:])
Expand Down
8 changes: 6 additions & 2 deletions airflow/providers/fab/auth_manager/fab_auth_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,9 @@ def init(self) -> None:
def is_logged_in(self) -> bool:
"""Return whether the user is logged in."""
user = self.get_user()
return not user.is_anonymous and user.is_active
return self.appbuilder.get_app.config.get("AUTH_ROLE_PUBLIC", None) or (
not user.is_anonymous and user.is_active
)

def is_authorized_configuration(
self,
Expand Down Expand Up @@ -365,7 +367,9 @@ def get_url_logout(self):

def get_url_user_profile(self) -> str | None:
"""Return the url to a page displaying info about the current user."""
if not self.security_manager.user_view:
if not self.security_manager.user_view or self.appbuilder.get_app.config.get(
"AUTH_ROLE_PUBLIC", None
):
return None
return url_for(f"{self.security_manager.user_view.endpoint}.userinfo")

Expand Down
8 changes: 7 additions & 1 deletion airflow/providers/fab/auth_manager/models/anonymous_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@ class AnonymousUser(AnonymousUserMixin, BaseUser):
_roles: set[tuple[str, str]] = set()
_perms: set[tuple[str, str]] = set()

first_name = "Anonymous"
last_name = ""

@property
def roles(self):
if not self._roles:
public_role = current_app.appbuilder.get_app.config["AUTH_ROLE_PUBLIC"]
public_role = current_app.appbuilder.get_app.config.get("AUTH_ROLE_PUBLIC", None)
self._roles = {current_app.appbuilder.sm.find_role(public_role)} if public_role else set()
return self._roles

Expand All @@ -48,3 +51,6 @@ def perms(self):
(perm.action.name, perm.resource.name) for role in self.roles for perm in role.permissions
}
return self._perms

def get_name(self) -> str:
return "Anonymous"
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ def auth_rate_limit(self) -> str:
@property
def auth_role_public(self):
"""Get the public role."""
return self.appbuilder.get_app.config["AUTH_ROLE_PUBLIC"]
return self.appbuilder.get_app.config.get("AUTH_ROLE_PUBLIC", None)

@property
def oauth_providers(self):
Expand Down Expand Up @@ -832,7 +832,6 @@ def _init_config(self):
app = self.appbuilder.get_app
# Base Security Config
app.config.setdefault("AUTH_ROLE_ADMIN", "Admin")
app.config.setdefault("AUTH_ROLE_PUBLIC", "Public")
app.config.setdefault("AUTH_TYPE", AUTH_DB)
# Self Registration
app.config.setdefault("AUTH_USER_REGISTRATION", False)
Expand Down Expand Up @@ -955,7 +954,8 @@ def create_db(self):
self.add_role(role_name)
if self.auth_role_admin not in self._builtin_roles:
self.add_role(self.auth_role_admin)
self.add_role(self.auth_role_public)
if self.auth_role_public:
self.add_role(self.auth_role_public)
if self.count_users() == 0 and self.auth_role_public != self.auth_role_admin:
log.warning(const.LOGMSG_WAR_SEC_NO_USER)
except Exception:
Expand Down
11 changes: 2 additions & 9 deletions airflow/www/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@
from sqlalchemy.sql import Select
from sqlalchemy.sql.operators import ColumnOperators

from airflow.www.extensions.init_appbuilder import AirflowAppBuilder


TI = TaskInstance

Expand Down Expand Up @@ -924,21 +922,16 @@ def __init__(
self.html = html
self.message = Markup(message) if html else message

def should_show(self, appbuilder: AirflowAppBuilder) -> bool:
def should_show(self) -> bool:
"""
Determine if the user should see the message.

The decision is based on the user's role. If ``AUTH_ROLE_PUBLIC`` is
set in ``webserver_config.py``, An anonymous user would have the
``AUTH_ROLE_PUBLIC`` role.
The decision is based on the user's role.
"""
if self.roles:
current_user = get_auth_manager().get_user()
if current_user is not None:
user_roles = {r.name for r in getattr(current_user, "roles", [])}
elif "AUTH_ROLE_PUBLIC" in appbuilder.get_app.config:
# If the current_user is anonymous, assign AUTH_ROLE_PUBLIC role (if it exists) to them
user_roles = {appbuilder.get_app.config["AUTH_ROLE_PUBLIC"]}
else:
# Unable to obtain user role - default to not showing
return False
Expand Down
4 changes: 1 addition & 3 deletions airflow/www/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1093,9 +1093,7 @@ def index(self):
section="webserver", key="instance_name_has_markup", fallback=False
)

dashboard_alerts = [
fm for fm in settings.DASHBOARD_UIALERTS if fm.should_show(get_airflow_app().appbuilder)
]
dashboard_alerts = [fm for fm in settings.DASHBOARD_UIALERTS if fm.should_show()]

def _iter_parsed_moved_data_table_names():
for table_name in inspect(session.get_bind()).get_table_names():
Expand Down
Loading