Repository: apache/superset
Pull request #32770: feat: Add current_user_roles() Jinja macro
Author: login: bmaquet
Labels: name: size/M
name: authentication:access-control
description: Rlated to access control
name: global:jinja
description: Related to Jinja templating
name: doc
description: Namespace | Anything related to documentation
Created: 2025-03-20T10:05:19Z | Merged: 2025-03-24T21:39:11Z

### SUMMARY
This PR adds a new `current_user_roles()` Jinja macro to Superset that returns the roles of the currently logged-in user. This enables role-based data access control when integrating with semantic layers like Cube.dev, allowing organizations to implement fine-grained access control policies based on user roles.

The implementation follows the pattern of existing user context macros (`current_username()`, `current_user_id()`, etc.) and includes proper cache key generation support.

Example usage:
```sql
SELECT
'{{ 'yes' if 'Alpha' in current_user_roles() else 'no'  }}' as is_alpha_in_roles
```
```sql
SELECT '{{ current_user_roles()|tojson }}' as roles
```

### BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
<img width="919" alt="Screenshot 2025-03-21 at 09 39 41" src="https://github.com/user-attachments/assets/abcc441c-a8ae-4db1-b5b1-46cb2ca98422" />
<img width="1133" alt="Screenshot 2025-03-21 at 09 39 51" src="https://github.com/user-attachments/assets/7f1d16c7-3b59-4bfd-a3fc-953d679862f1" />

Caching is enabled
<img width="1506" alt="Screenshot 2025-03-21 at 09 26 35" src="https://github.com/user-attachments/assets/80cf036a-df90-4b7c-a313-4ed782205139" />
<img width="1508" alt="Screenshot 2025-03-21 at 09 26 44" src="https://github.com/user-attachments/assets/68bec363-70ac-4026-8282-12a7b0a7204c" />


### TESTING INSTRUCTIONS
1. Launch superset and add roles to the admin user
2. Launch SQL Lab and run the below query
```
SELECT
  '{{current_user_roles()|tojson }}'
```
This should return the roles
4. Create a Regular RLS with clause `__role = '{{ current_user_roles()|tojson }}'`. Attach the roles added to the admin user in step 1. and attach a dataset (e.g. `bart_lines`)
5. Create a chart on the `bart_lines` dataset. You will notice the macro is returning the roles.

### ADDITIONAL INFORMATION
- [x] Has associated issue: https://github.com/apache/superset/discussions/32769
- [ ] Required feature flags: None
- [ ] Changes UI: No
- [ ] Includes DB Migration: No
- [x] Introduces new feature or API: Yes (new Jinja macro)
- [ ] Removes existing feature or API: No



Diff:
diff --git a/docs/docs/configuration/sql-templating.mdx b/docs/docs/configuration/sql-templating.mdx
index 0e618fd9c19..c16b7f3d598 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 a4d8e6d6a49..9592db4852c 100644
--- a/superset/jinja_context.py
+++ b/superset/jinja_context.py
@@ -46,6 +46,7 @@
     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 @@ def current_user_email(self, add_to_cache_keys: bool = True) -> Optional[str]:
             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
@@ -670,6 +686,9 @@ def set_context(self, **kwargs: Any) -> None:
                 "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 2b80c89f612..6caea19b1e2 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 c3f4328bd66..f19d495fd87 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", "abc@test.com"},
+            {1, "abc", "abc@test.com", '["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", "abc@test.com"},
+            {1, "abc", "abc@test.com", '["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="abc@test.com")
+@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="abc@test.com")
+@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 be6e5fb55e3..991212827ae 100644
--- a/tests/unit_tests/jinja_context_test.py
+++ b/tests/unit_tests/jinja_context_test.py
@@ -20,6 +20,7 @@
 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
@@ -358,6 +359,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(
@@ -366,11 +368,13 @@ def test_user_macros(mocker: MockerFixture):
     mock_g.user.id = 1
     mock_g.user.username = "my_username"
     mock_g.user.email = "my_email@test.com"
+    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() == "my_email@test.com"
-    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):
@@ -384,10 +388,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 = "my_email@test.com"
+    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) == "my_email@test.com"
+    assert cache.current_user_roles(False) == ["my_role1", "my_role2"]
     assert mock_cache_key_wrapper.call_count == 0
 
 
@@ -401,6 +407,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:


Reviews:
user: login: korbit-ai[bot]
body: ### Review by Korbit AI

