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: