You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/06/23 00:54:12 UTC
(superset) 01/01: feat(security): record audit metadata on guest token issuance
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch fix/guest-token-issuance-audit
in repository https://gitbox.apache.org/repos/asf/superset.git
commit d6c562173a6e53b095ff022907cfae60303d829c
Author: Amin Ghadersohi <am...@gmail.com>
AuthorDate: Mon Jun 22 17:53:47 2026 -0700
feat(security): record audit metadata on guest token issuance
The guest-token issuance endpoint logged only the coarse action via
@event_logger.log_this, without the metadata needed to investigate a misissued
or over-scoped token. Emit an explicit issuance event capturing the issuer user
id, source IP, granted resources, dataset allowlist, RLS dataset scope, and a
SHA-256 hash of the issued token (never the raw token; RLS clause text is
omitted since it can carry data values).
The payload is built by a pure helper (build_guest_token_audit_payload) with
unit tests.
Co-Authored-By: Claude Fable 5 <no...@anthropic.com>
---
superset/security/api.py | 15 ++++-
superset/security/guest_token.py | 35 +++++++++-
.../unit_tests/security/guest_token_audit_test.py | 74 ++++++++++++++++++++++
3 files changed, 122 insertions(+), 2 deletions(-)
diff --git a/superset/security/api.py b/superset/security/api.py
index c38b3c590fe..ebf888045f1 100644
--- a/superset/security/api.py
+++ b/superset/security/api.py
@@ -34,7 +34,11 @@ from superset.commands.dashboard.embedded.exceptions import (
from superset.commands.exceptions import ForbiddenError
from superset.exceptions import SupersetGenericErrorException
from superset.extensions import db, event_logger
-from superset.security.guest_token import GuestTokenResourceType
+from superset.security.guest_token import (
+ build_guest_token_audit_payload,
+ GuestTokenResourceType,
+)
+from superset.utils.core import get_user_id
from superset.views.base_api import (
BaseSupersetApi,
BaseSupersetModelRestApi,
@@ -204,6 +208,15 @@ class SecurityRestApi(BaseSupersetApi):
body["rls"],
**({"datasets": body["datasets"]} if "datasets" in body else {}),
)
+ logger.info(
+ "Guest token issued: %s",
+ build_guest_token_audit_payload(
+ issuer_user_id=get_user_id(),
+ source_ip=request.remote_addr,
+ body=body,
+ token=token,
+ ),
+ )
return self.response(200, token=token)
except EmbeddedDashboardNotFoundError as error:
return self.response_400(message=error.message)
diff --git a/superset/security/guest_token.py b/superset/security/guest_token.py
index 9f1b58fbd92..884d41d827c 100644
--- a/superset/security/guest_token.py
+++ b/superset/security/guest_token.py
@@ -14,8 +14,9 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
+import hashlib
import logging
-from typing import Optional, TypedDict, Union
+from typing import Any, Optional, TypedDict, Union
from flask_appbuilder.security.sqla.models import Group, Role
from flask_login import AnonymousUserMixin
@@ -24,6 +25,38 @@ from superset.utils.backports import StrEnum
logger = logging.getLogger(__name__)
+
+def build_guest_token_audit_payload(
+ issuer_user_id: Optional[int],
+ source_ip: Optional[str],
+ body: dict[str, Any],
+ token: str,
+) -> dict[str, Any]:
+ """Build security-relevant metadata for a guest-token issuance event.
+
+ Captures who issued the token, from where, what it grants, and a hash of
+ the issued token (never the raw token) so a later investigation into a
+ misissued or over-scoped token can be scoped from the audit log.
+ """
+ resources = body.get("resources") or []
+ rls = body.get("rls") or []
+ return {
+ "issuer_user_id": issuer_user_id,
+ "source_ip": source_ip,
+ "resources": [
+ f"{resource.get('type')}:{resource.get('id')}" for resource in resources
+ ],
+ "datasets": body.get("datasets"),
+ # RLS clauses can carry data values; record only the datasets in scope
+ # and the rule count, not the clause text.
+ "rls_datasets": [rule.get("dataset") for rule in rls],
+ "rls_rule_count": len(rls),
+ # Hash, not the raw token, so the log can be correlated without
+ # becoming a credential store.
+ "token_sha256": hashlib.sha256(token.encode("utf-8")).hexdigest(),
+ }
+
+
# JWT claim that carries the revocation version a token was minted with.
GUEST_TOKEN_REVOCATION_CLAIM = "rev" # noqa: S105
diff --git a/tests/unit_tests/security/guest_token_audit_test.py b/tests/unit_tests/security/guest_token_audit_test.py
new file mode 100644
index 00000000000..782c9ba548d
--- /dev/null
+++ b/tests/unit_tests/security/guest_token_audit_test.py
@@ -0,0 +1,74 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Tests for guest-token issuance audit metadata."""
+
+import hashlib
+
+from superset.security.guest_token import build_guest_token_audit_payload
+
+
+def test_build_guest_token_audit_payload_captures_issuance_metadata():
+ body = {
+ "user": {"username": "guest"},
+ "resources": [{"type": "dashboard", "id": "abc-123"}],
+ "datasets": [1, 2],
+ "rls": [{"dataset": 1, "clause": "tenant_id = 9"}],
+ }
+ payload = build_guest_token_audit_payload(
+ issuer_user_id=42,
+ source_ip="10.0.0.1",
+ body=body,
+ token="the-secret-token", # noqa: S106
+ )
+
+ assert payload["issuer_user_id"] == 42
+ assert payload["source_ip"] == "10.0.0.1"
+ assert payload["resources"] == ["dashboard:abc-123"]
+ assert payload["datasets"] == [1, 2]
+ assert payload["rls_datasets"] == [1]
+ assert payload["rls_rule_count"] == 1
+
+
+def test_build_guest_token_audit_payload_hashes_token_and_omits_raw():
+ token = "the-secret-token" # noqa: S105
+ payload = build_guest_token_audit_payload(
+ issuer_user_id=1,
+ source_ip=None,
+ body={"resources": [], "rls": []},
+ token=token,
+ )
+
+ # The raw token is never present; only its hash is recorded.
+ assert token not in payload.values()
+ assert payload["token_sha256"] == hashlib.sha256(token.encode("utf-8")).hexdigest()
+
+
+def test_build_guest_token_audit_payload_omits_rls_clause_text():
+ body = {
+ "resources": [],
+ "rls": [{"dataset": 7, "clause": "secret_value = 'pii'"}],
+ }
+ payload = build_guest_token_audit_payload(
+ issuer_user_id=1,
+ source_ip=None,
+ body=body,
+ token="t", # noqa: S106
+ )
+
+ # Clause text (which can carry data values) is not recorded.
+ assert "secret_value = 'pii'" not in str(payload)
+ assert payload["rls_datasets"] == [7]