sadpandajoe commented on code in PR #33924:
URL: https://github.com/apache/superset/pull/33924#discussion_r3606670573


##########
superset/security/api.py:
##########
@@ -69,6 +69,9 @@ 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), 
allow_none=True
+    )

Review Comment:
   The generated OpenAPI spec (`docs/static/resources/openapi.json`) was not 
regenerated for this new field, so `GuestTokenCreate`'s user schema still omits 
`attributes` and generated clients cannot discover or type it. Can we 
regenerate the spec with this schema change?



##########
superset/jinja_context.py:
##########
@@ -305,6 +311,67 @@ 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,
+        escape_result: 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
+            escape_result: Escape string values (and strings inside lists) 
using
+                the database dialect's quoting so they are safe to interpolate
+                into SQL, mirroring ``url_param``. Enabled by default; 
non-string
+                JSON types are returned unchanged. Set to False for the raw 
value.
+
+        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"
+        """
+
+        # The macro only applies to guest users (embedded). is_guest_user()
+        # handles the feature-flag and request-context checks internally.
+        if not security_manager.is_guest_user():
+            return default
+
+        token = g.user.guest_token
+        user_attributes = token.get("user", {}).get("attributes") or {}
+
+        # Only add to the cache key if the attribute exists in the guest token
+        if attribute_name not in user_attributes:
+            return default
+
+        result = user_attributes[attribute_name]
+        if add_to_cache_keys and result is not None:
+            # Use json.dumps for consistent serialization of all JSON-native 
types
+            cache_value = json.dumps(result, sort_keys=True)
+            self.cache_key_wrapper(
+                f"guest_user_attribute:{attribute_name}:{cache_value}"
+            )

Review Comment:
   When an attribute is null or absent (falling back to `default`), or when 
`add_to_cache_keys=False`, the value is interpolated into the SQL but never 
added to the cache key, so two guests whose tokens render different SQL can 
collide on the same chart and RLS cache key and receive each other's rows. 
Unlike `url_param` (which keys unconditionally) and `current_user_*` (no 
caller-supplied default), the diverging default breaks the isolation the cache 
key provides. Should the resolved value, including the default and null branch, 
always contribute to the key, and does guest-token RLS using this macro get 
picked up by `has_extra_cache_key_calls`, which currently scans only persisted 
RLS?



##########
superset/jinja_context.py:
##########
@@ -305,6 +311,67 @@ 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,
+        escape_result: 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
+            escape_result: Escape string values (and strings inside lists) 
using
+                the database dialect's quoting so they are safe to interpolate
+                into SQL, mirroring ``url_param``. Enabled by default; 
non-string
+                JSON types are returned unchanged. Set to False for the raw 
value.
+
+        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"
+        """
+
+        # The macro only applies to guest users (embedded). is_guest_user()
+        # handles the feature-flag and request-context checks internally.
+        if not security_manager.is_guest_user():
+            return default
+
+        token = g.user.guest_token
+        user_attributes = token.get("user", {}).get("attributes") or {}
+
+        # Only add to the cache key if the attribute exists in the guest token
+        if attribute_name not in user_attributes:
+            return default
+
+        result = user_attributes[attribute_name]
+        if add_to_cache_keys and result is not None:
+            # Use json.dumps for consistent serialization of all JSON-native 
types
+            cache_value = json.dumps(result, sort_keys=True)
+            self.cache_key_wrapper(
+                f"guest_user_attribute:{attribute_name}:{cache_value}"
+            )
+        # Guest attributes are interpolated into the rendered SQL, so escape
+        # string values (and strings within lists) with the dialect's quoting
+        # by default, mirroring url_param. Non-string JSON types pass through.
+        if escape_result:
+            result = self._escape_value(result)

Review Comment:
   On MySQL/MariaDB this escaping is bypassable. `_escape_value` doubles the 
quote but does not escape the backslash that MySQL treats as an escape 
character, so a guest attribute of `x\' OR 1=1 -- ` renders to `WHERE col = 
'x\'' OR 1=1 -- '`, which parses as `WHERE col = 'x' OR 1=1`. The docstring 
says the value is safe to interpolate into SQL, but that only holds where 
backslash is literal (Postgres, SQLite), and it conflicts with the docs note 
that says attributes are not escaped. Can we bind these values (or require 
allowlisting) instead of interpolating, and reconcile the docstring with that 
note? The same gap exists in `url_param`, which shares this helper.



-- 
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]

Reply via email to