codeant-ai-for-open-source[bot] commented on code in PR #40699:
URL: https://github.com/apache/superset/pull/40699#discussion_r3345966337


##########
superset/security/manager.py:
##########
@@ -3500,6 +3502,41 @@ def get_guest_user_from_request(self, req: Request) -> 
Optional[GuestUser]:
 
         return self.get_guest_user_from_token(cast(GuestToken, token))
 
+    def _guest_token_is_revoked(self, token: dict[str, Any]) -> bool:
+        """
+        Return True if any dashboard resource referenced by the token has been
+        revoked since the token was issued — i.e. the token's ``iat`` predates
+        that embedded dashboard's ``guest_token_revoked_before``.
+
+        Revocation is per embedded dashboard, so revoking one embed does not
+        affect guest tokens for other dashboards. Tokens with no ``iat`` (none
+        are issued without one) are treated as not revoked.
+        """
+        # pylint: disable=import-outside-toplevel
+        from datetime import timezone
+
+        from superset.daos.dashboard import EmbeddedDashboardDAO
+
+        iat = token.get("iat")
+        if iat is None:
+            return False
+        for resource in token.get("resources") or []:
+            if resource.get("type") != GuestTokenResourceType.DASHBOARD.value:
+                continue
+            embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
+            revoked_before = (
+                embedded.guest_token_revoked_before if embedded else None
+            )
+            if revoked_before is None:
+                continue

Review Comment:
   **Suggestion:** Revocation lookup only checks 
`EmbeddedDashboardDAO.find_by_id` using the token resource id as an embedded 
UUID, but guest tokens are still allowed to carry legacy dashboard IDs. For 
those tokens, lookup returns `None` and revocation is silently skipped, so 
previously-issued dashboard-id tokens remain valid after revoke. Resolve 
dashboard-id resources to their embedded config (or check both dashboard-id and 
embedded-uuid paths) before deciding not revoked. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ Per-dashboard revoke fails for dashboard-id guest tokens.
   - ⚠️ Revoked embeds still accessible via legacy-id guest tokens.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Mint a guest token via the public API `POST 
/api/v1/security/guest_token/` implemented
   in `superset/security/api.py:144-163`. The request body is validated by
   `GuestTokenCreateSchema` (`security/api.py:33-36`) and then passed to
   `validate_guest_token_resources` (`security/api.py:42-45`).
   
   2. In the request body, set `resources` to include a dashboard *ID* instead 
of an embedded
   UUID, for example: `{"type": "dashboard", "id": "<numeric_dashboard_id>"}`.
   `SupersetSecurityManager.validate_guest_token_resources` at
   `superset/security/manager.py:3428-3439` first calls 
`Dashboard.get(str(resource["id"]))`;
   since this id is a valid dashboard id, the check passes without requiring an
   `EmbeddedDashboard` uuid.
   
   3. Configure the same dashboard for embedding so it has an 
`EmbeddedDashboard` row, and
   then revoke its guest tokens via `POST 
/api/v1/dashboard/<id_or_slug>/embedded/revoke`.
   This hits `DashboardRestApi.revoke_embedded` in 
`superset/dashboards/api.py:2144-2183`,
   which calls `EmbeddedDashboardDAO.revoke_guest_tokens(dashboard)`
   (`superset/daos/dashboard.py:23-32`), stamping 
`embedded.guest_token_revoked_before` for
   each embedded config.
   
   4. Send the previously-minted token (whose resource id is the dashboard id, 
not the
   embedded uuid) in the guest token header to any embedded request, which is 
processed by
   `SupersetSecurityManager.get_guest_user_from_request`
   (`superset/security/manager.py:21-54`). The method decodes the JWT and then 
calls
   `_guest_token_is_revoked` (`manager.py:56-89`), which looks up each 
dashboard resource
   only via `EmbeddedDashboardDAO.find_by_id(str(resource["id"]))` 
(`manager.py:77`). For a
   numeric dashboard id, this returns `None`, `revoked_before` is `None`, and 
the loop
   `continue`s (`manager.py:78-81`), so `_guest_token_is_revoked` returns 
`False`. As a
   result, the guest user is accepted even though 
`EmbeddedDashboardDAO.revoke_guest_tokens`
   marked the associated embedded configuration as revoked.
   ```
   </details>
   
   [Fix in 
Cursor](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=df5cec8c328b422aa3410105fa6ee798&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
 | [Fix in VSCode 
Claude](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=df5cec8c328b422aa3410105fa6ee798&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
   
   *(Use Cmd/Ctrl + Click for best experience)*
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/security/manager.py
   **Line:** 3523:3531
   **Comment:**
        *Logic Error: Revocation lookup only checks 
`EmbeddedDashboardDAO.find_by_id` using the token resource id as an embedded 
UUID, but guest tokens are still allowed to carry legacy dashboard IDs. For 
those tokens, lookup returns `None` and revocation is silently skipped, so 
previously-issued dashboard-id tokens remain valid after revoke. Resolve 
dashboard-id resources to their embedded config (or check both dashboard-id and 
embedded-uuid paths) before deciding not revoked.
   
   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.
   Once fix is implemented, also check other comments on the same PR, and ask 
user if the user wants to fix the rest of the comments as well. if said yes, 
then fetch all the comments validate the correctness and implement a minimal fix
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F40699&comment_hash=ffc30e2bdabdb4050c4c6caffb36b0d967e0539f8018c6731b8cc9bcc8e3ea0a&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F40699&comment_hash=ffc30e2bdabdb4050c4c6caffb36b0d967e0539f8018c6731b8cc9bcc8e3ea0a&reaction=dislike'>👎</a>



##########
superset/security/manager.py:
##########
@@ -3500,6 +3502,41 @@ def get_guest_user_from_request(self, req: Request) -> 
Optional[GuestUser]:
 
         return self.get_guest_user_from_token(cast(GuestToken, token))
 
+    def _guest_token_is_revoked(self, token: dict[str, Any]) -> bool:
+        """
+        Return True if any dashboard resource referenced by the token has been
+        revoked since the token was issued — i.e. the token's ``iat`` predates
+        that embedded dashboard's ``guest_token_revoked_before``.
+
+        Revocation is per embedded dashboard, so revoking one embed does not
+        affect guest tokens for other dashboards. Tokens with no ``iat`` (none
+        are issued without one) are treated as not revoked.
+        """
+        # pylint: disable=import-outside-toplevel
+        from datetime import timezone
+
+        from superset.daos.dashboard import EmbeddedDashboardDAO
+
+        iat = token.get("iat")
+        if iat is None:
+            return False

Review Comment:
   **Suggestion:** Tokens without an `iat` claim are treated as not revoked, 
but `get_guest_user_from_request` does not require `iat`, so a validly-signed 
token missing `iat` bypasses the new revocation control. Reject guest tokens 
that lack `iat` during validation instead of returning `False` in revocation 
checks. [security]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ⚠️ Missing-iat guest tokens bypass per-dashboard revocation checks.
   - ⚠️ Weakens guarantee that revoked embeds lose access.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Construct a JWT manually using the same secret and algorithm as 
configured in Superset
   (`GUEST_TOKEN_JWT_SECRET` and `GUEST_TOKEN_JWT_ALGO`, read in
   `SupersetSecurityManager.create_guest_access_token` at
   `superset/security/manager.py:31-34` and `parse_jwt_guest_token` at 
`manager.py:97-107`).
   Omit the `iat` claim entirely, but include at least: `"user"`, `"resources"`,
   `"rls_rules"`, `"aud"` matching `_get_guest_token_jwt_audience` 
(`manager.py:1-6`),
   `"type": "guest"`, and an `"exp"` if desired.
   
   2. Ensure the token's `resources` reference an embedded dashboard that has 
been configured
   and later revoked via the per-dashboard revoke endpoint `POST
   /api/v1/dashboard/<id_or_slug>/embedded/revoke` 
(`superset/dashboards/api.py:2144-2183`),
   which calls `EmbeddedDashboardDAO.revoke_guest_tokens` to set 
`guest_token_revoked_before`
   (`superset/daos/dashboard.py:23-32`).
   
   3. Send this custom JWT as the guest token header in an HTTP request that 
reaches
   Superset. `SupersetSecurityManager.get_guest_user_from_request`
   (`superset/security/manager.py:21-54`) reads the header, calls 
`parse_jwt_guest_token`
   (`manager.py:97-107`) to decode and validate the signature and audience, and 
then performs
   basic claim checks for `"user"`, `"resources"`, `"rls_rules"`, and `"type"`
   (`manager.py:38-45). It does *not* require the presence of `"iat"`.
   
   4. `get_guest_user_from_request` then calls `_guest_token_is_revoked`
   (`manager.py:56-89`). Inside this method, `iat = token.get("iat")` is `None`
   (`manager.py:71`), so the early check `if iat is None: return False` 
(`manager.py:72-73`)
   is hit, and revocation is not enforced. The function returns `False` (not 
revoked), and
   `get_guest_user_from_request` proceeds to return a `GuestUser` 
(`manager.py:54, 91-95`),
   allowing a signed guest token without `iat` to remain valid even after 
revocation.
   ```
   </details>
   
   [Fix in 
Cursor](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=8ee4cf4cf5524d89997d98ef5ca8bb9e&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
 | [Fix in VSCode 
Claude](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=8ee4cf4cf5524d89997d98ef5ca8bb9e&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
   
   *(Use Cmd/Ctrl + Click for best experience)*
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/security/manager.py
   **Line:** 3520:3522
   **Comment:**
        *Security: Tokens without an `iat` claim are treated as not revoked, 
but `get_guest_user_from_request` does not require `iat`, so a validly-signed 
token missing `iat` bypasses the new revocation control. Reject guest tokens 
that lack `iat` during validation instead of returning `False` in revocation 
checks.
   
   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.
   Once fix is implemented, also check other comments on the same PR, and ask 
user if the user wants to fix the rest of the comments as well. if said yes, 
then fetch all the comments validate the correctness and implement a minimal fix
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F40699&comment_hash=c7ef7e251336302b73d62715e7caf59c87335e9eef0538eb83df7e33fa0ee2b9&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F40699&comment_hash=c7ef7e251336302b73d62715e7caf59c87335e9eef0538eb83df7e33fa0ee2b9&reaction=dislike'>👎</a>



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