You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by ka...@apache.org on 2021/09/10 13:24:27 UTC

[airflow] 03/04: Avoid endless redirect loop when user has no roles (#17613)

This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v2-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 464e1e14b0cd72a173464f0378818b6cae9cc193
Author: Jed Cunningham <66...@users.noreply.github.com>
AuthorDate: Wed Aug 18 05:56:09 2021 -0600

    Avoid endless redirect loop when user has no roles (#17613)
    
    (cherry picked from commit 6868ca48b29915aae8c131d694ea851cff1717de)
---
 airflow/www/auth.py                         |  5 +++-
 airflow/www/templates/airflow/no_roles.html | 37 ++++++++++++++++++++++++++
 airflow/www/views.py                        | 14 ++++++++++
 tests/www/views/test_views_acl.py           | 41 +++++++++++++++++++++++++++++
 4 files changed, 96 insertions(+), 1 deletion(-)

diff --git a/airflow/www/auth.py b/airflow/www/auth.py
index 8d42f51..b1218e0 100644
--- a/airflow/www/auth.py
+++ b/airflow/www/auth.py
@@ -18,7 +18,7 @@
 from functools import wraps
 from typing import Callable, Optional, Sequence, Tuple, TypeVar, cast
 
-from flask import current_app, flash, redirect, request, url_for
+from flask import current_app, flash, g, redirect, request, url_for
 
 T = TypeVar("T", bound=Callable)
 
@@ -30,6 +30,9 @@ def has_access(permissions: Optional[Sequence[Tuple[str, str]]] = None) -> Calla
         @wraps(func)
         def decorated(*args, **kwargs):
             appbuilder = current_app.appbuilder
+            if not g.user.is_anonymous and not g.user.roles:
+                return redirect(url_for("Airflow.no_roles"))
+
             if appbuilder.sm.check_authorization(permissions, request.args.get('dag_id', None)):
                 return func(*args, **kwargs)
             else:
diff --git a/airflow/www/templates/airflow/no_roles.html b/airflow/www/templates/airflow/no_roles.html
new file mode 100644
index 0000000..20f5eab
--- /dev/null
+++ b/airflow/www/templates/airflow/no_roles.html
@@ -0,0 +1,37 @@
+{#
+ 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.
+#}
+
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <title>Airflow</title>
+  <link rel="icon" type="image/png" href="{{ url_for('static', filename='pin_32.png') }}">
+</head>
+<body>
+  <div style="font-family: verdana; text-align: center; margin-top: 200px;">
+    <img src="{{ url_for('static', filename='pin_100.png') }}" width="50px" alt="pin-logo" />
+    <h1>Your user has no roles!</h1>
+    <p>Unfortunately your user has no roles, and therefore you cannot use Airflow.</p>
+    <p>Please contact your Airflow administrator
+      (<a href="https://airflow.apache.org/docs/apache-airflow/stable/security/webserver.html#web-authentication">authentication</a>
+      may be misconfigured) or <a href="{{ logout_url }}">log out</a> to try again.</p>
+    <p>{{ hostname }}</p>
+  </div>
+</body>
+</html>
diff --git a/airflow/www/views.py b/airflow/www/views.py
index b26df70..6296058 100644
--- a/airflow/www/views.py
+++ b/airflow/www/views.py
@@ -500,6 +500,20 @@ class Airflow(AirflowBaseView):
 
         return wwwutils.json_response(payload)
 
+    @expose('/no_roles')
+    def no_roles(self):
+        """Show 'user has no roles' on screen (instead of an endless redirect loop)"""
+        if g.user.is_anonymous or g.user.roles:
+            return redirect(url_for("Airflow.index"))
+
+        return render_template(
+            'airflow/no_roles.html',
+            hostname=socket.getfqdn()
+            if conf.getboolean('webserver', 'EXPOSE_HOSTNAME', fallback=True)
+            else 'redact',
+            logout_url=current_app.appbuilder.get_url_for_logout,
+        )
+
     @expose('/home')
     @auth.has_access(
         [
diff --git a/tests/www/views/test_views_acl.py b/tests/www/views/test_views_acl.py
index b5e5e99..b2d97c8 100644
--- a/tests/www/views/test_views_acl.py
+++ b/tests/www/views/test_views_acl.py
@@ -760,3 +760,44 @@ def test_refresh_failure_for_viewer(viewer_client):
     # viewer role can't refresh
     resp = viewer_client.post('refresh?dag_id=example_bash_operator')
     check_content_in_response('Redirecting', resp, resp_code=302)
+
+
+@pytest.fixture(scope="module")
+def user_no_roles(acl_app):
+    user = create_user(
+        acl_app,
+        username="no_roles_user",
+        role_name="no_roles_user_role",
+    )
+    user.roles = []
+    return user
+
+
+@pytest.fixture()
+def client_no_roles(acl_app, user_no_roles):
+    return client_with_login(
+        acl_app,
+        username="no_roles_user",
+        password="no_roles_user",
+    )
+
+
+@pytest.fixture()
+def client_anonymous(acl_app):
+    return acl_app.test_client()
+
+
+@pytest.mark.parametrize(
+    "client, url, expected_content",
+    [
+        ["client_no_roles", "/home", "Your user has no roles!"],
+        ["client_no_roles", "/no_roles", "Your user has no roles!"],
+        ["client_all_dags", "/home", "DAGs - Airflow"],
+        ["client_all_dags", "/no_roles", "DAGs - Airflow"],
+        ["client_anonymous", "/home", "Sign In"],
+        ["client_anonymous", "/no_roles", "Sign In"],
+    ],
+)
+def test_no_roles_redirect(request, client, url, expected_content):
+    resp = request.getfixturevalue(client).get(url, follow_redirects=True)
+    check_content_in_response(expected_content, resp)