###### Korbit automatically attempts to detect when you fix issues in new commits.

| Category | Issue | Fix Detected |
|----------|-------|----------|
| <sub>![Performance](https://img.shields.io/badge/Performance-4f46e5)</sub> | Missing Cache for User Roles ▹ [view]( https://github.com/apache/superset/pull/32770#discussion_r2005238413 ) ||
| <sub>![Error Handling](https://img.shields.io/badge/Error%20Handling-ea580c)</sub> | Silent Exception Handling in get_user_roles ▹ [view]( https://github.com/apache/superset/pull/32770#discussion_r2005238405 ) ||
| <sub>![Documentation](https://img.shields.io/badge/Documentation-7c3aed)</sub> | Incomplete return value documentation ▹ [view]( https://github.com/apache/superset/pull/32770#discussion_r2005238411 ) | :white_check_mark: |






<details>
  <summary>Files scanned</summary>
&#8203;



| File Path | Reviewed |
| ------------------ | -------------- |
| superset/jinja_context.py | ✅ |
| superset/utils/core.py | ✅ |


> Explore our [documentation](https://docs.korbit.ai) to understand the languages and file types [we support](https://docs.korbit.ai/configuration/supported-languages) and the [files we ignore](https://docs.korbit.ai/configuration/ignoring-files).

</details>




> Need a new review? Comment `/korbit-review` on this PR and I'll review your latest changes.
>
> <details>
>   <summary>Korbit Guide: Usage and Customization</summary>
>
> ## Interacting with Korbit
> - You can manually ask Korbit to review your PR using the `/korbit-review` command in a comment at the root of your PR.
> - You can ask Korbit to generate a new PR description using the `/korbit-generate-pr-description` command in any comment on your PR.
> - Too many Korbit comments? I can resolve all my comment threads if you use the `/korbit-resolve` command in any comment on your PR.
> - On any given comment that Korbit raises on your pull request, you can have a discussion with Korbit by replying to the comment.
> - Help train Korbit to improve your reviews by giving a 👍 or 👎 on the comments Korbit posts.
>
> ## Customizing Korbit
> - Check out our [docs](https://docs.korbit.ai/configuration/organization-settings) on how you can make Korbit work best for you and your team.
> - Customize Korbit for your organization through the [Korbit Console]().
>
>
>
>
>
>
>
> ## Feedback and Support
> - [Tell us what you think of Korbit](https://rfn13ctpmt0.typeform.com/to/DOoeLyoN)
> - [Schedule a call with our team](https://meetings.salesloft.com/korbittechnologies/korbitai)
> - Email us @ support@korbit.ai
> </details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-03-20T10:08:19Z
user: login: github-actions[bot]
body: Congrats on making your first PR and thank you for contributing to Superset! :tada: :heart:

We hope to see you in our [Slack](https://apache-superset.slack.com/) community too! Not signed up? Use our [Slack App](http://bit.ly/join-superset-slack) to self-register.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T10:09:03Z
user: login: mistercrunch
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T15:25:16Z
user: login: mistercrunch
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T15:26:34Z
user: login: mistercrunch
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T15:27:50Z
user: login: mistercrunch
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T15:29:39Z
user: login: bmaquet
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T15:30:03Z
user: login: mistercrunch
body: Left some comments. One thing I wanted to mention is that we should make sure that this will work in the context of "alerts and report", not sure whether/if the impersonation works there. Something to look into.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T15:31:01Z
user: login: bmaquet
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T15:44:02Z
user: login: bmaquet
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:17:17Z
user: login: bmaquet
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:21:38Z
user: login: mistercrunch
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T17:34:37Z
user: login: bmaquet
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T21:44:57Z
user: login: mistercrunch
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T21:50:09Z
user: login: bmaquet
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T08:31:54Z
user: login: mistercrunch
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T19:34:02Z
user: login: bmaquet
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T20:23:24Z
user: login: mistercrunch
body: LGTM - thanks for the contribution!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-24T20:42:34Z

Review comments:
diff_hunk: @@ -1290,6 +1290,18 @@ def get_user_email() -> str | None:
         return g.user.email
     except Exception:  # pylint: disable=broad-except
         return None
+    
+def get_user_roles() -> list[str] | None:
+    """
+    Get the roles (if defined) associated with the current user.
+
+    :returns: The roles
+    """
+
+    try:
+        return [role.name for role in g.user.roles]
+    except Exception:  # pylint: disable=broad-except
+        return None
path: superset/utils/core.py
user: login: korbit-ai[bot]
body: ### Silent Exception Handling in get_user_roles <sub>![category Error Handling](https://img.shields.io/badge/Error%20Handling-ea580c)</sub>

<details>
  <summary>Tell me more</summary>

###### What is the issue?
The get_user_roles() function silently catches all exceptions and returns None without any logging, which could hide important errors.

###### Why this matters
If there are errors accessing user roles (e.g., database connection issues), they will be hidden which makes debugging difficult in production environments.

###### Suggested change ∙ *Feature Preview*
Add error logging to help with debugging:
```python
def get_user_roles() -> list[str] | None:
    try:
        return [role.name for role in g.user.roles]
    except Exception as ex:  # pylint: disable=broad-except
        logger.exception("Failed to get user roles: %s", str(ex))
        return None
```


###### Provide feedback to improve future suggestions
[![Nice Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/0456ac0d-a402-48d2-9f32-e0ac344711a7/upvote) [![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/0456ac0d-a402-48d2-9f32-e0ac344711a7?what_not_true=true)  [![Not in Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/0456ac0d-a402-48d2-9f32-e0ac344711a7?what_out_of_scope=true) [![Not in coding standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/0456ac0d-a402-48d2-9f32-e0ac344711a7?what_not_in_standard=true) [![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/0456ac0d-a402-48d2-9f32-e0ac344711a7)
</details>

<sub>

💬 Looking for more details? Reply to this comment to chat with Korbit.
</sub>

<!--- korbi internal id:55b7aa12-705e-4b2d-bcf1-161a9fd9dc81 -->

[](55b7aa12-705e-4b2d-bcf1-161a9fd9dc81)
created_at: 2025-03-20T10:08:19Z
author_association: NONE
diff_hunk: @@ -1290,6 +1290,18 @@
         return g.user.email
     except Exception:  # pylint: disable=broad-except
         return None
+    
+def get_user_roles() -> list[str] | None:
+    """
+    Get the roles (if defined) associated with the current user.
+
+    :returns: The roles
+    """
path: superset/utils/core.py
user: login: korbit-ai[bot]
body: ### Incomplete return value documentation <sub>![category Documentation](https://img.shields.io/badge/Documentation-7c3aed)</sub>

<details>
  <summary>Tell me more</summary>

###### What is the issue?
The docstring does not adequately explain the return value format and failure case.

###### Why this matters
The function can return None or a list of role names, but this behavior is not clearly documented making it harder for developers to handle all cases correctly.

###### Suggested change ∙ *Feature Preview*
def get_user_roles() -> list[str] | None:
    """
    Get the roles (if defined) associated with the current user.

    :returns: List of role names if user is found and has roles, None if user not found or no roles
    """


###### Provide feedback to improve future suggestions
[![Nice Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/beb85d31-bb97-4086-ad35-ce47443c1c82/upvote) [![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/beb85d31-bb97-4086-ad35-ce47443c1c82?what_not_true=true)  [![Not in Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/beb85d31-bb97-4086-ad35-ce47443c1c82?what_out_of_scope=true) [![Not in coding standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/beb85d31-bb97-4086-ad35-ce47443c1c82?what_not_in_standard=true) [![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/beb85d31-bb97-4086-ad35-ce47443c1c82)
</details>

<sub>

💬 Looking for more details? Reply to this comment to chat with Korbit.
</sub>

<!--- korbi internal id:536e4058-9b98-4cf9-8da3-d3c74dbfb296 -->

[](536e4058-9b98-4cf9-8da3-d3c74dbfb296)
created_at: 2025-03-20T10:08:19Z
author_association: NONE
diff_hunk: @@ -1290,6 +1290,18 @@
         return g.user.email
     except Exception:  # pylint: disable=broad-except
         return None
+    
+def get_user_roles() -> list[str] | None:
+    """
+    Get the roles (if defined) associated with the current user.
+
+    :returns: The roles
+    """
+
+    try:
+        return [role.name for role in g.user.roles]
+    except Exception:  # pylint: disable=broad-except
+        return None
path: superset/utils/core.py
user: login: korbit-ai[bot]
body: ### Missing Cache for User Roles <sub>![category Performance](https://img.shields.io/badge/Performance-4f46e5)</sub>

<details>
  <summary>Tell me more</summary>

###### What is the issue?
The user roles are being fetched without any caching mechanism, potentially causing unnecessary database queries on every template render.

###### Why this matters
Without caching, each call to get_user_roles() will trigger a database query, creating performance overhead especially in templates where this could be called multiple times.

###### Suggested change ∙ *Feature Preview*
Implement caching for user roles using Flask-Caching or a similar mechanism. Store the roles in cache with a reasonable TTL. Example:
```python
@cache.memoize(timeout=300)  # Cache for 5 minutes
def get_user_roles() -> list[str] | None:
    try:
        return [role.name for role in g.user.roles]
    except Exception:
        return None
```


###### Provide feedback to improve future suggestions
[![Nice Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/f813366e-1650-4800-832e-9352346ddee3/upvote) [![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/f813366e-1650-4800-832e-9352346ddee3?what_not_true=true)  [![Not in Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/f813366e-1650-4800-832e-9352346ddee3?what_out_of_scope=true) [![Not in coding standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/f813366e-1650-4800-832e-9352346ddee3?what_not_in_standard=true) [![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/f813366e-1650-4800-832e-9352346ddee3)
</details>

<sub>

💬 Looking for more details? Reply to this comment to chat with Korbit.
</sub>

<!--- korbi internal id:fb643427-5e90-4842-9474-a8786fcb1c20 -->

[](fb643427-5e90-4842-9474-a8786fcb1c20)
created_at: 2025-03-20T10:08:19Z
author_association: NONE
diff_hunk: @@ -1292,6 +1292,19 @@ def get_user_email() -> str | None:
         return None
 
 
+def get_user_roles() -> list[str] | None:
path: superset/utils/core.py
user: login: mistercrunch
body: Not sure why we need this method in `core.py`. (?) Seems you could simply use this `[role.name for role in g.user.roles]` in `superset/jinja_context.py`
created_at: 2025-03-20T15:25:16Z
author_association: MEMBER
diff_hunk: @@ -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 roles
+    """
+
+    try:
+        return [role.name for role in g.user.roles]
path: superset/utils/core.py
user: login: mistercrunch
body: not sure why we need super generic error handling here. If anything I'd try to mimic whatever error handling is done (or not done) in `superset/jinja_context.py`
created_at: 2025-03-20T15:26:34Z
author_association: MEMBER
diff_hunk: @@ -172,6 +174,20 @@ def current_user_email(self, add_to_cache_keys: bool = True) -> Optional[str]:
             return email_address
         return None
 
+    def current_user_roles(self, add_to_cache_keys: bool = True) -> str | None:
+        """
+        Return the 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 as a JSON string
+        """
+
+        if user_roles := get_user_roles():
+            if add_to_cache_keys:
+                self.cache_key_wrapper(json.dumps(user_roles))
path: superset/jinja_context.py
user: login: mistercrunch
body: why `json.dumps` here? Wouldn't the jinja use case be around parsing actual objects not json (?)
created_at: 2025-03-20T15:27:50Z
author_association: MEMBER
diff_hunk: @@ -220,6 +220,21 @@ 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 the roles of the currently logged in user.
path: docs/docs/configuration/sql-templating.mdx
user: login: mistercrunch
body: Given your implementation now, it seems more like "returns a json-stringified array of roles for the logged in user".
created_at: 2025-03-20T15:29:39Z
author_association: MEMBER
diff_hunk: @@ -1292,6 +1292,19 @@ def get_user_email() -> str | None:
         return None
 
 
+def get_user_roles() -> list[str] | None:
path: superset/utils/core.py
user: login: bmaquet
body: I agree we don't necessarily need it in the context of this PR and we could use `[role.name for role in g.user.roles]` in `superset/jinja_context.py`.

I simply added it here to stay consistent with other macros, like `current_user_email()`, where `get_user_email()` is also defined in `core.py`, but only imported and used in `jinja_context.py`.

Happy to remove if we believe it's best to not have this function defined in `core.py`
created_at: 2025-03-20T15:30:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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 roles
+    """
+
+    try:
+        return [role.name for role in g.user.roles]
path: superset/utils/core.py
user: login: bmaquet
body: I decided to rely on the same general exception handling that is used in the [other functions to get user attributes](https://github.com/apache/superset/blob/master/superset/utils/core.py#L1251-L1292). 

In `superset/jinja_context.py`, there's no exception handling for these macros. It's all handled in `core.py`
created_at: 2025-03-20T15:44:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -172,6 +174,20 @@ def current_user_email(self, add_to_cache_keys: bool = True) -> Optional[str]:
             return email_address
         return None
 
+    def current_user_roles(self, add_to_cache_keys: bool = True) -> str | None:
+        """
+        Return the 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 as a JSON string
+        """
+
+        if user_roles := get_user_roles():
+            if add_to_cache_keys:
+                self.cache_key_wrapper(json.dumps(user_roles))
path: superset/jinja_context.py
user: login: bmaquet
body: For two reasons:
1. without `json.dumps`, SQL cannot render correctly when calling the macro (see screenshot)
<img width="1089" alt="Screenshot 2025-03-20 at 16 59 36" src="https://github.com/user-attachments/assets/1cb0daaf-5ca0-4c06-9834-d9a971062bbb" />

2. when `add_to_cache_keys` is True, we add the value of `user_roles` into the cache, as a cache key. If `user_roles` was a list this would raise `TypeError: unhashable type: 'list'`

If needed, users can still use JSON functions to parse the results
created_at: 2025-03-20T16:17:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -220,6 +220,21 @@ 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 the roles of the currently logged in user.
path: docs/docs/configuration/sql-templating.mdx
user: login: bmaquet
body: Yes I agree that's better. I've updated the doc
created_at: 2025-03-20T16:21:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -172,6 +174,20 @@ def current_user_email(self, add_to_cache_keys: bool = True) -> Optional[str]:
             return email_address
         return None
 
+    def current_user_roles(self, add_to_cache_keys: bool = True) -> str | None:
+        """
+        Return the 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 as a JSON string
+        """
+
+        if user_roles := get_user_roles():
+            if add_to_cache_keys:
+                self.cache_key_wrapper(json.dumps(user_roles))
path: superset/jinja_context.py
user: login: mistercrunch
body: there's value in having both, maybe we expose a `json_dump()` macro, so that people can do either:

```
SELECT
{{ 'yes' if 'Alpha' in current_user_roles() else 'no'  }},
'{{ json_dumps(current_user_roles() ) }}'
```
created_at: 2025-03-20T17:34:37Z
author_association: MEMBER
diff_hunk: @@ -172,6 +174,20 @@ def current_user_email(self, add_to_cache_keys: bool = True) -> Optional[str]:
             return email_address
         return None
 
+    def current_user_roles(self, add_to_cache_keys: bool = True) -> str | None:
+        """
+        Return the 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 as a JSON string
+        """
+
+        if user_roles := get_user_roles():
+            if add_to_cache_keys:
+                self.cache_key_wrapper(json.dumps(user_roles))
path: superset/jinja_context.py
user: login: bmaquet
body: I see your point now.
I believe we also could leverage jinja's `|tojson` to render the list as a json (this would remove the need for another `json_dump()` macro).

To re-use your example, this would work:
<img width="659" alt="Screenshot 2025-03-20 at 22 45 22" src="https://github.com/user-attachments/assets/643c69bc-f1da-4912-a9bc-85a574ab76e6" />

The issue I'm struggling with now though, is that a python list can't be used as a key for a python dict, so this line `self.cache_key_wrapper(user_roles)` wouldn't work.
created_at: 2025-03-20T21:44:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -172,6 +174,20 @@ def current_user_email(self, add_to_cache_keys: bool = True) -> Optional[str]:
             return email_address
         return None
 
+    def current_user_roles(self, add_to_cache_keys: bool = True) -> str | None:
+        """
+        Return the 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 as a JSON string
+        """
+
+        if user_roles := get_user_roles():
+            if add_to_cache_keys:
+                self.cache_key_wrapper(json.dumps(user_roles))
path: superset/jinja_context.py
user: login: mistercrunch
body: Oh nice, didn't know about `to_json`. Neat.
created_at: 2025-03-20T21:50:09Z
author_association: MEMBER
diff_hunk: @@ -172,6 +174,20 @@ def current_user_email(self, add_to_cache_keys: bool = True) -> Optional[str]:
             return email_address
         return None
 
+    def current_user_roles(self, add_to_cache_keys: bool = True) -> str | None:
+        """
+        Return the 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 as a JSON string
+        """
+
+        if user_roles := get_user_roles():
+            if add_to_cache_keys:
+                self.cache_key_wrapper(json.dumps(user_roles))
path: superset/jinja_context.py
user: login: bmaquet
body: Updated the function to use `json_dumps()` to store the list of roles as string into the cache. 
The function still returns a python list, it's just the key for the cache that is now a string.

When the query is run again, it's correctly fetching the results from the cache
<img width="1506" alt="Screenshot 2025-03-21 at 09 26 35" src="https://github.com/user-attachments/assets/f8e26e46-d39b-42f8-beb8-2a1d6bbd2f43" />
<img width="1508" alt="Screenshot 2025-03-21 at 09 26 44" src="https://github.com/user-attachments/assets/9ef5ca53-f453-4450-b90b-52788e46383e" />

created_at: 2025-03-21T08:31:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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 roles
+    """
+
+    try:
+        return [role.name for role in g.user.roles]
path: superset/utils/core.py
user: login: mistercrunch
body: how about `return sorted([role.name for role in g.user.roles])` so it's a bit more deterministic?
created_at: 2025-03-24T19:34:02Z
author_association: MEMBER
diff_hunk: @@ -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 roles
+    """
+
+    try:
+        return [role.name for role in g.user.roles]
path: superset/utils/core.py
user: login: bmaquet
body: Added `sorted()`
created_at: 2025-03-24T20:23:24Z
author_association: CONTRIBUTOR

PR comments:
user: login: bmaquet
created_at: 2025-03-20T10:07:58Z
body: I just noticed that my linked issue is actually a [discussion](https://github.com/apache/superset/discussions/32769). Should I create an issue instead? Should this be a SIP?
author_association: CONTRIBUTOR
user: login: andy1xx8
created_at: 2025-03-20T14:05:03Z
body: love this pr
author_association: NONE
user: login: rusackas
created_at: 2025-03-20T14:27:46Z
body: I don't think we need an Issue (unless there's an actual bug). We welcome enhancements. I also don't think this needs a SIP, as it's not changing any fundamental user experience. It might, however, be nice to get a nod of approval from the security/Jinja-minded people, e.g. @dpgaspar @mistercrunch @michael-s-molina.
author_association: MEMBER
user: login: codecov[bot]
created_at: 2025-03-20T14:31:33Z
body: ## [Codecov](https://app.codecov.io/gh/apache/superset/pull/32770?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 83.41%. Comparing base [(`89ce7ba`)](https://app.codecov.io/gh/apache/superset/commit/89ce7ba0b0dc83e44166672136fd62d58aef3f08?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) to head [(`c8dbbef`)](https://app.codecov.io/gh/apache/superset/commit/c8dbbef677d7890ed9884007558e4c133551196b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).
> Report is 17 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           master   #32770       +/-   ##
===========================================
+ Coverage        0   83.41%   +83.41%     
===========================================
  Files           0      549      +549     
  Lines           0    39474    +39474     
===========================================
+ Hits            0    32926    +32926     
- Misses          0     6548     +6548     
```

| [Flag](https://app.codecov.io/gh/apache/superset/pull/32770/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Coverage Δ | |
|---|---|---|
| [hive](https://app.codecov.io/gh/apache/superset/pull/32770/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `48.41% <18.18%> (?)` | |
| [mysql](https://app.codecov.io/gh/apache/superset/pull/32770/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `75.62% <54.54%> (?)` | |
| [postgres](https://app.codecov.io/gh/apache/superset/pull/32770/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `75.70% <54.54%> (?)` | |
| [presto](https://app.codecov.io/gh/apache/superset/pull/32770/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `52.89% <18.18%> (?)` | |
| [python](https://app.codecov.io/gh/apache/superset/pull/32770/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `83.41% <100.00%> (?)` | |
| [sqlite](https://app.codecov.io/gh/apache/superset/pull/32770/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `75.21% <54.54%> (?)` | |
| [unit](https://app.codecov.io/gh/apache/superset/pull/32770/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `61.39% <100.00%> (?)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/apache/superset/pull/32770?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: andy1xx8
created_at: 2025-03-20T14:36:13Z
body: > I don't think we need an Issue (unless there's an actual bug). We welcome enhancements. I also don't think this needs a SIP, as it's not changing any fundamental user experience. It might, however, be nice to get a nod of approval from the security/Jinja-minded people, e.g. @dpgaspar @mistercrunch @michael-s-molina.

this feature will help to broaden the usage off superset where many companies require to do custom RLS based on the role of users
author_association: NONE
user: login: villebro
created_at: 2025-03-20T16:12:10Z
body: > Left some comments. One thing I wanted to mention is that we should make sure that this will work in the context of "alerts and report", not sure whether/if the impersonation works there. Something to look into.

@mistercrunch by default we execute Alerts & Reports as the primary owner of the report (if there are many owners, it chooses the one that last edited it). So this should work out of the box. See https://github.com/apache/superset/blob/d319543377f69ec6f0848e0393033283b39b5733/superset/config.py#L1458
author_association: MEMBER
user: login: bmaquet
created_at: 2025-03-20T16:20:55Z
body: > Left some comments. One thing I wanted to mention is that we should make sure that this will work in the context of "alerts and report", not sure whether/if the impersonation works there. Something to look into.

Thanks for your review @mistercrunch , really appreciate it! I've tried to answer as best as I could. Let me know if I can clarify anything further!
author_association: CONTRIBUTOR
user: login: mistercrunch
created_at: 2025-03-20T17:36:06Z
body: Left an inline comment, but I think we should expose another `json_dumps` macro in the jinja context, letting people compose to serve their needs.

If it's a funky json blob and you want to do something like this:

```sql
{% if 'Alpha' in current_user_roles() %}
SELECT * FROM some_table
{% else %}
SELECT * FROM another_table
{% end if %}
```
this would be messy if you're handling a JSON-stringified string.
author_association: MEMBER
user: login: bmaquet
created_at: 2025-03-21T08:37:38Z
body: I've updated the logic based on @mistercrunch 's recommandations.

The function now returns an array of roles, instead of a JSON-stringified array.
This allows users to leverage arrays to run statements like 
```
SELECT
'{{ 'yes' if 'Alpha' in current_user_roles() else 'no'  }}' as is_alpha_in_roles
```
but also to get the list of roles in a properly formatted SQL string by using `|tojson`
```
SELECT '{{ current_user_roles()|tojson }}' as roles
```

The array of roles is added to the cache key by turning it into a string using `json.dumps()`. This allows the query to be cached and returned from the cache.

<img width="1506" alt="Screenshot 2025-03-21 at 09 26 35" src="https://github.com/user-attachments/assets/f8e26e46-d39b-42f8-beb8-2a1d6bbd2f43" />
<img width="1508" alt="Screenshot 2025-03-21 at 09 26 44" src="https://github.com/user-attachments/assets/9ef5ca53-f453-4450-b90b-52788e46383e" />

author_association: CONTRIBUTOR
user: login: mistercrunch
created_at: 2025-03-21T18:30:33Z
body: Looking solid. Some thoughts:
- based on your usage with JSON serialization, you may want to sort the roles to have a more deterministic output. It looked like you were doing potentially equality `==` operations on it, and the role sort order may change randomly. Not required for the PR, but just raising given your usage.
- would be great to have a similar function to get or check user specific perms, but probably also out of scope for this PR. Idea is someone could check if a user has access to a given table, structure or just about anything handled through perms. Note that the list of perms can get very large (hundreds), `current_user_check_perm(perm)` might be useful for folks out there doing more advanced things with jinja. Tagging @Vitor-Avila since he's been a jinja wizard.
author_association: MEMBER
user: login: Vitor-Avila
created_at: 2025-03-24T18:19:26Z
body: This looks really great! (btw TIL about the `|tojson` filter -- that's likely helpful for the `filter_values` macro too).

I totally agree there's value in having both options. We also have the `|where_in` filter that could make this into `('Admin', 'Public', 'Alpha')` if someone needs to use this in a SQL statement, for example. Not sure if you want to add this to the docs too.

I think it might be interesting to sort the roles specially before using them as a cache key, to avoid un-necessary hits to the DB just because of sorting changes. 
author_association: CONTRIBUTOR
user: login: Vitor-Avila
created_at: 2025-03-24T18:21:21Z
body: @bmaquet thank you so much for this contribution! My only cents here would be the sorting for cache purposes. @mistercrunch are you good with your comments? Exicted to get this one merged.
author_association: CONTRIBUTOR
user: login: mistercrunch
created_at: 2025-03-24T19:37:29Z
body: > @mistercrunch are you good with your comments?

LGTM but would like the sort comment to be addressed. Should be just the line I suggested
author_association: MEMBER
user: login: bmaquet
created_at: 2025-03-24T20:41:33Z
body: I've addressed your latest comments:
- Added `sorted()`
- Updated the docs to show how users can use `|where_in` with the new macro
author_association: CONTRIBUTOR