codeant-ai-for-open-source[bot] commented on code in PR #33924:
URL: https://github.com/apache/superset/pull/33924#discussion_r2585049409
##########
superset/jinja_context.py:
##########
@@ -121,7 +126,8 @@ class ExtraCache:
r"current_user_rls_rules\([^()]*\)|"
r"current_user_roles\([^()]*\)|"
r"cache_key_wrapper\([^()]*\)|"
- r"url_param\([^()]*\)"
+ r"url_param\([^()]*\)|"
+ r"get_guest_user_attribute\([^()]*\)|"
Review Comment:
**Suggestion:** The regular expression alternation in `ExtraCache.regex`
ends with a trailing '|' which creates an empty alternative, causing the inner
group to match an empty string and therefore match templates even when none of
the listed macros are present — this breaks the intended macro-detection logic
and can cause incorrect cache-key behavior and false positives. Remove the
trailing '|' from the last alternation so the group doesn't include an empty
alternative. [logic error]
**Severity Level:** Minor ⚠️
```suggestion
r"get_guest_user_attribute\([^()]*\)"
```
<details>
<summary><b>Why it matters? ⭐ </b></summary>
The suggestion correctly identifies a logic bug: a trailing '|' before the
closing alternation creates an empty alternative which makes the regex match
when none of the intended macros are present. Removing the trailing '|' from
the final alternative fixes the false-positive matching behaviour with a
minimal, local change.
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset/jinja_context.py
**Line:** 130:130
**Comment:**
*Logic Error: The regular expression alternation in `ExtraCache.regex`
ends with a trailing '|' which creates an empty alternative, causing the inner
group to match an empty string and therefore match templates even when none of
the listed macros are present — this breaks the intended macro-detection logic
and can cause incorrect cache-key behavior and false positives. Remove the
trailing '|' from the last alternation so the group doesn't include an empty
alternative.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
superset/security/api.py:
##########
@@ -57,6 +57,7 @@ class UserSchema(PermissiveSchema):
username = fields.String()
first_name = fields.String()
last_name = fields.String()
+ attributes = fields.Dict(keys=fields.String(), values=fields.Raw(),
allow_none=True)
Review Comment:
**Suggestion:** Marshmallow will reject None values inside the dict because
`values=fields.Raw()` by default does not allow None; if a client sends an
attribute with a None value or the downstream code expects None to be
preserved, validation will fail. Make the inner value field allow None. [type
error]
**Severity Level:** Minor ⚠️
```suggestion
attributes = fields.Dict(
keys=fields.String(), values=fields.Raw(allow_none=True),
allow_none=True
)
```
<details>
<summary><b>Why it matters? ⭐ </b></summary>
Correct: fields.Dict(allow_none=True) only allows the entire dict to be
None, it does not make the nested value_field accept None. If callers may send
attribute values that are explicit nulls, changing
values=fields.Raw(allow_none=True) is a minimal, correct fix to avoid
ValidationError. This is a targeted schema change and aligns with marshmallow
semantics.
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset/security/api.py
**Line:** 60:60
**Comment:**
*Type Error: Marshmallow will reject None values inside the dict
because `values=fields.Raw()` by default does not allow None; if a client sends
an attribute with a None value or the downstream code expects None to be
preserved, validation will fail. Make the inner value field allow None.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
tests/integration_tests/security/api_tests.py:
##########
@@ -123,6 +123,174 @@ def test_post_guest_token_authorized(self):
assert user == decoded_token["user"]
assert resource == decoded_token["resources"][0]
+ @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+ def test_post_guest_token_with_attributes(self):
+ """
+ Security API: Create a guest token with user attributes
+ """
+ self.dash =
db.session.query(Dashboard).filter_by(slug="births").first()
+ self.embedded = EmbeddedDashboardDAO.upsert(self.dash, [])
+ self.login(ADMIN_USERNAME)
+
+ user = {
+ "username": "bob_with_attrs",
+ "first_name": "Bob",
+ "last_name": "Also Bob",
+ "attributes": {
+ "department": "Engineering",
+ "region": "US",
+ "role": "developer",
+ "team": "data-platform",
+ "clearance_level": "standard",
+ "projects": ["analytics", "ml-platform"],
+ "team_lead": True,
+ },
+ }
+ resource = {"type": "dashboard", "id": str(self.embedded.uuid)}
+ rls_rule = {"dataset": 1, "clause": "1=1"}
+ params = {"user": user, "resources": [resource], "rls": [rls_rule]}
+
+ response = self.client.post(
+ self.uri, data=json.dumps(params), content_type="application/json"
+ )
+
+ self.assert200(response)
+ token = json.loads(response.data)["token"]
+ decoded_token = jwt.decode(
+ token,
+ self.app.config["GUEST_TOKEN_JWT_SECRET"],
+ audience=get_url_host(),
+ algorithms=["HS256"],
+ )
+
+ # Verify user attributes are preserved in the token
+ assert user == decoded_token["user"]
+ assert "attributes" in decoded_token["user"]
+ assert decoded_token["user"]["attributes"]["department"] ==
"Engineering"
+ assert decoded_token["user"]["attributes"]["region"] == "US"
+ assert decoded_token["user"]["attributes"]["role"] == "developer"
+ assert decoded_token["user"]["attributes"]["team"] == "data-platform"
+ assert decoded_token["user"]["attributes"]["clearance_level"] ==
"standard"
+ assert decoded_token["user"]["attributes"]["projects"] == [
+ "analytics",
+ "ml-platform",
+ ]
+ assert decoded_token["user"]["attributes"]["team_lead"] is True
+ assert resource == decoded_token["resources"][0]
+
+ @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+ def test_post_guest_token_with_empty_attributes(self):
+ """
+ Security API: Create a guest token with empty user attributes
+ """
+ self.dash =
db.session.query(Dashboard).filter_by(slug="births").first()
+ self.embedded = EmbeddedDashboardDAO.upsert(self.dash, [])
+ self.login(ADMIN_USERNAME)
+
+ user = {
+ "username": "bob_empty_attrs",
+ "first_name": "Bob",
+ "last_name": "Also Bob",
+ "attributes": {},
+ }
+ resource = {"type": "dashboard", "id": str(self.embedded.uuid)}
+ rls_rule = {"dataset": 1, "clause": "1=1"}
+ params = {"user": user, "resources": [resource], "rls": [rls_rule]}
+
+ response = self.client.post(
+ self.uri, data=json.dumps(params), content_type="application/json"
+ )
+
+ self.assert200(response)
+ token = json.loads(response.data)["token"]
+ decoded_token = jwt.decode(
+ token,
+ self.app.config["GUEST_TOKEN_JWT_SECRET"],
+ audience=get_url_host(),
+ algorithms=["HS256"],
+ )
+
+ # Verify empty attributes are preserved in the token
+ assert user == decoded_token["user"]
Review Comment:
**Suggestion:** The test compares the entire `user` dict to the decoded
token's `user` claim, which will fail if the backend adds/omits non-essential
fields; check the core user fields instead (username/first_name/last_name) to
avoid brittle tests. [logic error]
**Severity Level:** Minor ⚠️
```suggestion
# Verify empty attributes are preserved in the token (check core
fields explicitly)
assert decoded_token["user"]["username"] == user["username"]
assert decoded_token["user"]["first_name"] == user["first_name"]
assert decoded_token["user"]["last_name"] == user["last_name"]
```
<details>
<summary><b>Why it matters? ⭐ </b></summary>
As above, full equality will break if the implementation adds harmless
fields. Checking core fields keeps the assertion resilient while still
validating the important data.
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** tests/integration_tests/security/api_tests.py
**Line:** 213:214
**Comment:**
*Logic Error: The test compares the entire `user` dict to the decoded
token's `user` claim, which will fail if the backend adds/omits non-essential
fields; check the core user fields instead (username/first_name/last_name) to
avoid brittle tests.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
tests/integration_tests/security_tests.py:
##########
@@ -2078,6 +2078,170 @@ def test_get_guest_user(self):
assert guest_user is not None
assert "test_guest" == guest_user.username
+ def create_guest_token_with_attributes(self):
+ user = {
+ "username": "test_guest_with_attrs",
+ "first_name": "Test",
+ "last_name": "Guest",
+ "attributes": {
+ "department": "Engineering",
+ "region": "US",
+ "role": "developer",
+ "team": "data-platform",
+ },
+ }
+ resources = [{"some": "resource"}]
+ rls = [{"dataset": 1, "clause": "access = 1"}]
+ return security_manager.create_guest_access_token(user, resources, rls)
+
+ def test_create_guest_access_token_with_attributes(self):
+ """Test creating guest access token with user attributes."""
+ user_with_attributes = {
+ "username": "test_guest_attrs",
+ "first_name": "Test",
+ "last_name": "Guest",
+ "attributes": {
+ "department": "Engineering",
+ "region": "US",
+ "clearance_level": "standard",
+ "projects": ["analytics", "ml-platform"],
+ "team_lead": True,
+ },
+ }
+ resources = [{"type": "dashboard", "id": "test-dashboard"}]
+ rls = [{"dataset": 1, "clause": "id = 1"}]
+
+ token = security_manager.create_guest_access_token(
+ user_with_attributes, resources, rls
+ )
+
+ # Decode and verify the token contains attributes
+ aud = get_url_host()
+ decoded_token = jwt.decode(
+ token,
+ self.app.config["GUEST_TOKEN_JWT_SECRET"],
+ algorithms=[self.app.config["GUEST_TOKEN_JWT_ALGO"]],
+ audience=aud,
+ )
+
+ assert "user" in decoded_token
+ user = decoded_token["user"]
+ assert "attributes" in user
+ assert user["attributes"]["department"] == "Engineering"
+ assert user["attributes"]["region"] == "US"
+ assert user["attributes"]["clearance_level"] == "standard"
+ assert user["attributes"]["projects"] == ["analytics", "ml-platform"]
+ assert user["attributes"]["team_lead"] is True
+
+ def test_get_guest_user_with_attributes(self):
+ """Test that guest user properly retains attributes from token."""
+ token = self.create_guest_token_with_attributes()
+ fake_request = FakeRequest()
+ fake_request.headers[current_app.config["GUEST_TOKEN_HEADER_NAME"]] =
token
+
+ guest_user = security_manager.get_guest_user_from_request(fake_request)
+
+ assert guest_user is not None
+ assert "test_guest_with_attrs" == guest_user.username
+
+ # Verify attributes are accessible through guest_token
+ assert hasattr(guest_user, "guest_token")
+ token_user = guest_user.guest_token["user"]
Review Comment:
**Suggestion:** The test mutates the class-level `FakeRequest.headers` dict
by setting a header key on the instance (via item assignment).
`FakeRequest.headers` is defined as a class attribute (a shared dict), so
modifying it in one test will leak state across tests and cause flakiness. Also
the test relies on `guest_user` having a `guest_token` side-effect which may
not be guaranteed; decode the token directly to verify attributes instead of
relying on `guest_user.guest_token`. [possible bug]
**Severity Level:** Critical 🚨
```suggestion
# Ensure headers is an instance dict to avoid mutating the
class-level dict shared across tests.
fake_request.headers =
{current_app.config["GUEST_TOKEN_HEADER_NAME"]: token}
guest_user =
security_manager.get_guest_user_from_request(fake_request)
assert guest_user is not None
assert "test_guest_with_attrs" == guest_user.username
# Decode the token directly inside an application context to verify
attributes,
# instead of relying on a possible side-effect attaching
`guest_token` to the user.
with self.client.application.test_request_context():
aud = get_url_host()
decoded_token = jwt.decode(
token,
self.app.config["GUEST_TOKEN_JWT_SECRET"],
algorithms=[self.app.config["GUEST_TOKEN_JWT_ALGO"]],
audience=aud,
)
assert "user" in decoded_token
token_user = decoded_token["user"]
```
<details>
<summary><b>Why it matters? ⭐ </b></summary>
This is a correct and important catch: FakeRequest.headers is defined as a
class-level mutable dict, so setting an item on an instance without first
creating an instance-level dict will mutate shared state across tests and cause
flakiness. The improved code assigns an instance dict (avoiding the class-level
mutation) and verifying the token directly is a reasonable, more robust
assertion. The suggested extra jwt.decode inside a request context is harmless
and defensive.
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** tests/integration_tests/security_tests.py
**Line:** 2140:2149
**Comment:**
*Possible Bug: The test mutates the class-level `FakeRequest.headers`
dict by setting a header key on the instance (via item assignment).
`FakeRequest.headers` is defined as a class attribute (a shared dict), so
modifying it in one test will leak state across tests and cause flakiness. Also
the test relies on `guest_user` having a `guest_token` side-effect which may
not be guaranteed; decode the token directly to verify attributes instead of
relying on `guest_user.guest_token`.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
tests/integration_tests/security/api_tests.py:
##########
@@ -123,6 +123,174 @@ def test_post_guest_token_authorized(self):
assert user == decoded_token["user"]
assert resource == decoded_token["resources"][0]
+ @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+ def test_post_guest_token_with_attributes(self):
+ """
+ Security API: Create a guest token with user attributes
+ """
+ self.dash =
db.session.query(Dashboard).filter_by(slug="births").first()
+ self.embedded = EmbeddedDashboardDAO.upsert(self.dash, [])
+ self.login(ADMIN_USERNAME)
+
+ user = {
+ "username": "bob_with_attrs",
+ "first_name": "Bob",
+ "last_name": "Also Bob",
+ "attributes": {
+ "department": "Engineering",
+ "region": "US",
+ "role": "developer",
+ "team": "data-platform",
+ "clearance_level": "standard",
+ "projects": ["analytics", "ml-platform"],
+ "team_lead": True,
+ },
+ }
+ resource = {"type": "dashboard", "id": str(self.embedded.uuid)}
+ rls_rule = {"dataset": 1, "clause": "1=1"}
+ params = {"user": user, "resources": [resource], "rls": [rls_rule]}
+
+ response = self.client.post(
+ self.uri, data=json.dumps(params), content_type="application/json"
+ )
+
+ self.assert200(response)
+ token = json.loads(response.data)["token"]
+ decoded_token = jwt.decode(
+ token,
+ self.app.config["GUEST_TOKEN_JWT_SECRET"],
+ audience=get_url_host(),
+ algorithms=["HS256"],
+ )
+
+ # Verify user attributes are preserved in the token
+ assert user == decoded_token["user"]
+ assert "attributes" in decoded_token["user"]
+ assert decoded_token["user"]["attributes"]["department"] ==
"Engineering"
+ assert decoded_token["user"]["attributes"]["region"] == "US"
+ assert decoded_token["user"]["attributes"]["role"] == "developer"
+ assert decoded_token["user"]["attributes"]["team"] == "data-platform"
+ assert decoded_token["user"]["attributes"]["clearance_level"] ==
"standard"
+ assert decoded_token["user"]["attributes"]["projects"] == [
+ "analytics",
+ "ml-platform",
+ ]
+ assert decoded_token["user"]["attributes"]["team_lead"] is True
+ assert resource == decoded_token["resources"][0]
+
+ @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+ def test_post_guest_token_with_empty_attributes(self):
+ """
+ Security API: Create a guest token with empty user attributes
+ """
+ self.dash =
db.session.query(Dashboard).filter_by(slug="births").first()
+ self.embedded = EmbeddedDashboardDAO.upsert(self.dash, [])
+ self.login(ADMIN_USERNAME)
+
+ user = {
+ "username": "bob_empty_attrs",
+ "first_name": "Bob",
+ "last_name": "Also Bob",
+ "attributes": {},
+ }
+ resource = {"type": "dashboard", "id": str(self.embedded.uuid)}
+ rls_rule = {"dataset": 1, "clause": "1=1"}
+ params = {"user": user, "resources": [resource], "rls": [rls_rule]}
+
+ response = self.client.post(
+ self.uri, data=json.dumps(params), content_type="application/json"
+ )
+
+ self.assert200(response)
+ token = json.loads(response.data)["token"]
+ decoded_token = jwt.decode(
+ token,
+ self.app.config["GUEST_TOKEN_JWT_SECRET"],
+ audience=get_url_host(),
+ algorithms=["HS256"],
+ )
+
+ # Verify empty attributes are preserved in the token
+ assert user == decoded_token["user"]
+ assert "attributes" in decoded_token["user"]
+ assert decoded_token["user"]["attributes"] == {}
+ assert resource == decoded_token["resources"][0]
+
+ @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+ def test_post_guest_token_with_null_attributes(self):
+ """
+ Security API: Create a guest token with null user attributes
+ """
+ self.dash =
db.session.query(Dashboard).filter_by(slug="births").first()
+ self.embedded = EmbeddedDashboardDAO.upsert(self.dash, [])
+ self.login(ADMIN_USERNAME)
+
+ user = {
+ "username": "bob_null_attrs",
+ "first_name": "Bob",
+ "last_name": "Also Bob",
+ "attributes": None,
+ }
+ resource = {"type": "dashboard", "id": str(self.embedded.uuid)}
+ rls_rule = {"dataset": 1, "clause": "1=1"}
+ params = {"user": user, "resources": [resource], "rls": [rls_rule]}
+
+ response = self.client.post(
+ self.uri, data=json.dumps(params), content_type="application/json"
+ )
+
+ self.assert200(response)
+ token = json.loads(response.data)["token"]
+ decoded_token = jwt.decode(
+ token,
+ self.app.config["GUEST_TOKEN_JWT_SECRET"],
+ audience=get_url_host(),
+ algorithms=["HS256"],
+ )
+
+ # Verify null attributes are preserved in the token
+ assert user == decoded_token["user"]
Review Comment:
**Suggestion:** The test asserts full equality of `user` vs the decoded
token `user` while also checking `attributes` is present and None; strict dict
equality is fragile if implementation injects other claims — verify core fields
explicitly and keep the null attribute assertion separate. [logic error]
**Severity Level:** Minor ⚠️
```suggestion
# Verify null attributes are preserved in the token (check core
fields explicitly)
assert decoded_token["user"]["username"] == user["username"]
assert decoded_token["user"]["first_name"] == user["first_name"]
assert decoded_token["user"]["last_name"] == user["last_name"]
```
<details>
<summary><b>Why it matters? ⭐ </b></summary>
The suggested change prevents false negatives caused by extra fields
injected by the token builder while still asserting that attributes are null.
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** tests/integration_tests/security/api_tests.py
**Line:** 251:252
**Comment:**
*Logic Error: The test asserts full equality of `user` vs the decoded
token `user` while also checking `attributes` is present and None; strict dict
equality is fragile if implementation injects other claims — verify core fields
explicitly and keep the null attribute assertion separate.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
tests/integration_tests/security/api_tests.py:
##########
@@ -123,6 +123,174 @@ def test_post_guest_token_authorized(self):
assert user == decoded_token["user"]
assert resource == decoded_token["resources"][0]
+ @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+ def test_post_guest_token_with_attributes(self):
+ """
+ Security API: Create a guest token with user attributes
+ """
+ self.dash =
db.session.query(Dashboard).filter_by(slug="births").first()
+ self.embedded = EmbeddedDashboardDAO.upsert(self.dash, [])
+ self.login(ADMIN_USERNAME)
+
+ user = {
+ "username": "bob_with_attrs",
+ "first_name": "Bob",
+ "last_name": "Also Bob",
+ "attributes": {
+ "department": "Engineering",
+ "region": "US",
+ "role": "developer",
+ "team": "data-platform",
+ "clearance_level": "standard",
+ "projects": ["analytics", "ml-platform"],
+ "team_lead": True,
+ },
+ }
+ resource = {"type": "dashboard", "id": str(self.embedded.uuid)}
+ rls_rule = {"dataset": 1, "clause": "1=1"}
+ params = {"user": user, "resources": [resource], "rls": [rls_rule]}
+
+ response = self.client.post(
+ self.uri, data=json.dumps(params), content_type="application/json"
+ )
+
+ self.assert200(response)
+ token = json.loads(response.data)["token"]
+ decoded_token = jwt.decode(
+ token,
+ self.app.config["GUEST_TOKEN_JWT_SECRET"],
+ audience=get_url_host(),
+ algorithms=["HS256"],
+ )
+
+ # Verify user attributes are preserved in the token
+ assert user == decoded_token["user"]
Review Comment:
**Suggestion:** The test asserts full dictionary equality between the
original `user` input and the decoded token `user` claim; this is brittle
because the service may add extra fields (timestamps, ids) to the token or
normalize values, making the equality fail even when the important fields are
preserved. Replace the strict equality with explicit checks for the required
fields so the test validates intent without being fragile. [logic error]
**Severity Level:** Minor ⚠️
```suggestion
# Verify user attributes are preserved in the token (check core
fields explicitly)
assert decoded_token["user"]["username"] == user["username"]
assert decoded_token["user"]["first_name"] == user["first_name"]
assert decoded_token["user"]["last_name"] == user["last_name"]
```
<details>
<summary><b>Why it matters? ⭐ </b></summary>
The strict dict equality is brittle — if the token includes additional
metadata (timestamps/ids) tests will fail even though core behavior is correct.
Replacing the equality with explicit checks for
username/first_name/last_name reduces flakiness and preserves intent.
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** tests/integration_tests/security/api_tests.py
**Line:** 166:167
**Comment:**
*Logic Error: The test asserts full dictionary equality between the
original `user` input and the decoded token `user` claim; this is brittle
because the service may add extra fields (timestamps, ids) to the token or
normalize values, making the equality fail even when the important fields are
preserved. Replace the strict equality with explicit checks for the required
fields so the test validates intent without being fragile.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
superset/jinja_context.py:
##########
@@ -293,6 +299,70 @@ def url_param(
self.cache_key_wrapper(result)
return result
+ def get_guest_user_attribute(
+ self,
+ attribute_name: str,
+ default: JsonValue = None,
+ add_to_cache_keys: bool = True,
+ ) -> JsonValue:
+ """
+ Get a specific user attribute from guest user.
+
+ This function retrieves attributes from the guest user token and
supports
+ all JSON-native types (string, number, boolean, array, object, null).
+
+ Args:
+ attribute_name: Name of the attribute to retrieve
+ default: Default value if attribute not found (can be any
JSON-native type)
+ add_to_cache_keys: Whether the value should be included in the
cache key
+
+ Returns:
+ The attribute value from the guest user token, or the default
value.
+ Can be any JSON-native type: string, number, boolean, array,
object, or
+ null.
+
+ Examples:
+ {{ get_guest_user_attribute('department') }} # Returns:
"Engineering"
+ {{ get_guest_user_attribute('is_admin') }} # Returns: True
+ {{ get_guest_user_attribute('permissions') }} # Returns: ["read",
"write"]
+ {{ get_guest_user_attribute('config') }} # Returns: {"theme":
"dark"}
+ {{ get_guest_user_attribute('missing', 'default') }} # Returns:
"default"
+ """
+
+ # Check if we have a request context and user
+ if not has_request_context():
+ return default
+
+ if not hasattr(g, "user") or g.user is None:
+ return default
+
+ user = g.user
+
+ # Check if current user is a guest user
+ if not (hasattr(user, "is_guest_user") and user.is_guest_user):
+ return default
+
+ # Get attributes from guest token
+ if hasattr(user, "guest_token") and user.guest_token:
+ token = user.guest_token
+ token_user = token.get("user", {})
Review Comment:
**Suggestion:** `get_guest_user_attribute` assumes `user.guest_token` is a
mapping and calls `.get(...)` without validating its type; if `guest_token` is
a string or another non-dict truthy object this will raise an AttributeError
during template rendering — add explicit checks that `guest_token` and the
nested `token_user` are dictionaries before using `.get`, and return `default`
if they are not. [possible bug]
**Severity Level:** Critical 🚨
```suggestion
"""
# Check if we have a request context and user
if not has_request_context():
return default
if not hasattr(g, "user") or g.user is None:
return default
user = g.user
# Check if current user is a guest user
if not (hasattr(user, "is_guest_user") and user.is_guest_user):
return default
# Get attributes from guest token
if hasattr(user, "guest_token") and user.guest_token:
token = user.guest_token
# ensure token is a mapping before calling .get
if not isinstance(token, dict):
return default
token_user = token.get("user", {})
if not isinstance(token_user, dict):
return default
```
<details>
<summary><b>Why it matters? ⭐ </b></summary>
The suggestion is a correct defensive improvement: if guest_token is ever a
non-mapping truthy value (string, object, etc.), calling .get will raise an
AttributeError during template rendering. Adding explicit isinstance checks for
dict (and for token_user) prevents that and returns the safe default path. It's
a small, safe hardening that avoids runtime crashes.
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset/jinja_context.py
**Line:** 313:348
**Comment:**
*Possible Bug: `get_guest_user_attribute` assumes `user.guest_token` is
a mapping and calls `.get(...)` without validating its type; if `guest_token`
is a string or another non-dict truthy object this will raise an AttributeError
during template rendering — add explicit checks that `guest_token` and the
nested `token_user` are dictionaries before using `.get`, and return `default`
if they are not.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
docs/docs/configuration/sql-templating.mdx:
##########
@@ -301,6 +301,111 @@ Here's a concrete example:
WHERE country_code = 'US'
```
+**Guest User Attributes**
+
+The `{{ get_guest_user_attribute('attribute_name') }}` macro returns a
specific attribute value from the
+guest user context. This is useful when working with embedded Superset where
guest tokens can contain
+custom attributes that need to be accessed in SQL queries.
+
+This macro supports all JSON-native types (string, number, boolean, array,
object, null) for both attribute values
+and default values. The return type depends on the type of the attribute
stored in the guest token.
+
+This macro only works when the current user is a guest user (authenticated via
guest token). If the current user is
+not a guest user, or if the specified attribute doesn't exist, the macro will
return `None` or the provided default value.
+
+Guest user attributes are set when creating guest tokens via the
`/security/guest_token` API endpoint.
+The attributes are passed in the `user` object within the request payload.
+
+Here's an example of how to create a guest token with custom attributes of
various types:
+
+```json
+{
+ "user": {
+ "username": "bob_with_attrs",
+ "first_name": "Bob",
+ "last_name": "Smith",
+ "attributes": {
+ "department": "Engineering",
+ "region": "US",
+ "role": "developer",
+ "team": "data-platform",
+ "clearance_level": "standard",
+ "projects": ["analytics", "ml-platform"],
+ "team_lead": true,
+ "employee_id": 12345,
+ "budget_limit": 50000.75,
+ "config": {
+ "theme": "dark",
+ "notifications": true
+ }
+ }
+ },
+ "resources": [{
+ "type": "dashboard",
+ "id": "dashboard-uuid"
+ }],
+ "rls": []
+}
+```
+
+The `attributes` field in the `user` object can contain any custom key-value
pairs that your application needs.
+These attributes will be available in SQL queries through the
`get_guest_user_attribute()` macro and can be
+any JSON-native type.
+
+For more information about setting up embedded Superset and creating guest
tokens, see the
+[embedded SDK
documentation](https://www.npmjs.com/package/@superset-ui/embedded-sdk) and
[networking
settings](/docs/configuration/networking-settings#html-embedding-of-dashboards-and-charts).
+
+If you have caching enabled in your Superset configuration, then by default
the attribute 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 attribute value in the calculation of the
+cache key by adding the following parameter to your Jinja code:
+
+```
+{{ get_guest_user_attribute('department', add_to_cache_keys=False) }}
+```
+
+You can also provide a default value if the attribute is not found. The
default value can be any JSON-native type:
+
+```
+{{ get_guest_user_attribute('region', default='US') }}
+{{ get_guest_user_attribute('team_lead', default=False) }}
+{{ get_guest_user_attribute('budget_limit', default=0) }}
+{{ get_guest_user_attribute('projects', default=[]) }}
+```
+
+Here are concrete examples of using guest user attributes of different types
in queries:
+
+```sql
+-- String attributes
+SELECT *
+FROM sales_data
+WHERE region = '{{ get_guest_user_attribute("region", default="global") }}'
+ AND department = '{{ get_guest_user_attribute("department") }}'
+
+-- Boolean attributes
+SELECT *
+FROM sensitive_data
+WHERE {{ get_guest_user_attribute("has_access", default=False) }} = true
Review Comment:
**Suggestion:** Boolean rendering ambiguity: the boolean example
interpolates the attribute directly and compares to the SQL literal `true`;
depending on how Jinja serializes booleans this may render capitalized Python
booleans or strings, causing dialect-dependent failures—serialize booleans to
JSON (lowercase true/false) or otherwise normalize them before comparison.
[possible bug]
**Severity Level:** Critical 🚨
```suggestion
WHERE {{ get_guest_user_attribute("has_access", default=False) | tojson }} =
true
```
<details>
<summary><b>Why it matters? ⭐ </b></summary>
Jinja may render Python booleans as "True"/"False" (capitalized) which some
SQL dialects don't accept. Using |tojson
yields lowercase true/false and is a practical way to make the example
dialect-robust.
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** docs/docs/configuration/sql-templating.mdx
**Line:** 390:390
**Comment:**
*Possible Bug: Boolean rendering ambiguity: the boolean example
interpolates the attribute directly and compares to the SQL literal `true`;
depending on how Jinja serializes booleans this may render capitalized Python
booleans or strings, causing dialect-dependent failures—serialize booleans to
JSON (lowercase true/false) or otherwise normalize them before comparison.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
docs/docs/configuration/sql-templating.mdx:
##########
@@ -301,6 +301,111 @@ Here's a concrete example:
WHERE country_code = 'US'
```
+**Guest User Attributes**
+
+The `{{ get_guest_user_attribute('attribute_name') }}` macro returns a
specific attribute value from the
+guest user context. This is useful when working with embedded Superset where
guest tokens can contain
+custom attributes that need to be accessed in SQL queries.
+
+This macro supports all JSON-native types (string, number, boolean, array,
object, null) for both attribute values
+and default values. The return type depends on the type of the attribute
stored in the guest token.
+
+This macro only works when the current user is a guest user (authenticated via
guest token). If the current user is
+not a guest user, or if the specified attribute doesn't exist, the macro will
return `None` or the provided default value.
+
+Guest user attributes are set when creating guest tokens via the
`/security/guest_token` API endpoint.
+The attributes are passed in the `user` object within the request payload.
+
+Here's an example of how to create a guest token with custom attributes of
various types:
+
+```json
+{
+ "user": {
+ "username": "bob_with_attrs",
+ "first_name": "Bob",
+ "last_name": "Smith",
+ "attributes": {
+ "department": "Engineering",
+ "region": "US",
+ "role": "developer",
+ "team": "data-platform",
+ "clearance_level": "standard",
+ "projects": ["analytics", "ml-platform"],
+ "team_lead": true,
+ "employee_id": 12345,
+ "budget_limit": 50000.75,
+ "config": {
+ "theme": "dark",
+ "notifications": true
+ }
+ }
+ },
+ "resources": [{
+ "type": "dashboard",
+ "id": "dashboard-uuid"
+ }],
+ "rls": []
+}
+```
+
+The `attributes` field in the `user` object can contain any custom key-value
pairs that your application needs.
+These attributes will be available in SQL queries through the
`get_guest_user_attribute()` macro and can be
+any JSON-native type.
+
+For more information about setting up embedded Superset and creating guest
tokens, see the
+[embedded SDK
documentation](https://www.npmjs.com/package/@superset-ui/embedded-sdk) and
[networking
settings](/docs/configuration/networking-settings#html-embedding-of-dashboards-and-charts).
+
+If you have caching enabled in your Superset configuration, then by default
the attribute 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 attribute value in the calculation of the
+cache key by adding the following parameter to your Jinja code:
+
+```
+{{ get_guest_user_attribute('department', add_to_cache_keys=False) }}
+```
+
+You can also provide a default value if the attribute is not found. The
default value can be any JSON-native type:
+
+```
+{{ get_guest_user_attribute('region', default='US') }}
+{{ get_guest_user_attribute('team_lead', default=False) }}
+{{ get_guest_user_attribute('budget_limit', default=0) }}
+{{ get_guest_user_attribute('projects', default=[]) }}
+```
+
+Here are concrete examples of using guest user attributes of different types
in queries:
+
+```sql
+-- String attributes
+SELECT *
+FROM sales_data
+WHERE region = '{{ get_guest_user_attribute("region", default="global") }}'
+ AND department = '{{ get_guest_user_attribute("department") }}'
Review Comment:
**Suggestion:** Security risk: the examples interpolate string guest
attributes directly into SQL without escaping, enabling SQL injection if an
attribute contains malicious characters; escape single quotes before inserting
into quoted SQL literals (or use a safe-quoting filter) to prevent injection.
[security]
**Severity Level:** Critical 🚨
```suggestion
WHERE region = '{{ get_guest_user_attribute("region", default="global") |
replace("'", "''") }}'
AND department = '{{ get_guest_user_attribute("department") | replace("'",
"''") }}'
```
<details>
<summary><b>Why it matters? ⭐ </b></summary>
The examples interpolate user-provided attributes directly into
single-quoted SQL literals. If an attribute
contains a single quote (or other malicious content) it will break the SQL
and could enable injection in real usage.
Escaping single quotes (or otherwise serializing safely) in the example is a
practical mitigation and improves the docs.
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** docs/docs/configuration/sql-templating.mdx
**Line:** 384:385
**Comment:**
*Security: Security risk: the examples interpolate string guest
attributes directly into SQL without escaping, enabling SQL injection if an
attribute contains malicious characters; escape single quotes before inserting
into quoted SQL literals (or use a safe-quoting filter) to prevent injection.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
docs/docs/configuration/sql-templating.mdx:
##########
@@ -301,6 +301,111 @@ Here's a concrete example:
WHERE country_code = 'US'
```
+**Guest User Attributes**
Review Comment:
**Suggestion:** The new section header is formatted as bold text instead of
using a heading level consistent with the rest of the document; this breaks the
document structure and will not appear in the generated table of contents or
match other macro sections. Replace the bold formatting with a proper heading
(e.g., "### Guest User Attributes") to be consistent. [possible bug]
**Severity Level:** Critical 🚨
```suggestion
### Guest User Attributes
```
<details>
<summary><b>Why it matters? ⭐ </b></summary>
The file shows other macro sections using "###" headings. The added line is
bold text, not a heading,
so it won't be treated the same by the renderer/TOC generator. Changing this
single line to "### Guest User Attributes"
fixes structure and is a simple, correct doc change.
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** docs/docs/configuration/sql-templating.mdx
**Line:** 304:304
**Comment:**
*Possible Bug: The new section header is formatted as bold text instead
of using a heading level consistent with the rest of the document; this breaks
the document structure and will not appear in the generated table of contents
or match other macro sections. Replace the bold formatting with a proper
heading (e.g., "### Guest User Attributes") to be consistent.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
tests/integration_tests/security/api_tests.py:
##########
@@ -123,6 +123,174 @@ def test_post_guest_token_authorized(self):
assert user == decoded_token["user"]
assert resource == decoded_token["resources"][0]
+ @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+ def test_post_guest_token_with_attributes(self):
+ """
+ Security API: Create a guest token with user attributes
+ """
+ self.dash =
db.session.query(Dashboard).filter_by(slug="births").first()
+ self.embedded = EmbeddedDashboardDAO.upsert(self.dash, [])
+ self.login(ADMIN_USERNAME)
+
+ user = {
+ "username": "bob_with_attrs",
+ "first_name": "Bob",
+ "last_name": "Also Bob",
+ "attributes": {
+ "department": "Engineering",
+ "region": "US",
+ "role": "developer",
+ "team": "data-platform",
+ "clearance_level": "standard",
+ "projects": ["analytics", "ml-platform"],
+ "team_lead": True,
+ },
+ }
+ resource = {"type": "dashboard", "id": str(self.embedded.uuid)}
+ rls_rule = {"dataset": 1, "clause": "1=1"}
+ params = {"user": user, "resources": [resource], "rls": [rls_rule]}
+
+ response = self.client.post(
+ self.uri, data=json.dumps(params), content_type="application/json"
+ )
+
+ self.assert200(response)
+ token = json.loads(response.data)["token"]
+ decoded_token = jwt.decode(
+ token,
+ self.app.config["GUEST_TOKEN_JWT_SECRET"],
+ audience=get_url_host(),
+ algorithms=["HS256"],
+ )
+
+ # Verify user attributes are preserved in the token
+ assert user == decoded_token["user"]
+ assert "attributes" in decoded_token["user"]
+ assert decoded_token["user"]["attributes"]["department"] ==
"Engineering"
+ assert decoded_token["user"]["attributes"]["region"] == "US"
+ assert decoded_token["user"]["attributes"]["role"] == "developer"
+ assert decoded_token["user"]["attributes"]["team"] == "data-platform"
+ assert decoded_token["user"]["attributes"]["clearance_level"] ==
"standard"
+ assert decoded_token["user"]["attributes"]["projects"] == [
+ "analytics",
+ "ml-platform",
+ ]
+ assert decoded_token["user"]["attributes"]["team_lead"] is True
+ assert resource == decoded_token["resources"][0]
+
+ @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+ def test_post_guest_token_with_empty_attributes(self):
+ """
+ Security API: Create a guest token with empty user attributes
+ """
+ self.dash =
db.session.query(Dashboard).filter_by(slug="births").first()
+ self.embedded = EmbeddedDashboardDAO.upsert(self.dash, [])
+ self.login(ADMIN_USERNAME)
+
+ user = {
+ "username": "bob_empty_attrs",
+ "first_name": "Bob",
+ "last_name": "Also Bob",
+ "attributes": {},
+ }
+ resource = {"type": "dashboard", "id": str(self.embedded.uuid)}
+ rls_rule = {"dataset": 1, "clause": "1=1"}
+ params = {"user": user, "resources": [resource], "rls": [rls_rule]}
+
+ response = self.client.post(
+ self.uri, data=json.dumps(params), content_type="application/json"
+ )
+
+ self.assert200(response)
+ token = json.loads(response.data)["token"]
+ decoded_token = jwt.decode(
+ token,
+ self.app.config["GUEST_TOKEN_JWT_SECRET"],
+ audience=get_url_host(),
+ algorithms=["HS256"],
+ )
+
+ # Verify empty attributes are preserved in the token
+ assert user == decoded_token["user"]
+ assert "attributes" in decoded_token["user"]
+ assert decoded_token["user"]["attributes"] == {}
+ assert resource == decoded_token["resources"][0]
+
+ @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+ def test_post_guest_token_with_null_attributes(self):
+ """
+ Security API: Create a guest token with null user attributes
+ """
+ self.dash =
db.session.query(Dashboard).filter_by(slug="births").first()
+ self.embedded = EmbeddedDashboardDAO.upsert(self.dash, [])
+ self.login(ADMIN_USERNAME)
+
+ user = {
+ "username": "bob_null_attrs",
+ "first_name": "Bob",
+ "last_name": "Also Bob",
+ "attributes": None,
+ }
+ resource = {"type": "dashboard", "id": str(self.embedded.uuid)}
+ rls_rule = {"dataset": 1, "clause": "1=1"}
+ params = {"user": user, "resources": [resource], "rls": [rls_rule]}
+
+ response = self.client.post(
+ self.uri, data=json.dumps(params), content_type="application/json"
+ )
+
+ self.assert200(response)
+ token = json.loads(response.data)["token"]
+ decoded_token = jwt.decode(
+ token,
+ self.app.config["GUEST_TOKEN_JWT_SECRET"],
+ audience=get_url_host(),
+ algorithms=["HS256"],
+ )
+
+ # Verify null attributes are preserved in the token
+ assert user == decoded_token["user"]
+ assert "attributes" in decoded_token["user"]
+ assert decoded_token["user"]["attributes"] is None
+ assert resource == decoded_token["resources"][0]
+
+ @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+ def test_post_guest_token_without_attributes_backward_compatibility(self):
+ """
+ Security API: Create a guest token without attributes (backward
compatibility)
+ """
+ self.dash =
db.session.query(Dashboard).filter_by(slug="births").first()
+ self.embedded = EmbeddedDashboardDAO.upsert(self.dash, [])
+ self.login(ADMIN_USERNAME)
+
+ user = {
+ "username": "bob_no_attrs",
+ "first_name": "Bob",
+ "last_name": "Also Bob",
+ # Note: no attributes field
+ }
+ resource = {"type": "dashboard", "id": str(self.embedded.uuid)}
+ rls_rule = {"dataset": 1, "clause": "1=1"}
+ params = {"user": user, "resources": [resource], "rls": [rls_rule]}
+
+ response = self.client.post(
+ self.uri, data=json.dumps(params), content_type="application/json"
+ )
+
+ self.assert200(response)
+ token = json.loads(response.data)["token"]
+ decoded_token = jwt.decode(
+ token,
+ self.app.config["GUEST_TOKEN_JWT_SECRET"],
+ audience=get_url_host(),
+ algorithms=["HS256"],
+ )
+
+ # Verify user without attributes works and no attributes field is
present
Review Comment:
**Suggestion:** The test asserts the entire `user` dict equals the decoded
`user` and separately asserts `attributes` is absent; equality will fail if the
backend adds metadata fields even when `attributes` is absent. Replace the
full-dict equality with explicit checks for required fields and the absence of
`attributes`. [logic error]
**Severity Level:** Minor ⚠️
```suggestion
# Verify user without attributes works and no attributes field is
present (check core fields explicitly)
assert decoded_token["user"]["username"] == user["username"]
assert decoded_token["user"]["first_name"] == user["first_name"]
assert decoded_token["user"]["last_name"] == user["last_name"]
```
<details>
<summary><b>Why it matters? ⭐ </b></summary>
The equality assert will fail if harmless extra fields exist; checking core
user fields plus asserting absence of the attributes key is clearer and less
brittle.
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** tests/integration_tests/security/api_tests.py
**Line:** 289:289
**Comment:**
*Logic Error: The test asserts the entire `user` dict equals the
decoded `user` and separately asserts `attributes` is absent; equality will
fail if the backend adds metadata fields even when `attributes` is absent.
Replace the full-dict equality with explicit checks for required fields and the
absence of `attributes`.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
tests/unit_tests/jinja_context_test.py:
##########
@@ -1639,3 +1640,963 @@ def test_jinja2_server_error_handling(mocker:
MockerFixture) -> None:
assert "Internal Jinja2 template error" in str(exception)
assert "MemoryError" in str(exception)
assert "Out of memory" in str(exception)
+
+def test_get_guest_user_attribute_no_request_context(mocker: MockerFixture) ->
None:
+ """
+ Test that get_guest_user_attribute returns default when no request context.
+ """
+ mocker.patch("superset.jinja_context.has_request_context",
return_value=False)
+
+ cache = ExtraCache()
+ result = cache.get_guest_user_attribute("department", "default_dept")
+ assert result == "default_dept"
+
+
+def test_get_guest_user_attribute_no_user(mocker: MockerFixture) -> None:
+ """
+ Test that get_guest_user_attribute returns default when no user in context.
+ """
+ mocker.patch("superset.jinja_context.has_request_context",
return_value=True)
+ mock_g = mocker.patch("superset.jinja_context.g")
+ mock_g.user = None
+
+ cache = ExtraCache()
+ result = cache.get_guest_user_attribute("department", "default_dept")
+ assert result == "default_dept"
+
+
+def test_get_guest_user_attribute_not_guest_user(mocker: MockerFixture) ->
None:
+ """
+ Test that get_guest_user_attribute returns default when user is not a
guest user.
+ """
+ mocker.patch("superset.jinja_context.has_request_context",
return_value=True)
+ mock_g = mocker.patch("superset.jinja_context.g")
+
+ # Create a regular user (not guest)
+ regular_user = mocker.Mock()
+ regular_user.is_guest_user = False
+ mock_g.user = regular_user
+
+ cache = ExtraCache()
+ result = cache.get_guest_user_attribute("department", "default_dept")
+ assert result == "default_dept"
+
+
+def test_get_guest_user_attribute_with_attributes(mocker: MockerFixture) ->
None:
+ """
+ Test that get_guest_user_attribute returns correct attribute value for
guest user.
+ """
+ mocker.patch("superset.jinja_context.has_request_context",
return_value=True)
+ mock_g = mocker.patch("superset.jinja_context.g")
+
+ # Create guest user with attributes
+ guest_user = mocker.Mock()
+ guest_user.is_guest_user = True
+ guest_user.guest_token = {
+ "user": {
+ "username": "test_guest",
+ "attributes": {
+ "department": "Engineering",
+ "region": "US",
+ "role": "developer",
+ },
+ },
+ "resources": [{"type": "dashboard", "id": "test-id"}],
+ "rls_rules": [],
+ }
+ mock_g.user = guest_user
+
+ cache = ExtraCache()
+
+ # Test existing attribute
+ result = cache.get_guest_user_attribute("department")
+ assert result == "Engineering"
+
+ # Test another existing attribute
+ result = cache.get_guest_user_attribute("region")
+ assert result == "US"
+
+ # Test non-existing attribute returns default
+ result = cache.get_guest_user_attribute("non_existing", "default_value")
+ assert result == "default_value"
+
+
+def test_get_guest_user_attribute_without_attributes(mocker: MockerFixture) ->
None:
+ """
+ Test that get_guest_user_attribute returns default when guest user has no
+ attributes.
+ """
+ mocker.patch("superset.jinja_context.has_request_context",
return_value=True)
+ mock_g = mocker.patch("superset.jinja_context.g")
+
+ # Create guest user without attributes
+ guest_user = mocker.Mock()
+ guest_user.is_guest_user = True
+ guest_user.guest_token = {
+ "user": {"username": "test_guest"},
+ "resources": [{"type": "dashboard", "id": "test-id"}],
+ "rls_rules": [],
+ }
+ mock_g.user = guest_user
+
+ cache = ExtraCache()
+ result = cache.get_guest_user_attribute("department", "default_dept")
+ assert result == "default_dept"
+
+
+def test_get_guest_user_attribute_empty_attributes(mocker: MockerFixture) ->
None:
+ """
+ Test that get_guest_user_attribute returns default when guest user has
empty
+ attributes.
+ """
+ mocker.patch("superset.jinja_context.has_request_context",
return_value=True)
+ mock_g = mocker.patch("superset.jinja_context.g")
+
+ # Create guest user with empty attributes
+ guest_user = mocker.Mock()
+ guest_user.is_guest_user = True
+ guest_user.guest_token = {
+ "user": {"username": "test_guest", "attributes": {}},
+ "resources": [{"type": "dashboard", "id": "test-id"}],
+ "rls_rules": [],
+ }
+ mock_g.user = guest_user
+
+ cache = ExtraCache()
+ result = cache.get_guest_user_attribute("department", "default_dept")
+ assert result == "default_dept"
+
+
+def test_get_guest_user_attribute_null_attributes(mocker: MockerFixture) ->
None:
+ """
+ Test that get_guest_user_attribute returns default when guest user has null
+ attributes.
+ """
+ mocker.patch("superset.jinja_context.has_request_context",
return_value=True)
+ mock_g = mocker.patch("superset.jinja_context.g")
+
+ # Create guest user with null attributes
+ guest_user = mocker.Mock()
+ guest_user.is_guest_user = True
+ guest_user.guest_token = {
+ "user": {"username": "test_guest", "attributes": None},
+ "resources": [{"type": "dashboard", "id": "test-id"}],
+ "rls_rules": [],
+ }
+ mock_g.user = guest_user
+
+ cache = ExtraCache()
+ result = cache.get_guest_user_attribute("department", "default_dept")
+ assert result == "default_dept"
+
+
+def test_get_guest_user_attribute_cache_key_behavior(mocker: MockerFixture) ->
None:
+ """
+ Test that get_guest_user_attribute correctly handles cache key behavior.
+ """
+ mocker.patch("superset.jinja_context.has_request_context",
return_value=True)
+ mock_g = mocker.patch("superset.jinja_context.g")
+
+ # Create guest user with attributes
+ guest_user = mocker.Mock()
+ guest_user.is_guest_user = True
+ guest_user.guest_token = {
+ "user": {
+ "username": "test_guest",
+ "attributes": {"department": "Engineering", "region": "US"},
+ },
+ "resources": [{"type": "dashboard", "id": "test-id"}],
+ "rls_rules": [],
+ }
+ mock_g.user = guest_user
+
+ cache = ExtraCache()
+ # Mock the cache_key_wrapper method
+ mock_cache_wrapper = mocker.Mock()
Review Comment:
**Suggestion:** Replacing `cache.cache_key_wrapper` with a plain Mock
without a return value can change observable behavior (the real method returns
the key). Use a Mock that preserves the input by returning it
(side_effect=lambda x: x) so tests that rely on behavior or callers expecting
the original return value won't break. [logic error]
**Severity Level:** Minor ⚠️
```suggestion
# Use a mock that preserves the original method contract by returning
the passed value.
mock_cache_wrapper = mocker.Mock(side_effect=lambda value: value)
```
<details>
<summary><b>Why it matters? ⭐ </b></summary>
Replacing the real method with a Mock that preserves the contract (returns
the passed key)
is a harmless improvement and prevents subtle breakage if callers start
using the return
value. The suggested change (side_effect=lambda v: v) better mirrors the
original behavior
and is low cost; it's a sensible test hardening.
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** tests/unit_tests/jinja_context_test.py
**Line:** 1815:1815
**Comment:**
*Logic Error: Replacing `cache.cache_key_wrapper` with a plain Mock
without a return value can change observable behavior (the real method returns
the key). Use a Mock that preserves the input by returning it
(side_effect=lambda x: x) so tests that rely on behavior or callers expecting
the original return value won't break.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
docs/docs/configuration/sql-templating.mdx:
##########
@@ -301,6 +301,111 @@ Here's a concrete example:
WHERE country_code = 'US'
```
+**Guest User Attributes**
+
+The `{{ get_guest_user_attribute('attribute_name') }}` macro returns a
specific attribute value from the
+guest user context. This is useful when working with embedded Superset where
guest tokens can contain
+custom attributes that need to be accessed in SQL queries.
+
+This macro supports all JSON-native types (string, number, boolean, array,
object, null) for both attribute values
+and default values. The return type depends on the type of the attribute
stored in the guest token.
+
+This macro only works when the current user is a guest user (authenticated via
guest token). If the current user is
+not a guest user, or if the specified attribute doesn't exist, the macro will
return `None` or the provided default value.
+
+Guest user attributes are set when creating guest tokens via the
`/security/guest_token` API endpoint.
+The attributes are passed in the `user` object within the request payload.
+
+Here's an example of how to create a guest token with custom attributes of
various types:
+
+```json
+{
+ "user": {
+ "username": "bob_with_attrs",
+ "first_name": "Bob",
+ "last_name": "Smith",
+ "attributes": {
+ "department": "Engineering",
+ "region": "US",
+ "role": "developer",
+ "team": "data-platform",
+ "clearance_level": "standard",
+ "projects": ["analytics", "ml-platform"],
+ "team_lead": true,
+ "employee_id": 12345,
+ "budget_limit": 50000.75,
+ "config": {
+ "theme": "dark",
+ "notifications": true
+ }
+ }
+ },
+ "resources": [{
+ "type": "dashboard",
+ "id": "dashboard-uuid"
+ }],
+ "rls": []
+}
+```
+
+The `attributes` field in the `user` object can contain any custom key-value
pairs that your application needs.
+These attributes will be available in SQL queries through the
`get_guest_user_attribute()` macro and can be
+any JSON-native type.
+
+For more information about setting up embedded Superset and creating guest
tokens, see the
+[embedded SDK
documentation](https://www.npmjs.com/package/@superset-ui/embedded-sdk) and
[networking
settings](/docs/configuration/networking-settings#html-embedding-of-dashboards-and-charts).
+
+If you have caching enabled in your Superset configuration, then by default
the attribute 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 attribute value in the calculation of the
+cache key by adding the following parameter to your Jinja code:
+
+```
+{{ get_guest_user_attribute('department', add_to_cache_keys=False) }}
+```
+
+You can also provide a default value if the attribute is not found. The
default value can be any JSON-native type:
+
+```
+{{ get_guest_user_attribute('region', default='US') }}
+{{ get_guest_user_attribute('team_lead', default=False) }}
+{{ get_guest_user_attribute('budget_limit', default=0) }}
+{{ get_guest_user_attribute('projects', default=[]) }}
+```
+
+Here are concrete examples of using guest user attributes of different types
in queries:
+
+```sql
+-- String attributes
+SELECT *
+FROM sales_data
+WHERE region = '{{ get_guest_user_attribute("region", default="global") }}'
+ AND department = '{{ get_guest_user_attribute("department") }}'
+
+-- Boolean attributes
+SELECT *
+FROM sensitive_data
+WHERE {{ get_guest_user_attribute("has_access", default=False) }} = true
+
+-- Numeric attributes
+SELECT *
+FROM transactions
+WHERE amount <= {{ get_guest_user_attribute("budget_limit", default=1000) }}
Review Comment:
**Suggestion:** Type/formatting issue: numeric example inserts the attribute
raw into SQL; if the attribute is non-numeric (string) the resulting SQL will
be invalid or behave unexpectedly. Coerce the value to a numeric type in the
template (e.g., apply a float/int filter) to ensure valid SQL numeric
comparison. [type error]
**Severity Level:** Minor ⚠️
```suggestion
WHERE amount <= {{ get_guest_user_attribute("budget_limit", default=1000) |
float }}
```
<details>
<summary><b>Why it matters? ⭐ </b></summary>
If the attribute comes back as a string (or null) the rendered SQL may be
invalid or behave unexpectedly.
Applying a numeric coercion like |float in the example makes the intent
explicit and yields safer, more predictable SQL.
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** docs/docs/configuration/sql-templating.mdx
**Line:** 395:395
**Comment:**
*Type Error: Type/formatting issue: numeric example inserts the
attribute raw into SQL; if the attribute is non-numeric (string) the resulting
SQL will be invalid or behave unexpectedly. Coerce the value to a numeric type
in the template (e.g., apply a float/int filter) to ensure valid SQL numeric
comparison.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]