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

vavila pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 6b96b37c38 feat: Add current_user_roles() Jinja macro (#32770)
6b96b37c38 is described below

commit 6b96b37c3831375a84203a81794ead84d9ca6577
Author: bmaquet <[email protected]>
AuthorDate: Mon Mar 24 22:39:07 2025 +0100

    feat: Add current_user_roles() Jinja macro (#32770)
---
 docs/docs/configuration/sql-templating.mdx   | 30 ++++++++++++++++++++++++++++
 superset/jinja_context.py                    | 19 ++++++++++++++++++
 superset/utils/core.py                       | 13 ++++++++++++
 tests/integration_tests/sqla_models_tests.py | 17 +++++++++++-----
 tests/unit_tests/jinja_context_test.py       |  9 ++++++++-
 5 files changed, 82 insertions(+), 6 deletions(-)

diff --git a/docs/docs/configuration/sql-templating.mdx 
b/docs/docs/configuration/sql-templating.mdx
index 7f094f1d87..dcd5bb0869 100644
--- a/docs/docs/configuration/sql-templating.mdx
+++ b/docs/docs/configuration/sql-templating.mdx
@@ -220,6 +220,36 @@ cache key by adding the following parameter to your Jinja 
code:
 {{ current_user_email(add_to_cache_keys=False) }}
 ```
 
+**Current User Roles**
+
+The `{{ current_user_roles() }}` macro returns an array of roles for the 
logged in user.
+
+If you have caching enabled in your Superset configuration, then by default 
the roles value will be used
+by Superset when calculating the cache key. A cache key is a unique identifier 
that determines if there's a
+cache hit in the future and Superset can retrieve cached data.
+
+You can disable the inclusion of the roles value in the calculation of the
+cache key by adding the following parameter to your Jinja code:
+
+```python
+{{ current_user_roles(add_to_cache_keys=False) }}
+```
+
+You can json-stringify the array by adding `|tojson` to your Jinja code:
+```python
+{{ current_user_roles()|tojson }}
+```
+
+You can use the `|where_in` filter to use your roles in a SQL statement. For 
example, if `current_user_roles()` returns `['admin', 'viewer']`, the following 
template:
+```python
+SELECT * FROM users WHERE role IN {{ current_user_roles()|where_in }}
+```
+
+Will be rendered as:
+```sql
+SELECT * FROM users WHERE role IN ('admin', 'viewer')
+```
+
 **Custom URL Parameters**
 
 The `{{ url_param('custom_variable') }}` macro lets you define arbitrary URL
diff --git a/superset/jinja_context.py b/superset/jinja_context.py
index 0f56886dd2..c32d097b5b 100644
--- a/superset/jinja_context.py
+++ b/superset/jinja_context.py
@@ -46,6 +46,7 @@ from superset.utils.core import (
     FilterOperator,
     get_user_email,
     get_user_id,
+    get_user_roles,
     get_username,
     merge_extra_filters,
 )
@@ -108,6 +109,7 @@ class ExtraCache:
         r"current_user_id\([^()]*\)|"
         r"current_username\([^()]*\)|"
         r"current_user_email\([^()]*\)|"
+        r"current_user_roles\([^()]*\)|"
         r"cache_key_wrapper\([^()]*\)|"
         r"url_param\([^()]*\)"
         r")"
@@ -172,6 +174,20 @@ class ExtraCache:
             return email_address
         return None
 
+    def current_user_roles(self, add_to_cache_keys: bool = True) -> list[str] 
| None:
+        """
+        Return the list of roles of the user who is currently logged in.
+
+        :param add_to_cache_keys: Whether the value should be included in the 
cache key
+        :returns: List of role names
+        """
+
+        if user_roles := get_user_roles():
+            if add_to_cache_keys:
+                self.cache_key_wrapper(json.dumps(user_roles))
+            return user_roles
+        return None
+
     def cache_key_wrapper(self, key: Any) -> Any:
         """
         Adds values to a list that is added to the query object used for 
calculating a
@@ -689,6 +705,9 @@ class JinjaTemplateProcessor(BaseTemplateProcessor):
                 "current_user_email": partial(
                     safe_proxy, extra_cache.current_user_email
                 ),
+                "current_user_roles": partial(
+                    safe_proxy, extra_cache.current_user_roles
+                ),
                 "cache_key_wrapper": partial(safe_proxy, 
extra_cache.cache_key_wrapper),
                 "filter_values": partial(safe_proxy, 
extra_cache.filter_values),
                 "get_filters": partial(safe_proxy, extra_cache.get_filters),
diff --git a/superset/utils/core.py b/superset/utils/core.py
index 2b80c89f61..6caea19b1e 100644
--- a/superset/utils/core.py
+++ b/superset/utils/core.py
@@ -1292,6 +1292,19 @@ def get_user_email() -> str | None:
         return None
 
 
+def get_user_roles() -> list[str] | None:
+    """
+    Get the roles (if defined) associated with the current user.
+
+    :returns: The sorted list of roles
+    """
+
+    try:
+        return sorted([role.name for role in g.user.roles])
+    except Exception:  # pylint: disable=broad-except
+        return None
+
+
 @contextmanager
 def override_user(user: User | None, force: bool = True) -> Iterator[Any]:
     """
diff --git a/tests/integration_tests/sqla_models_tests.py 
b/tests/integration_tests/sqla_models_tests.py
index c3f4328bd6..f19d495fd8 100644
--- a/tests/integration_tests/sqla_models_tests.py
+++ b/tests/integration_tests/sqla_models_tests.py
@@ -797,9 +797,10 @@ def test_none_operand_in_filter(login_as_admin, 
physical_dataset):
             SELECT
             '{{ current_user_id() }}' as id,
             '{{ current_username() }}' as username,
-            '{{ current_user_email() }}' as email
+            '{{ current_user_email() }}' as email,
+            '{{ current_user_roles()|tojson }}' as roles
             """,
-            {1, "abc", "[email protected]"},
+            {1, "abc", "[email protected]", '["role1", "role2"]'},
             True,
         ),
         (
@@ -809,9 +810,10 @@ def test_none_operand_in_filter(login_as_admin, 
physical_dataset):
             SELECT
             '{{ current_user_id() }}' as id,
             '{{ current_username() }}' as username,
-            '{{ user_email }}' as email
+            '{{ user_email }}' as email,
+            '{{ current_user_roles()|tojson }}' as roles
             """,
-            {1, "abc", "[email protected]"},
+            {1, "abc", "[email protected]", '["role1", "role2"]'},
             True,
         ),
         (
@@ -830,7 +832,8 @@ def test_none_operand_in_filter(login_as_admin, 
physical_dataset):
             SELECT
             '{{ current_user_id(False) }}' as id,
             '{{ current_username(False) }}' as username,
-            '{{ current_user_email(False) }}' as email
+            '{{ current_user_email(False) }}' as email,
+            '{{ current_user_roles(False)|tojson }}' as roles
             """,
             [],
             True,
@@ -841,7 +844,9 @@ def test_none_operand_in_filter(login_as_admin, 
physical_dataset):
 @patch("superset.jinja_context.get_user_id", return_value=1)
 @patch("superset.jinja_context.get_username", return_value="abc")
 @patch("superset.jinja_context.get_user_email", return_value="[email protected]")
+@patch("superset.jinja_context.get_user_roles", return_value=["role1", 
"role2"])
 def test_extra_cache_keys(
+    mock_get_user_roles,
     mock_user_email,
     mock_username,
     mock_user_id,
@@ -883,7 +888,9 @@ def test_extra_cache_keys(
 @patch("superset.jinja_context.get_user_id", return_value=1)
 @patch("superset.jinja_context.get_username", return_value="abc")
 @patch("superset.jinja_context.get_user_email", return_value="[email protected]")
+@patch("superset.jinja_context.get_user_roles", return_value=["role1", 
"role2"])
 def test_extra_cache_keys_in_sql_expression(
+    mock_get_user_roles,
     mock_user_email,
     mock_username,
     mock_user_id,
diff --git a/tests/unit_tests/jinja_context_test.py 
b/tests/unit_tests/jinja_context_test.py
index 5cc6f218a8..1654e26781 100644
--- a/tests/unit_tests/jinja_context_test.py
+++ b/tests/unit_tests/jinja_context_test.py
@@ -21,6 +21,7 @@ from datetime import datetime
 from typing import Any
 
 import pytest
+from flask_appbuilder.security.sqla.models import Role
 from freezegun import freeze_time
 from jinja2 import DebugUndefined
 from jinja2.sandbox import SandboxedEnvironment
@@ -360,6 +361,7 @@ def test_user_macros(mocker: MockerFixture):
         - ``current_user_id``
         - ``current_username``
         - ``current_user_email``
+        - ``current_user_roles``
     """
     mock_g = mocker.patch("superset.utils.core.g")
     mock_cache_key_wrapper = mocker.patch(
@@ -368,11 +370,13 @@ def test_user_macros(mocker: MockerFixture):
     mock_g.user.id = 1
     mock_g.user.username = "my_username"
     mock_g.user.email = "[email protected]"
+    mock_g.user.roles = [Role(name="my_role1"), Role(name="my_role2")]
     cache = ExtraCache()
     assert cache.current_user_id() == 1
     assert cache.current_username() == "my_username"
     assert cache.current_user_email() == "[email protected]"
-    assert mock_cache_key_wrapper.call_count == 3
+    assert cache.current_user_roles() == ["my_role1", "my_role2"]
+    assert mock_cache_key_wrapper.call_count == 4
 
 
 def test_user_macros_without_cache_key_inclusion(mocker: MockerFixture):
@@ -386,10 +390,12 @@ def test_user_macros_without_cache_key_inclusion(mocker: 
MockerFixture):
     mock_g.user.id = 1
     mock_g.user.username = "my_username"
     mock_g.user.email = "[email protected]"
+    mock_g.user.roles = [Role(name="my_role1"), Role(name="my_role2")]
     cache = ExtraCache()
     assert cache.current_user_id(False) == 1
     assert cache.current_username(False) == "my_username"
     assert cache.current_user_email(False) == "[email protected]"
+    assert cache.current_user_roles(False) == ["my_role1", "my_role2"]
     assert mock_cache_key_wrapper.call_count == 0
 
 
@@ -403,6 +409,7 @@ def test_user_macros_without_user_info(mocker: 
MockerFixture):
     assert cache.current_user_id() == None  # noqa: E711
     assert cache.current_username() == None  # noqa: E711
     assert cache.current_user_email() == None  # noqa: E711
+    assert cache.current_user_roles() == None  # noqa: E711
 
 
 def test_where_in() -> None:

Reply via email to